wxWidgets
DirectXな"Hello World" with wxWidgets
(2010. 5. 5)
wxWidgetsを使うとVC++でツールアプリケーションがサクサク作れそうな気がしてきました。でも、ゲームなツールを考えるなら、やはりDirectXとの連携がうまく取れるかテストする必要があります。そこで、DirectXベースでHello
Worldを表示させつつwxWidgetsと連携する簡易なアプリを作ってみます。
@ ループの決め手はRequestMoreだった!
wxWidgetsはイベントハンドラ型のライブラリです。よって、メインループは奥底の方にしまわれていて直接はもちろん触れません。実装を見るとメッセージループは絶えず回っているのですが、基本的には何かメッセージがポストされないとハンドラは呼ばれません。
一方、DirectXは基本的には常に更新が必要になります。このため、wxWidgetsのアプリケーションレベルのクラスでループ中にガンガン呼ばれるメソッドが必要になります。ところが、そういうメソッドは見当たりません。「ん〜〜困ったなぁ」とGoogle先生に尋ねてみると、なるほど密かにそういう用意がされていました。
まず、アプリケーションでもフレームでも良いのですが、アイドル中に呼ばれるOnIdleハンドラを作成します。続いて、そのハンドラとメッセージを結びつけます。その部分のソースコードは次のとおりです:
MyFrame.h #include "wx/wx.h"
// メインフレームクラス
class MyFrame : public wxFrame {
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
void OnIdle(wxIdleEvent& event);
DECLARE_EVENT_TABLE()
};
OnIdelメソッドの引数はwxIdelEvent&型にします。実装部はこうです:
MyFrame.cpp // イベントとハンドラを結合
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_IDLE(MyFrame::OnIdle)
END_EVENT_TABLE()
// アイドル時に呼び出し
void MyFrame::OnIdle(wxIdleEvent& event) {
event.RequestMore(true);
}
EVT_IDLEマクロを用いてアイドル時にOnIdleメソ度を呼び出してもらうようにします。当初これでメッセージが無い時にOnIdleメソッドがガンガン呼ばれるのかなぁと思っていたのですが、実際は何と逆で、何らかのメッセージがポストされないと呼ばれません!なんじゃそりゃーと思ったのですが、ここに裏技チックなものがありました。OnIdleメソッドの中で上の赤文字で示したwxIdleEvent::RequestMoreメソッドを呼びます。こうすると、アプリケーションフレームワークが気を利かせて、このメソッドをガンガン呼んでくれるようになります。
これでOnIdleメソッドを基点にしてDirectXが要求するループを追加していく事ができそうです。
A ウィンドウハンドルはどうする?
続いて必要なのはウィンドウハンドルです。これは簡単でwxWindow::getHWNDメソッドというそのまんまのメソッドが用意されています。wxFrameはwxWindowが親なので同じメソッドを持っています。ただ、このメソッドの戻り値はWXHWND型の変数です。これはマルチプラットフォームを意識しているためにHWNDをそのまま返せないという事情があるためと思われます。ただ、WindowsならばそのままHWNDにキャストしてしまって構いません。
B さて、では本番
@とAでDirectXでの描画に必要な物が揃いました。さっそくwxWidgetsを用いたDirectXなHello
Worldプログラムを作ってみます。
全体像をざっくり考えてみます。まずDirectXな描画はメインウィンドウに行い、そこに文字列を描画します。ただ描画するだけだとさすがにつまらないので、ダイアログを1枚出して、そこにエディットボックスを1つ追加します。そのエディットボックスに書き込んだテキストがDirectXの画面に反映されるようにしましょう。
以上から必要なクラスは、
・ アプリケーションクラス(MyApp : public wxApp)
・ メインフレームクラス(MainFrame : public wxFrame)
・ ダイアログクラス(Dialog : public wxFrame)
といったところでしょうか。
C メインフレームクラス
メインフレームクラスはDirectXな描画を行うクラスです。よって、このクラスにIDirect3DDevice9インターフェイスなどのDirectXのコンポーネントを持つことになります。またフォント描画も必要なのでID3DXFontも保持です。
クラスの宣言部を見てみましょう:
MainFrame.h #ifndef IKD_DIX_MAINFRAME_H
#define IKD_DIX_MAINFRAME_H
#include <d3d9.h>
#include <d3dx9.h>
#include "wx/wx.h"
#include "Dialog.h"
// メインフレームクラス
class MainFrame : public wxFrame {
IDirect3D9 *d3d9;
IDirect3DDevice9 *dev;
ID3DXFont *font;
Dialog *dialog;
protected:
void initD3D();
void draw();
public:
MainFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
~MainFrame();
// アイドル中にガンガン呼ばれる
void OnIdle(wxIdleEvent& event);
DECLARE_EVENT_TABLE()
};
#endif
メインフレームはwxFrameクラスを継承します。メンバにDialogクラスのオブジェクトがあります。このクラスが持っているエディットボックスに入力された文字列を頂戴しようという目論見です。
publicメソッドはコンストラクタデストラクタを除けばたったの一つ、OnIdleメソッドしかありません。このメソッドはアプリケーションからガンガン呼ばれる予定です。
非公開のメソッドにはinitD3Dメソッドとdrawメソッドを用意しました。双方とも名前の通りです。drawメソッドがOnIdleメソッド内でガンガン呼ばれるのは言うまでもありません。
では実装部です:
MainFrame.cpp #include "MainFrame.h"
// イベントとハンドラを結合
BEGIN_EVENT_TABLE(MainFrame, wxFrame)
EVT_IDLE(MainFrame::OnIdle)
END_EVENT_TABLE()
//MainFrameコンストラクタ
MainFrame::MainFrame(const wxString& title, const wxPoint& pos, const wxSize& size) :
wxFrame(NULL, -1, title, pos, size),
d3d9(), dev(), font(), dialog()
{
// ダイアログ作成
dialog = new Dialog(_("Widgets Window"), wxSize(350, 200));
dialog->Show();
initD3D();
}
MainFrame::~MainFrame() {
if (font)
font->Release();
if (dev)
dev->Release();
if (d3d9)
d3d9->Release();
if (dialog)
dialog->Close();
}
// Direct3Dの初期化
void MainFrame::initD3D() {
if( !(d3d9 = Direct3DCreate9( D3D_SDK_VERSION )) ) return;
D3DPRESENT_PARAMETERS d3dpp = {0,0,D3DFMT_UNKNOWN,0,D3DMULTISAMPLE_NONE,0,D3DSWAPEFFECT_DISCARD,NULL,TRUE,0,D3DFMT_UNKNOWN,0,0};
HWND hWnd = (HWND)this->GetHWND();
if( FAILED( d3d9->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &dev ) ) ) {
d3d9->Release();
return;
}
// フォントの生成
D3DXFONT_DESC lf = {24, 0, 0, 1, 0, SHIFTJIS_CHARSET, OUT_TT_ONLY_PRECIS, PROOF_QUALITY, FIXED_PITCH | FF_MODERN, _T("MS ゴシック")};
D3DXCreateFontIndirect(dev, &lf, &font);
}
// 描画
void MainFrame::draw() {
dev->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
dev->BeginScene();
// Dialogから文字列をゲットして描画
if (dialog) {
wxString text = dialog->getText();
RECT r = {0, 0, 640, 480};
font->DrawTextA(NULL, text.c_str(), -1, &r, DT_LEFT | DT_SINGLELINE, 0xffffffff);
}
dev->EndScene();
dev->Present( NULL, NULL, NULL, NULL );
}
// 更新
void MainFrame::OnIdle(wxIdleEvent& event) {
event.RequestMore(true); // これでOnIdleメソッドが呼ばれまくりになります!
draw();
}
ん〜短い!よし。
ポイントが沢山あります。まず、OnIdleハンドラを呼んでもらうには、EVT_IDLEマクロを使ってOnIdleメソッドを登録する必要があります。コンストラクタでは各種初期化を行っています。Dialogウィンドウの作成も行っています。またDirectXの初期化を行うinitD3Dメソッドもここで呼んでいます。最初「コンストラクタの段階ではウィンドウハンドルが取れないかなぁ」と思ったのですが、至ってあっさりと取得できました。これで2段階初期化をする必要がなくなって、コードがよりすっきりしました。
デストラクタでは各種解放を行っています。DialogもCloseメソッドで閉じています。このメソッドはwxWidgetsが最初から用意してくれているウィンドウを閉じるメソッドです。
initD3DメソッドではDirectXの各種コンポーネントの作成とフォントオブジェクトの作成を行っています。この辺りはもうすっかりおなじみですね。
drawメソッド内のポイントは、Dialogウィンドウが持っているエディットボックスに打ち込まれた文字列を取得して描画している部分です。Dialog::getTextメソッドは自前で用意するメソッドです。
OnIdleメソッド内ではAで説明したwxIdleEvent::RequestMoreメソッドを呼びます。これでガンガン呼ばれることになります。後はdrawメソッドもガンガン呼びます。今はこれで問題ありません。
D Dialogクラス
Dialogクラスはエディットボックス(テキストコントロール)を1つ持つシンプルなダイアログを想定しています。wxWidgetsを使うと、これが笑えるほど短いソースでできてしまいます。コードは以下の通りです:
Dialog.h #ifndef IKD_DIX_WXWIDGETS_DIALOG_H
#define IKD_DIX_WXWIDGETS_DIALOG_H
#include "wx/wx.h"
#include "wx/textctrl.h"
class Dialog : public wxFrame {
wxTextCtrl *textCtrl;
public:
// コンストラクタ
Dialog(const wxString &title, const wxSize &size) :
wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, size)
{
wxPanel *panel = new wxPanel(this, wxID_ANY);
textCtrl = new wxTextCtrl(panel, 0, "", wxPoint(10, 10), wxSize(160, 20));
}
// テキスト取得
wxString getText() const {
if (textCtrl)
return textCtrl->GetValue();
return "";
}
};
#endif
コンストラクタでテキストコントロールを1つ作っています。これだけでテキストコントロールが出るのですから、本当に素晴らしい(T-T)g。後はテキストコントロールに打ち込んだ文字列を取得するgetTextメソッドを公開するだけです。非常〜〜〜に簡単ですね。
E アプリケーションクラス
アプリケーションクラスも今回のサンプルでは大変に短いコードで終わりです。アプリケーションがする事はメインフレームを作るだけです:
MyApp.h #ifndef IKD_DIX_MYAPP_H
#define IKD_DIX_MYAPP_H
#include "wx/wx.h"
#include "MainFrame.h"
class MyApp : public wxApp {
public:
virtual bool OnInit() {
MainFrame *mainFrame = new MainFrame(_("DirectXとの連携テスト"), wxPoint(100, 100), wxSize(640, 480));
mainFrame->Show(true);
return true;
}
};
#endif
アプリケーションの初期化時に必ず呼ばれるOnInitメソッド内でメインフレームを作って終わりです。不思議なのが、ローカルでnewしてほったらかしという部分ですよね。これでちゃんとウィンドウを管理してくれているのですから面白いです。
F main関数
メイン関数(エントリ関数)はもう究極に短いです。以下の通りです:
main.cpp #include "wx/wx.h"
#include "MyApp.h"
IMPLEMENT_APP(MyApp)
すげーー!っと私は思いましたよ、はい。マクロが巧妙に駆使されるとこうなるんだなぁという好例です。
G 実行結果
コードは以上です。これを実行すると次のような2枚のウィンドウが出現します。エディットボックスに打ち込んだ文字列が直ちにDirectXな描画に反映しています。やったぜい!
と言うことで、めでたくwWidgetsとDirectXの連携ができました。後はもうダイアログ部分を発展させて行けばいくらでもやりたい事ができるようになります。嬉しいのがそれを非常に短いコードで実現できてしまう事です。上のコードも大部分はDirectXの描画で占められていて、ダイアログの作成やテキストコントロールの追加は実質1〜2行です。ホント、wxWidgetsすばらしいよう〜(T-T)g
あ、ちなみですね、上のコードには一つ致命的な部分があります。それは、ダイアログを閉じると落ちます。これは、ダイアログの閉じるボタンをクリックしてしまうと、ダイアログウィンドウ自体がアプリケーションによって削除されてしまうためです。ウィンドウの消去権限をメインウィンドウに移せば直ると思うのですが、今回の本質では無いため省きました。悪しからずご了承下さい。
DirectXベースのツールを作る基盤が、これで出来たかなぁと感じます。後は創意工夫で何でもできそうですね。