D言語でウインドウクラスを実装したときの話
目次
1 始めに
C言語でウインドウアプリを作っていた時の事です。スライダー部品を作ってみたのですが、 それをいくつも並べたいと思った時、はて?どうしたものかと悩んでしまいました。 結局、うまい解決方法が思いつかず放置してしまいます。その後、D言語で同じく ウインドウアプリが作れないものかと試行錯誤します。なんだかうまくいきそうで うまくいかなくて あれこれいじくり回し、なんとなくこうすればうまくいったかも?という道筋を見つけ ます。そんな訳で世の中の殆どの人にとってはどうでも良い話を記します。
2 C言語でウインドウ部品を作って困ったこと
C言語でウインドウの部品を作ったときに困った事がありました。自作のスライダー 部品を作ってなんとなく動くところまでできたのですが、これを沢山並べようと 思うと、はて?困った事になりました。
- 沢山並べるのは良いが関数としては一つにしたい。でも、Webとかでは 一つの部品関数を違う目的で複数個使用する例が無いなぁ?
- 例えば 色を調節するのに、RGBのそれぞれに1つずつスライダーを割り当て ようと思うと、関数の中にR,G,Bそれぞれの現在値を保持するのはダメで 別途ハンドラを用意して値の保持やら、各スライダからの応答がRGBのどれに 対応するかを結びつける為の仕掛けが必要な気が?
Windowsだとウインドウを生成した時にウインドウハンドラ(HWND)が返ってきますので、 それを使って 実際に操作する値とを結びつければ良いと思ったのですが、 ハッシュ関数的なものを書く必要が出てきたり、イベントの扱いなど難し過ぎて 挫折します。
原理的にできる事は理解できても、技術力が無さ過ぎて実装できないってのは よくある事です(^^;;
3 D言語でウインドウクラスを作ってみた(1)
なんとなくよさげな感じがするという事でD言語を使ってみるようになります。 でも、本へっぽこのコンテンツをざっと読んでいただくと判るように、DMDのオリジナル ではなく GDCをメインに使うという棘の道を逆立ちで進むような事をやってるが故に 色々ハマります(^^;
さておき、C言語では難しかった点がD言語(というよりはオブジェクト指向言語)では うまく解決できます。一つのウインドウ部品関数をいくつも並べたいという表現は、 クラスで部品を作ってインスタンスをいくつも並べれば良いという感じでサックリ 表現できます。例えばRGBスライダにしても、R,G,Bそれぞれの現在値の保持やウインドウ からのイベントの送り先は、それぞれのインスタンスに対して行えば良いという事になります。 ある意味言語レベルで部品のハンドラ表現ができていると言えるのかも知れません。
4 D言語でウインドウクラスを作ってみた(2)
そんな訳で実装方法を考えてみます。WindowsAPIではCreateWindowEx()などを使用 してウインドウを生成します。この時、ウインドウに関する各種情報をWNDCLASSEX型の 変数に設定して、CreateWindowEx()の引数として与えます。このWNDCLASSEXの中の メンバー変数lpfnWndProcに、ウインドウ内で実際にイベントを処理する為の関数へ のポインタを与えています。
まず、単純にクラスの中のメソッド(メンバー関数)の一つをlpfnWndProcに指定する 方法を思いつきました。でもこれはうまくいきませんでした。関数の呼び出し方法が うまくマッチしないのか、詳しい事は良くわかりませんでしたが、とにかくアクセス例外 であえなく撃沈されました。
そんな訳で続いて考えたのは、CreateWindowsEx()などで返ってくるウインドウハンドラを キーにして、全インスタンスで共通に使用する「HWND⇒クラス内メソッドへの呼び出し」 を行う関数を用意すれば良いんじゃね?という方法です。ザックリした仮想コードで 示すと以下のような感じ。
WindowBase[HWND] _WindowList ; class WindowBase{ HWND hwnd ; WNDCLASSEX winc ; : this(...){ winc.lpfnWndProc = cast(WNDPROC)(&_WndProc) ; : } int create(...){ hwnd = CreateWindowEx(..., winc, ...) ; if( hwnd!=null ){ _WindowList[hwnd]=this ; } : } LRESULT proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp){ : } } extern (Windows) LRESULT _WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if( (hwnd in _WindowList) != null ){ : return( _WindowList[hwnd].proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) ) ; } : }
次のような流れでクラス内のproc()メソッドが ウインドウプロシージャ関数として機能 するのを想定してます。
- クラスWindowBaseを全てのウインドウの基底クラスとしています。
- コンストラクタでwinc.lpfnWndProcにプロシージャ関数 _WndProcへのポインタを結びつける。
- create()メソッドを実行することでウインドウを生成する。CreateWindowEx()の戻り値である hwndを使ってWindowBaseのインスタンスへの参照を 連想配列 _WindowList[]に追加する。これで呼び出し準備は完了。 ハッシュ関数を自分で書かなくて良いのはステキです(^_^)
- どのウインドウもメッセージループからは _WndProc()が実行されます。 hwndをキーに_WindowListを引けば、対応するインスタンスへの参照を得られますのでproc()を呼び出せる。
というのが大筋の目論見になります。でもこの手順ではうまくいきませんでした(^^;;;;;;
5 D言語でウインドウクラスを作ってみた(3)
(2)でうまくいかない理由は「CreateWindowEX()から戻ってくる前にプロシージャ関数が呼び出されている」 為でした。実はこれ非常に困った事になってます。CreateWindowEX()が完了しないとhwndの値が 得られないので、_WindowListにキーを登録する事ができません。でも、CreateWindowEX()は (_WindowListを引けないので)プロシージャ関数が実行できないとCreateWindowEX()が失敗して しまいます。つまり卵と鶏の関係、デッドロック状態になっているのです。
仕方無いので次のようにしてみました。 CreateWindowEx()関数の最後の引数はユーザが リソースへのポインタを登録するのに 使って良いという事なので、インスタンスへのポインタを渡すのに使用します。 続いて、_WndProc()内でWM_CREATEメッセージだった場合はCreateWindowEx()の最後の引数 で渡したインスタンスへのポインタが LPARAMとして渡ってきます。この時 hwndも 決定されていますので、これを使って 一時的に_WindowList に追加するという訳です。 こうすることで、インスタンス内のメソッドをプロシージャ関数として間接的に呼び出す 事ができるようになりました。
実は、LPARAMからWindowBaseクラスのインスタンスへの参照にキャストするのに、
コンパイラがエラーしないように訳の判らないキャスト手順を踏む必要があったのは
ハマり所の一つでした(^^;;というか、今でもDMDでコンパイルするとキャストでコンパイル
エラーが出ます(^^;;;;
→gdc-2.065でDMDと同じようにコンパイルエラーするようになったため修正しました。
6 色々あってなんとなく使える感じに
そんな訳で、なんとなく使える感じになったものを置いてみる。
Windows API (githubに移った模様)プロジェクトのWinAPIバインディングを利用させていただいています。 Win64対応の修正を少し入れてあります。 D言語はコンパイラバージョンが変わると、すぐにコンパイルが通らなくなりますので、 コンパイルできるセットとしてwin32-r433b.tar.xzとして勝手に固めさせてもらいました。 そんな訳で、Windowsでしか動きません(^^;
zlib , libpng , libjpeg を、mingw用に野良ビルドしたものをスタティックリンク しています。
ビルド方法は以下の通り。~/Downloads/ の下にtar.xzアーカイブをダウンロードしたとします。 作業ディレクトリは適当な場所だとします。
xz -dc ~/Downloads/winapp_160814.tar.xz | tar xf -
cd winapp_160814/src
xz -dc ~/Downloads/win32-r433b.tar.xz | tar xf -
./mk.sh -a
#クロスコンパイラでは ./mk.sh -a -b i686-pc-mingw32- -l'-lgphobos2 -lgdruntime -lws2_32' とか
MinGW-gdc-2.067(MSYS2でビルドしたMinGWネイティブコンパイラ) および MinGW-gdc-2.068(Cygwinでビルドしたクロスコンパイラ)でビルド確認 しました。オリジナルのDMDでコンパイルできるかは確認していません。
ツリービューとかはAPIをラップしてみただけなので、ちゃんと使えるか 良くわかりません(^^;
世の中のGUIツールキットとかWindowウィジットと呼ばれるライブラリで、C++などの オブジェクト指向言語で書かれているものは、例外なくイベントを全てメソッドと して呼び出す仕掛けになってます。各人は用事のあるメソッドをオーバーライドする形で 利用するのが流儀のようです。例えば、マルチプラットフォームを謳う場合、 WindowsAPIとX-Windowとではイベントの名前などかなり違うので、 イベントにくくり付けたメソッドで隠蔽しないとうまくいかないと考えられます。 でも、Windowsでしか使わないというのであれば、そこまで高度に ラップしなくてもいいんじゃね?というのがTANEの個人的な意見です。 そんな訳で、WindowsAPIがそのまま見える感じになってます。人によっては なんかイマイチに感じるかも知れませんがそういうものだという事で(^^;
7 履歴
2011/06/06 : 初版 2011/07/14 : ソースを色々変更して winapp_110714.zipとして置いた。 アーカイブに同梱していたWinAPIラッパーを別アーカイブに分離した。 スクリーンショットをwinapp_110714版で差し替えた。 本ページ内誤記を修正した。 2012/03/21 : ページをEmacsのorg-modeを使用して生成してみた。内容は2011/07/14と同じです。 2012/12/31 : winapp_121231.tar.xz版にアップデート。 スクリーンショットをwinapp_121231版に差し替えた。 2013/07/15 : winapp_130715.tar.xz版にアップデート。 スクリーンショットをwinapp_130715版に差し替えた。 2013/10/16 : winapp_131016.tar.xz版にアップデート。 スクリーンショットをwinapp_131016版に差し替えた。 2013/10/17 : 文書微修正。 2013/10/27 : 文書微修正。 2014/01/01 : winapp_140101.tar.xz版にアップデート。 スクリーンショットをwinapp_140101版に差し替えた。 2015/07/25 : winapp_150725.tar.xz版にアップデート。 スクリーンショットをwinapp_150725版に差し替えた。 文書微更新。 2016/01/02 : winapp_160102.tar.xz版にアップデート。 スクリーンショットをwinapp_160102版に差し替えた。 文書微更新。 2016/08/14 : winapp_160814.tar.xz版にアップデート。 スクリーンショットをwinapp_160814版に差し替えた。 文書微更新。