ATL/WTL
Win32ネイティブなGUIを書くなら、これが一番だと思います。
全てヘッダファイルで構成されていて、速くてコンパクトで書きやすい…はず。
ATL/WTLによるWindowsプログラミングを参考にさせていただいています。ありがとうございます。
インストール〜サンプル動かす
-
まずはMicrosoftかSourceforgeのサイトからATL7.1に相当するファイルを落としてくる。Administrater権限は特に必要ではなくて、ファイルを展開するだけでOK。大事なのはinclude/ってフォルダ内のヘッダファイル。これをインクルードパスに含めないといけない。
インクルードパスを変更するには、メインのVCのウィンドウで
ツール→オプション→プロジェクト→VC++ディレクトリと進んで、「ディレクトリを表示するプロジェクト」で「インクルードファイル」を選択。新しいパスを追加します。
-
とりあえずこれをコピペしたら動く、はずのテンプレート。(上のサイトからいただきました)
stdafx.h・atl.cpp・MainWindow.h
「Win32プロジェクトを作成」として、出来た雛形(スケルトンコード)を大胆に削除して貼り付けます。コンパイル&実行できたらOK。ただしMainWindow.hのOnPaintが空っぽなので、まだ何も起こりません…
-
文字を書きます。
#pragma once
class CMyWindow : public CWindowImpl {
public:
DECLARE_WND_CLASS(_T("Hello"));
private:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_PAINT(OnPaint)
END_MSG_MAP()
void OnPaint(HDC /*hDC*/){
PAINTSTRUCT ps;
HDC hDC = BeginPaint(&ps);
RECT rect;
GetClientRect(&rect);
DrawText(hDC, _T("Hello, ATL/WTL"),
-1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(&ps);
}
};
dcが描画オブジェクトで、これに対してフォントとか色とか線の色とか太さとかを指定します。
CRectは長方形です。r_allにウィンドウの大きさを取得して、その中にぴったり収まるように字を書きます。長方形領域にはRECTとかHDCRECTとかいくつか定義があってややこしいですが、とりあえずCRect使っておけばいいと思います。
ウィンドウ…CWindow
-
上のサンプルで使っているのは、CWindowというクラス。これはいわゆるウィンドウだけでなく、ボタンやテキストボックスの親にもなっている。Windowsでは全てのGUI部品は"Window"と呼ばれる。だから、例えばOKボタンが付いたダイアログは「全体」と「ボタン」の二つの要素から構成されている。"Window"は背景色・位置・サイズを持ち、また独自の再描画関数(OnPaint)を持っている。Windowsがハングアップしかけたとき、例えばウィンドウは真っ白なのにボタンだけ正しく描画されたりするのは、ボタンとウィンドウの描画関数が別々であることを示している。
-
普通にウィンドウを作って、そこに文字を描画したり絵をロードしたりするのは、CWindowImplを継承したクラスを書いて、これを然るべき方法でWinMainでインスタンスを作って実行すればよい。(ボタンなどは追加できない) ウィンドウでは色々なイベントが発生するが、CWindowImplではこれらのイベントが起こったときに呼び出す関数を指定できる。例えば、「読み込まれたときに〜する」「描画さするときに〜する」「クリックされたら〜する」などなど。例えば描画される時にhoge()を呼び出すなら、
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_PAINT(hoge)
END_MSG_MAP()
と(publicなとこに)書く。見て分かるようにこれはマクロで、一度展開されて正しいCのプログラムになり、その後コンパイルされる。ここではhogeの引数や返り値はさっぱり分からないけど、引数は描画関数としてふさわしく書かれていないといけない。WM_PAINTの場合は引数は(HDC)と書くらしい。あと、hogeでは何の関数か一見して分からないから、普通はOnPaintという名前を使う習慣である。
-
OnPaintでは字を書いたり絵を張ったりできる。例えば字を書きたければ、
void OnPaint(HDC /*hDC*/){
CPaintDC dc(m_hWnd);
CRect r;
PAINTSTRUCT ps;
dc.DrawText(_T("Kei"), -1, &r, DT_CENTER | DT_VCENTER);
EndPaint(&ps);
}
とすればよい。
-
フォントを変えるには、
CFont font;
font.CreatePointFont(200, _T("Arial Black"));
dc.SetTextColor(RGB(0, 0, 0));
HFONT oldFont = dc.SelectFont(font);
dc.SelectFont(hOldFont);
oldFontは使いたかったら後で使える。
-
文字列が描画されるサイズ・位置を取得できる。こうすると、文字列の部分にマウスが乗ったら…というような判定が出来る。
先ほどのDrawTextで、
dc.DrawText(_T("Kei"), -1, &r_kei, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_CALCRECT );
という風にDT_CALCRECTを追加しておくと、r_kei(元々の描画領域を指定していた長方形)に実際に文字列が描画されるサイズが代入される。例えば下のコードを実行すると、真ん中に描画される"Kei"という文字列の幅と、下からの位置が表示される。
void OnPaint(HDC /*hDC*/){
CPaintDC dc(m_hWnd);
CRect r_all, r_kei;
PAINTSTRUCT ps;
GetClientRect(r_kei);
dc.DrawText(_T("Kei"), -1, &r_kei, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_CALCRECT );
GetClientRect(r_all);
dc.DrawText(_T("Kei"), -1, &r_all, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
char buf[256];
sprintf(buf, "btm : %d, rgh : %d", r_kei.bottom, r_kei.right);
GetClientRect(r_all);
dc.DrawText(_T(buf), -1, &r_all, DT_SINGLELINE | DT_LEFT | DT_TOP );
dc.DrawText(_T(buf), -1, &r_all, DT_SINGLELINE | DT_LEFT | DT_TOP );
EndPaint(&ps);
}
-
CIdleHandlerを継承すると、OnIdle()関数を用いて、アイドル時の処理を行えるようになる。(アニメーションとか? ) アイドル時に何もしたくなければ、継承する必要はない。
-
ウィンドウの位置・サイズは、MoveWindow関数を用いて移動できる。
MoveWindow(0, 0, 500, 500);
のように使う。
-
何かATLのサンプルをコピーして動かそうとすると、下のAssertでエラーになって落ちる。
BOOL ShowWindow(int nCmdShow) throw()
{
ATLASSERT(::IsWindow(m_hWnd));
return ::ShowWindow(m_hWnd, nCmdShow);
}、
色々いじっているといつの間にか動くようになった。
でもどこをなおしたのか不明…
一つ変えたのは、ソースファイルを右クリックして、"プロパティ"で"ATLを使用しない"となっていたのを静的リンクに変更したことだけど、試しに"使用しない"に戻してみてもちゃんと動いた。嫌だ…
ダイアログ (ボタンやテキストボックスの付いたウィンドウ)
-
上の例のような単純なウィンドウ(ボタンやテキストボックスがない)なら、C++ファイルとヘッダだけで作業は完了。でも、普通はウィンドウに図形を描画しておしまい…ということはなくて、ボタンやテキストを配置したくなる。こういうウィンドウは「ダイアログ」と呼ばれ、ATL(WTL?)では"CDialogImpl"を継承することで作られる。
CDialogImplの使い方はCWindowImpl とほぼ同じだけど、例えばCreateする時のパラメーターが違ったりする。
-
ボタンやテキストボックス(Textitem)を追加するときは、リソースエディタを用いるのが一般的な方法。
VisualStudioで例えばhogeというプロジェクトを作ると、自動的にhoge.rcとresource.hというファイルが作られている。hoge.rcを開いて、「ダイアログを追加」とすると、VisualBasicみたいなウィンドウのお絵かきみたいなのができる。VisualBasicと異なるのは、ボタンを押した時の動作・スクロールバーをスクロールした時の動作みたいなのを、ユーザーが明示的に関数に関連づけないといけないこと。
とりあえずこのダイアログもクラスに関連付けないと、ただのお絵かきで終わってしまう。ダイアログのとこで右クリックして「プロパティ」を選び、IDを"IDD_DIALOG1"みたいに(適当に)設定する。そして、これと同じIDを、先ほどのCDialogImpl (を継承した自分のクラス)の中で
class CMainDlg : public CDialogImpl
{
public:
enum { IDD = IDD_DIALOG1 };
...
のように設定する。enumで定数を設定するのはC++では定石らしい。
-
とりあえず何も考えずに一つボタンを配置してみる。ボタンを右クリックして「プロパティ」を開くと、IDという項目がある。これがプログラム中からこのボタン(というWindow)を操作するのに使うキーワード(ID)である。IDにはユニークな数字が割り振られる。これはresource.hに自動的に書かれて、普通は勝手に変更してはいけない。もし同じIDを持つコントロール(ボタン・テキストボックスetc.)が複数あると、コンパイル時にバグる。ここでは仮にIDOKとしておく。
-
さてボタンが押された時の動作を記述してみる。「ボタンが押された時に〜する」というのも、マクロを用いて登録する。
どんなイベントが起こったのかは関数内に書く。例えばIDOKが押されたときに"hoge()"を呼ぶには、
BEGIN_MSG_MAP_EX(CMainDlg)
COMMAND_ID_HANDLER_EX(IDOK, hoge)
END_MSG_MAP()
と書く。さっきの描画と異なるのは、イベントが起こったコントロール(ボタンのこと)のIDを明示しないといけない点。
hoge()は以下のように書く。
void hoge(UINT uNotifyCode, int nID, HWND hWndCtl){
...
}
-
Javaでボタンを作るには、
Button button = new Button();
みたいに書いてあって、buttonのメソッドを呼び出すことで色んな事(ラベルを変えるとか、色を変えるとか、非アクティブにするとか)ができる(気がする。)
しかし、今まで書いてきた方法では、ボタンはIDしか分かっていなくて、ボタンの属性をどうやって変えるのか不明である。
実は、ATLでもボタンに関するインスタンスは内部で生成されていて、GetDlgItemという関数で参照を取得できる。
でもまずはATLではなく、より分かりやすいMFCについて書く。
MFCではCWndやCButtonというクラスがあって、それぞれウィンドウ・ボタンを表す。全てのGUIコントロール(ボタンやテキストアイテム)はCWndを親クラスとして持っている。GetDlgItemという関数を用いると、IDからその実体への参照が帰ってくる。
CWnd *cwnd = GetDlgItem( IDC_BUTTON1 );
のような感じ。
GetDlgItemで取得出来るのはウィンドウを表す"CWndへの"参照であって、ボタンを表す"CButtonへの"参照ではない。
しかし、そのIDがボタンであることが分かっていれば、(CButton *)というキャストをすることでCButtonとして用いることができる。
さて、ATLでも同名の関数を用いるが、取得されるものは何でもCButton (なぜか実体)に突っ込むことができる。
CButton btn = GetDlgItem( IDOK );
みたいな感じ。
CButtonはCWindowを継承しているので、例えばMoveWindowを呼び出すと、ボタンの位置やサイズを変えることができる。
CWindow btn = GetDlgItem( IDOK );
btn.MoveWindow(100, 100, 100, 100);
みたいに。
-
ATLのダイアログ、CDialogImplで背景色を変更する方法。
ウィンドウハンドルを取得してみたり、マクロを試したりしたけど、ちょうどそのトピックについて書かれたドキュメント見つけました。ここです。
一応メモっておくと、
- クラス変数 HBRUSH m_hDialogBrush を定義
- コンストラクタで初期化、デストラクタで破棄。
m_hDialogBrush = CreateSolidBrush(RGB(0, 0, 255));
...
みたいに。
-
関数OnCtlColorDlg()を追加。
LRESULT OnCtlColorDlg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled){
return (long) m_hDialogBrush;
}
-
メッセージマップで、
MESSAGE_HANDLER(WM_CTLCOLORDLG, OnCtlColorDlg)
を追加。
-
-
CDialog::RedrawWindow()を呼び出すと、まず背景が描画され、その後OnPaint()に書いてある、ユーザーレベルでの描画が行われる。デフォルトの背景色は灰色(Windowsでフォームの色として設定した色)で、先ほど書いたように、独自のブラシを登録することで背景色を変更できる。
何らかの理由で背景を再描画したくない場合、ブラシとして「無効ブラシ」を登録すると実現できる。これは単純にブラシにNULLを代入するのではなく、CreateBrushIndirect()を用いて作成する。
//(コンストラクタ内にて)
// m_hDialogBrush = CreateSolidBrush(RGB(255, 255, 255));
LOGBRUSH logBrush;
logBrush.lbStyle = BS_NULL;
m_hDialogBrush = CreateBrushIndirect(&logBrush);
これで、背景の再描画がされなくなる。
リソースエディタで、ラジオボタン(オプションボタン)のグループ化(連動)しようと思って試行錯誤したけど、いまいち分からず。検索したら、こんなページを発見。どうやらグループボックスは飾りらしく、タブオーダーと"グループ"という項目を組み合わせて実現するらしい。ボタンのプロパティで"Group"がオフのものは、その「前」のボタンとグループになっている。「前」とは、タブオーダー(おそらくresource.hの定数の順番)で定義される。実際的には、ボタンを順番に作って、グループを切りたい先頭のボタンで"Group"をオンにすればいい。
一ヶ月も悩んでたバグがついに解決。
フォントを扱うクラスCFontをスタック変数に取り、色んなサイズのフォントを作ろうとしてみた。
CFont font;
font.CreatePointFont(20, _T("Arial"));
...
font.CreatePointFont(12, _T("Arial"));
...
そしたら、ATLで展開された部分のAssertで落ちる。
ATLASSERT(m_hFont == NULL)
CreatePointFontは二度呼び出してはいけないらしい。
一々new()とdelete()を繰り返すことにして解決したけど、
多分Destroyなんとかっていうメソッドを呼び出してもいいと思う。
CFont *font = new CFont();
font->CreatePointFont(20, _T("Arial"));
...
delete(font);
font = new CFont();
font->CreatePointFont(12, _T("Arial"));
...
今の仕事は既存のプログラムを書き換えてるんだけど、srdafx.hにはもともとこう書いてあった。
#define _ATL_APARTMENT_THREADED
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS
#include <atlbase.h>
#include <atlcom.h>
#include <atlapp.h>
#include <atlwin.h>
#include <atlcrack.h>
#include <atlmisc.h>
#include <atlctrls.h>
...
このdefineをうっかり#includeの後にしてしまうと、謎なエラー(デッドロックの可能性? )が出た。
0x8001010D: アプリケーションが入力同期呼び出しをディスパッチしているため、呼び出せません。。
これの解説はここにあるけど、明示的にSendMessageなんて呼んだ覚えがないから焦りました。
CWindow:OnPaint()の中で色々描画するときは、CPaintDCを用いる。例えば、ウィンドウ一杯を黒で塗りつぶすときは、
void OnPaint(HDC /*hDC*/){
CPaintDC dc(m_hWnd);
CBrush brush;
brush.CreateSolidBrush(RGB(0,0,0));
CRect screen;
GetClientRect(screen);
dc.FillRect(screen, brush);
}
とするとよい。
このCPaintDC dc(m_hWnd)という宣言、OnPaint()以外で行っても、正しいCPaintDCが返ってこないみたい。 (m_hWndはこのウィンドウのハンドルだから、どこでもいい気はするんだけど)
だから、例えばこの「画面黒塗りつぶし操作」を他の関数に出すときは、CPaintDC&を引数に取るようにする。
void OnPaint(HDC /*hDC*/){
CPaintDC dc(m_hWnd);
myFillBlack(dc);
}
void myFillBlack(CPaintDC& dc){
...
}