概要
- C++ 用 GUI フレームワークとして定評のある Qt を Ruby でコントロール
- ちょっとした GUI プログラムや、プロトタイプの作成にはいいかもしれない
C++ は難しい・面倒とか、Ruby命とか、行末にセミコロンを書くのは美しくないと感じる人にお勧め - Qt
- C++ 用アプリケーションフレームワーク
- マルチプラットフォーム(Windows/Mac/Linux/組み込み)対応
- ライセンス: LGPL版・GPL版・商用版
- moc, シグナルスロット, 高直交性・洗練されたデザイン, 実用的なGUI部品がたくさん、 webkit などの豊富なモジュール、動的翻訳(国際化・多言語化)対応
- 多くの採用事例
GoogleEarth, PhotoshopElement, Skype, VCL などなど - C++ 以外の言語のバインディングがたくさんある。http://ja.wikipedia.org/wiki/Qt のバインダの部分参照
- 今回の資料作りのために、サンプルをいくつか作ってみたが、何故か非常に楽しかった!
- テーマソング (「ハニー」→「ルビー」と脳内補完して聴いてね)
インストール
Qt のインストール
QtRuby を使うだけなら、Qt のインストールは不要みたい。だけど、クラスリファレンス等のヘルプを参照するのに必要だよ。
http://qt.nokia.com/ から各プラットフォーム用のSDKをDLし、インストール
ボタンをポチポチ押すだけでOK
ただし、Mac の場合は xcode をあらかじめインストールしておかないと、C++ コンパイラ(gcc)が入ってないよ
Windows
- Ruby 1.9.2 では何故かダメみたい。 ActiveScriptRuby 1.8.7 ならおk
- 上記だけに気をつけて、gem を使えば、インストールは簡単だった(@おいらの開発マシン Win7 x64)
- 手順
- 念のために実行: gem update --system
- rakeをインストール: gem install rake
- QtRuby をインストール: gem install qtruby4
- 環境によって(?)は、gem でエラーが出なかったのに、ruby -e "require 'qt4'" を実行すると、qtruby4 が無いと言われる場合がある。
これは、qtruby4 をセットアップしたディレクトリが、ruby が参照するパスに含まれていないためである(インストーラのバグ?)。
このような場合は以下の対処を行うとよい。
※ [ruby]QtRubyをインストールしてみた を参考にさせていただきました(感謝)- gem which qtruby4 でインストールされたディレクトリを確認

- RUBYLIB 環境変数を作り、上記のディレクトリを見て以下のように <QtRuby位置>/bin;<QtRuby位置>/lib を設定する。

- gem which qtruby4 でインストールされたディレクトリを確認
Mac OS X
gem install qtruby4 を実行すると qtruby4 が無いと怒られた(しょぼーん)。
OSX LeopardにQt4-qtruby-2.0.3のインストール に、インストール方法が書いてあるが、cmake しなくてはならず、ちょと面倒?
Linux X11
Mac と同じ様に cmake すればいいと思われる(未確認)。
動作確認
コンソールで ruby -e "require 'qt4'" とやって、エラーが出なければ、たぶんインストール成功。
★ 演習問題 ★
- 各自の環境に QtRuby をセットアップしなさい。
デモによる紹介
Hello, World
1: require 'qt4'
2: app = Qt::Application.new(ARGV);
3: hello = Qt::PushButton.new("Hello, World.");
4: hello.show();
5: app.exec();

お約束の「Hello, World.」
1行目で QtRuby をインクルード?し、2行目でアプリケーションオブジェクトを生成。
3行目で「Hello, World.」のボタンオブジェクトを生成し、4行目で表示。
※ 3, 4行目はまとめて、Qt::PushButton.new("Hello, World.").show(); でも可
最後に、5行目で、イベントループを開始。(Qt はイベントドリブンなフレームワーク)
※ 行末のセミコロンは気にしないでください。
Qt のクラス名は上記のように「Qt::」を前置し、Qt の元のクラス名から先頭の「Q」を取り除く。
「QApplication」→「Qt::Application」
Qt には様々な Widget(GUI部品)が用意されている。詳しくは Qt のヘルプを見てね。
1: $KCODE = "u"
2: require 'qt4'
3: app = Qt::Application.new(ARGV);
4: hello = Qt::PushButton.new("こんにちは。");
5: hello.show();
6: app.exec();

