2. ウィンドウを表示

(2019.12.22. 公開)

 「手軽にできる」って言っているからには、ウィンドウぐらい「さくっ」と出せないといけませんね。やってみましょう。

2-1. どんなアプリケーションを作るか

 その前に、「何を作るか」を決めておきます。ここでは、下のように「数式を入れたらそのグラフを表示してくれる」アプリを作ることにします。

 このアプリを選んだ理由はいくつかあります。

 まあ、三番目はやってみないとわからないので、眉に唾をつけといてください。

2-2. ウィンドウの設計

 ウィンドウの構成を考えます。必要な部品は、下の通りです。

 これらを wxWidgets でどう実現するかを知らないといけません。wxWidgets のドキュメントは膨大で、慣れないとなかなか大変です。ここでは "List of Available Classes" を紹介しておきます。日本語版がないのが残念です。英語ドキュメントを頑張って読んでください。(この時点ですでに「手軽じゃない」かも?)

 上の部品は、それぞれ次のようなクラスで実現できます。

2-3. wxLua で部品を作成する

 wxLua のコードで、必要な部品を作成していきましょう。まず、ウィンドウ本体の作成です。ウィンドウ本体は、wxFrame というクラスです。

  frame = wx.wxFrame(wx.NULL, -1, "LuaAppMaker グラフ計算機")
  frame:SetClientSize(640, 480)  -- ウィンドウサイズを 640x480 にする

 wxLua では、wxWidgets のクラスは wx.wxXXXX という形で記述します。wx がかぶっとるやないかい、という文句は wxLua の原作者に言ってください。wxLua が世に出てから結構時間がたっていますので、今さら変えられません。

 wx.wxFrame(...) は、wxFrame のコンストラクタ、つまり wxFrame オブジェクトを作成するための関数呼び出しです。最初の引数は「この wxFrame の親になるウィンドウ」ですが、ここではウィンドウは1個しか作らないので、wx.NULL を置きます。(Lua 的には nil を渡せるようにして欲しかったところですが。) 2番目の引数は、ウィンドウの ID(識別番号)です。ここでは、特に必要ないので、-1 を渡しておきます。3番目の引数はウィンドウのタイトルです。

 続いて、この wxFrame の子ウィンドウとして、wxStaticText, wxButton, wxTextCtrl, wxPanel を作成します。

  frame.stext = wx.wxStaticText(frame, -1, "数式")
  frame.button = wx.wxButton(frame, -1, "更新")
  frame.textctrl = wx.wxTextCtrl(frame, -1, "", wx.wxDefaultPosition, wx.wxSize(100, 32), wx.wxTE_MULTILINE)
  frame.panel = wx.wxPanel(frame, -1, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFULL_REPAINT_ON_RESIZE)

  wxTextCtrl の引数にある wx.wxTE_MULTILINE, および wxPanel の引数にある wx.wxFULL_REPAINT_ON_RESIZE は、「ウィンドウスタイル」の指定です。wx.wxTE_MULTILINE は、「複数行にわたるテキストが入力できる」というスタイルです。wx.wxFULL_REPAINT_ON_RESIZE は、「サイズが変更された時に全体を書き換える」というスタイルです。なぜこのスタイルを指定するかは、グラフを描画する段階で説明することにします。

 frame は、あとで何度も使うので、グローバル変数として定義します(Lua では、local をつけない変数はグローバル変数と解釈されます)。他の部品もあとで使うのですが、グローバル変数があまり多くなると収拾がつかなくなるので、ここでは frame のテーブルフィールドとして格納しています。wxLua では、wx.wxXXXX タイプのオブジェクトは Lua の「ユーザーデータ」型として定義されていますが、このユーザーデータ型は、テーブルフィールドを持つことができるようになっています (内部で __index, __setindex メタメソッドを持っているのでしょう)。関係の深いオブジェクトをまとめて記述できるので、この機能は積極的に使っていきたいところです。

 wxStaticTextwxButton への引数は、先ほどと同様です。wxTextCtrl には、4番目・5番目の引数として、位置とサイズを渡しています。位置はあとで自動調整するのですが、サイズはここで決めておく必要があります。6番目の引数 wx.wxTE_MULTILINE は、複数行を入力できるようにするための設定です。

