MFCダイアログでUpdateDataのTRUEとFALSEを取り違えたり、呼ぶ場所を間違えたりすると、入力した値がメンバ変数に入らなかったり、画面の入力が古い値で上書きされたりします。
TRUEとFALSEの向き
UpdateDataは、引数で、画面とメンバ変数のどちらからどちらへ値を写すかが決まります。
- UpdateData(TRUE):画面 → メンバ変数(取り込む)
- UpdateData(FALSE):メンバ変数 → 画面(書き戻す)
画面に入力された値を処理に使いたいならTRUE、メンバ変数に入れた値を画面に見せたいならFALSEです。逆に書くと、画面の入力がメンバ変数に入らないまま処理が進みます。
UpdateDataの中で呼ばれるDoDataExchange
UpdateDataはCWndのメンバー関数で、中では仮想関数のDoDataExchange(CDataExchange*) を呼びます。自分のダイアログでオーバーライドしたDoDataExchangeには、DDX_Text / DDX_Check / DDX_CBIndexなどの行を並べます。その1行が、1つのコントロールと1つのメンバ変数の間で値を写します。
void CMySampleDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
// DDX: エディットと m_strName の間で値を写す
DDX_Text(pDX, IDC_EDIT_NAME, m_strName);
// DDV: 32 文字までに制限する(UpdateData(FALSE) のときはエディットの入力上限も 32 にする)
DDV_MaxChars(pDX, m_strName, 32);
}
UpdateDataを1回呼ぶと、ここに並んでいるDDX_* の行が上から順にすべて実行されます。1つのコントロールだけを同期することはできません。
自動で呼ばれるタイミング
自分でUpdateDataを書かなくても、MFCが自動で呼ぶタイミングが2つあります。
| タイミング | 呼ばれる向き | 意味 |
|---|---|---|
| ダイアログ表示直後(OnInitDialog内部) | UpdateData(FALSE) | メンバ変数の初期値を画面に載せる |
| OKボタン押下時(OnOKの先頭) | UpdateData(TRUE) | 画面の入力をメンバ変数に吸い上げ、DDVで検証 |
この2か所では、自分でUpdateDataを書かなくても同期されます。「Applyボタンで一度確定したい」「計算結果をすぐ画面に出したい」のように、ほかの場面で画面と同期したいときに自分で呼びます。
UpdateData(TRUE) がFALSEを返すとき
UpdateData(TRUE) は、DDV_* のバリデーションに失敗するとFALSEを返します。このときMFCは問題のコントロールに自動でフォーカスを移し、AfxMessageBoxでエラー文言を表示します。
ただ、DDV_MaxCharsの場合、ユーザーがキーで打った文字では失敗しません。DDV_MaxCharsはUpdateData(FALSE) のときに、エディットへ入力できる文字数の上限(EM_SETLIMITTEXT)も同じ32文字に設定するので、33文字目からは入らないからです。SetWindowTextなどプログラムから33文字以上を入れたあとでUpdateData(TRUE) を呼ぶと、32文字以内で入力するように求めるメッセージが表示されてFALSEが返ります。
FALSEが返ったときも、m_strNameには画面の文字列がもう入っています。DoDataExchangeの中ではDDX_TextがDDV_MaxCharsより先に実行され、そこでメンバー変数へ書き込むためです。FALSEのときはメンバー変数の値を使わずに戻ります。
void CMySampleDlg::OnBnClickedFetch()
{
if (!UpdateData(TRUE)) // DDV に失敗したら false
{
// ここに来たときは、既に MFC 側でフォーカス移動と
// 警告メッセージまで済んでいるので、自前で何かを出し直す必要はない
// m_strName には検証に通らなかった値が入っているので使わない
return;
}
CString msg;
msg.Format(_T("m_strName = [%s]"), (LPCTSTR)m_strName);
AfxMessageBox(msg);
}
一方でUpdateData(FALSE) 側は、書き戻しに「失敗」という概念がほぼないため、戻り値を見る必要はありません。
書き間違えたときに起きること
- TRUEとFALSEを逆に書くと、画面の入力を読みたい場面で古いメンバ変数が画面を上書きしてしまいます。画面の値を読みたいのか、画面に値を出したいのかで、向きを確かめます
- メンバ変数に値を入れても、UpdateData(FALSE) を呼ばなければ画面は更新されません。代入のあとの書き戻しを忘れないようにします
- OnInitDialogでCDialogEx::OnInitDialog() を呼んだあとにメンバ変数へ代入しても、画面には出ません。基底クラスのOnInitDialog() の中で、UpdateData(FALSE) がもう済んでいるためです。初期値はコンストラクタか基底呼び出しの前に入れます。基底呼び出しのあとで入れる場合は、続けてUpdateData(FALSE) を呼びます
- ワーカースレッドからUpdateDataを呼ばないでください。UpdateDataはUIスレッドで呼ぶ前提なので、別スレッドからはPostMessageでUIスレッドに処理を戻してから呼びます
まとめたコード例
ここまでの書き方を1つのダイアログにまとめると、次のようになります。名前を入力するエディットボックス(IDC_EDIT_NAME)を1つ置き、DoDataExchangeは上のコードのままです。「取り込む」ボタンのハンドラーでUpdateData(TRUE)、「書き戻す」ボタンのハンドラーでUpdateData(FALSE) を呼び、それぞれ片方向だけ同期します。OnInitDialogでは基底呼び出しのあとに初期値を入れるので、UpdateData(FALSE) で画面へ出しています。
BOOL CMySampleDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_strName = _T("Hello");
UpdateData(FALSE); // メンバ変数 -> 画面
return TRUE;
}
void CMySampleDlg::OnBnClickedFetch() // 取り込む
{
if (!UpdateData(TRUE)) return; // 画面 -> メンバ変数(DDV 込み)
// ここで m_strName は画面の最新値
}
void CMySampleDlg::OnBnClickedApply() // 書き戻す
{
m_strName = _T("MFCGuide");
UpdateData(FALSE); // メンバ変数 -> 画面
}
まとめ
- 画面に入力された値を使いたいならTRUE、メンバ変数の値を画面に出したいならFALSE
- UpdateDataを呼ぶと、DoDataExchangeのDDX_* の行がすべて実行される。1つのコントロールだけの同期はできない
- OnInitDialogとOnOKではMFCが自動でUpdateDataを呼ぶ。ほかの場面で同期したいときに自分で呼ぶ
- UpdateData(TRUE) はDDVの検証に通らないとFALSEを返す。そのときMFCがフォーカス移動とメッセージ表示まで済ませている
- 別スレッドから直接呼ばない。PostMessageでUIスレッドに戻してから呼ぶ
入力中の文字を数字だけに絞りたいときは、DDVでは止められません。DDVの検証はUpdateDataが呼ばれたときにしか走らないためです。キー入力や貼り付けの段階で止める書き方はCEditを数値入力に制限するに書いています。ダイアログの中でコントロールの位置や大きさを取って動かすときは、関数ごとに座標の基準が違うのでGetWindowRectとGetClientRectの違いも参考にしてください。
