Windowsプログラミング(MFC編)
by K.I
2009/05/28
Index
- これまで、SDKでWindowsプログラムを書いていたが、MFCのプログラムを弄る必要があったので、使い方のメモ。
- MFCを使った事が無かったので、分からないことだらけだ。
- なので以下の内容は、ほとんどは想像と仮定のもとに書いています。
- おそらくCWinAppを継承したクラスが、アプリケーションのメインクラスになる。
class CMyApp : public CWinApp
で、最初はアプリケーションのインスタンスを生成するところから始まる。
CMyApp theApp;
- 多分、最初にInitInstanceメソッドが実行されるので、ここに初期化処理を記述すれば良い
- 例えば、ここでDialogを作って、起動処理を実行する。
- MFCでプログラムを作ると、アプリケーションのメインループのあるWinMainが何処にも無い。
- これは気持ち悪いが、最初にアプリケーションのインスタンス生成すると、
- CWinAppの、コンストラクタが、WinMainのメインループを作ってくれているみたいだ。
- あとは、メッセージを処理するルーチンを書くだけで良いらしい。
- クラス定義の最後にある、DECLARE_MESSAGE_MAPマクロが、Windowsからのメッセージを処理する宣言をしているらしい。
BEGIN_MESSAGE_MAP(CMyView, CButton)
//{{AFX_MSG_MAP(CMyView)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
- UpdateDataを呼出すと、これが実行される。
- Dialogのコントロールと変数の間で、データの入替えを纏めてやってくれるらしい。
void CMyView::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMyView)
DDX_Control(pDX, IDC_MYLIST, mylist);
DDX_CBString(pDX, IDC_MYNAME, myname);
DDX_Text(pDX, IDC_NUMBER, number);
//}}AFX_DATA_MAP
}
- 例えば、文字列をクラスのメンバ変数として用意しておき、
CString number;
UpdateData(true);
UpdateData(false);
- Windowsでのプログラムは、ウィンドウでのイベント発生時に、対応する処理を行う。
- まず、イベントと実行するプログラムを対応させなければならない。
- MFCでは、イベントにMFCのクラスメソッドを対応させることになる。
- VC5とかだと、クラスウィザードを使うんだけど、VS2005だと無くなってしまった。
- VS2005は、リソースのプロパティウィンドウ上の稲妻マークをクリックすることで、イベントに関数を割当てることが出来る。
- VS2005の場合、まずリソースエディタでコントロールをクリックして選択状態にする。
- 次に、プロパティウィンドウ上の稲妻ボタンを押すと、イベント一覧が出てくる。
- それで、処理したいイベントをクリックして、右側に対応する関数名を入れる。
- イベント名とIDを組み合わせた関数名が、デフォルトで設定されるので、通常はそれをそのまま使えば良い。
- そうすると、関数のプロトタイプが自動的に生成される。
- 必要な構造体のキャストまでやってくれるので、かなり便利!
- 最後に、*pResult = 0;が付いてるけど、これは普通はこのままで良いみたいだが、
- 0以外にすると、デフォルト以外の動作をするものもあるらしい。
- プロパティウィンドウのイベントの関数名を削除すれば、
- 関数が自動的にコメントアウトされる。但し、復活させる時は手動になる。
- 文字列処理は、charの配列ばかり使っていたので、CStringは毛嫌いしていたけど、
- MFCでは、使わざるを得ないので、ちょっと使ってみたら、意外と便利だった。
CString line; // で定義したら、
line = "ABC"; // 文字列の代入も
line += "CDE"; // 連結も簡単!
char ccc[256]="XYZ"; // char文字列を
line = ccc; // CStringに代入したり
strcpy(ccc,line); // 逆にchar文字列に代入するのも簡単だ
- MFCを使うなら、CStringを積極的に使っても良さそうだ。
- 文字列の配列を使いたい場合は、CStringArrayというのがあるが、
- 次に説明しているvectorで、構造体を配列にする方が、自分的には使いやすそうな気がする。
- 可変長の配列を扱うクラス。
- どんな型の変数でも、大きさを自由に変更可能な配列として使うことが出来る
- 構造体も、vectorで配列のように使うことが出来る。
std::vector< int > vdata; →intの配列を作るイメージ
vdata.push_back(100); →最後に値を追加
i = vdata.at(1); →指定したインデックスのデータを読む
j = vdata.back(); →最後のデータを読む
k = vdata.size(); →データの個数
vdata.assign(0,100); →指定したデータをn個代入する(Fillみたいな)
vdata.clear(); →データを削除、データ数は0になる
- 但し、通常の配列のようにメモリ上に連続して確保される(多分)ので、
- ほとんど、配列と同様に使用することも可能。→[ ]も使える!1
- だけど先頭から途中にデータを挿入するのは苦手。普通の配列と同様に全部ズラさないといけない。
- 途中にデータを挿入したい場合は、listを使う。
- listは連続したメモリに確保されるわけではなく、ポインタで繋がっているので、
- Sortとかはやり易いが、[ ]のような直接アクセスは出来ない。
- VC5のMFCでは、vectorで構造体が使えない2っぽい。
- ガッカリしていたら、どうもMFCにはCArrayというものがあるらしい。
- std::vectorと、ほとんど同じようなものらしい。
- afxtempl.hのインクルードが必要。
CArray <kozotai,kozotai> kdata; →構造体の配列を作る
kozotai i,j,k;
kdata.Add(x); →最後に値を追加
i = kdata.GetAt(1); →指定したインデックスのデータを読む
k = kdata.GetSize(); →データの個数
j = kdata.GetAt(kdata.GetSize-1); →最後のデータを読む
vdata.RemoveAll(); →データを削除、データ数は0になる
1但し、atを使えばout of rangeを検出するので、そっちの方が良いけど。
2何か間違っているのかもしれないけど。
[top]
- MFCでのプログラム例として、ListViewをDialog上に配置して、クリックした時に動作するプログラムする場合を考える。
- MFCでListViewというと、CListViewという、ListCtrlをさらに継承したクラスを指すようだ。
- そちらを使った方が簡単な気もするが、動作が分かりやすい、SDKと比べやすいという意味でCListCtrlを使う。
- 以下、CListCtrlの記述だが、ListViewと呼ぶ3ことにする。
- リソースの作成で、ツールボックスから、ListViewを選んで配置するのは、SDKと同じ。
- 例えばIDを、IDC_MYLISTとか適当に付けておく。
- Viewを、レポートに設定しておく。
- MFCでは、ListViewは、CListCtrlというクラスになっている。
- ここではListviewの値のインスタンスを、メンバ変数として作っておくことにする。
CListCtrl m_mylist;
- DoDataExchangeで、コントロールと作ったメンバを対応させておく。
- あとは初期化処理等で、UpdateData(true)で読込むようにすれば良い。
DDX_Control(pDX, IDC_MYLIST, m_mylist);
m_mylist.InsertColumn(0,"COL1",LVCFMT_LEFT,100,-1);
m_mylist.InsertColumn(1,"COL2",LVCFMT_LEFT,100,-1);
これをコンパイルすると、カラムを2つ持ったListViewとしてDialog上に表示される。
- 通常のListViewは、これにアイテムを追加していけば良いが、仮想ListViewにするつもりなので、アイテムは追加しない。
- 通常のListViewでカラムが表示されることを確認したら、仮想ListViewに変更する。
- まずListCtrlのプロパティで、OwnerDataをTrueにする。基本的には、これだけで仮想ListViewになる。
- 仮想ListViewでは、自分でデータを管理することになる。
- 仮想ListViewは、リストの表示のみを行い、データは管理しない。
- 表示に必要な時だけ、イベントでデータを要求するので、必要なデータを渡してやれば良い。
- 通常、データはプログラムが持っているのが普通なので、むしろ仮想ListViewの方がやりやすかったりする。
- また、ListViewはデータが少し多くなると、とんでもなく遅いので、常に仮想ListViewにした方が良いと思う。
- 仮想ListViewでは、データは管理していないので、
- 表示に必要な時に、イベントでデータを要求してくる。
- 仮想ListViewでは、ListViewからのアイテム番号、カラム番号の問合せに対して、対応するデータを返せば良い。
- 具体的には、LVN_GETDISPINFOを処理する関数を定義する。
- VS2005なら、ListViewのプロパティウィンドウの稲妻ボタンを押して,
- LVN_GETDISPINFOを選択、右側の関数名を設定すると、自動的に関数のプロトタイプが出来る。
- SDKなら、WM_NOTIFYイベントを捕まえて、lparamに入ってるポインタから、LVN_GETDISPINFO要求であることを確認しなきゃいけないので、かなり楽だ。
- アイテム番号→pDispInfo->item.item
- カラム番号→pDispInfo->item.subItem
- pDispInfo->item.mask→LVIF_TEXTならテキストデータ、LVIF_IMAGEなら画像データの要求になる
- テキストならば4、item.pszTextに、テキストデータをコピーする
- 但し、pDispInfo->item.cchTextMaxを超えないように
CString line;
if( pDispInfo->item.mask & LVIF_TEXT ) { // TEXTの場合
line.Format("%d,%d",pDispInfo->item.iItem,pDispInfo->item.iSubItem); // とりあえずアイテム番号を表示
line.Left( pDispInfo->item.cchTextMax ); // 一応、文字数を制限(この場合は、やらなくて良いけど)
lstrcpy( pDispInfo->item.pszText, line); // 文字列をコピー
}
- LVN_GETDISPINFOのイベント処理に、追加するのはこれだけか。うーん、簡単だ〜。
- ListViewがクリックした時に、プログラムが実行されるように、イベントにメソッドを割当てる。
- といっても、イベントが沢山あるのでどれだか分からなかったりする。
- NM_CLICKで、クリックした場所を調べても良い気がするが、
- LVN_ITEMCHANGINGで、アイテムのクリック時に実行されるようだ。しかし同じ項目でもクリックする度に実行される
- LVN_ITEMCHANGEDは、選択したアイテムに変化があった時だけ実行されるらしいので、これが良さそう
ハンドルされていない例外が発生しました。
どうも、out_of_rangeらしい。何が?
- デバッガで追うと、LVN_ITEMCHANGEDが発生して、関数に飛んでくるのはOK
- でも、pNMLV->iItemの値が-1になっている。うーん。
仕方が無いので、pNMLV->iItemが-1より大きい時だけ実行するようにした。
CString line;
if (pNMLV->iItem>-1) {
&& (pNMLV->uNewState & LVIS_SELECTED)) {
line.Format("%d\n",pNMLV->iItem);
OutputDebugString(line);
}
これでとりあえず動作するようになった。
- もしかすると選択が外れた時には、pNMLV->iItemは-1になるのかなぁ。
- 調べてみると、
- 1回目は、pNMLV->iItem=アイテム番号
- 2回目は、pNMLV->iItem=-1、続いてpNMLV->iItem=元のアイテム番号、さらにpNMLV->iItem=次のアイテム番号
- じゃ、同じ場所をクリックすると、
- pNMLV->iItem=-1、続いてpNMLV->iItem=同じアイテム番号
- これは選択の切り替わりに出るわけじゃなく、状態が変化するとイチイチ出るようだ。
CString line;
if (pNMLV->iItem>-1 && (pNMLV->uNewState & LVIS_SELECTED)) {
line.Format("%d\n",pNMLV->iItem);
OutputDebugString(line);
}
こうすると、クリックした時だけ実行するようになったが、、
- 同じとこをクリックしても、MouseUpの時に実行される。
- でも複数選択していくと、切り替わる度に実行される。。まぁ、いいか。。。
- こちらの方が最後に確定した時だけイベントが出るので、良いかなと思ったけど、
- カーソルキーの移動により、セレクト位置が変化した場合は反応しない。。
- しかし何で、CListBoxのLBN_SELCHANGEのように、選択状態変化のイベントが無いんだろう?
- ListViewは、左端のSubItemの文字部分しかクリックが効かないのが不便で、
- これは、SDKでも使えるんだろうか。知らんかった。。
- ちょっと追記のメモ
- ソート用のコールバックルーチンには、Index値ではなくユーザーデータの値が渡される5。
- ユーザデータとは、ListViewにオプション的に自由に付加できるデータで、InsertItem時にLVIF_PARAMを指定することで付加できる。
- だからソートする場合は、ユーザデータにソートに必要な情報、例えばデータ配列のIndexを入れておく必要がある。
3ListViewの方が、自分的にはしっくり来るので。
4テキストしか使った事ないので、イメージについては良くわからん。
5なんで、こんな仕様なんだろう。ソートルーチンにはIndexを渡すのが素直だと思うんだけど。。でも、自由度がある方法なのかもしれない。
[top]
[top]
[プログラムの部屋に戻る]