MFC 對話框數據交換(DDX/DDV)機制中,UpdateData(TRUE) 執行驗證失敗時默認彈出通用錯誤提示,無法在代碼中捕獲具體錯誤類型(如「輸入非數字」、「超出範圍」)。本文詳解如何通過重寫 DDV 函數、捕獲 CUserException、自定義驗證邏輯三種方式實現精確異常處理。
UpdateData(TRUE):調用 DoDataExchange → 執行 DDX_* 數據傳輸 → 執行 DDV_* 驗證。CUserException(繼承自 CException),UpdateData 捕獲後彈出 AfxMessageBox 並返回 FALSE。UpdateData 內部已捕獲並處理異常,調用者無法直接 catch 到異常對象。
// MyDDV.h
void AFXAPI DDV_MyInteger(CDataExchange* pDX, int& value, int min, int max, UINT nIDPrompt = 0);
void AFXAPI DDV_MyDouble(CDataExchange* pDX, double& value, double min, double max, UINT nIDPrompt = 0);
void AFXAPI DDV_MyString(CDataExchange* pDX, CString& value, int nMaxChars, UINT nIDPrompt = 0);
// MyDDV.cpp
#include "stdafx.h"
#include "MyDDV.h"
void AFXAPI DDV_MyInteger(CDataExchange* pDX, int& value, int min, int max, UINT nIDPrompt) {
if (pDX->m_bSaveAndValidate) {
// 1. 類型轉換錯誤已由 DDX_Text 捕獲 (非數字輸入)
// 2. 範圍檢查
if (value < min || value > max) {
CString msg;
if (nIDPrompt) msg.LoadString(nIDPrompt);
else msg.Format(_T("請輸入 %d 到 %d 之間的整數"), min, max);
// 拋出用戶異常,攜帶控件 ID 以便定位
CUserException* pEx = new CUserException(msg);
pEx->m_nIDHelp = pDX->m_idLastControl; // 記錄出錯控件 ID
throw pEx;
}
}
}
// 類似實現 DDV_MyDouble、DDV_MyString...
void CMyDialog::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_AGE, m_age);
DDV_MyInteger(pDX, m_age, 1, 120, IDS_AGE_RANGE_ERROR); // 自定義驗證
DDX_Text(pDX, IDC_EDIT_PRICE, m_price);
DDV_MyDouble(pDX, m_price, 0.01, 99999.99);
}
void CMyDialog::OnBnClickedOk() {
if (!UpdateData(TRUE)) {
// UpdateData 內部已捕獲 CUserException 並彈窗,這裡捕獲不到
// 需改用方案二或三
return;
}
// 驗證通過,處理業務邏輯
OnOK();
}
繼承 CDialogEx 重寫 UpdateData,或直接複製 MFC 源碼邏輯並暴露異常:
BOOL CMyDialog::MyUpdateData(BOOL bSaveAndValidate, CUserException** ppException = nullptr) {
ASSERT(m_hWnd != NULL);
CDataExchange dx(this, bSaveAndValidate);
try {
DoDataExchange(&dx);
dx.UpdateData();
} catch (CUserException* e) {
if (ppException) *ppException = e; // 返回異常指針
else e->Delete(); // 避免內存洩漏
return FALSE;
} catch (CException* e) {
// 其他異常 (內存不足等)
e->ReportError();
e->Delete();
return FALSE;
}
return TRUE;
}
// 調用
void CMyDialog::OnBnClickedOk() {
CUserException* pEx = nullptr;
if (!MyUpdateData(TRUE, &pEx)) {
if (pEx) {
// 解析異常
int failedCtrlId = pEx->m_nIDHelp; // 獲取出錯控件 ID
CString errMsg = pEx->m_strMessage; // 獲取錯誤信息
// 可以根據 failedCtrlId 設置焦點、高亮提示等
CEdit* pEdit = (CEdit*)GetDlgItem(failedCtrlId);
if (pEdit) pEdit->SetFocus();
pEx->Delete();
}
return;
}
OnOK();
}
BOOL CMyDialog::ValidateInput(CString& errorMsg, int& errorCtrlId) {
UpdateData(TRUE); // 僅做數據傳輸,不依賴 DDV 驗證
// 手動驗證年齡
if (m_age < 1 || m_age > 120) {
errorMsg = _T("年齡必須在 1-120 之間");
errorCtrlId = IDC_EDIT_AGE;
return FALSE;
}
// 手動驗證價格格式 (防止科學記數法等)
CString priceStr;
GetDlgItemText(IDC_EDIT_PRICE, priceStr);
double price;
if (!_stscanf_s(priceStr, _T("%lf"), &price) || price < 0.01 || price > 99999.99) {
errorMsg = _T("價格格式無效,範圍 0.01-99999.99");
errorCtrlId = IDC_EDIT_PRICE;
return FALSE;
}
// 正則驗證郵箱、手機號等
static const CString emailRegex = _T("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
// 使用 ATL::CAtlRegExp 或 boost::regex 實現
return TRUE;
}
void CMyDialog::OnBnClickedOk() {
CString errMsg;
int errCtrlId;
if (!ValidateInput(errMsg, errCtrlId)) {
AfxMessageBox(errMsg);
GetDlgItem(errCtrlId)->SetFocus();
return;
}
UpdateData(FALSE); // 同步回控件 (可選)
OnOK();
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 重寫 DDV | MFC 正統、復用性強、與 DDX 無縫集成 | 需維護自定義 DDV 庫、學習成本高 | 大型項目、多對話框統一驗證風格 |
| 捕獲 CUserException | 改動小、能獲取控件 ID 和錯誤信息 | 需重寫 UpdateData、破壞封裝 | 中小項目、需精確定位錯誤控件 |
| 手動驗證 | 完全可控、支援正則/跨字段驗證、無 MFC 依賴 | 代碼量大、需自行同步數據 | 複雜業務規則、非標準驗證、現代化重構 |
// 對話框頭文件
afx_msg void OnEnChangeEditAge();
// 消息映射
ON_EN_CHANGE(IDC_EDIT_AGE, &CMyDialog::OnEnChangeEditAge)
// 實現
void CMyDialog::OnEnChangeEditAge() {
CString text;
GetDlgItemText(IDC_EDIT_AGE, text);
int val = _ttoi(text);
CEdit* pEdit = (CEdit*)GetDlgItem(IDC_EDIT_AGE);
if (text.IsEmpty() || (val >= 1 && val <= 120)) {
pEdit->SetBkColor(RGB(255,255,255)); // 白色背景
} else {
pEdit->SetBkColor(RGB(255,200,200)); // 紅色背景提示
}
pEdit->Invalidate(); // 觸發重繪
}
MFC UpdateData 默認吞沒驗證異常,無法直接捕獲錯誤類型。重寫 DDV 函數是架構最清晰的方案;捕獲 CUserException改動最小;手動驗證靈活性最高。建議新項目採用「手動驗證 + 實時提示」現代化模式,老項目維護用「捕獲 CUserException」快速修復。