GNU Smalltalk で GTK

GNU Smalltalk 3.2系のウリは GTK なので、そこらへんで遊んでみました。まずは一番簡単なコードをGNU Smalltalk GTK 2.0 Tutorialを見ながら作成。

#!/usr/bin/env gst

PackageLoader fileInPackage: 'GTK'.

Object subclass: HelloWorld [
    | button window |
    hello [
        'hello world' printNl
    ]

    delate [
        'delete event occured' printNl.
        ^false
    ]

    destroy [
        GTK.Gtk mainQuit
    ]

    show [
        window := GTK.GtkWindow new: GTK.Gtk gtkWindowToplevel.
        window connectSignal: 'delete_event' 
            to: self selector: #delete userData: nil.
        window connectSignal: 'destroy'
            to: self selector: #destroy userData: nil.

        window setBorderWidth: 10.

        button := GTK.GtkButton newWithLabel: 'Hello World'.
        button connectSignal: 'clicked'
            to: self selector: #hello userData: nil.
        button connectSignal: 'clicked'
            to: self selector: #destroy userData: nil.

        window add: button.
        button show.
        window show.
    ]
].

Eval [
    hello := HelloWorld new.
    hello show.
    GTK.Gtk main
]


Tk とかに馴染んでいれば、特に解説は要らないごく普通な操作感。ただ、GTK パッケージの部品は委譲で使って、インターフェイスはダックタイピング任せ・・というのが標準的なスタイルの様子なのは、ちょっとだけ「あれ?」と思うポイントでしょうか。Window を継承しないのは新鮮。

あと、detele_event イベントは何かというと、ウィンドウのバッテンボタンを押したりすると出るイベントで、false で返すことで さらにdestroy イベントが発生するそうな。trueで返すと destroy は発生しないわけで、「本当に終了しますか Y/N」みたいなのを出すとかに使うみたいです。

パッキングボックス

ウィジェットの配置は、やっぱりお馴染みの パッキングボックスを使います。

#!/usr/bin/env gst

PackageLoader fileInPackage: 'GTK'.

Object subclass: HelloWorld [
    | box1 button1 button2  window |
    hello: aWidget string: aString [
        ('Hello agein - ', aString, ' was pressed') printNl
    ]

    delete: aWiget event: aGdkEvent [
        GTK.Gtk mainQuit.
        ^false
    ]

    show [
        window := GTK.GtkWindow new: GTK.Gtk gtkWindowToplevel.
        window setTitle: 'Hello Buttons!'.
        window connectSignal: 'delete_event'
            to: self selector: #delete:event: userData: nil.

        window setBorderWidth: 10.

        box1 := GTK.GtkHBox new: false spacing: 0.
        window add: box1.


        button1 := GTK.GtkButton newWithLabel: 'Hello World'.
        button1 connectSignal: 'clicked'
            to: self selector: #hello:string: userData: 'button 1'.
        box1 packStart: button1 expand: true fill: true padding: 0.
        button1 show.

        button2 := GTK.GtkButton newWithLabel: 'Button 2'.
        button2 connectSignal: 'clicked'
            to: self selector: #hello:string: userData: 'button 2'.
        box1 packStart: button2 expand: true fill: true padding: 0.
        button2 show.

        box1 show.
        window show.
    ]
].

Eval [
    hello := HelloWorld new.
    hello show.
    GTK.Gtk main
]

パッキングボックスには GTK.GtkHBox と GTK.GtkVBox があって、それぞれ水平詰めと垂直詰めに用います。#packStart:expand:fill:padding: メッセージと #packEnd:expand:fill:padding: メッセージでウィジェットを積めていきます(Start と End の違いは箱の頭から詰めるかお尻から詰めるかです)。

expand は ボックス内のスペースが余る時、スペースを残さないようにウィジェットの領域を広げるか否かを指定します。fill はその時にウィジェット自体でその領域を埋めるように膨らますか、ウィジェットのまわりにパディングするかの指定です。

GtkTextView / GtkTextBuffer

堪え性のないわたしは、テキストを編集するウィジェットに目移りしてしまって、いきなりチュートリアルを脱線してしまう...。

#!/usr/bin/env gst

PackageLoader fileInPackage: 'GTK'.

Object subclass: PettitWorkspace [
    | box1 sw textview  textbuf doitBtn window |

    doit [
        ^Behavior evalString: textbuf text to: self.
    ]

    delete: aWiget event: aGdkEvent [
        GTK.Gtk mainQuit.
        ^false
    ]

    show [
        window := GTK.GtkWindow new: GTK.Gtk gtkWindowToplevel.
        window setTitle: 'Pettit Workspace'.
        window connectSignal: 'delete_event' 
            to: self selector: #delete:event: userData: nil.

        window setBorderWidth: 10.

        box1 := GTK.GtkVBox new: false spacing: 0.
        window add: box1.

        textview := GTK.GtkTextView new.
        textbuf  := textview getBuffer.
        sw := GTK.GtkScrolledWindow withChild: textview.
        sw setPolicy: GTK.Gtk gtkPolicyAutomatic
           vscrollbarPolicy: GTK.Gtk gtkPolicyAutomatic.

        box1 packStart: sw expand: true fill: true padding: 0.
        sw show.
        textview show.

        doitBtn := GTK.GtkButton newWithLabel: 'Doit'.
        doitBtn connectSignal: 'clicked'
            to: self selector: #doit userData: nil.
        box1 packStart: doitBtn expand: true fill: true padding: 0.
        doitBtn show.


        box1 show.
        window show.
    ]
]

