ダイアログ・アプリ
超簡易電卓
ちょっとしたツールを作るときは、ダイアログ形式のアプリケーションで充分なことが多々あります。ここではそんなアプリケーションの作り方を覚えましょう。
サンプルプログラムは整数の足し算・引き算が行える超簡易電卓です。
Figure 4.1: 超簡易電卓の実行画面
以下ソースコードを掲載します。
require 'wx' class CalcApp < Wx::App private def on_init CalcDialog.new.show_modal return false end end class CalcDialog < Wx::Dialog private def initialize super(nil, -1, 'Calculator') top_sizer = Wx::BoxSizer.new(Wx::VERTICAL) @display = Wx::TextCtrl.new(self, -1, '0', :style => Wx::TE_READONLY) top_sizer.add(@display, 0, Wx::ALL | Wx::EXPAND, 10) top_sizer.add(create_buttons, 0, Wx::ALL, 10) set_sizer(top_sizer) top_sizer.set_size_hints(self) end def create_buttons buttons = {} 10.times do |i| buttons[i.to_s] = Wx::Button.new(self, -1, i.to_s) evt_button(buttons[i.to_s].get_id) { |event| on_number(i) } end buttons['AC'] = Wx::Button.new(self, -1, 'AC') buttons['+'] = Wx::Button.new(self, -1, '+') buttons['-'] = Wx::Button.new(self, -1, '-') buttons['='] = Wx::Button.new(self, -1, '=') evt_button(buttons['AC'].get_id) { |event| on_ac } evt_button(buttons['+'].get_id) { |event| on_plus } evt_button(buttons['-'].get_id) { |event| on_minus } evt_button(buttons['='].get_id) { |event| on_equal } buttons_sizer = Wx::GridSizer.new(4, 4, 5, 5) buttons_sizer.add(buttons['7']) buttons_sizer.add(buttons['8']) buttons_sizer.add(buttons['9']) buttons_sizer.add(buttons['AC']) buttons_sizer.add(buttons['4']) buttons_sizer.add(buttons['5']) buttons_sizer.add(buttons['6']) buttons_sizer.add(buttons['+']) buttons_sizer.add(buttons['1']) buttons_sizer.add(buttons['2']) buttons_sizer.add(buttons['3']) buttons_sizer.add(buttons['-']) buttons_sizer.add(buttons['0']) buttons_sizer.add(nil) buttons_sizer.add(nil) buttons_sizer.add(buttons['=']) return buttons_sizer end def on_number(num) str = @display.get_value str = '' if str == '0' str += num.to_s @display.set_value(str) end def on_ac @display.set_value('0') @pre_val = nil end def on_plus @pre_val = @display.get_value.to_i @sign = 1 @display.set_value('0') end def on_minus @pre_val = @display.get_value.to_i @sign = -1 @display.set_value('0') end def on_equal if @pre_val != nil num = @pre_val + @display.get_value.to_i * @sign @display.set_value(num.to_s) @pre_val = nil end end end CalcApp.new.main_loop
桁数チェックなどのエラー処理はありませんし、細かいところで問題はあるかもしれませんが、wxRubyの使い方を覚えるにはシンプルでわかりやすいかと思います。
各種コントロール
電卓アプリでは数字文字列の表示にTextCtrlクラスを、数字の入力などにButtonクラスを利用しています。他にも様々なコントロールが用意されているので、詳しくはドキュメントのControlsの部分を参照してください。
TextCtrlやButtonのコンストラクタの第1引数は親ウィンドウです。これを指定すると、コンストラクタ内部で親ウィンドウに対して自身を子ウィンドウとして登録します。そして親ウィンドウは自身が消滅する時に、子供を一緒に削除します。また第2引数はウィンドウIDで、-1を指定すれば勝手に割り当ててくれます。-1は行儀が悪いと思う人はWx::ID_ANYを指定してください。当然ですが各コントロールによって引数の仕様が異なります。
ここではTextCtrlのスタイルにWx::TE_REAONLYを指定して、ユーザーが編集できないようにしました。本当はWx::TE_RIGHTも指定して文字列を右揃えにしたかったのですが、定数が存在しないというエラーが発生しました。wxRubyのファイルにgrepをかけてみたところ、サンプルプログラムにdeprecatedとあったので、意図的に廃止されたようです。理由はわかりませんが、WindowsとGTK+のみで有効だからでしょうか?
wxWidgetsのヘッダーファイルをgrepしてみると、Wx::TE_RIGHTはWx::ALIGN_RIGHTと同じ値です。重複するから削除されたんでしょうか?試しに“Wx::TE_READONLY | Wx::ALIGN_RIGHT”を指定してみたら、あっけなく右揃えができました。裏ではwxWidgetsが動作しているので、当たり前と言えば当たり前ですが。
Figure 4.2: TextCtrlの文字列右揃え
よりRubyらしく
以下の部分では第4引数、第5引数で指定する位置とサイズを省略して、第6引数のスタイルを指定しています。
@display = Wx::TextCtrl.new(self, -1, '0', :style => Wx::TE_READONLY)
このようにコンストラクタで名前付き引数が使えるように拡張されています。
これは下記のようにしても同じです。
@display = Wx::TextCtrl.new(self, -1, '0', Wx::DEFAULT_POSITION, Wx::DEFAULT_SIZE, Wx::TE_READONLY)
第1〜第3引数を名前付き引数にして以下のようにしても同じです。
@display = Wx::TextCtrl.new(:parent => self, :id => Wx::ID_ANY, :value => '0', :style => Wx::TE_READONLY)
Wx::TextCtrlクラスの文字列は、set_value/get_valueメソッドで読み書きできます。set_やget_で始まるsetter/getterメソッドは以下のようにプロパティとして扱うことができます。またis_で始まるメソッドも最後に?を付けるRuby風に書くことができます。
@textctrl = Wx::TextCtrl.new(self, Wx::ID_ANY, 'wxRuby') @textctrl.set_value('wxSugar') # 上の行は以下のように書くことができる @textctrl.value = 'wxSugar' str = @textctrl.get_value # 上の行は以下のように書くことができる str = @textctrl.value modified = @textctrl.is_modified # 上の行は以下のように書くことができる modified = @textctrl.modified?
ただし@textctrl.get_idを@textctl.idと書けるということは、Ruby自身のObjectクラスのidプロパティと名前が衝突するということです。Rubyでは全てのクラスはObjectから派生しているわけですが、Objectクラスのidを取得するときはwxRubyではobject_idを利用するようになっています。
コンストラクタの名前付き引数やsetter/getterのプロパティ化は、wxRubyをよりRubyらしくする記述するために開発されているwxSugarから取り入れられた機能です。
他にも位置やサイズを配列で指定する機能があります。
@textctrl = Wx::TextCtrl.new(self, Wx::ID_ANY, 'wxRuby', Wx::Point.new(10, 20), Wx::Size.new(120, 20)) # 上の行は以下のように書くことができる @textctrl = Wx::TextCtrl.new(self, Wx::ID_ANY, 'wxRuby', [10, 20], [120, 20]) @textctrl.set_size(Wx::Size.new(200, 40)) # 上の行は以下のように書くことができる @textctrl.size = [200, 40]
Dialogのデザイン
ダイアログをデザインする方法は大きく分けて2通りあります。
1つはXRCというXMLで記述されたリソースファイルを利用する方法です。この方法はWYSIWYGなビジュアルツールを使って、各種コントロールをダイアログ上に貼り付けてデザインすることを想定しています。このビジュアルツールがXRC形式でデザイン結果を出力し、wxRubyではそのリソースを使ってダイアログを生成します。
デザインツールはwxCommunity - Development Toolsで探せます。現時点(2008/10)でフリーなツールとしてはVisualWx, FarPy GUIE, wxFormBuilder, wxGlade, XRCedが見つかりました。またマルチプラットフォームの統合開発環境Code::Blocks6のプラグインとしてwxSmith RADが用意されています。
Figure 4.3: wxFormBuilderの画面
もう1つは直接コーディングする方法です。ここではこの方法を採用しました。HTMLタグ打ちでテーブルデザインするイメージに近いです。勿論WYSIWYGなツールでコードを生成させることも考えられますが、現時点(2008/10)でRuby対応のツールはVisualWxだけのようです。
Sizerの使い方
Sizerは内部に部品を並べるための箱のようなものです。Sizerの派生クラスには、BoxSizer, StaticBoxSizer, GridSizer, FlexGridSizer, GridBagSizer, StdDialogButtonSizerがあります。
電卓アプリは以下のように2つのSizerで構成されています。BoxSizerで縦にTextCtrlとGridSizerを並べ、GridSizerは4×4に設定して各ボタンを配置しています。
Figure 4.4: 電卓アプリにおけるSizerによる部品の配置
最も良く利用するのはBoxSizerクラスでしょう。このクラスのnewでWx::VERTICALかWx::HORIZONTALを指定して、垂直に部品を並べるか、水平に部品を並べるかを指示します。
その後addメソッドで内部に部品を追加していきます。第2引数proportionについては後述します。第3引数のWx::ALLフラグは第4引数のborder(余白)を上下左右に適用するという意味です。Wx::TOP | Wx::BOTTOM | Wx::LEFT | Wx::RIGHTと等価です。
以下に文字列入力コントロール(TextCtrl)とボタン・コントロール(Button)を横に並べて表示する例を示します。
class SizerDialog < Wx::Dialog def initialize super(nil, -1, 'Sizer') search_text = Wx::TextCtrl.new(self, -1) search_button = Wx::Button.new(self, -1, 'Search') sizer = Wx::BoxSizer.new(Wx::HORIZONTAL) sizer.add(search_text, 0, Wx::ALL, 20) sizer.add(search_button, 0, Wx::ALL, 20) set_sizer(sizer) sizer.set_size_hints(self) end end
これを表示すると、以下のように配置されます。
Figure 4.5: BoxSizerのサンプル
Dialogクラスの(正確にはその親クラスであるWindowクラスの)set_sizerメソッドにより、指定したSizerがDialogの所有となり、Dialogが消滅するときに一緒に削除されます。またset_sizer内部ではset_auto_layout(true)が呼び出され、Dialogの大きさが変化したときに自動的にlayoutメソッドを呼び出して中身を再整列させるようにします。またSizerクラスのset_size_hintsメソッドにDialog自身をセットすることにより、Dialogの大きさに合わせてSizerの大きさが変化するようになります。
上記の例ではダイアログの大きさは変更できませんので、別の例を紹介します。以下ソースコードで変更点以外の大部分を省略しています。
class SizerDialog < Wx::Dialog def initialize super(nil, -1, 'Sizer', :style => Wx::DEFAULT_DIALOG_STYLE | Wx::RESIZE_BORDER) # 以前と同じ sizer.add(search_text, 1, Wx::ALL, 20) # 以前と同じ end end
DialogのスタイルにWx::DEFAULT_DIALOG_STYLEとWx::RESIZE_BORDERを設定してみました。Wx::DEFAULT_DIALOG_STYLEは引数省略時のデフォルト値で、Wx::CAPTION | Wx::CLOSE_BOX | Wx::SYSTEM_MENUと等価(ただしUnix系ではSYSTEM_MENUはなし)です。追加でRESIZE_BORDERを設定することで、ダイアログの大きさを自由に変更できるようにしました。
さらにsearch_textをaddするときに第2引数のproportionを1にしました。これを1にすると余った領域が割り当てられるようになります。つまり、Dialogを大きくしたときにsearch_textの領域が大きくなります。
Figure 4.6: search_textのproportionを1にして拡大
上図ではウィンドウを大きくしてみました。search_textの領域だけが大きくなっているのがわかります。下図はsearch_buttonをaddするときにもproportionを1にした場合です。今度は両方の領域が拡大されています。
Figure 4.7: 両方ともproportionを1にして拡大
proportionを1に設定することで、BoxSizerの部品を並べる方向(この例では横向き)には部品が拡大しましたが、縦方向には拡大していません。部品を並べる方向とは別方向にも拡大させるには、Wx::EXPANDを指定します。
sizer.add(search_text, 1, Wx::ALL | Wx::EXPAND, 20)
としてみましょう。
Figure 4.8: EXPANDフラグを設定して拡大
このようにsearch_textが縦に拡大されるようになりました。Wx::EXPANDでなくWx::SHAPEDを指定するとアスペクト比を保ったまま拡大します。
その他にもアライメントを指定するフラグがあります。以下はsearch_buttonをaddするときにWx::ALIGN_CENTER_VERTICALを設定しています。
sizer.add(search_button, 1, Wx::ALL | Wx::ALIGN_CENTER_VERTICAL, 20)
これで縦方向のアライメントが中心になります。
Figure 4.9: ボタンの縦方向アライメントを中心に
他によく使うSizerとしてはStaticBoxSizerがあります。実際、BoxSizerとStaticBoxSizerさえ使えれば何とかなると思います。TextCtrlと“Search”Buttonを並べた最初の例で、BoxSizerをStaticBoxSizerに変更してみましょう。
sizer = Wx::StaticBoxSizer(Wx::HORIZONTAL, self, 'Search')
Figure 4.10: StaticBoxSizerのサンプル(マージンなし)
これだとマージンがなくて見栄えが悪いので、StaticBoxSizerに部品を格納してから、それをBoxSizerに格納してみます。
class SizerDialog < Wx::Dialog def initialize super(nil, -1, 'Sizer') search_text = Wx::TextCtrl.new(self, -1) search_button = Wx::Button.new(self, -1, 'Search') static_boxsizer = Wx::StaticBoxSizer(Wx::HORIZONTAL, self, 'Search') static_boxsizer.add(search_text, 0, Wx::ALL, 20) static_boxsizer.add(search_button, 0, Wx::ALL, 20) sizer = Wx::BoxSizer.new(Wx::HORIZONTAL) sizer.add(static_boxsizer, 0, Wx::ALL, 20) set_sizer(sizer) sizer.set_size_hints(self) end end
Figure 4.11: StaticBoxSizerのサンプル(マージン付き)
ラベルが付いて全体を枠で囲う以外は、基本的にBoxSizerと同じです。newの引数は並べる方向, 親ウィンドウ, ラベル文字列です。
今回使ったもう1つのSizerがGridSizerです。こちらは単に指定した行と列に同じ大きさで部品を並べるだけのものです。newの引数は順に、行数, 列数, 垂直方向のギャップ(部品同士の隙間), 水平方向のギャップです。
buttons_sizer = Wx::GridSizer.new(4, 5, 5)
上記のように引数3つの場合は列数, 垂直ギャップ, 水平ギャップで、行数は加える子供の数によって決まります。
BoxSizer/StaticBoxSizer/GridSizerの詳細やその他のSizer派生クラスについては、ドキュメントのWindow Layoutの部分を参照してください。
イベント処理
EvtHandlerクラスの派生クラスはイベントを処理することができます。EvtHandlerからWindowクラスが派生し、Dialogや各種コントロールなどは(中間クラスもありますが)Windowクラスから派生しています。以下にこれらのクラス階層を図示しますが、先頭の“Wx::”は省略しています。ObjectはWx::Objectであり、RubyのObjectクラスではありません。
Figure 4.12: wxRubyのWindowクラス階層
Eventクラスの派生クラスも様々な種類があります。今回利用しているのはCommandEventクラスで、各種コントロールやメニューを操作したときに発生します。CommandEventクラスには様々なイベント型があり、ボタンを押したときはEVT_COMMAND_BUTTON_CLICKED型のCommandEventが発生します。
これを処理するには、
evt_button(ウィンドウID) { |event| 処理 }
という記述を使います。
電卓アプリでは数字ボタンの処理はon_numberで一括して処理しています。on_numberには押されたボタンが表す数値を渡していますが、ここにeventを渡す方法も考えられます。
buttons[i.to_s] = Wx::Button.new(self, -1, i.to_s) evt_button(buttons[i.to_s].get_id) { |event| on_number(event) } ... def on_number(event) str = @display.get_value str = '' if str == '0' str += event.get_event_object.get_label @display.set_value(str) end
Wx::Event.get_event_objectメソッドでイベントを発生させたオブジェクトを取得できます。このオブジェクトはButtonクラスなので、get_labelメソッドでラベル文字列(つまりどの数字か)を取得します。
もっと詳細が知りたい場合は、EventクラスやCommandEventクラス, Buttonクラスのドキュメントを参照してください。
6主にC++向けの開発環境です。プラグインで拡張できるようですが、現時点でRuby用のプラグインは存在しないようです。