日本語を記述する場合は、最初に「$KCODE = "u"」を書き、ソースの文字コードを UTF-8 にするとよい。
※ ほんとの Qt の作法では、ソースには tr("...") で囲った英文を書き、各言語ごとの翻訳ファイルを用意しておいて、
動的翻訳するんだけど、QtRuby でのやり方を調べていない。
国際化されたアプリを作るのでなければ、このように日本語を直接書いてもおk。
単に表示するだけなので、ボタンではなく、ラベル(Qt::Label)でも可。
Qt::Label の場合は、HTML を直接記述することが可能
1: require 'qt4'
2: Qt::Application.new(ARGV) do
3: Qt::Label.new("<h1>Hello, World.</h1>") do
4: resize(200, 50);
5: show();
6: end
7: exec();
8: end

上記コードは 'Rubyish' way によるコードに変えてみたもの。Ruby 使いの人には説明は不要だろう。
ちなみに、下記のように do-end は { } でも可。行末にセミコロンが無いと不安になるおいらにも安心。
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: Qt::Label.new("<h1>Hello, World.</h1>") {
4: resize(200, 50);
5: show();
6: }
7: exec();
8: }
ここまでは、Qt が用意しているテキストを表示できるウィジットを利用したが、
下記の様に、GUI部品の基底クラスである Qt::Widget の派生クラスを定義し、描画イベントハンドラを実装することも可能
1: require 'qt4'
2: class MyWidget < Qt::Widget
3: def paintEvent(event)
4: Qt::Painter.new(self) {
5: setFont(Qt::Font.new("Arial", 20));
6: drawText(100, 100, "hello, world")
7: self.end()
8: }
9: end
10: end
11: Qt::Application.new(ARGV) {
12: MyWidget.new() {
13: show();
14: }
15: exec();
16: }

2〜10行目が派生クラスの定義。描画ハンドラ「paintEvent(event)」を再実装している。
Qt::Painter は Widget に描画するためのクラス。
フォント・ペン・ブラシ等の設定が可能で、テキスト・矩形・楕円・曲線などを描画できる。
7行目で Qt::Painter.end() しているのは、オブジェクトを強制的に削除するためだと思われる。
イベントハンドラについては、後でもう少し詳しく説明するはず
★ 演習問題 ★
- 表示するメッセージを変更して、実行してみなさい。
- QLineEdit に "Hello, World" を表示してみなさい。
- Qt が用意している様々なウィジットを画面に表示してみなさい。
シグナル・スロット
「シグナル・スロット」は Qt の最も特徴的な機能のひとつである。
QObject 派生クラスは、状態が変化した時などに発行するシグナルと、
シグナルを受けるメソッドのスロットを定義することができる。
シグナル、スロットは下記の QObject のスタティック関数 connect() により結合することができる。
Qt::Object::connect(subject-obj, SIGNAL(signal-method),
observer-obj, SLOT(slot-method))
signal-method、slot-method は後の例のように文字列で指定する。
シグナルが発行されると、コネクトされたスロットがコールされる。
デザインパターンの Observer とだいたい同じ。
Qt のほとんどのクラスには有用なシグナルとスロットが定義されている。
GUI 部品を画面に配置し、それらのシグナル・スロットを適切に電子ブロックの様にコネクトすることが、
Qt プログラミングとなる。
1: require 'qt4'
2: app = Qt::Application.new(ARGV);
3: quit = Qt::PushButton.new("Quit");
4: quit.resize(200, 50);
5: Qt::Object.connect(quit, SIGNAL('clicked()'), app, SLOT('quit()'));
6: quit.show();
7: app.exec();

上記は、Quit ボタンを表示し、それを押すと終了するプログラム。
PushButton がクリックされた時に発行される clicked() シグナルを、アプリケーションオブジェクトの quit() スロットにコネクトしている。
シグナル・スロットを用いることで、コンパイル時依存性が無くなるので、GUIウィジットなどの部品を作りやすくなる。
コールバック関数や、通知メッセージに比べてとてもスマートかつ安全である。
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: slider = Qt::Slider.new(Qt::Horizontal) {
4: resize(100, 30);
5: show();
6: }
7: spinBox = Qt::SpinBox.new() {
8: show();
9: }
10: Qt::Object.connect(slider, SIGNAL('valueChanged(int)'), spinBox, SLOT('setValue(int)'));
11: Qt::Object.connect(spinBox, SIGNAL('valueChanged(int)'), slider, SLOT('setValue(int)'));
12: exec();
13: }

