Section.5にて作成したサンプルで、何となくでもWindowsアプリの片鱗が見えてきたという人は居るかも知れない。しかし、『ある程度複雑なアプリとなると、CreateWindow()関数を、死ぬほど呼ばなきゃいけないな…』などと、思ってしまう筈だ。これはさすがに面倒臭いし、何か良い方法は無いものだろうか?と考えるのは当然だろう。これを解決するのに、クラスという概念が使える訳である。やっと、C++らしくなってくる訳だ。ここでは、Section.5で使用したボタンをCoolButton(名前は勝手に付けた[COOLはChara Ozaki Objective Labo.の略])クラスとして定義する事によって、Windowsの部品をクラス化する手段を知り、これによって、MFC等の部品クラスが、どの様な役割をしているのかを考えてみたい。更に、これを発展させて、Cool Windows Class Lib.等と言う代物が構築出来れば、少なくともWinMainは書けるクラスライブラリを、安価に入手したい等という、自分の野望も達成される訳だ。
- クラス化する部分
まず、ボタン部分のみのクラス化を考える。これが出来れば、STATICや他の部品,ひいては親Windowまでもがクラス化出来るのでは無いだろうか?(現行はあくまでも推測)。あとは、このクラスを、上手に組み合わせていけば、Windowsのアプリは簡単に出来てしまう訳である。
- class CoolButtonの定義
ボタンの部品をクラス化する最大の理由は、部品操作の簡略化である。現在のサンプルでは16個のボタンを表示させるのであるが、その全てに対して同じ様なパラメータを設定してCreateWindow()を呼ばなければならない。まずは、この、同じ様なパラメータ部分を、設定しなくて済む様にする。即ち、同じ設定を使う部分は、クラスの生成時(インスタンス化時[new するとき])に、デフォルトとしてクラス内に生成し、変更の必要がある場合だけ、メンバ関数によって変更するという仕組みである。ここで、同じ様なパラメータとして、ターゲットにしたのは以下の点である。
- LPCTSTR lpClassName:部品クラス名
ここは、クラスをボタンとして定義するなら,当然、"BUTTON"であるので、Createした時に、勝手に設定してしまえば良い。
- DWORD dwStyle:ボタンのスタイル
圧倒的に、多く使うスタイルは、BS_PUSHBUTTON|WS_CHILD|WS_VISIBLEであるから、これをデフォルトにする。
将来的に、変更の必要が有る場合は、メンバ関数(SetButtonStyle())を追加して、必要な時だけ書き換える様にすれば良い。
- HWND hWndParent:親ウインドウのハンドル
沢山生成する場合、毎回設定するのは面倒なので、最初に親ウインドウを生成した際にアプリのインスタンスと組で登録(RegistWindowSet())してしまい、クラスの生成時に、その登録ウインドウ番号にて指定する様にした。
- HANDLE hInstance:アプリのインスタンス
上記親ウインドウのハンドル同様,毎回設定するのは面倒なので、登録ウインドウ番号にて管理する様にした。
- LPVOID lpParam:不明
これについて、内容は不明なのだが、常にNULLを設定しているので、ユーザは指定しなくて良い様にする。
以上の項目を、ユーザがいちいち関知しなくてよい項目として、クラスの中だけで扱う様にすれば、あと、CreateWindow()に必要なパラメータは、ボタンを配置する座標と、ボタンに表示する文字列と、ボタンのメッセージIDだけなのである。あっと、親Windowの番号も必要か…。従って、ユーザは、まず,
CoolButton *mybtn ;
// … 使うクラスの宣言
mybtn=new CoolButton(wnumb,IDM_MYBTN) ;
// … 親Window番号と、メッセージIDをパラメータとして、
// コンストラクタを呼び出す→インスタンス化
|
の様な手順で、クラスをインスタンス化し、
mybtn->Create("押して",0,0,80,20) ;
// … ボタンに表示する文字列と、座標(Left,Top,Width,Hightの順/単位=ドット)
// を指定して、CreateWindow()を実施
|
として、ボタンを具現化する。ソースを見ていただければ分かるが、メンバ関数Createが、
CreateWindow()を実施している訳である。因みに、CreateWindow()は、呼び出す順序さえ誤らなければ、クラスの中で扱っても支障は無い(若干速度は落ちるが…)ので、クラス化してしまえば簡単に呼び出して使える訳だ。また、Createする前ならば、設定するパラメータを変更する事も可能であるから、メンバ関数を追加すれば、Createする前に変えたい部分だけを変える事も出来るのである。
あっと忘れる所だった。アプリの中でボタンを使わなくなったら、
delete mybtn ;
// … CoolButton *mybtnインスタンスの消去
// デストラクタ呼び出し
|
を実施する事を忘れずに!ボタン自体は親ウインドウ消滅と同時に消えてしまうかも知れないが、少なくともインスタンス化されたクラス(の変数部分)は残ってしまうので、要注意だ。
実は、これについて、別にC++言語に媚びて、クラスにこだわる必要は無くて、『関数でまとめてしまえば良いじゃん?』などという意見も有ったのたが、将来的に考えて、クラス(インスタンス)内にて保持しなければならないデータも有るし、データ管理の方法についてもクラスの方がスマートであったので、殆ど疑う余地もなくクラス化に走った。
クラスCoolButtonの定義
クラス定義部分(ccollib.h)
メンバ関数定義部分(coollib.cpp)
さて、クラス化の結果の動作はソースを見ていただこう。
テストサンプル004
クラス化には関係無いが、上述した通り、RegistWindowSet()関数を追加している。CreateWindow()にて毎回指定するアプリのインスタンス(hInstance)と、親ウインドウのハンドル(hwnd)については、同一Windowの中で使う部品クラスには共通で使われるので、いっそのこと最初に親Windowを生成した際に、登録してしまい、2つの組み合わせを1セットとしてグローバル変数として定義した構造体で管理する事を考えた。ただ、今後、複数のWindowを生成する場合も有ると考えて、この構造体の配列を定義し、登録した順に番号を付け、その番号をクラスに渡す事によって、どのセットを参照すれば良いかを知ると言った構造にしている。
何はともあれ、クラス化は苦もなく進行したのであった。ソースに同梱の_test004.cppが、最初に使ってみたモデルである。クラスについて、インスタンス生成(new)と削除(delete)の部分が増えてしまったので、全体的にみて、さほど簡単になったというイメージは持てないかも知れないが、実際にCreateWindow()を行っている部分は、WinMainの中程、btnx->Create()等を行っている部分である。この部分だけを見れば、かなりスッキリした!と思えるのでは無いだろうか?しかしながら、少々クラス化したメリットが見えなかった為、CoolButtonを配列にて定義し、インスタンス生成及び削除を、forループで行ってみたのが、test004.cppである。どうだろう?test003.cppとは比べものにならない程スッキリしているとは思えないか?
- 注意しなければならない点
分かってる人は分かっているのだろうが、CoolButtonのクラス定義ファイルcoollib.h中で使っている、HWND,HMENU等の型は、少なくともANSI-C++の標準の型では無いので、windows.hをインクルードしないと使えないのである。でも、coollib.hには、windows.hがインクルードされていなくてもエラー無く動作するじゃないか??等と疑問に思う人も居ると思う(実は、後輩から質問を受けた)ので、説明しておく。
サンプルでは、coollib.cppにも、test004.cppにも、coollib.hをインクルードする前に、windows.hがインクルードされている。コンパイラがコンパイルする対象は、あくまでもこれらのCPPファイルであるので、コンパイルする場合は、これらのヘッダ内容を、インクルード命令の場所に展開する形でコンパイルする訳である。即ち、この時点で、coollib.hにも、windows.hがインクルードされたのと同じになるのである。即ち、このライブラリを使う場合は、coollib.hより先に、必ずwindows.hを、インクルードしなければならないのである。
尚、自分は、なるべくヘッダファイルのインクルードは減らしたいと考える方なので、この様な方法を採るが、なるべく汎用に使いたいという方は、coollib.hにも、windows.hをインクルードすると良いだろう。
- 少々疑問が残る部分
クラスの生成,及び使用についてはスムーズに考える事が出来たが、問題は削除の方であった。メッセージループの終了で、親ウインドウが消滅した時点で、子Windowの一つの形態として生成されるボタンも消えてしまうのでは無いだろうか??すると、CoolButtonの削除時に、DestroyWindowを実施するのは無意味という事になる。無論,親Windowが消えたとしても、ユーザが管理しているCoolButtonのインスタンスは消えないので、deleteにて消去するのは必須であるのだが、消滅子(destractor:CoolButtonでは~CoolButton)にて、DestroyWindowを実施するのが『?』なのである。現行は、とりあえず動作しているので、このまま使用するが、何か分かり次第ここに追記する。
- 他の部分もクラス化
ボタンのクラス化が、どうにか出来た所で、STATICとメインのWINDOWフレームの部分も、CoolStatic,MainWindowと言う名前でクラス化してみた。(CoolWindowClassと呼ぶ)WinMainが、かなりスッキリしたと感じないだろうか?ソースの内容について、説明し出すとキリが無いので省略するが、結局,動作的には、Section.4にて扱った例題と同じ手順にて動作しているという事を頭に置いて考えれば、クラス内の動作については難なく理解出来る筈である。
テストサンプル005ソースのダウンロード
↑試してみたい人は、ここからダウンロード
因みに、最初のサンプル(HelloWorld)に、ここで使用したCoolWindowClassを使用した例を作成してみたので、こちらも参照してみると、かなり簡単にWindowが開ける様になったのが分かると思う。
テストサンプル001bソースのダウンロード
↑試してみたい人は、ここからダウンロード
ここで、ちょっと追記,上の例では、分かりやすくする為に、シーケンシャルに処理が追える構造で処理を切ってみたが、MFCなど、実際のクラスライブラリの使われ方(と言っても、自分の場合、16bitの頃のものしか知らないのだが…)を見ると、CFrameWnd(ここではMainWindowに当たるのだろうか?)のクラスを継承してMyApp等というクラスを作成し、そのクラス内のプロパティ(変数)として、使用する部品(Button等)を定義するという方法を採っている。確かに、この方法であれば、CoolWindowClassで使用したRegistWindowSet()によって、メインのWindowのセットを登録して、番号で管理する等の作業は、事実上必要無くなるので、非常に合理的である。ただ、この様な方法では、なかなかプログラム全体の流れをシーケンシャルに追う事が難しいのも事実である。CoolWindowClassでは、なるべくシーケンシャルにプログラムを組みたいので、このまま進めるつもりであるが、将来的には、方向転換するかも知れない。
とりあえず、ここまで来れば、クラス化の方法については、何となくでも分かってきていると思うし、クラスというのは、自分の使いやすい様に作成(改造)していくものであると思うので、ここで紹介したものは、あくまでも理解を深める為のサンプルだと考えて、各個人なりのクラスライブラリを作成して、利用する様にする事がベストである。
- ちょっとした小技
- ボタンのグレーアウト
上述の例題でも、さりげなく使ってみたのだが、ボタンのグレーアウトは、EnableWindow()関数を使用すればコントロール出来る。この関数の引数は、
BOOL EnableWindow(
HWND hWnd, … ボタンのハンドル
BOOL bEnable … ボタンを有効(TRUE)/無効(FALSE)で指定
);
であり、簡単に制御可能である。一応、CoolWindowClassにも組み込んでみたので、制御方法を確認して欲しい。因みに、CoolButtonはデフォルトでEnable(TRUE)になっている。
- ついでにメニューのグレーアウト
グレーアウトの方法を記述したので、ついでに、メニューの項目のグレーアウトについても、記述しておく。ここで使用する、EnableMenuItem()の引数は以下の通り,
BOOL EnableMenuItem(
HMENU hMenu, … メニューのハンドル(GetMenuで取得)
UINT uIDEnableItem, … メニューアイテム(IDM_ABOUT等)
UINT uEnable … コントロールフラグ
);
であるが、説明が難しいので、例として、hwndのハンドルを持つWindowに登録されているメニューのヘルプ(IDM_ABOUT)をグレーアウトしたい場合は、
EnableMenuItem(GetMenu(hwnd),IDM_ABOUT,MF_DISABLED | MF_GRAYED) ;
とすれば良い。また、復活させるには、
EnableMenuItem(GetMenu(hwnd),IDM_ABOUT,MF_ENABLED) ;
と指定すれば良い。
- プロトタイプ宣言…言いそびれていた事
今までのサンプルでは、最初に造ったHelloWorldを徐々に改造する形で生成してきているので、ずっと言い忘れてしまったのだが、普通,WinMainをWndProcより先に書く…という人も居るので、プロトタイプ宣言について、説明しておかなければならない。C++(本当はCでも同じなのだが、C++は関数のオーバライドが認められているので、必須となっている…)では、関数を使用する場合、必ずプロトタイプ宣言が無くてはならない,という決まりが有るのだ。これを頭に入れて、サンプルを見てみると、WinMainの中で、WinProcを使っている(プロシージャとして登録する為に参照していると言った方が良いか?)のにも関わらず、WndProcのプロトタイプ宣言が無いでは無いか?これは、C言語時代から慣用的に使われているテクニックで、使用する関数本体(ここではWndProc)が、それを使用する関数(ここではWinMain)より先に記述されている場合、その関数本体がプロトタイプ宣言の代わりをするのである。
もともとプロトタイプ宣言は、コンパイラがソースを解析する際、目的の関数をコールしている記述を発見する前に、目的の関数の引数と、戻り値の形を把握する為に使用されるのである。…マシン語レベルで考えてみよう。普通,関数はサブルーチンとして構築され、サブルーチンへの引数の渡し方は、レジスタに入れるか、若しくはスタックに積むという方法で渡されるのである。関数サブルーチン側では、スタックから値を掘り出して使う訳だが、コンパイルする際に、コンパイラが引数の形を知らずにコンパイルしてしまうと、適当な形でスタックに放り込んでしまうので、サブルーチン側で掘り出した時に値がズレたりしてしまう訳だ。例えば、このプロトタイプ宣言無しで、関数がコールされていた場合、コンパイラは多分、デフォルトでint型の引数と判断してスタックに積む様にマシン語を生成すると思う。でも、本当はchar型の引数を持った関数だったとすると、関数側で掘り出してみたら、全然違う値に読めてしまった…となる訳である。まぁ、コンパイラが一回最後までソースを読んで、全て把握した時点で改めてコンパイルすれば良いのであるが、なるべくソースを見直す回数を減らす為にも、このプロトタイプ宣言は有効なのである。
そういう意味、コンパイラに言わせれば、目的の関数が、それを使う以前に記述されていれば、引数と戻り値の形は把握出来る訳であるから、プロトタイプ宣言は不要という訳である。
尚、本来は、WinMainより以前に、
long WINAPI WndProc(HWND,UINT,UINT,LONG) ;
の様に、プロトタイプ宣言の記述を行うのが正式である。
|