2-4. wxBoxSizer を使う

 wxWidgets では、ウィンドウ内の部品の位置を自動調整するために「サイザー (Sizer)」という仕組みを使います。ここでは、最も単純な wxBoxSizer を使うことにします。wxBoxSizer は、水平方向または垂直方向に部品を並べて、与えられた規則に従ってその大きさを調節するものです。wxBoxSizer の中には、ボタンやテキストなどの GUI 部品の他に、別の wxBoxSizer を入れることもできます。

 今回のウィンドウは、下のように2つの wxBoxSizer を使ってレイアウトすることができます。

 最初に、StaticText と Button を含む水平方向のサイザーから考えます。水平方向のサイザーに部品を入れるときは、その部品について「水平方向に伸び縮みできるか」、および「隣の部品との間にスペースを入れるかどうか」をまず考えます。この場合は、StaticText は伸び縮みできて、ボタンは伸び縮みしないことにしましょう。また、隣の部品との間隔は 5 ポイントとします。

 サイザーに入れる部品の設定は、wxSizerFlags を使って記述します。StaticText の設定は、下のようになります。

 Button の設定は、下のようになります。

 これらの設定を使って、水平方向のサイザーに部品を入れます。下のようなコードになります。

  local hsizer1 = wx.wxBoxSizer(wx.wxHORIZONTAL)
  hsizer1:Add(frame.stext, wx.wxSizerFlags(1):Border(wx.wxLEFT + wx.wxRIGHT, 5))
  hsizer1:Add(frame.button, wx.wxSizerFlags():Border(wx.wxRIGHT, 5))

 今度は、垂直方向のサイザーを考えます。先ほどの水平方向のサイザーと、TextCtrl, Panel を入れます。今度は「縦方向に伸び縮みできるか」を決めなくてはなりません。今回は、水平サイザーと TextCtrl の高さは固定にして、Panel だけが縦方向に伸び縮みできることにしましょう。余白も、図に示したように入れることにします。先ほどは使わなかった設定として、3つの部品の「幅を揃える」ようにします。これを指定しておかないと、ウィンドウのサイズが変わっても、部品の幅は変わらないことになってしまいます。

 コードは下のようになります。Expand() というのが、「幅を揃える」指示です。なお、水平サイザーの部品に Expand() を指定すると、「高さを揃える」指示になります。

  local vsizer1 = wx.wxBoxSizer(wx.wxVERTICAL)
  vsizer1:Add(hsizer1, wx.wxSizerFlags():Expand():Border(wx.wxTOP, 5))
  vsizer1:Add(frame.textctrl, wx.wxSizerFlags():Expand():Border(wx.wxTOP + wx.wxLEFT + wx.wxRIGHT, 5))
  vsizer1:Add(frame.panel, wx.wxSizerFlags(1):Expand():Border(wx.wxTOP, 5))

 全部の部品を入れ終わったら、一番外側のサイザーを frame に設定して、Layout() を呼びます。これで、ウィンドウのサイズに合わせて部品がレイアウトされます。最後に Show() を呼べば、ウィンドウが画面に表示されます。

  frame:SetSizer(vsizer1)
  vsizer1:Layout()
  frame:Show(true)

2-5. メインルーチンは?

 ウィンドウの作成はできました。それでは、このウィンドウを表示する「だけ」のプログラムを作ってみましょう。

function NewFrame()
  --  wxFrame を作る
  frame = wx.wxFrame(wx.NULL, -1, "LuaAppMaker グラフ計算機")
  frame:SetClientSize(640, 480)
  --  部品を配置する
  ...(中略)...
  --  ウィンドウを表示
  frame:Show(true)
end

NewFrame()

 ……あれ、これだけ? ウィンドウを表示したらすぐ終了しちゃうんじゃない?

 実は、LuaAppMaker は、このプログラムを読み込むと、まず書いてあることをすべて実行します。それが終わったら、自動的に wxWidgets の「イベントループ」に入ります。イベントループは、ユーザーからの入力を待って、それに応じて「イベント」を発生させる仕組みです。実行中のプログラムは、「イベント」を受け取って、それに対応する動作を行います。このプログラムでは、「イベント」を受け取る記述がどこにもないので、発生したイベントは wxWidgets または LuaAppMaker が受け取って、決められた動作を行います。「ウィンドウのクローズボタンが押されたら、ウィンドウを閉じて、プログラムは終了する」などです。

2-6. 実行する

 LuaAppMaker のプログラムを実行するには、作成したプログラムのファイルを LuaAppMaker のアイコン上にドラッグ&ドロップしてください。下のように、ウィンドウが開くはずです。ウィンドウのサイズを変更してみて、ボタンの位置や TextCtrl のサイズが狙い通りに変わることを確かめてください。

 どこかでエラーが起きると、下のような画面になります。これは、32行目の frame.buttonframe.botton とミスタイプしたときのエラー表示です。なかなか手強いですね……

 stack traceback: の下を見ると、32行目でエラーが出たことが示されています。まずはエラーの出た行を丁寧に見ることから始めましょう。

 プログラムが正しく走っている時、TextCtrl 上で文字をタイプすれば、文字が入力されます。「更新」ボタンを押すこともできます。ただし、上にも書いた通り、イベントを受け取る記述を何も書いていないので、何の動作もしてくれません。

 次にやるべきことは、「更新」ボタンが押された時に、そのイベントを受け取って、画面にグラフを表示することです。次章以降で、そのようなコーディングを進めていきましょう。

 本章のプログラム: [graphcalc02.wx.lua]

目次