【追記 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の完成度
- GTKのオブジェクト指向
- GtkApplicationの書き方
- シグナル
- GtkApplicationWindowの書き方
- ビルダーの使い方
- アクションとアクセラレータ
- CSSの使い方
- エントリー
- テキストビュー
- GTKのコンポジット・オブジェクト
- ラッパークラス
- GObjectプロパティの定義の仕方
- GtkColumnViewの使い方
- Wordbookアプリ
- まとめ
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のオブジェクト指向
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ハンドラの中で行う。
- スコープがウィンドウであるアクションを追加するには、
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
にあります。
CSSの使い方
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の子孫ウィジェットのGtkButtonbox > button
⇒ GtkBoxの子ウィジェットのGtkButton。直下の子のみで孫以下は入らない
この他にもありますが、詳細はAPIリファランスを参照してください。
CSS プロパティ
CSSプロパティはセレクタに続いて波括弧{}
で囲んで指定します。
例えばボタンの色(文字色)を赤にしたいときは
button {color; red;}
とします。 このあたりはウェブのCSSと同じです。 詳細はAPIリファランスを参照してください。
CSSの適用方法
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", # name "en", # nick "English", # blurb "", # default value GLib::Param::READWRITE # flag ) ) install_property(GLib::Param::String.new("jp", # name "jp", # nick "Japanese", # blurb "", # default value GLib::Param::READWRITE # flag ) ) install_property(GLib::Param::String.new("note", # name "note", # nick "Notes of the English word", # blurb "", # default value GLib::Param::READWRITE # flag ) ) 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
にあります。
プログラムを以下に示します。 長いですが、そのほとんどは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は単語帳アプリで、英語、日本語訳、備考を書きこむことができます。 データは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!