C++で作ったwxWindowに、Luaで作ったコントロールを設置する(wxWidgets 2.8.10を使用)

wxLua を使うと Lua から wxWidgets を簡単に使うことができる。もちろん、フレームから何から全部 Lua で書けてしまうので、そうしても構わないのだけど、コアはやっぱり C++ で書きたい! GUI も大枠は C++ で書いて、局所的に Lua で拡張したい!! そして、それができれば、 LuaGUI を拡張するプラグインなんかも簡単にサポートできちゃうし、夢が広がりすぎて困る。

wxLua はそんな欲求も満たしてくれる。

試しにやってみること

C++ で wxFrame を作成し、その上に Lua で wxButton を2個作って設置(ついでに wxBoxSizer も使ってみる)。片方のボタンは Lua 側でイベントを処理する。もう片方のボタンがクリックされたら C++ 側の wxFrame でイベントを処理するようにする。

サンプルコード

wxLua の中のサンプルがでかすぎたので、自分で小さめの奴を書いた。コンパイル&実行には LuawxLuawxWidgets と boost が必要。

また、コンパイル時に -I$(include-dir)/wxbind/setup 的なオプションを与えておく必要がある。

#include <boost/shared_ptr.hpp>
#include <wx/wx.h>
#include <wxlua/include/wxlstate.h>
#include <wxbind/include/wxbinddefs.h>
#include <wxbind/include/wxcore_bind.h>
WXLUA_DECLARE_BIND_WXCORE

class Frame : public wxFrame {
  DECLARE_EVENT_TABLE()
public:
  Frame() : wxFrame(NULL, -1, wxT("test"), wxDefaultPosition, wxDefaultSize) {}
  void OnHogePushed(wxCommandEvent& event);
};

BEGIN_EVENT_TABLE(Frame, wxFrame)
  EVT_BUTTON(10, Frame::OnHogePushed)
END_EVENT_TABLE()

void Frame::OnHogePushed(wxCommandEvent& event) {
  wxMessageBox(wxT("hoge!!"));
}

class App : public wxApp {
public:
  virtual bool OnInit();

private:
  boost::shared_ptr<wxLuaState> state;
};

IMPLEMENT_APP(App);

bool App::OnInit() {
  WXLUA_IMPLEMENT_BIND_WXCORE

  Frame* frame = new Frame();

  state.reset(new wxLuaState(true));
  if (state->LuaDoFile(wxT("test.lua")) != 0) return false;
  state->lua_GetGlobal("init");
  state->wxluaT_PushUserDataType(frame, wxluatype_wxFrame, true);
  if (state->LuaPCall(1, 0) != 0) return false;

  frame->Show(true);
  SetTopWindow(frame);
  return true;
}

test.lua

require "wx"

function init(window)
  sizer = wx.wxBoxSizer(wx.wxVERTICAL)

  pushMe = wx.wxButton(window, -1, "Push me!!")
  pushMe:Connect(wx.wxEVT_COMMAND_BUTTON_CLICKED,
                 function (event) wx.wxMessageBox("ktkr") end)
  sizer:Add(pushMe)
  sizer:Add(wx.wxButton(window, 10, "Hoge"))
  window:SetSizer(sizer)
end

cpp の方の説明

wxWidgetsLua と C の連携方法の説明は省略する。

まず色々作業を始める前に、バインディングを初期化しておく必要がある。これは簡単で、まずソースファイルのどこかに

#include <wxbind/include/wxbinddefs.h>
WXLUA_DECLARE_BIND_***

と書いておく。***の部分には WXCORE, WXBASE, WXXRC など、使いたい wxWidgets の機能を入れる。 wxbinddefs の中に全部かいてあるので見ながら使いたいものを書けばいいし、困ったら wxLUA_DECLARE_BIND_ALL としておけばいい。

次に、ソースのどこか(初期化処理をしているところが望ましそう)に

WXLUA_IMPLEMENT_BIND_***

と書く。今回は App::OnInit に書いた。 *** の部分はさっき書いた WXLUA_DECLARE_BIND_*** と同じものを書く。

初期化できたら Lua の関数を呼んでみる。

  state.reset(new wxLuaState(true));
  if (state->LuaDoFile(wxT("test.lua")) != 0) return false;
  state->lua_GetGlobal("init");
  state->wxluaT_PushUserDataType(frame, wxluatype_wxFrame, true);
  if (state->LuaPCall(1, 0) != 0) return false;

ここでは、

  1. lua_State を初期化(wxLuaStateを利用)
  2. test.lua をロード
  3. init関数を呼ぶ

という感じの処理をしている。

Lua の関数を呼ぶときに問題となったのは、C++ の wxObject* をどうやって Lua に渡すか、という部分。 もちろん userdata として渡すんだけど、ちゃんと使うためには metatable の設定をしなければならない。各 wxWidgets のクラス用の metatable は一応 wxLua のレジストリ(Windows のではなく Lua の中のレジストリと呼ばれるテーブル)に入っているらしい。でも、 push する度にわざわざユーザデータを構築するのは相当面倒くさい。

そこで、 wxLuaState::wxluaT_PushUserDataType を利用する。

state->wxluaT_PushUserDataType(frame, wxluatype_wxFrame, true);

1個目の引数が wxObject* 、2個目が型情報。型情報には wxluatype_*** という、目的の型のものを渡せば良い。今回は wxFrame を渡したかったので、 wxluatype_wxFrame とした。良くわからなかったら wxLua に入っているサンプルの wxluacan では wxDC を渡しているので、その辺も見てみると良いかもしれない。 wxluatype_*** は wxbind/include 以下のどこかに書かれているので適当に grep すると良い。

3個目の引数(track)はよくわからないんだけど、wxlstate.hには

// If track=true then push the obj_ptr as a lightuser data key into the
// wxlua_lreg_weakobjects_key table of the Lua LUA_REGISTRYINDEX table so
// that if we need to push it again we just push the already created full userdata value.

という説明がある。本当に良く分からない。とりあえずコードを読んでみた感じだと、 Luagc に関係するものらしいので、もう少し勉強しないと正確なことは書けなさそう。サーセン

まあ後は wxLua とは直接関係ないんだけど、 id=10 のボタンがクリックされたら Frame::OnHogePushed が呼ばれるイベントを定義してる。クリックされるとメッセージボックスで "Hoge!!" というのが出てくる。

Lua コードの説明

init に渡された window にコントロールを追加している。 init では wxBoxSizer を作って、そこにボタンを追加している。

1個目のボタンは、 Lua 側でイベント処理をしており、クリックされたら ktkr というメッセージを出す。2個目のボタンは id=10 としておいて、クリックしたら C++ 側で定義されているイベントが呼ばれるといいなーという感じ。

実行してみる

めんどいのでスクリーンショットなどは貼らないけど、ちゃんと動いた!

終わりに

wxLua を C++ から使うための情報がかなり少なかったので、色々調べて書いてみた。 wxLua は使えるとかなり便利なライブラリではあるものの、関数に変なプリフィックスがついていたり、pkg-config を使えないので configure.ac を書くときにいろいろと大変だったりと、面倒くさい部分も多い。Lua から使う分には、かなり直感的に書けるようになっているので素晴らしいんだけどね・・・。

まあそんなわけで、wxWidgets はいろんな言語のバインディングがあるけど、ここまで簡単に C++ とやりとりできるのは多分 wxLua だけなのでおすすめ!(と書くと他の人が「それ、○○でもできるよ」という記事を書いてくれるメソッド)