Ruby/Qte普及委員会 最初のプログラム

XREAAD
Create  Edit  Diff  FrontPage  Index  Search  Changes  History  RSS  Login

Ruby/Qteプログラミング入門

ここは一つ、ペンに沿ってペンギンが動くプログラムをいきなり作ってみましょう。

テストを繰り返しながら書いていく

Qtのドキュメントやソースコードをじっくり読んでから設計、そしてコーディングに取りかかる、というのが王道ですが、せっかく手軽に実行できるRuby環境があるのですから、ちょっとずつ動作を試しながら進めていくのがよいと思います。もちろん、Qtに関わるところに限らず、自分が書いたコードの各部分が正しく動くのを確認してから先に進められる、というメリットもあります。

testunit を使って、unit test を繰り返す習慣をつけるのもよいと思います。いちおう パッケージ化 してみました (test/unit/ui/qte/testrunner.rb も同梱してあります)。ただ、私の環境ではCUIでもメモリ不足がよく起こります。使い方自体は簡単ですが、今回はとりあえず説明を省きます。

設計

これから作ろうとしているプログラムは、次のようなものです。

  • ウィンドウ内にペンギンの画像が表示される。
  • ペンの動きに合わせてペンギンの画像がウィンドウ内を動きまわる。

ペンのドラッグに合わせて動くのはちょっと難しそうなので、とりあえずペンでタップした位置にペンギンの画像が移動することでよしとしておきます。

必要なのは、ウィンドウの表示、ペンギンの表示、それからペンの動きを感知してペンギンの表示位置を変える仕組みです。

最初のコード

Qtのドキュメントを見ると、画面上を図形やテキストが移動するような表示には QCanvasView? というウィジェットが使えそうなことがわかったので、サンプルやチュートリアルのコードを見よう見まねで、次のようなコードを書いてみます (001_penguin.rb)。

require 'qte'
require 'qpe'
require 'qtecanvas'

module PenguinField

  include Qte
  include Qpe
  include Qtecanvas

  PIX_PENGUIN = '/home/QtPalmtop/pics/tux-logo.png'

  class Penguin
  end

  class Field <QCanvas
  end

  class FieldView <QCanvasView
  end

  class MainWindow <QMainWindow
    def initialize(parent = nil, name = '')
      super(parent, name)
      setCaption('PenguinField')
      @field = Field.new(640, 480)
      @fieldView = FieldView.new(@field, self)
      setCentralWidget(@fieldView)
    end
  end
end


if $0 == __FILE__ then
  a = Qpe::QPEApplication.new([$0] + ARGV)
  mw = PenguinField::MainWindow.new
  a.showMainWidget(mw)
  a.exec
end

さっそく動かしてみましょう。

ruby 001_penguin.rb

何もない真っ白なウィンドウが表示されて、タイトルバーに PenguinField? と表示されれば成功です。

ペンをタップした位置を感知させる

次に、ウィンドウの表示領域 (FieldViewオブジェクト) のどこかをタップすると、その位置をアプリケーションが知ることができるような仕組みを考えます。

シグナル・スロットとイベントハンドラ

ウィジェット (=GUIの部品) はあらかじめ、ユーザーの入力に対して反応するように作られています。しかし、どのような反応をするかは自分でコードを書かなければなりません。

その手法は大きくわけて二つあります。

  • コールバック関数 (イベントハンドラ) を実装する。
  • ウィジェットが状態の変更を他のオブジェクトに通知する仕組みを使い、ウィジェットの外部で、状態の変更通知を受けとって対応する処理を行うコードを書く。

後者のほうは、RubyではObservableモジュールをクラスにインクルードするという手法を用いることができますが、Qtでは「シグナル・スロット」機構を使います。

たとえば、QPushButtonクラスのオブジェクトは、クリックされると clicked() というシグナルを送出します。一方、これを受けとって何らかの処理を行うオブジェクトのほうには、「スロット」というメソッドを用意します。そして、connect関数で両者をつなげます。

ここでは、ペンのタップに対応する適当なシグナルがないため、イベントハンドラ contentsMousePressed() を上書き実装しますが、そのなかではタップした位置の記憶とシグナルの送出という最低限のことだけをするようにしてみました。