上記は、スライダーとスピンボックスを表示し、それぞれの値を同期させるプログラム。
値が変化した時に発行される valueChanged(int) シグナルを、相手の setValue(int) スロットにコネクトしている。
このように、引数は C++ の型をそのまま記述する。
スロット定義:
「slots」に続けて「メソッド名(引数型列)」を記述することで、自分で定義しているクラスの通常メソッドをスロットにすることができる。
引数型は C++ の型を指定する。
1: require 'qt4'
2: class MyWidget < Qt::Widget
3: def initialize(parent = nil)
4: super;
5: @val = 0;
6: end
7: slots "setValue(int)";
8: def setValue(v)
9: @val = v;
10: self.update();
11: end
12: def paintEvent(event)
13: painter = Qt::Painter.new(self);
14: painter.drawEllipse(10, 10, @val * 2, @val);
15: painter.end();
16: end
17: end
18: Qt::Application.new(ARGV) {
19: w = MyWidget.new() { resize(400, 200); show(); }
20: s = Qt::Slider.new(Qt::Horizontal) { resize(400, 50); show(); }
21: Qt::Object.connect(s, SIGNAL("valueChanged(int)"), w, SLOT("setValue(int)"));
22: exec();
23: }

上記は、スライダーを動かすと、楕円の大きさが変化するプログラム。
21行目で、スライダーの valueChanged(int) シグナルを、MyWidget の setValue(int) にコネクトしている。
MyWidget は QWidget 派生クラスで、paintEvent(event) を再実装することで、指定されたサイズの楕円を描画する。
シグナル定義:
「signals」に続けて「シグナル名(引数型リスト)」を記述することで、シグナルを定義できる
実装を行う必要はない
1: require 'qt4'
2: class MyWidget < Qt::Widget
3: def initialize(parent = nil)
4: super;
5: @val = 10;
6: @timer = Qt::Timer.new();
7: connect(@timer, SIGNAL("timeout()"), self, SLOT("onTimer()"));
8: @timer.start(500); # 500msec
9: end
10: slots "onTimer()"
11: def onTimer
12: @val -= 1;
13: update();
14: emit triggered() if @val == 0
15: end
16: signals "triggered()";
17: def paintEvent(event)
18: painter = Qt::Painter.new(self);
19: setFont(Qt::Font.new("Arial", 20));
20: painter.drawText(100, 100, @val.to_s);
21: painter.end();
22: end
23: end
24: Qt::Application.new(ARGV) {
25: w = MyWidget.new() { resize(400, 200); show(); }
26: Qt::Object.connect(w, SIGNAL("triggered()"), self, SLOT("quit()"));
27: exec();
28: }

上記は10秒経過すると、終了するプログラム。
MyWidget は QWidget 派生クラスで、タイマーにより10秒経過すると、triggered() シグナルを発行する。
14行目の emit シグナル名 でシグナルの発行(エミット)を行う。
26行目で、QApplication オブジェクトの quit() スロットにコネクトしている。
画面には残り秒数が表示される。
補足:
- シグナル・スロットのコネクトは、1:多、多:1が可能
- コネクトされているオブジェクトが削除されると、コネクションは自動的に削除される
- シグナルをシグナルにコネクトすることが出来る
- connect() の第5引数で、コネクションタイプ(Qt::QueuedConnection 等がある)を指定できる
- disconnect() でコネクションを解除できる
★ 演習問題 ★
- タイマーで自動終了するプログラムのウィジットの基底クラスをスライダーに変更し、 残り秒数を数値で表示するのではなく、スライダーのノブの位置で表現するようにしてみなさい。
レイアウト
setGeometry
下記は、ウィジットにラベル・ボタンを子ウィジットとして配置した例
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: w = Qt::Widget.new();
4: Qt::Label.new("AAA", w) { setGeometry(10, 10, 80, 20); }
5: Qt::Label.new("BBB", w) { setGeometry(10, 40, 80, 20); }
6: Qt::PushButton.new("PushMe", w) { setGeometry(80, 10, 100, 20); }
7: w.show();
8: exec();
9: }

