おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

「GTK 4 チュートリアル」のアップデート(完了)

ようやく「GTK 4 チュートリアル」のマイナー・アップデートが終わりました。

セクション28のGtkExpressionの全面書き直しに時間がかかりました。 「おもこん」の記録を見ると、37日もかかっていました。 アップデートされたチュートリアルはバージョン0.85としました。

何が変わったのか?

まず、GTK 4のアプリケーション・フラグの定数が「G_APPLICATION_FLAGS_NONE」から「G_APPLICATION_DEFAULT_FLAGS」に変わりました。 これはすべてのアプリケーションに使われているので、すべてのプログラムを書き換えることになりました。

プログラムの見直しをすべてについて行いました。

  • 誤りの訂正もかなり多かった
  • より分かりやすく、短くした

本文の記述において、おかしな文章を直した(おかしな英文を直した)。

本文の記述において、

  • より正確な記述に努めた
  • 無駄な記述を削除した

最後に一部のセクションは全面的に書き換えました。

今後はどうなるのか?

今回のバージョンアップでわかったのは、とても時間がかかるということでした。 それは分量が多いということが主な原因だと思います。

それで、内容追加を考えていたのですが、慎重に行うことにしました。 今考えているのはドラッグ・アンド・ドロップですが、少し時間をおいてからにします。 また、それによって分量が大きく増えるようならやめるかもしれません。

現状のメンテナンスは続けていく予定です。

現在のUBUNTUGTK 4バージョンは4.8ですが、開発サイトの最新版は4.10です。 バージョン4.10ではかなり変更があり、depricatedになる関数も多いです。 それにチュートリアルを合わせるのは、今後の仕事(大仕事)です。 現状ではアップデートは時期尚早であり、タイミングとしてはUBUNTUが4.10をサポートするようになってからになります。

休養が大事

しばらくは休養をとりたい・・・

休むことは次の仕事の質的向上にとっても大事です。

ブログの内容も少し手軽なものにするかもしれません。

「GTK 4 チュートリアル」のアップデート(続き)

GTK 4 チュートリアルのセクション22,24,25をアップデートしました。

セクション25のTiny Turtle Interpreter も若干のアップデートがあります。

  • tl (tern left) 左への方向変更の命令が加わった
  • rp (repeat) 回数を指定したループ命令が加わった
  • リサイズ時に図形を表示し続けるようになった。

例えば次のプログラムは正方形(一辺が100px)を描きます。

pd              # ペン・ダウン。これにより、前進時に線を描く
repeat (4) {    # 4回繰り返す
  fd 100        # 100px 前進する
  tl 90         # 左に90度向きを変える
}

昔流行ったLOGOというプログラムの簡易版です。 しかし、これを使うとフラクタル図形が描けて、ちょっと面白いです。

雪の結晶を模したフラクタル図形

この図形はコッホ曲線を6つ繋げたものです。

画面はturtleのウィンドウで、左半分がテキストエディタ、右半分が描画エリアになっています。 プログラムは保存/読込が可能です。

「GTK 4 チュートリアル」の一部アップデート(続き)

新年あけましておめでとうございます。 今年もよろしくお願いいたします。

さて、GTK 4 チュートリアルのセクション17から21までをアップデートしました。 時間がかかってしまいましたが、それはエディタのプログラム(tfe6とtfe7)を見直すのに時間がかかったからです。 しかし、そのリファクタリングによって、プログラムの質は向上したと思います。

tfeはシンプルなエディタですが、「一応」使えるエディタです。 GTK 4のチュートリアル用で、実用を目指したものではないので最小限の機能ですが・・・

しばらくは、これを使ってみてバグだしをしようと思っています。 (ないことを願っていますが)

残りのセクション22から29までは、2回に分けてアップデートする予定です。 最も時間がかかりそうなのは、タートル・グラフィックスのプログラムで、それを思うと気が重い・・・

「GTK 4 チュートリアル」を一部アップデート

GTK 4チュートリアル」のセクション1から16をアップデートしました。 なお、このチュートリアルは英語で書かれています。

このチュートリアルは?

このチュートリアルLinux上でC言語GTK 4ライブラリを使うためのチュートリアルです。

おかしな英語を多少まともにした

このチュートリアルを書いたのが2年以上前でした。 今読み返してみると、おかしな英語が多々有り、力不足を痛感しています。 逆にいえば、2年間で多少英語のレベルアップができていたのかも・・・?

