単項マイナスと構文解析
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。
プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。
レポジトリをダウンロードし、ディレクトリ_example/word_book_gtk4
をカレントディレクトリにして、ruby wordbook.rb
を実行してください。
【追記 2022/12/9】Rubygemsに置いてある「gtk4」gemのバージョンが4.0.4に上がっていました。 これを使うと、コンポジット・ウィジェットのところで出たエラーが無くなりました。 バグだったようです。 詳しくはこの記事の GTKのコンポジット・オブジェクトのところをご覧ください。
wordbook.rbのプログラムを書き直しました。 このレポジトリの古い版をクローンした方は「git pull」して新しい版に更新してください。 EditWindowまわりのバグ修正が更新内容です。 【追記は以上】
今回の記事は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はアプリケーションのクラスです。 Rubyのクラスと同様に、このクラスのインスタンスを作り、それを走らせることによってアプリケーションが動き出します。
require 'gtk4'
application = Gtk::Application.new("com.github.toshiocp.wordbook", :default_flags)
application.run
: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シグナルに特定のプログラム(ハンドラという)を結びつけておけば、シグナル発生時にそのプログラムが実行されます。 シグナルは、ユーザが作成したクラスに定義することもできますが、ここではそのトピックはとりあげません。
シグナルの使い方は次のような流れになります。
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の関数の形で書かれています。
response_id
は、例えば、ダイアログのOKボタンが押されたとか、Cancelボタンが押されたとかの情報。
どのボタンにどのIDが割り振られるかはダイアログの定義時にプログラマーが決める例えばダイアログのインスタンスを変数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クラスを使うのが良いです。 このクラスは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」で使用したウィジェットは
です。 GTK 4で用意されているウィジェットのサンプルはGTK 4のAPIリファランスのウィジェット・ギャラリーを参照してください。
ウィジェットを配置するのをプログラムでやるのは非常に手間がかかるので、代わりに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>
Gtk::Builder
クラスのオブジェクトで利用する。
もしそこで使う予定がなければidを省略して良いこの例のウィンドウとボタンのように、ウィジェットには親子関係が発生します。 外側のウィジェットが親、その中に配置されるウィジェットが子です。 この親子関係がつながると孫以下のウィジェットも出てきます。 このウィジェットの親子関係と、クラスの親子関係は全く別のものなので、混同しないようにしてください。
UIファイルではpropertyタグが頻繁に現れます。 これはオブジェクトのプロパティを設定するためのものです。 オブジェクトのプロパティはGTK 4のAPIリファランスに書かれています。 例えばGtkWindowのプロパティは、リファランスから
リファランス⇒class⇒Window⇒property
とたどります。 ここには多くのプロパティがありますが、例をあげると
です。 title、default-widthなどの文字列はプロパティ名です。 プロパティはRubyのハッシュのように、キーと値の形式になっていて、キーが「プロパティ名」で値が「プロパティの値」です。 プロパティの値には文字列、数値、真偽値などがありますが、UIファイル内では(テキストファイルなので当然ながら)すべて文字列で表現します。
これらの説明は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リファランスでは、次の場所を調べてください。
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シグナルにハンドラを結びつけておきます。
では、どのようなときにアクションがアクティブになるのでしょうか?
一般にアクションに結びついているウィジェットは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
action-name
プロパティの値を”app.hello”に設定する。
この値はスコープ付きのアクション名。これで、ボタンがクリックされたときに”app.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の子孫ウィジェットのGtkButtonbox > button
⇒ GtkBoxの子ウィジェットのGtkButton。直下の子のみで孫以下は入らないこの他にもありますが、詳細はAPIリファランスを参照してください。
CSSプロパティはセレクタに続いて波括弧{}
で囲んで指定します。
例えばボタンの色(文字色)を赤にしたいときは
button {color; red;}
とします。 このあたりはウェブのCSSと同じです。 詳細はAPIリファランスを参照してください。
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"]
ユーザがエントリーに入力した文字列を入手するにはシグナルを使います。
シグナルのハンドラの中で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;}
その他のフォント情報の設定についてはAPIリファランスを参照してください。
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))
set_template(:data => 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
をキーとするハッシュの形で与えます。
【追記 2022/12/9】GBytesの代わりに単に文字列を与えても良いことがわかりました。 プログラムもそのように修正しています。
なお、このプログラムでは文字列から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ボタン)をクリックしたときに発生します。
このシグナルをそのままにしてウィンドウを閉じるとエラーが発生します。
私にはエラーの原因は分かっていませんが、次のようなことではないかと想像しています。
確認はできていません。
いずれにしてもエラーになります。
それで、close-requiest
を捕まえ、ウィンドウを閉じるのをストップします。
ハンドラがtrueを返せば、以後のウィンドウを閉じるルーチンはスキップされるのを利用します。
同時に、ハンドラの中でアプリケーションを停止(quit)します。
これで、アプリケーションから順にオブジェクトの解放が行われていくのでエラーを回避できます。
【追記 2022/12/9】「gtk4」gemのバージョン4.0.4ではエラーが出なくなりました。
バグだったのかもしれません。
プログラムのclose-requiest
のシグナル処理はコメントアウトしました。
さて、プログラムを動かしてみましょう。
$ 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メソッドでインスタンスを呼び、さらにそのインスタンスのメソッドを呼び出します。
プログラムの説明はここまで読み進めた読者には必要ないでしょう。 大事なことは、ウィンドウごとにクラスでまとめれば、よりわかりやすいプログラムになるということです。
wordbookにGtkColumnViewを使うことを決めた時、そのリストにはGListStoreを使うのが良いと思いました。
アイテムを表すクラス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は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
<![CDATA[... ...]]
タグで表している。
内部の文字列は外部のUI文字列とは独立なので、インデントは左端から始まるプログラムは短いですがUI文字列が長いので、読むのが大変です。 UI文字列が実質的にプログラムの代わりをしていることがわかります。
wordbookでは以下の機能をGtkColumnViewまわりに追加しています。
Wordbookのプログラムは_example/word_book_gtk4
ディレクトリに置いてあります。
そこには全部で5つのファイルがあります。
wordbookは単語帳アプリで、英語、日本語訳、備考を書きこむことができます。 データはCSV形式で、「word.csv」に保存されます。 また、アクセラレータ(キー操作)をサポートしているので、マウスを使わずに素早くコマンドを入力できます。
リスト内のデータを変更したいときは、その行をダブルクリックするとその行に関する編集画面が開きます。 変更をして「保存」ボタンをクリックまたは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!
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ
StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例
lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて
文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。
Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。
Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。
今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。
Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...
今回はGTK 3とGTK 4をRubyで使うライブラリについて書きたいと思います。
今回もRubyとGUIのトピックです。 Glimmerを取り上げます。
Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。
Rails7におけるシステムテストについて書きます。
前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。
今回はRailsの慣例に沿った形でWordbookを作り直します。
今回はWordBookの検索と削除についてです。
今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。
一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。
Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...
Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。
minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...
今回もminitestの話です。 mockとstubに焦点をあて説明します。
アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。
今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。
今回は特異メソッド、特異クラス定義、名前空間、モジュール関数について説明します。
2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。
if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。
Procオブジェクトを生成するメソッドlambdaについて説明します。
今回はブロックを一般化したオブジェクトProcを説明します。
ブロック付きメソッドの作り方を説明します。
モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。
クラスの親子関係
Rubyの演算子とその再定義について書きます。
今回からクラスとインスタンスを定義、生成する方法を説明します
Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。
ここでは私が便利だと思ったメソッドを紹介します。
実数
今回はシンボルとハッシュについて説明します。
文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。
配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。
今回の目標はインスタンスです。 インスタンスを説明するために、ローカル変数と文字列オブジェクトを事前に扱います。
今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。
ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。
ここではRubyの最も基本的なオブジェクトである整数について説明します。
「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。