Eval [
    hello := HelloWorld new.
    hello show.
    GTK.Gtk main
]

せっかくなので Smalltalk の Workspace 風にしてみました。

GtkTextView は そのモデル GtkTextBuffer とセットで扱うようにできています。 テキストへの操作(選択されている文字列の取得や、タグ付けなど)は Buffer に対して行います。

一応 Workspace の端くれなので、Smalltalk コードを実行できます。

Behavior >> #evalString:to: で、self (PettitWorkspaceオブジェクト) を渡しているので、実行したまま GUI をコネコネできます。プロパティ変更だけじゃなくってウィジェットを増やしたりとかも。

Before


After


こうしておけば、クラスブラウザやリファレンスを眺めながら適当しながら 使い方を模索できるのでいい感じ。

こういうこと(動的プログラムこねこね)をやりたいと思ってしまうのは、ごく普通の要求のなのか、わたしがSmalltalk に馴れてしまった変態さんだからかは解らないのですが、GUI フレームワークの勉強の良い方法だとわたしは思います。

感想

  • Blox (TK バインディング)にくらべて、Smalltalkっぽくアレンジされてない
  • 薄皮ラッパーなのと、中身が c-call ばっかりなので、クラスブラウザで「読む」ようにコードができていないのは Smalltalk として非常に違和感。(同等コードもコメントもない)
  • 名前空間があるのに、クラス名がいちいち GtkHogeHoge なのはどうなんでしょう。

GNU Smalltalk 3.0 系は、GNU というOSの中のスクリプティング言語 という割り切りが凄くって、文法面よりもむしろ文化や哲学面の方が強く Smalltalk らしくないと感じさせられます。

でも、こういう処理系もあってもいいよね、というか、わたしはこれはこれで好きだったりします。

GNU Smalltalk GTK で 他文字コードの表示とか

ハマったのでメモ。


* * *


こんな風に ワークスペースとして作っておけば、そのアプリに存在しない機能も直接コード実行でどうにかなっちゃうものです。というわけでテキストエディタ風に使ってみようとファイルアクセス。

| file |
file := '/home/minekoa/install-memo' asFile.
textbuf insertAtEnd: file contents.


おぉ、いい感じ!・・と思ったのもつかの間、別のファイルを開こうとすると、そちらでは開けません。

(gst:18604): Gtk-CRITICAL **: gtk_text_buffer_emit_insert: assertion `g_utf8_validate (text, len, NULL)' failed
(ip 8)GtkTextBuffer>>#insert:text:len:
(ip 16)GtkTextBuffer>>#insert:text:
(ip 8)GtkTextBuffer>>#insertAtEnd:
(ip 18)[] in PettitWorkspace>>#Doit
(ip 6)PettitWorkspace>>#Doit
(ip 6)[] in Behavior>>#evalString:to:
(ip 14)
(ip 54)Behavior class(Behavior)>>#evalString:to:
(ip 10)PettitWorkspace>>#doit
(ip 0)

あー、文字コードですかー。最初のテキストは UTF-8 で作成されていたので、何もしなくても問題なく表示されていたのですが、今回開こうとしたのは Shirt-JIS。

というわけで クラスブラウザで関係しそうなあたりをあさってみたけれど Squeak の文字コードいじりっぽいのは無い。

ていうか、

String >> encoding [
    "Answer the encoding of the receiver.  This is not implemented unless
     you load the Iconv package."

    <category: 'converting'>
    self notYetImplemented
]

とか書いてあって、なるほど GNU (というかUnix) ライズされてるなーとか思ったり。でもパッケージを読み込むと String なんて超基本的なクラスにメソッドが追加されていっちゃうあたりは、非常に Smalltalk っぽい ので何とも複雑な気分です。

| file |
PackageLoader fileInPackage: 'Iconv'.

file := '/home/minekoa/sample.txt' asFile.
textbuf insertAtEnd: (file contents asUnicodeString: 'CP932') 

でも、今度は別のエラー。

./pettitworkspace.st:73: Attempt to pass an instance of UnicodeString as a char *
(ip 8)GtkTextBuffer>>#insert:text:len:
(ip 16)GtkTextBuffer>>#insert:text:
(ip 8)GtkTextBuffer>>#insertAtEnd:
(ip 22)[] in PettitWorkspace>>#Doit
(ip 6)PettitWorkspace>>#Doit
(ip 6)[] in Behavior>>#evalString:to:
(ip 14)
(ip 54)Behavior class(Behavior)>>#evalString:to:
(ip 10)PettitWorkspace>>#doit
(ip 0)

で、ここでしばらく「なんだろ〜」とハマって I18N.EncodedString とかをこねこねしてたのですが、結局は単純な話で、

| file |
PackageLoader fileInPackage: 'Iconv'.

file := '/home/minekoa/sample.txt' asFile.
textbuf insertAtEnd: (file contents asUnicodeString: 'CP932') asByteArray

と、asByteArray すれば OK でした。


よかった、できて。


* * *


文字コードUnicode じゃなきゃだめなのに、 渡すときは char* になる Smalltalk オブジェクト(=ByteArray) じゃないとだめというところが味噌でした。

理屈は分かるけれど、これは GtkTextBuffer >> inseratAtEnd: とか中で asByteArray するようにしといて欲しいと思いました。