(002_penguin.rbの一部)

  class FieldView <QRCanvasView
    def initialize(field, parent = nil, name = '')
      super(field, parent, name)
      @penPos = QPoint.new(0, 0)
      @tapped = RSignal.new
      catchEvent
    end

    attr_reader :tapped
    attr_reader :penPos

    def contentsMousePressEvent(ev)
      @penPos.setX(contentsX + ev.x)
      @penPos.setY(contentsY + ev.y)
      @tapped.send
    end
  end

ここで、少し補足説明が必要かもしれません。まず、QCanvasView だったのが QRCanvasView? に変わっています。また、initialize メソッドの末尾に catchEvent というのが付け加わっています。これは、C++のバーチャル関数のオーバーロードを特別にサポートするために用意されている仕組みです。

また、Rubyのオブジェクトにシグナルを追加するには、RSignalクラスのインスタンスを使います。

テストする

次のステップに進む前に、上記のコードが正しく動作しているかどうか試してみます。そのために、test_penguin.rb というファイルを作り、次のように書いてください。

require '002_penguin'

class Receiver <Qte::QObject
  def initialize(fieldView)
    @fieldView = fieldView
  end

  def slot_tapped
    puts "Tapped: (#{@fieldView.penPos.x},#{@fieldView.penPos.y})"
  end
end

include Qte
include Qpe

a = QPEApplication.new([$0] + ARGV)
field = PenguinField::Field.new(640, 480)
fieldView = PenguinField::FieldView.new(field)
receiver = Receiver.new(fieldView)
connect(fieldView.tapped, receiver, "slot_tapped")
a.showMainWidget(fieldView)
a.exec

このテストスクリプトは、PenguinField::MainWindow? ではなく PenguinField?::FieldView? をメインウィジェットにしています。そして、fieldViewが送出する tapped シグナルを、Receiver オブジェクトの slot_tapped にコネクトしています。

ruby test_penguin.rb

を実行して、ウィンドウのいろんな箇所をペンでタップし、右上の×ボタンを押して終了すると、ターミナルの画面に、タップした座標が表示されているはずです。

試しに、002_penguin.rb の QRCanvasView? を QCanvasView? に戻してみたり、catchEvent を削除してみたりして、動作を確認してみてください。

ペンギンの表示と移動

これで、ペンのタップ位置をアプリケーションが感知する仕組みができあがりました。あとは、その位置にペンギンの画像が移動するコードを付け加えるだけです。

(penguin.rb の一部)

  class Penguin <QRCanvasRectangle
    def initialize(field)
      super(field)
      @pix = QPixmap.new(PIX_PENGUIN)
      setSize(@pix.width, @pix.height)
      @adjustX = @pix.width / 2
      @adjustY = @pix.height / 2
    end

    def move(x, y)
      super(x - @adjustX, y - @adjustY)
    end

    def drawShape(painter)
      painter.drawPixmap(x, y, @pix)
    end
  end

QCanvasRectangle? ではなく QRCanvasRectangle? になっているのは、drawShape メソッドを上書き実装するためです。

先に、FieldView が確実にペンのタップを感知できることが確認済みなので、ここでもしペンギンが表示されなかったり、移動しなかったりしたら、こちらのコードに問題があることになります。

シグナルとスロットをつなげる役目は、メインウィンドウのオブジェクトにさせることにしました。

  class MainWindow <QMainWindow
    def initialize(parent = nil, name = '')
      super(parent, name)
      setCaption('PenguinField')
      @field = Field.new(640, 480)
      @fieldView = FieldView.new(@field, self)
      @penguin = Penguin.new(@field)
      @penguin.show
      connect(@fieldView.tapped, self, "move_penguin")
      setCentralWidget(@fieldView)
    end

    def move_penguin
      @penguin.move(@fieldView.penPos.x, @fieldView.penPos.y)
      @penguin.show
      @field.update
    end
  end

これで完成しました。

ruby penguin.rb

を実行して、動作を試してみてください。

おわりに

Qtのウィジェットの動作や、Ruby/Qteならではのつまづきどころがいくつか出てきました。そこで混乱しないで、確実に原因を追及できるようにするには、一つ一つのパーツの動作を確かめながら進めていくのがベターだということがわかるでしょう。

ソースコードのダウンロード

Last modified:2008/01/22 17:30:00
Keyword(s):
References:[Ruby/Qteプログラミング入門]