技術文章Qt

 

QtRuby - キュートに Ruby でGUI -
Nobuhide Tsuda
24-Nov-2011

概要

インストール

Qt のインストール

QtRuby を使うだけなら、Qt のインストールは不要みたい。だけど、クラスリファレンス等のヘルプを参照するのに必要だよ。

http://qt.nokia.com/ から各プラットフォーム用のSDKをDLし、インストール
ボタンをポチポチ押すだけでOK
ただし、Mac の場合は xcode をあらかじめインストールしておかないと、C++ コンパイラ(gcc)が入ってないよ

Windows

Mac OS X

gem install qtruby4 を実行すると qtruby4 が無いと怒られた(しょぼーん)。

OSX LeopardにQt4-qtruby-2.0.3のインストール に、インストール方法が書いてあるが、cmake しなくてはならず、ちょと面倒?

Linux X11

Mac と同じ様に cmake すればいいと思われる(未確認)。

動作確認

コンソールで ruby -e "require 'qt4'" とやって、エラーが出なければ、たぶんインストール成功。

演習問題

  1. 各自の環境に 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() しているのは、オブジェクトを強制的に削除するためだと思われる。

イベントハンドラについては、後でもう少し詳しく説明するはず

演習問題

  1. 表示するメッセージを変更して、実行してみなさい。
  2. QLineEdit に "Hello, World" を表示してみなさい。
  3. 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. タイマーで自動終了するプログラムのウィジットの基底クラスをスライダーに変更し、 残り秒数を数値で表示するのではなく、スライダーのノブの位置で表現するようにしてみなさい。

レイアウト

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 が用意されている。

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++ のコードに比べて非常に分かりやすい気がする。

演習問題

  1. 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: }

カラム位置が揃って、見苦しさが無くなった。

まとめ

サンプルプログラム

参考サイト

  1. Development/Languages/Ruby