ウィジット生成時に、第2引数で親ウィジットを指定可能。
setGeometry(x, y, wd, ht) で、子ウィジット位置を、親ウィジット座標系で指定できる
このような指定方法は変更容易性に劣り、好ましくないのは言うまでもない
レイアウトクラス
Qt にはウィジットを簡単に配置するためのクラス:QLayout が用意されている。

- ウィジットは setLayout により、QLayout 派生クラスを指定することができる。
- QLayout 派生クラスは addWidget により、ウィジットを追加することができる。
- QLayout 派生クラスは addLayout により、レイアウトを追加することができる。
- addStretch() により、伸縮する部分を指定することが出来る。
QVBoxLayout
Qt::VBoxLayout を使うことで、ウィジットを縦方向に並べることが出来る。
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: Qt::Widget.new() {
4: setLayout( Qt::VBoxLayout.new() {
5: addWidget(Qt::Label.new("AAA"));
6: addWidget(Qt::PushButton.new("PushMe"));
7: #addWidget(Qt::TextEdit.new());
8: #addStretch();
9: addWidget(Qt::Label.new("BBB"));
10: });
11: show();
12: }
13: exec();
14: }

上記は Qt::VBoxLayout を使って、ラベル・プッシュボタンを縦に並べたもの。

ウィンドウを縦方向に拡大すると、上図のように、間隔が均等に拡大される。

先のリストの7行目のコメントを外すと、上図のようにテキストエディットが表示される。
並んだウィジットの中に拡大縮小可能なものがあれば、ウィンドウを拡大縮小したときは、
それが拡大縮小され、ウィジット間隔は変化しない。

上図は先のリストの8行目のコメントを外したもの。
addStretch() により、拡大縮小する部分を指定することが出来る。
QVBoxLayout・QHBoxLayout による階層化
QVBoxLayout・QHBoxLayout を組み合わせることで、階層的な画面を作成することが出来る。
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: Qt::Widget.new() {
4: setLayout Qt::VBoxLayout.new() {
5: addLayout Qt::HBoxLayout.new() {
6: addWidget Qt::Label.new("hoge:");
7: addWidget Qt::LineEdit.new();
8: }
9: addLayout Qt::HBoxLayout.new() {
10: addWidget Qt::Label.new("foobar:");
11: addWidget Qt::LineEdit.new();
12: }
13: addStretch();
14: addLayout Qt::HBoxLayout.new() {
15: addStretch();
16: addWidget Qt::PushButton.new("PushMe");
17: addStretch();
18: }
19: }
20: show();
21: }
22: exec();
23: }

上記は、QVBoxLayout に QHBoxLayout を追加したもの。
C++ のコードに比べて非常に分かりやすい気がする。
★ 演習問題 ★
- QHBoxLayout, QVBoxLayout を使って、下記のようなレイアウトのウィジットを記述しなさい。

QFormLayout
前節の画面はカラムが揃ってなくて、見苦しい。
こんなときは QFormLayout を使うといいぞ。
1: require 'qt4'
2: Qt::Application.new(ARGV) {
3: Qt::Widget.new() {
4: setLayout Qt::VBoxLayout.new() {
5: addLayout Qt::FormLayout.new() {
6: setLabelAlignment Qt::AlignRight;
7: addRow "hoge:", Qt::LineEdit.new();
8: addRow "foobarbar:", Qt::LineEdit.new();
9: }
10: addStretch();
11: addLayout Qt::HBoxLayout.new() {
12: addStretch();
13: addWidget Qt::PushButton.new("PushMe");
14: addStretch();
15: }
16: }
17: show();
18: }
19: exec();
20: }

カラム位置が揃って、見苦しさが無くなった。
まとめ
- Qt, QtRuby について簡単に紹介し、インストール方法、HelloWorld、シグナル・スロット、レイアウトについて デモを交えて簡単に説明した。
- Qt についての知識が無いと、実際に QtRuby プログラムを作成するのは、なかなか大変だとは思うが、 便利そうで生産性が高そうなことは分かっていただけたものと思う。
- Qt の勉強会も福岡やっているので、興味ある人は是非参加してね。
サンプルプログラム
参考サイト