【追記 2022/12/9】プログラムのバグ修正と「gtk4」gemのバージョン4.0.4(最初にこの記事を書いたときには4.0.3)について、「徒然なるままにRuby 」のGitHub レポジトリに書き加えました。
追加、修正については申し訳ありませんがこちら をご覧になってください。
はてなブログ の以下の記事は修正されていません。【追記以上】
Ruby /Gtk の記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Rails で作った単語帳プログラム)のGTK 4版を作りました。
プログラムは「徒然なるままにRuby 」のGitHub レポジトリに置いてあります。
レポジトリをダウンロードし、ディレクト リ_example/word_book_gtk4
をカレントディレクト リにして、ruby wordbook.rb
を実行してください。
今回の記事はRuby /GTK4の使い方に関するものです。
ただし、私自身Ruby /GTK4を良く分かっているわけではないので、内容に誤りがあるかもしれません。
この記事をもとにプログラムする場合は自己責任で行ってください。
それによる損害については何の保証もできないことをあらかじめご了承ください。
Ruby /Gtk4の完成度
Ruby /Gtk4を使ってみて、完成の域にあると感じました。
一般にはGTK 4のRuby へのバインディング は開発中という印象があると思いますが、そうではありません。
もっと、Ruby /GTK4を世界にアピールすべきだと思います。
Ruby /GTK4はgemで提供されます。
$ gem install gtk4
これでgemがインストールされます。
今回の単語帳プログラムでは、GTK 4で追加された「GtkColumnView」を試してみました。
結果、見事に動作しました。
素晴らしい!!
この記事でプログラムを全て紹介するのは、量的に無理があります。
そこで、プログラムをご覧になりたい方は、「徒然なるままにRuby」のレポジトリ の_example/word_book_gtk4
を参照してください。
ここでは、Ruby でGtk4を使うためのポイントを書きます。
それでも相当長い記事になりますが、ご容赦ください。
GTK はC言語 で書かれたライブラリですが、オブジェクト指向 で書かれており、Ruby 同様にクラスとインスタンス があります。
クラスには親子関係があり、ほとんどのクラスのトップ(ファンダメンタルという)はGObjectです。
「ほとんど」と書いたのは、GObject以外にもファンダメンタルなクラスがあるからで、しかもそれらも良く使われます。
このオブジェクト指向 のプログラミングに関するドキュメントはGObject APIリファランス です。
これは結構難しいです。
それよりは分かりやすいチュートリアル にGObject tutorial for beginners があります。
このへんが理解できていると、GTK もより確かなものにできます。
GtkApplicationの書き方
GtkApplicationはアプリケーションのクラスです。
Ruby のクラスと同様に、このクラスのインスタンス を作り、それを走らせることによってアプリケーションが動き出します。
require ' gtk4 '
application = Gtk ::Application .new(" com.github.toshiocp.wordbook " , :default_flags )
application.run
第1引数はアプリケーションIDでユニークな文字列でなければならない。URI と逆順のパターンで書く
第2引数はアプリケーションフラグ。通常は「:default_flag
」でよい
上記のプログラムはウィンドウが無いためにアプリケーションはすぐに終了してしまいます。
実行しても何も起こらないのはそのためで、エラーではありません。
ウィンドウの設定は後ほど述べます。
アプリケーションが起動されると(runメソッドが実行されると)startupとactivateというシグナルが発せられます。
startupはGtkApplicationのサブクラスを定義してクラスメソッド(バーチャル関数)を書き換えるときに使います。
GtkApplicationをそのまま使うときはほとんど必要ないと思います。
activateシグナルのハンドラでは
などをします。
Ruby /GTK では、ハンドラはsignal_connect
メソッドのブロックで表します。
runする前に定義しておくことが必要です。
次のプログラムの「... ... ...」の部分にsignal_connect
のハンドラを記述します。
require ' gtk4 '
application = Gtk ::Application .new(" com.github.toshiocp.wordbook " , :default_flags )
application.signal_connect " activate " do |app|
... ... ...
end
application.run
ブロックのパラメータapp
にはインスタンス application
が代入されます。
シグナル
ここで、シグナルについて説明しておきたいと思います。
シグナルはGObjectの持つ機能です。
その子孫クラスもすべてシグナルを定義することができます。
GTK では複数のスレッドが非同期に動きます。
このスレッドはRuby のスレッドとは違います。
そして、GtkApplicationのrunメソッドが呼び出されてからリターンするまで様々なスレッドが動きます。
このスレッドは様々なイベント(例えばボタンがクリックされたとかウィンドウが閉じたなど)が起こるタイミングで起動されます。
イベントはすべてシグナルという形で伝えられます。
例えばボタンがクリックされたときには「clicked」という名前のシグナルが発せられます。
あらかじめclickedシグナルに特定のプログラム(ハンドラという)を結びつけておけば、シグナル発生時にそのプログラムが実行されます。
シグナルは、ユーザが作成したクラスに定義することもできますが、ここではそのトピックはとりあげません。
シグナルの使い方は次のような流れになります。
各クラスがどのようなシグナルを持っているかをあらかじめ調べておく。例えばGtkButtonはclickedシグナルを持っている
シグナルが発せられたときに起動したいプログラム(ハンドラ)を考える
signal_connect
メソッドでシグナルとハンドラを結びつける
ポイントはsignal_connect
ハンドラの使い方になります。
インスタンス.signal_connect シグナル名 do |インスタンス, ... ... |
ハンドラ
end
この形で使います。
例えばボタン(このインスタンス が変数bに代入されているとする)がクリックされたときに"Hello"と標準出力に出力したければ
b.signal_connect " clicked " do
print " hello \n"
end
というプログラムになります。
ブロックがハンドラになります。
ブロックの引数の第1引数は常にそのシグナルを発したインスタンス になります。
その後の引数がどうなるかは、GTK 4のドキュメントの、各クラスのシグナルの項目に書かれています。
例えばGtkDialogのresponseシグナル の項目を見ると
ハンドラは次の形になっています。
void
response (
GtkDialog* self,
gint response_id,
gpointer user_data
)
これはCの関数の形で書かれています。
第1引数のselfはそのボタンのインスタンス のこと
第2引数のresponse_id
は、例えば、ダイアログのOKボタンが押されたとか、Cancelボタンが押されたとかの情報。
どのボタンにどのIDが割り振られるかはダイアログの定義時にプログラマー が決める
第3引数のuser_dataは、すべてのシグナルのハンドラに出てくるが、Ruby でプログラムする場合はほとんど必要ない
例えばダイアログのインスタンス を変数dに代入していたとするとプログラムは次のようになります。
d.signal_connect " response " do |dialog, response|
if response == Gtk ::ResponseType ::OK
OKボタンが押されたときの処理
elsif response == Gtk ::ResponseType ::CANCEL
キャンセルボタンが押されたときの処理
else
それ以外の状態でダイアログが閉じたときの処理
end
end
ダイアログの使い方としては、(1)ダイアログのインスタンス を生成(2)シグナルとハンドラを結合(3)ダイアログを表示、という順になります。
GtkApplicationWindowの書き方
アプリケーションのメインウィンドウにはGtkApplicationWindowクラスを使うのが良いです。
このクラスはGtkWindowのサブクラスで、GtkWindowよりアプリケーションのメインウィンドウに適した機能を持っています。
ただウィンドウを表示するだけなら、GtkApplicationWindowのインスタンス を生成し、showメソッドを呼び出せば良いです。
require ' gtk4 '
application = Gtk ::Application .new(" com.github.toshiocp.wordbook " , :default_flags )
application.signal_connect " activate " do |app|
Gtk ::ApplicationWindow .new(app).show
end
application.run
これでウィンドウが表示されます。
newメソッドにはアプリケーションのインスタンス app
を渡すことを忘れないでください。
この引数により、アプリケーションとウィンドウのインスタンス が結び付けられます。
GtkApplicationWindowではなくGtkWindowを用いる場合もアプリケーションとの結びつけが必要なケースがあります。
例えばウィンドウのボタンとアプリケーションに設定したアクションオブジェクトを結びつけたいときは、これが必要になります。
GtkWindowのset_application(アプリケーション)
メソッドでアプリケーションとウィンドウが結び付けられます。
ウィンドウにはボタン、エントリー(入力枠)などを配置します。
ウィジェット とは画面に表示するウィンドウ、ボタン、エントリーなどを指します。
オブジェクトの考え方からいえば、ウィジェット はGtkWidgetクラスの子孫クラスのことになります。
今回作成した単語帳「wordbook」で使用したウィジェット は
GtkApplicationWindow ⇒ メインウィンドウ
GtkBox ⇒ 複数のウィジェット を縦または横に配置するためのコンテナ
GtkButton ⇒ ボタン
GtkSearchEntry ⇒ 検索用の入力枠
GtkLabel ⇒ ラベル。文字列を表示するためのもの
GtkScrolledWindow ⇒ 内部に配置したウィジェット をスクロールする
GtkColumnView ⇒ 表
GtkColumnViewColumn ⇒ 表の中の一列
GtkWindow ⇒ 一般的なウィンドウ
GtkTextView ⇒ 複数行にわたるテキストを編集する
です。
GTK 4で用意されているウィジェット のサンプルはGTK 4のAPIリファランスのウィジェット・ギャラリー を参照してください。
UIファイル
ウィジェット を配置するのをプログラムでやるのは非常に手間がかかるので、代わりにUIファイルを使うのが良い方法です。
UIファイルはXML 形式でウィジェット を表現します。
例えば、ウィンドウの中にボタンを配置するのは次のようにします。
xml version ="1.0" encoding ="UTF-8"
<interface>
<object id ="window" class ="GtkWindow" >
<child>
<object id ="button_append" class ="GtkButton" >
</object>
</child>
</object>
</interface>
UIファイルの先頭にXML の定義を書く(単にコピペすれば良い)
最も外側のタグはinterfaceタグ
ウィジェット などのオブジェクトはobjectタグで表す。そのclassアトリビュート がクラス名を表す。
idタグはUIファイル内でユニークなオブジェクト名で、後ほどGtk::Builder
クラスのオブジェクトで利用する。
もしそこで使う予定がなければidを省略して良い
ウィンドウの内側にボタンを配置する場合は、childタグを用いてウィンドウのobjectタグ内にボタンのobjectタグを書く
この例のウィンドウとボタンのように、ウィジェット には親子関係が発生します。
外側のウィジェット が親、その中に配置されるウィジェット が子です。
この親子関係がつながると孫以下のウィジェット も出てきます。
このウィジェット の親子関係と、クラスの親子関係は全く別のものなので、混同しないようにしてください。
UIファイルではpropertyタグが頻繁に現れます。
これはオブジェクトのプロパティを設定するためのものです。
オブジェクトのプロパティはGTK 4のAPI リファラ ンスに書かれています。
例えばGtkWindowのプロパティ は、リファラ ンスから
リファランス⇒class⇒Window⇒property
とたどります。
ここには多くのプロパティがありますが、例をあげると
title ⇒ ウィンドウの上部のバーに表示されるタイトル
default-width ⇒ ウィンドウのデフォルトの幅
default-height ⇒ ウィンドウのデフォルトの高さ
です。
title、default-widthなどの文字列はプロパティ名です。
プロパティはRuby のハッシュのように、キーと値の形式になっていて、キーが「プロパティ名」で値が「プロパティの値」です。
プロパティの値には文字列、数値、真偽値などがありますが、UIファイル内では(テキストファイルなので当然ながら)すべて文字列で表現します。
文字列 ⇒ その文字列で表現
数値 ⇒ その数値の文字列、例えば0、-1、100、1.23など
真偽値 ⇒ true、false、yes、no、1、0など。大文字も可
これらの説明はGTK 4のAPI リファラ ンスのBuilder のところに書かれています。
案外書き方には自由度があることがわかります。
UIファイルではプロパティは次のように書きます。
<object id ="window" class ="GtkApplicationWindow" >
<property name ="title" > 単語帳</property>
<property name ="default-width" > 1200</property>
<property name ="default-height" > 800</property>
</object>
nameアトリビュート にはプロパティ名、タグで囲まれた部分にプロパティ値を書きます。
UIファイルでは主にobjectとpropertyタグを用いますが、他にどのようなタグがあるかを調べるのは少々複雑です。
GTK 4のAPI リファラ ンスでは、次の場所を調べてください。
Gtkbuilder(Builderクラス)の説明
各ウィジェット の説明の中で「クラス名 as GtkBuildable」の項目。
例えば、GtkDialogのその箇所 を見ると、action-widget タグが使えることが分かる。
ここで説明されているタグは、各ウィジェット 固有のものになる
wordbookには2つのuiファイルがあり、それぞれ別のウィンドウを記述しています。
非常に長いUIファイルになっていますので、ここには書きません。
興味のある方は、この記事の最初に書いた方法でレポジトリをダウンロードしてソースファイルを見てください。
ビルダーの使い方
UIファイルからウィジェット のインスタンス を生成するにはGtkBuilderを使います。
Ruby ではGtk ::Builderクラスのインスタンス を生成するときにウィジェット のインスタンス も生成されます。
そして、ウィジェット のインスタンス を取り出すには[]
メソッドを使います。
builder = Gtk ::Builder .new(file : " wordbook.ui " )
window = builder[" window " ]
button = builder[" button_append " ]
一行目でUIファイル「wordbook.ui」を読み込み、ウィジェット のインスタンス を生成するとともに、Builderクラスのインスタンス を変数builderに代入します。
ウィジェット はビルダーインスタンス の[]
メソッドで取り出します。
このメソッドの引数はUIファイルのオブジェクトのidアトリビュート です。
idアトリビュート を書かなかったウィジェット を取り出すことはできません。
ビルダーで作成するウィジェット に対して頻繁に生成/消滅をする場合(例えばダイアログ)は、いちいちファイルにアクセスするのは時間がもったいないです。
ファイルを読み込んだ文字列に対してビルダーを使えば、ファイルの読み書きが無くなって効率が良くなります。
Edit_window_ui = File .read(" edit_window.ui " )
builder = Gtk ::Builder .new(string : Edit_window_ui )
newメソッドの引数にstring:
をつけると文字列からUIデータを取り込みます。
このstring:
が何なのか疑問に思う人がいるかもしれません。
これはEdit_window_ui
とセットになってハッシュを表しています。
Ruby のメソッドのきまりでは、引数の最後のハッシュは波括弧を省略することができます。
省略しなければ
Edit_window_ui = File .read(" edit_window.ui " )
builder = Gtk ::Builder .new({string : Edit_window_ui })
または
Edit_window_ui = File .read(" edit_window.ui " )
builder = Gtk ::Builder .new({:string => Edit_window_ui })
と表せます。
UIファイルにクロージャ やシグナルハンドラが書かれていることがあります。
例えばwordbook.ui
の160行にはクロージャ があります。
<closure type ="gchararray" function ="nl2sp" >
シグナルハンドラについては、次のような例を考えてみましょう。
なお、このプログラムのファイルはレポジトリの_example/ruby_gtk4/ui_signal.rb
です。
require ' gtk4 '
ui_string = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object id="window" class="GtkWindow">
<child>
<object class="GtkButton">
<property name="label">Button</property>
<signal name="clicked" handler="button_cb"></signal>
</object>
</child>
</object>
</interface>
EOS
def button_cb (button)
print " clicked \n"
end
application = Gtk ::Application .new(" com.github.toshiocp.test " , :default_flags )
application.signal_connect " activate " do |app|
builder = Gtk ::Builder .new(string : ui_string)
window = builder[" window " ]
window.set_application(app)
window.show
end
application.run
このUI文字列は、ボタンのclicked
シグナルとbutton_cb
ハンドラをsignalタグで結びつけています。
このとき、ハンドラはトップレベ ルの同名メソッドになります。
例ではボタンがクリックされると、端末画面に「clicked」と表示されます。
このように、UIファイルまたはUI文字列のボタンオブジェクトにsignalタグを書いておけば、ハンドラのメソッドを書いておくだけですみ、signal_connect
メソッドは要らなくなります。
このように、本体のプログラムの負担はますます軽くなり、UIファイルの比率がますます高くなっていきます。
アクションとアクセラ レータ
アクションとはその名の通り「動作」「行動」のことです。
Gtk ではアクションがアクティブになるとactivateシグナルが発せられます。
プログラマー はあらかじめactivateシグナルにハンドラを結びつけておきます。
では、どのようなときにアクションがアクティブになるのでしょうか?
メニューとアクションが結びついているときには、そのメニューをクリックすると対応するアクションがアクティブになる
アクセラ レータ(Ctrl-Cのようなキーで、それがアクションに結びついたもの)により対応するアクションがアクティブになる
ボタンなどのウィジェット がアクションに結びついているとき、そのボタンがクリックされると対応するアクションがアクティブになる
一般にアクションに結びついているウィジェット はGtkActionableインターフェースをインプリメント (実装)しています。
GTK 4 API リファラ ンスのGtkActionableを見ると、インプリメント されているウィジェット が書かれています。
ユーザが新たにウィジェット を定義し、GtkActionableをインプリメント することも可能です。
ボタンのクリックにハンドラを対応させるにはclicked
シグナルと結びつける方法と、アクションを介して結びつける方法があります。
そのハンドラの動作にアクセラ レータやメニューも結びつけたいときはアクションを介するのが効率的です。
wordbookでは、「追加」「終了」「キャンセル」「削除」「保存」ボタンがアクションと結び付けられています。
アクションのスコープ、名前、ステート、ターゲット
アクションには「アプリケーション(app)」または「ウィンドウ(win)」というスコープがあります。
例えば「アプリケーションを終了させるアクション」はアプリケーションに関わるアクションなので、スコープをappにします。
「トップウィンドウをフルスクリーンにする/しない」というアクションはウィンドウに関わるアクションなので、スコープをwinにします。
多くのアクションはappスコープです。
appスコープのアクションはGtkApplicationに登録します。
winスコープのアクションはGtkApplicationWindowに登録します。
アクションは状態を保持することができます。
例えばフルスクリーンのon/offを切り替えるアクションは現在のスクリーンの状態を保持しています。
このような状態のことを「ステート」、状態を持っているアクションは「ステートフル」であるといいます。
また、ボタンの色を赤、黄、緑に変えるようなアクションでは、どの状態にするかのパラメータをつけることができます。
そのアクションの名前をcolored-button
とし、赤、黄、緑に変えるときのパラメータをred
、yellow
、green
とします。
そのとき、これらを組み合わせたものをdetailed name(詳しい名前)といい、colored-button::red
、colored-button::yellow
、colored-button::green
になります。
::
の後ろは、アクションが状態を変えるためのパラメータで、「ターゲット」といいます。
以上から、アクションは次の3つに分けて考えることができます。
ステートレス。状態を持たないアクション。単にハンドラを起動するだけ。
ステートフルでパラメータなし。例えばフルスクリーンの切り替えのようなトグル動作をするもの
ステートフルでパラメータあり。上のボタンの色を変えるアクションのようなもの
また、アクションにスコープをつけてapp.colored-button::red
と書くこともあります。
スコープをつけるかどうかはそのアクションを書くときの場面によります。
多くのアクションはステートレスで、wordbookのアクションもすべてステートレスです。
この記事ではステートレス・アクションのみを取り上げます。
ステートフル・アクションについてはAPI リファラ ンスなどを参照してください。
アクションの書き方
ここでは、ボタンをクリックしたときにアクションをアクティブにし、ハンドラを起動するプログラムを書いてみましょう。
アクションのクラスにはGSimpleActionとGPropertyActionがありますが、ほとんどの場合GSimpleActionを使うことになると思います。
次のプログラムでは「Hello」と表示されたボタンをクリックすると、端末画面にHelloが表示されます。
プログラムはレポジトリの_example/ruby_gtk4/action.rb
にあります。
require ' gtk4 '
ui_string = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object id="window" class="GtkWindow">
<child>
<object class="GtkButton">
<property name="label">Hello</property>
<property name="action-name">app.hello</property>
</object>
</child>
</object>
</interface>
EOS
def print_hello
print " Hello \n"
end
application = Gtk ::Application .new(" com.github.toshiocp.test " , :default_flags )
application.signal_connect " activate " do |app|
builder = Gtk ::Builder .new(string : ui_string)
window = builder[" window " ]
window.set_application(app)
action = Gio ::SimpleAction .new(" hello " )
action.signal_connect(" activate " ) do |_action, _parameter|
print_hello
end
app.add_action(action)
window.show
end
application.run
UI文字列のボタンにaction-name
プロパティの値を"app.hello"に設定する。
この値はスコープ付きのアクション名。これで、ボタンがクリックされたときに"app.hello"アクションがアクティブになる
アクションの定義はアプリケーションのactivateハンドラの中で行う。
helloという名前のアクションを生成(ここにはスコープは要らない)
アクションとハンドラ「print_hello」をsignal_connect
メソッドでつなげる。
ブロックの2つのパラメータは使われていない。
それらは、アクション・インスタンス とアクション・パラメータ(ある場合のみ有効で、ない場合はnil )である
add_action
メソッドでアプリケーションにアクションを登録する。
これによって、アクションのスコープがアプリケーションであることが確定する
スコープがウィンドウであるアクションを追加するには、window.add_action(アクション名)
を代わりに使う
アクションは通常5,6個は必要になると思います。
そのときは、Ruby の配列とeachコマンドを使うなどしてプログラムが大きくならないよう工夫してください。
アクセラ レータはキー入力とハンドラを結びつけたものです。
例えば「Ctrl-q」と押すとプログラムが終了するようなものです。
前のサブセクションで書いたアクションのプログラムにアクセラ レータを加えてみましょう。
そのためには、アプリケーションのactivateハンドラの中(window.showよりは前のどこか)に
app.set_accels_for_action(" app.hello " , [" <Control>h " , " <Alt>h " ])
を加えます。
このメソッドの第1引数はスコープ付きアクション名、第2引数がキーの配列です。
この例のように、同一のアクションに複数のキーを割り当てることができます(たいていはひとつで十分ですが)。
ひとつのキーだけを割り当てるときにも第2引数は配列でなければならないことに注意してください。
コントロール キーは<Control>
、<Ctrl>
。<Ctl>
などとします。
Altキーは<Alt>
、シフトキーとコントロール キーを同時に押すときは<Shift><Control>
などとします。
どのような記述が可能かについてはgtk_accelerator_parse を参考にしてください。
アクセラ レータを加えたプログラムはレポジトリの_example/ruby_gtk4/accel.rb
にあります。
GTK 4ではウィジェット の「見た目」をCSS (Cascading Style Sheets 、スタイルシート ともいう)で指定することができます。
CSS はウェブで用いられてきましたが、原理的にはGTK 4のようなGUI にもインプリメント できるものです。
どのようなスタイルシート が適用できるかは、APIリファランス を参照してください。
CSS を適用するにはセレクタ が必要です。
GTK 4ではノード、オブジェクト名(インスタンス 名)、スタイルクラス(文脈からCSS のクラスを指していることが明らかな場合は単に「クラス」ということもある)がセレクタ を構成します。
ノード名とクラス名はAPI リファラ ンスの各ウィジェット ・クラスの説明の「CSS nodes」のところに書かれています。
例えば、GtkButtonのノード名は「button」です。
また、スタイルクラスには「image-button」や「text-button」などがあります。
ボタンは画像を貼り付けたボタンと、文字列(ラベル)を貼り付けたボタンに分けることができ、さきほどのクラスはそれぞれのボタンのグループを表します。
クラスはノードの後ろにドット(.
)で区切って記述します。
button.text-button
このセレクタ は、ボタンのうち「文字列を書いたボタン」のみを対象にします。
また、独自のスタイルクラスをオブジェクトに追加するにはadd_css_class(クラス名)
メソッドを使います。
オブジェクト名はそのウィジェット のnameプロパティに設定されている文字列です。
それを設定するには、UIファイルでnameプロパティを指定するか、プログラム中でname=
メソッドを使います。
異なるオブジェクトに同じ名前をつけることはできません。
オブジェクト名はウェブのCSS でいえば、IDにあたります。
オブジェクト名はノード名の後ろにナンバー記号(#
)で区切って記述します。
button#delete_button
これは「delete_button」という名前をもったボタンを指します。
対象になるボタンは唯ひとつです。
ウェブのセレクタ の構文でGTK 4でも使えるものがあります。
*
⇒ 任意のノード
box button
⇒ GtkBoxの子孫ウィジェット のGtkButton
box > button
⇒ GtkBoxの子ウィジェット のGtkButton。直下の子のみで孫以下は入らない
この他にもありますが、詳細はAPIリファランス を参照してください。
CSS プロパティはセレクタ に続いて波括弧{}
で囲んで指定します。
例えばボタンの色(文字色)を赤にしたいときは
button {color; red;}
とします。
このあたりはウェブのCSS と同じです。
詳細はAPIリファランス を参照してください。
CSS は個々のウィジェット に適用する方法とアプリケーション画面全体に適用する方法がありますが、ここでは後者を説明します。
メイン・ウィンドウからGdkDisplayオブジェクトを取得する
GtkCssProviderインスタンス を生成し、そのインスタンス にCSS を記述した文字列をセットする
そのGdkDisplayオブジェクトにGtkCssProviderインスタンス を加える
css_text = " button {color: blue;} "
provider = Gtk ::CssProvider .new
provider.load_from_data(css_text)
Gtk ::StyleContext .add_provider_for_display(window.display, provider, :user )
このプログラムでは、CSS 文字列をcss_text
変数に代入しています。
このケースでは短い文字列なので、3行目のload_from_data
メソッドの引数に直接代入しても構いません。
2行目でGtkCssProviderのインスタンス を生成し、3行目でCSS 文字列をセットしています。
最終行の変数window
はトップレベ ルのウィンドウで、GtkApplicationWindowまたはGtkWindowのインスタンス を指しています。
display
メソッドはそのウィンドウからGdkDisplayインスタンス を取得します。
引数の最後の:user
は、GTK 4では、GTK_STYLE_PROVIDER_PRIORITY_USER
という定数として定義されています。
この定数はCSS を適用するプライオリティ(優先順位)の中で最も適用される度合いが高いものです。
エントリー
アプリケーションのユーザに文字を入力してもらうためのウィジェット がエントリーです。
wordbookアプリ(単語帳アプリ)では、英単語と日本語訳の入力にエントリーを用いています。
エントリーをウィンドウのパーツとして埋め込むのはUIファイルの中で記述するのが最も良く使われる方法です。
... ... ...
<child>
<object id ="entry" class ="GtkEntry" >
</object>
</child>
... ... ...
埋め込んだGtkEntryのオブジェクトは、プログラム中ではビルダーで取り出します。
builder = Gtk ::Builder .new(file : UIファイル名 )
entry = builder[" entry " ]
ユーザがエントリーに入力した文字列を入手するにはシグナルを使います。
エントリーのactivateシグナルを使う。
エントリーの入力でEnterキーが押されるとこのシグナルが発せられる。
入力終了したら、ボタンをユーザがクリックし、そのclickedシグナルを用いる。
ボタン以外の方法(例えばキー入力とアクセラ レータ)もある
シグナルのハンドラの中でtext
メソッドを使えば入力文字列を取り出すことができます。
s = entry.text
エントリーには、検索に特化したGtkSearchEntryやパスワード入力用のGtkPasswordEntryもあります。
テキストビュー
テキストビューは複数行にわたるテキストを編集するウィジェット です。
スクリーンエディタの画面のようなものと考えれば良いです。
GTK 4にはGtkTextViewクラスが用意されています。
テキストビューをウィンドウの中に埋め込むには、UIファイルを用います。
テキストビューではスクロールが必要になりますので、GtkScrolledWindowオブジェクトの子オブジェクトがGtkTextViewになるようにします。
<child>
<object class ="GtkScrolledWindow" >
<property name ="hexpand" > true</property>
<property name ="vexpand" > true</property>
<child>
<object id ="textview" class ="GtkTextView" >
<property name ="hexpand" > true</property>
<property name ="vexpand" > true</property>
<property name ="wrap-mode" > GTK_WRAP_WORD</property>
</object>
</child>
</object>
</child>
この例ではスクロールウィンドウもテキストビューもできる範囲で幅と高さをいっぱいにとるようにhexpand
とvexpand
をTRUEに設定しています。
これで問題ないケースがほとんどでしょうが、実装の実情に応じて変更してください。
wrap-mode
プロパティは画面の右端に達したときにテキストが折り返すときのモードを設定したものです。
例では、単語単位で折り返すようになっていますが、これは最も普通の設定です。
テキストビューにはactivateシグナルはないので、ボタンなどのシグナルを使ってテキストを取り出すようにします。
テキストはtext
メソッドで取り出すことができます。
フォントを設定したい場合は、CSS を使います。
GtkTextViewのノード名はtextviewです。
例を示します。
textview { font-family : "Noto Sans CJK JP" ; font-size : 12pt ; font-style : normal ;}
font-family ⇒ フォント名を指定
font-size ⇒ フォントサイズ
font-style ⇒ ノーマル(normal)、イタリック(italic)、斜体(oblique)を指定
その他のフォント情報の設定についてはAPIリファランス を参照してください。
GTK のコンポジット・オブジェクト
GTK のアプリケーションはメイン・ウィンドウ以外にダイアログ(ダイアログは一種のウィンドウ)やサブ・ウィンドウを使います。
それらのウィンドウには多くのウィジェット が埋め込まれています。
個々のウィジェット をまとまりなく記述すればプログラムは複雑になり、とても管理できないでしょう。
それを避けるためにGTK 4はコンポジット・オブジェクトという仕組みを提供しています。
コンポジット・オブジェクトはウィンドウとそこに配置されるウィジェット をまとめたオブジェクトです。
この仕組みはCでプログラムする上では非常に助かるものです。
しかし、Ruby の場合は「Ruby のクラス」を使ってウィンドウとそこに配置されたウィジェット をまとめていくことができます。
この方が分かりやすいかもしれません。
この「Ruby のクラス」をこの記事では「ラッパークラス」と呼ぶことにします。
ラッパークラスは次のセクションで説明します。
コンポジット・オブジェクトは(インスタンス ではなく)クラスのレベルで複数のオブジェクトを組み合わせます。
そのトップになるクラスは、UIファイルではtemplateタグで表します。
それ以外は通常のUIファイルと同じです。
UIデータを取り込むにはset_template
メソッドを用います。
簡単なサンプルプログラムを使って説明します。
サンプルプログラムはレポジトリの_example/ruby_gtk4/composite_window.rb
にあります。
require ' gtk4 '
class TopWindow < Gtk ::ApplicationWindow
type_register
class << self
def init
ui_string = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="TopWindow" parent="GtkApplicationWindow">
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="spacing">5</property>
<child>
<object id="search_entry" class="GtkSearchEntry">
</object>
</child>
<child>
<object id="print_button" class="GtkButton">
<property name="label">端末に出力</property>
</object>
</child>
</object>
</child>
</template>
</interface>
EOS
set_template(:data => GLib ::Bytes .new(ui_string))
bind_template_child(" search_entry " )
bind_template_child(" print_button " )
end
end
def initialize (app)
super (application : app)
print_button.signal_connect(" clicked " ) { print "#{ search_entry.text}\n" }
signal_connect(" close-request " ) { app.quit; true }
end
end
application = Gtk ::Application .new(" com.github.toshiocp.subclass " , :default_flags )
application.signal_connect " activate " do |app|
TopWindow .new(app).show
end
application.run
GtkApplicationWindowの子クラスとしてTopWindowを定義します。
TopWindowクラスにはGtkApplication内に配置されるGtkEntryとGtkButtonも組み込まれています。
そして、TopWindowのインスタンス をnewメソッドで生成すると同時にGtkEntryとGtkButtonも生成されます。
TopWindowはGTK のクラスとして定義するので、タイプシステムに登録しなければなりません。
それをするのがtype_register
メソッドです。
UI文字列でTopWindowの構造を定義します。
この構造はクラスにテンプレートとして設定するので、クラスメソッド(クラスの特異メソッド)のinitメソッドを使います。
class << self
によってクラスメソッドを定義します。
このメソッドは最初のインスタンス を生成するときに呼び出されます。
UI文字列は通常のUI文字列と大筋変わりませんが、templateタグの部分が違います。
最も外側のTopWindowの定義はtemplateタグを使い、TopWindowの親クラスGtkApplicationWindowをparentアトリビュート で指定します。
UI文字列からクラスの構造をテンプレートとして登録するにはset_template
メソッドを使います。
このテンプレートにセットするのは文字列ではなく、GBytesというオブジェクトです。
ui_stringからGBytesを生成するには、GLib::Bytes.new(ui_string)
とします。
set_template
の引数は:data
をキーとするハッシュの形で与えます。
なお、このプログラムでは文字列からGBytesを経由してテンプレートに代入しましたが、resourceという形を使うこともできます。
その方法はRuby-gnome のgtk3ディレクト リ以下のサンプルプログラム を参照してください。
bind_template_child
メソッドは、テンプレート内のウィジェット にアクセスするアクセサーを生成します。
例えば、UIファイル上でidがsearch_entry
であるGtkEntryは、bind_template_child("search_entry")
によって、TopWindowクラスのsearch_entry
メソッドで参照できるようになります。
コンストラク タ(インスタンス を生成するときにその初期化をするメソッド)は、initializeメソッドが行います。
これは一般にRuby でinitializeメソッドを用いるのと同じです。
superを使って親クラスのインスタンス 初期化を行いますが、super(application: app)
のように引数がハッシュになることに注意してください。
ボタンのシグナルとハンドラを結びつける方法は、既に説明したので省略します。
close-request
シグナルはウィンドウが閉じる際に発せられるシグナルです。
このプログラムでは、ウィンドウのクローズボタン(右上のxボタン)をクリックしたときに発生します。
このシグナルをそのままにしてウィンドウを閉じるとエラーが発生します。
私にはエラーの原因は分かっていませんが、次のようなことではないかと想像しています。
GtkApplicationWindowが閉じる。TopWindowはまだ残っている
GtkApplication終了時にTopWindowの終了処理をする
TopWindowは子オブジェクトのGtkApplicationを終了させようとするが、すでに無くなっている
エラーになる
確認はできていません。
いずれにしてもエラーになります。
それで、close-requiest
を捕まえ、ウィンドウを閉じるのをストップします。
ハンドラがtrueを返せば、以後のウィンドウを閉じるルーチンはスキップされるのを利用します。
同時に、ハンドラの中でアプリケーションを停止(quit)します。
これで、アプリケーションから順にオブジェクトの解放が行われていくのでエラーを回避できます。
さて、プログラムを動かしてみましょう。
$ ruby composite_window.rb
エントリに文字列を入力し「端末に出力」のボタンを押すと、端末にエントリの文字列が出力されます。
ウィンドウのクローズボタン(右上のxボタン)をクリックし、終了します。
ラッパークラス
コンポジット・オブジェクトの組み立ては、Ruby がGTK の仕組みに立ち入るので、やや複雑な処理になりました。
ラッパークラスは、単にRuby のクラスでウィジェット をまとめるだけなので、分かりやすいものです。
例として、wrapper.rb
を作りました。
このプログラムでは、ラッパークラスMainWindowを作り、そのインスタンス 変数にGtkApplicationWindow以下のオブジェクトを代入します。
このウィンドウに対する操作はMainWindowのインスタンス メソッドにまとめます。
プログラムはレポジトリの_example/ruby_gtk4/wrapper.rb
です。
require ' gtk4 '
class MainWindow
def initialize (app)
ui_string = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object id="window" class="GtkApplicationWindow">
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="spacing">5</property>
<child>
<object id="search_entry" class="GtkSearchEntry">
</object>
</child>
<child>
<object id="print_button" class="GtkButton">
<property name="label">端末に出力</property>
</object>
</child>
</object>
</child>
</object>
</interface>
EOS
builder = Gtk ::Builder .new(string : ui_string)
@window = builder[" window " ]
@window .set_application(app)
@print_button = builder[" print_button " ]
@search_entry = builder[" search_entry " ]
@print_button .signal_connect(" clicked " ) { do_print }
end
def window
@window
end
private
def do_print
print "#{ @search_entry .text}\n"
end
end
application = Gtk ::Application .new(" com.github.toshiocp.subclass " , :default_flags )
application.signal_connect " activate " do |app|
MainWindow .new(app).window.show
end
application.run
MainWindowクラスのインスタンス はwindowメソッドでGtkApplicationWindowインスタンス を返します。
外部からウィンドウに対するメソッドを呼び出したいときはwindowメソッドでインスタンス を呼び、さらにそのインスタンス のメソッドを呼び出します。
プログラムの説明はここまで読み進めた読者には必要ないでしょう。
大事なことは、ウィンドウごとにクラスでまとめれば、よりわかりやすいプログラムになるということです。
GObjectプロパティの定義の仕方
wordbookにGtkColumnViewを使うことを決めた時、そのリストにはGListStoreを使うのが良いと思いました。
GListStoreはGObjectまたはその子孫しかリスト・アイテムにすることができない
そのアイテム・オブジェクトは、ファクトリ(後述)を簡単にするためにプロパティを持っていることが望ましい
アイテムを表すクラスWBRecordはこの2つの条件を満たすように作られました。
class WBRecord < GLib ::Object
type_register
def initialize (*record)
super ()
if record.size == 1 && record[0 ].instance_of?(Array )
record = record[0 ]
end
unless record[0 ].instance_of?(String ) && record[1 ].instance_of?(String ) && record[2 ].instance_of?(String )
record = ["" , "" , "" ]
end
set_property(" en " , record[0 ])
set_property(" jp " , record[1 ])
set_property(" note " , record[2 ])
end
def to_a
[en, jp, note]
end
install_property(GLib ::Param ::String .new(" en " ,
" en " ,
" English " ,
"" ,
GLib ::Param ::READWRITE
)
)
install_property(GLib ::Param ::String .new(" jp " ,
" jp " ,
" Japanese " ,
"" ,
GLib ::Param ::READWRITE
)
)
install_property(GLib ::Param ::String .new(" note " ,
" note " ,
" Notes of the English word " ,
"" ,
GLib ::Param ::READWRITE
)
)
private
def en= (e); @en =e; end
def jp= (j); @jp =j; end
def note= (n); @note =n; end
def en ; @en ; end
def jp ; @jp ; end
def note ; @note ; end
end
WBRecordはGLib::Objectのサブクラスとし、type_register
でタイプシステムに登録します。
これでWBRecordはGTK の世界のオブジェクトとして扱うことができるようになります。
そして、GListStoreのリスト・アイテムにすることもできるようになります。
プロパティを定義するにはinstall_property
メソッドを使います。
その引数にはGParamSpecオブジェクトを与えます。
GParamSpecはGObjectとは異なる系列のオブジェクトです。
両者の間に親子関係はありません。
また、GParamSpecの子クラスとしてGParamSpecString(文字列型のパラメータ)、GParamSpecInt(整数型のパラメータ)、GParamSpecDouble(倍精度型のパラメータ)などがあります。
上のプログラムでは文字列型のパラメータを使っています。
GLib::Param::String.new(名前、ニックネーム、説明、デフォルト値、読み書きに関するフラグ)
のようにしてGPramSpecオブジェクトを生成します。
パラメータ型によって、引数が違うので、GObjectのAPIリファランス を参照してください。
例えば、文字列型のパラメータ生成は(Functionのセクションの)param_spec_stringのところを見ます。
パラメータをセットするにはset_property
、参照するにはget_property
メソッドが使えます。
これらのメソッドは外部からパブリック・メソッドとして使います。
プロパティはインスタンス ごとに保持される値で、プロパティと同名のインスタンス 変数に保持されます。
set_property
やget_property
はその内部で、アクセサーを使ってインスタンス 変数にアクセスします。
そのために、privateメソッドでアクセサーを定義しておきます。
上のプログラムではdef en=(e); @en=e; end
がインスタンス 変数への代入、def en; @en; end
がインスタンス 変数の参照になります。
プロパティを定義することにより、GTK レベルのオブジェクトからプロパティ値を代入または参照することができるようになります。
GtkColumnViewの使い方
GtkColumnViewはGTK 4で新たに導入されたクラスです。
GTK のなかでも複雑で理解しにくいクラスです。
GtkColumnViewは表で、データベースのように列がフィールドを表し、行がレコードを表します。
GtkColumnViewは表示のための仕組みで、データを保存する仕組みはリストです。
リストに追加する個々のデータをアイテムといいます。
リストにはいくつかの種類がありますし、自分で新しいリストの仕組みを作ることも可能です。
しかし、GListStoreというリストは「任意のGObject子孫クラス」のリストを作ることができる汎用のリストですので、それを利用すれば新たなリストを作成する必要はほとんどありません。
wordbookでは、前セクションで紹介したWBRecordをGListStoreのアイテムとしました。
さて、wordbookはGtkColumnViewの機能をフルに使っているのでUIファイルが大きく、説明するとなると相当の分量が必要です。
このセクションでは、2列のシンプルなGtkColumnViewの例を作り、それを説明しようと思います。
プログラムはレポジトリの_example/ruby_gtk4/column.rb
にあります。
GtkColumnView
プログラムを以下に示します。
長いですが、そのほとんどはUI文字列です。
require ' gtk4 '
ui_string = <<EOS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object id="window" class="GtkApplicationWindow">
<property name="title">GtkColumnView</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="GtkColumnView">
<property name="model">
<object class="GtkNoSelection">
<property name="model">
<object id="liststore" class="GListStore"></object>
</property>
</object>
</property>
<child>
<object class="GtkColumnViewColumn">
<property name="title">英語</property>
<property name="expand">false</property>
<property name="fixed-width">250</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
<object class="GtkLabel">
<property name="hexpand">TRUE</property>
<property name="xalign">0</property>
<binding name="label">
<lookup name="en" type = "EngJap">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</property>
</template>
</interface>
]]></property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkColumnViewColumn">
<property name="title">日本語</property>
<property name="expand">true</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
<object class="GtkLabel">
<property name="hexpand">TRUE</property>
<property name="xalign">0</property>
<binding name="label">
<lookup name="jp" type = "EngJap">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</property>
</template>
</interface>
]]></property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
EOS
class EngJap < GLib ::Object
type_register
attr_accessor :en , :jp
def initialize (en,jp)
super ()
set_property(" en " , en)
set_property(" jp " , jp)
end
install_property(GLib ::Param ::String .new(" en " , " en " , " English " , "" , GLib ::Param ::READWRITE ))
install_property(GLib ::Param ::String .new(" jp " , " jp " , " Japanese " , "" , GLib ::Param ::READWRITE ))
private :en , :en= , :jp , :jp=
end
application = Gtk ::Application .new(" com.github.toshiocp.subclass " , :default_flags )
application.signal_connect " activate " do |app|
builder = Gtk ::Builder .new(string : ui_string)
window = builder[" window " ]
window.set_application(app)
liststore = builder[" liststore " ]
liststore.append(EngJap .new(" hello " , " こんにちは " ))
liststore.append(EngJap .new(" good-bye " , " さようなら " ))
window.show
end
application.run
UI文字列内で、GtkApplicationWindow、GtkScrolledWindow、GtkColumnView、2つのGtkColumnViewColumnの順に親子関係になっている
GtkColumnViewが参照するリストは、GtkNoSelection、GListStoreとなっている。
直接GListStoreを参照するのでなく、GtkNoSelectionを介して参照する。
このような中間におくセレクション関係のリストには、GtkSingleSelection(一行だけ選択できる)やGtkMultiSelection(複数行選択できる)がある。
それぞれ、選択した行をダブルクリックするとシグナルを発することができる。
GtkColumnViewColumnは「GtkColumnViewの1列」を表すオブジェクト。各プロパティは
title ⇒ 行のヘッダーに現れるタイトル
expand ⇒ 幅を可能な限り広げる
fixed-width ⇒ expandプロパティがfalseのときの幅
factory ⇒ 列内の1つひとつのアイテムを生成するファクトリの指定
ファクトリには2種類あるが、ここではGtkBuilderListItemFactoryを使っている。
そのbytesプロパティに、アイテムを生成する方法を書いたUI文字列のGBytesオブジェクトを代入する。
その文字列はXML の<![CDATA[... ...]]
タグで表している。
内部の文字列は外部のUI文字列とは独立なので、インデントは左端から始まる
GBytesの内容は外側から、GtkListItem(テンプレート)、GtkLabelとなっている。
GtkListItemは各アイテムを表すオブジェクトであるが、これはウィジェット ではない。
表示されるウィジェット はその子オブジェクトのGtkLabelである。
GtkListItemがtemplateタグになっているのは、コンポジット・ウィジェット の定義と同様に、GtkListItemインスタンス を生成するためにクラスが保持するテンプレートだからである。
bindingタグはデータを結合するためのもので、結合元はタグで囲んだ内側(コンテンツ)、結合先はnameアトリビュート で指定する。
この場合は、GtkListItemの子オブジェクトのGtkLabelのlabelプロパティ
lookupタグはオブジェクトのプロパティを参照してその値を返す。
オブジェクトのタイプはtypeアトリビュート で、プロパティ名はnameアトリビュート で指定する。
このプログラムではEngJapクラス(プログラム内で作成したカスタム・クラス)のenプロパティを参照する
lookupタグのitemアトリビュート はGtkListItemのitemプロパティを指す。
GtkListItemのitemプロパティはGtkColumnViewが指定したリストの中でGtkListItem(の何行目か)に対応するアイテムを指している。
lookupタグで囲まれたGtkListItemはファクトリが生成したインスタンス 。
EngJapクラスをGObjectの子オブジェクトとして定義し、プロパティenとjpを定義する(WBRecordと同様)
アプリケーションのアクティベートハンドラ内で、UIファイルを組み立て、リストに2つのサンプルデータを追加し、ウィンドウを表示
プログラムは短いですがUI文字列が長いので、読むのが大変です。
UI文字列が実質的にプログラムの代わりをしていることがわかります。
wordbookでは以下の機能をGtkColumnViewまわりに追加しています。
GtkSingleSelectionを使い1行セレクトできる。
その行をダブルクリックすると行内容を編集するウィンドウが現れるように、シグナルハンドラを設定している
ソートをサポートしている。各行のヘッダをクリックすることによりその行を基準に(表全体を)昇順または降順に並べ替える
フィルターをサポートしており、エントリーで入力した文字列に(英単語のデータが)マッチする行のみを表示する
Wordbookアプリ
Wordbookのプログラムは_example/word_book_gtk4
ディレクト リに置いてあります。
そこには全部で5つのファイルがあります。
Wordbook メイン画面
Wordbook 編集画面
wordbookは単語帳アプリで、英語、日本語訳、備考を書きこむことができます。
データはCSV 形式で、「word.csv 」に保存されます。
また、アクセラ レータ(キー操作)をサポートしているので、マウスを使わずに素早くコマンドを入力できます。
Ctrl-n ⇒ 新規単語の入力画面を表示
Ctrl-w ⇒ アクティブ・ウィンドウを閉じる
Ctrl-q ⇒ アプリ終了
Ctrl-k ⇒ 編集画面で、編集をキャンセル(Ctrl-cがコピペと重なるのでCtrl-kとした)
Ctrl-d ⇒ 編集画面で、編集中のデータをリストから削除
Ctrl-s ⇒ 編集画面で、編集データをリストに保存(追加または変更)
編集時には、一般によく用いられる「Ctrl-a(全選択)」「Ctrl-c(コピペ)」なども可能
リスト内のデータを変更したいときは、その行をダブルクリックするとその行に関する編集画面が開きます。
変更をして「保存」ボタンをクリックまたはCtrl-sで修正内容がリストに反映されます。
まとめ
最近のいくつかの記事でRuby にグラフィック機能を拡張するgemについて書きましたが、総合的に見て、Ruby /Gtk4が最も良くできています。
それは、その背後にあるGTK 4が充実しているからです。
GTK 4はそのベースにGObject、GLib2などの膨大なライブラリを含んでおり、その習得には相当の時間を要します。
Ruby /GTK4のインターフェース部分は比較的容易に理解ができるでしょう。
ただ、問題はRuby /GTK4のドキュメントがない(少ない?)ことです。
それがあれば、もっと多くの人がRuby /GTK4を使うことでしょう。
今回の記事が多少なりともRuby /GTK4のユーザの役にたてば幸いです。
また、この記事とは別に本格的にRuby /GTK4のチュートリアル を書くのも有意義かな、と考えています。
実現には相当の時間が必要でしょうが・・・
長い記事をお読みいただきありがとうございました。
Ruby /GTK4でRuby のGUI アプリを作って楽しみましょう。
Happy progrmming!