現在の英語力もまだまだに違いありませんが、それでもあえて英語で書くことで自身の勉強にはなるはず・・・と思っています。 いろいろご指摘いただければありがたいです。

プログラムの修正

サンプルプログラムのバグ取りをしました。 大きなバグではないのですが、小さなメモリーリークがあったり、動作の正確さに欠ける部分がありました。 また、無駄な部分を削除しました。

内容の追加

説明を少し詳しくしました。 例えばフローティング・リファレンスについてはサブセクションをひとつ作りました。

今後の予定

残りのセクションの修正を優先して行います。 その後は、まだ構想段階ですが、ドラッグアンドドロップを加えようかと思っています。 勉強もしなければ書けないので、時間はかかると思います。

ご興味のあるかたはぜひレポジトリをご覧ください。 GitHub PagesのHTML版のサイトもあります。

GObject tutorial をアップデート

長らく放置していた「GObject tutorial」の全体を見直し、アップデートしました。 バージョンを0.5から0.6にしました。 レポジトリはこちらです。

また、GitHub Pagesを使ったウェブサイトはこちらです。

GObject tutorial とは

GObject tutorialは、その名の通りGObjectを学ぶ人のためのチュートリアルで、ビギナー向けです。

GObjectと聞いてピンと来ない人もいるかもしれません。 GObject自体はあまり有名ではないからです。 しかし、GTK 4ならばかなりの人が知っているのではないでしょうか。 Linuxのグラフィックシステムの最有力ライブラリだからです。 GObjectはGTK 4のベースになっているオブジェクト指向のCライブラリです。 解説つきのAPIリファランスがあるのですが、なかなか難しく、苦しんでいる人が多いです。 GObject tutorialはそれを補完するものです。

今回のバージョンアップは?

今回のバージョンアップはマイナーアップデートで、大きな追加はありません。 現行の内容をより良いものに書き直したものです。 特に後半のクラスの継承の部分を書き直しました。 例として挿入されているプログラムもより洗練されたものになっています。

どのような人が読んだらよいか?

GTK 4を学びたい人が、そのベースになる考え方を習得するのに良いチュートリアルです。 興味のある人は「GTK 4 tutorial」と併せて読まれることをお勧めします。

最後になりましたが、英語で書かれています。

徒然Ruby(37)Ruby/GTK4

【追記 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オブジェクト指向

GTKC言語で書かれたライブラリですが、オブジェクト指向で書かれており、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とし、赤、黄、緑に変えるときのパラメータをredyellowgreenとします。 そのとき、これらを組み合わせたものをdetailed name(詳しい名前)といい、colored-button::redcolored-button::yellowcolored-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にあります。

CSSの使い方

GTK 4ではウィジェットの「見た目」をCSSCascading 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 プロパティ

CSSプロパティはセレクタに続いて波括弧{}で囲んで指定します。 例えばボタンの色(文字色)を赤にしたいときは

button {color; red;}

とします。 このあたりはウェブのCSSと同じです。 詳細はAPIリファランスを参照してください。

CSSの適用方法

CSSは個々のウィジェットに適用する方法とアプリケーション画面全体に適用する方法がありますが、ここでは後者を説明します。

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>

この例ではスクロールウィンドウもテキストビューもできる範囲で幅と高さをいっぱいにとるようにhexpandvexpandを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ボタン)をクリックし、終了します。

ラッパークラス

コンポジット・オブジェクトの組み立ては、RubyGTKの仕組みに立ち入るので、やや複雑な処理になりました。 ラッパークラスは、単に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_propertyget_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でRubyGUIアプリを作って楽しみましょう。

Happy progrmming!

「はじめてのJekyll + GitHub Pages」マイナーアップデート

「はじめてのJekyll + GitHub Pages」をマイナーアップデートしました。

アップデートの内容は

  • 誤りの訂正
  • site.baseurlについての記述の追加
  • その他

です。

このサイトのGitHubレポジトリはこちらです。

あらためて全部読み返しました。 結構時間がかかりました。 訂正箇所を探しながら読んだということもありますが、書いてからしばらくたって内容を忘れていたのが大きかったです。

初心者むけと銘打っていながら、ところどころで深堀していて、やっていることが矛盾している部分もありました。

続編も書いてみたいと思っています。 今は「徒然Ruby」に時間をかけているので、すぐには無理ですが・・・

それから、Jekyllについては「おもこん」でも書いていますが、「はじめてのJekyll + GitHub Pages」の方がよりメンテナンスができています。 「おもこん」よりもそちらを見てください。