在MFC中用正則表達式對窗體進(jìn)行有效性驗證
原著(zhù):Paul DiLascia
翻譯:Northxizang
下載源代碼:CATWork0504.exe (248KB)
原文出處:C++At Work: Form Validation with Regular Expressions in MFC
本文發(fā)布后有更新參見(jiàn)文中的:[編輯更新]
我想利用本月的專(zhuān)欄描述一個(gè)有趣的應用程序,這個(gè)程序是用本期我的一篇文章:“使用ManWrap 庫在本機 C++ 代碼中調用.NET”所討論的 RegexWrap 庫生成的。RegexForm 是一個(gè)基于正則表達式的MFC窗體有效性驗證系統。為了實(shí)現這個(gè)程序,我首先實(shí)現了RegexWrap。但因為許多細節與正則表達式本身無(wú)關(guān),所以感覺(jué)在這里描述 RegexForm 更好些。
正如我在這篇文章中所言,正則表達式最實(shí)用的一個(gè)地方是驗證用戶(hù)輸入。它可以輕松驗證郵編、電話(huà)號碼、信用卡號碼——以及現實(shí)世界中各種類(lèi)型的信息。一個(gè)正則表達式可以替換成打甚至上百行過(guò)程代碼。UNIX和 Web 編程語(yǔ)言如 Perl從一開(kāi)始就有正則表達式,但在 Windows 世界或MFC,從來(lái)都是使用第三方庫,一直到 .NET框架才結束這個(gè)局面。[編輯更新——3/15/2005:ATL Server Library 中的 CAtlRegExp 和CAtlREMatchContext 類(lèi)為正則表達式提供支持。] 因此現在 .NET 提供一個(gè)完整的正則表達式庫,為什么不在MFC應用程序中使用它呢?并且利用我在前述文章中描述的RegexWrap 庫,你甚至都不需要托管擴展或 /clr。
MFC 已經(jīng)具備一種稱(chēng)為“對話(huà)框數據交換”(Dialog Data Exchange,即 DDX)以及“對話(huà)框數據驗證”(DialogData Validation,即 DDV)的機制來(lái)驗證對話(huà)框輸入。從技術(shù)上講,DDX 只是在屏幕和你的對話(huà)框對象之間傳輸數據,而 DDV才驗證數據。當你從對話(huà)框的 OnOK 處理例程中調用 UpdateData 時(shí) DDX 才開(kāi)始工作。
// user pressed OK:void CMyDialog::OnOK() {UpdateData(TRUE); // 獲得對話(huà)框數據...}UpdateData 是一個(gè)虛擬 CWnd函數,你可以在自己的對話(huà)框中重寫(xiě)這個(gè)函數。其布爾型(Boolean)參數告知是將信息拷貝到屏幕還是相反從屏幕拷貝信息。(你可以在OnInitDialog 中調用 UpdateData(FALSE)以便初始化對話(huà)框)。默認的 CWnd 實(shí)現創(chuàng )建一個(gè)CDataExchange 對象并將它傳遞到另一個(gè)虛擬函數,DoDataExchange,你得重寫(xiě)這個(gè)函數去調用專(zhuān)門(mén)的 DDX函數來(lái)為單獨的數據成員傳遞數據:
void CMyDialog::DoDataExchange(CDataExchange* pDX) {CDialog::DoDataExchange(pDX);DDX_Text(pDX, IDC_NAME, m_name);DDX_Text(pDX, IDC_AGE, m_age);...// etc.} 這里 IDC_NAME 和 IDC_AGE 是編輯控制的 IDs,m_name 和 m_age 分別是 CString 和 int數據成員。DDX_Text 將用戶(hù)輸入的 Name 和 Age 拷貝到 m_name 和 m_age(用一個(gè)重載順便將 Age 轉變成int)。DDX 函數知道走哪條路,因為當從屏幕拷貝到對話(huà)框時(shí),CDataExchange::m_bSaveAndValidate 為T(mén)RUE,反之則為 FALSE。MFC 為各種數據和控制類(lèi)型加載 DDX 函數。例如,DDX_Text至少有一些重載函數用來(lái)將輸入文本拷貝和轉換成不同的類(lèi)型,如 CString、int、double、COleCurrency等等。DDX_Check 用來(lái)將復選框的狀態(tài)轉換成整型值,DDX_Radio 則對單選按鈕做同樣的事情。
DDX 函數傳輸數據;DDV 函數則驗證它。例如,為了限制用戶(hù)名稱(chēng)為 35個(gè)字符,你可以這樣做:
// in CMyDialog::DoDataExchangeDDX_Text(pDX, IDC_NAME, m_sName); // 獲得/設置值DDV_MaxChars(pDX, m_sName, 35); // 驗證
為了限定你的用戶(hù)年齡為 1-120之間的一個(gè)整數,你可以這樣寫(xiě):
// m_age is intDDX_Text(pDX, IDC_AGE, m_age);DDV_MinMaxInt(pDX, m_age, 1, 120);
雖然 DDX 工作表現得很好,DDV 是不免有點(diǎn)老土。MFC在有效性驗證方面所能做到的很有限。你可以在文本域中限制數字字符,不同類(lèi)型的最小/最大約束。最小/最大是不錯,但如果你想驗證郵編或電話(huà)號碼怎么辦?MFC對此無(wú)能為力。你不得不編寫(xiě)自己的 DDV 函數。當我第一次用正則表達式實(shí)現有效性驗證時(shí),我只要寫(xiě)一個(gè)函數即可,就像這樣:
void DDV_Regex(CDataExchange* pDX, CString& val,LPCTSTR pszRegex){if (pDX->m_bSaveAndValidate) {CMRegex r(pszRegex);if (!r.Match(val).Success()) {pDX->Fail(); // throws exception}}} 這使你很容易象下面這樣用正則表達式驗證輸入:
// in CMyDialog::DoDataExchangeDDX_Text(pDX, IDC_ZIP, m_zip);DDV_Regex(pDX, m_zip,_T("^\\d{5}(-\\d{4})?$"));好酷啊,僅用四行代碼就搞掂。(當然,那要假設你有 RegexWrap——否則你得使用托管擴展直接調用框架 Regex類(lèi)。)DDV_Regex 在 MFC 的 DDX/DDV 方案中工作表現很完美,但是當我開(kāi)始添加更多的域時(shí),我馬上發(fā)現一些 DDX/DDV的主要缺點(diǎn),其一,如果域輸入無(wú)效,則每個(gè) DDV函數都顯示一個(gè)出錯消息框并丟出異常,那么要是有五個(gè)無(wú)效域,用戶(hù)就會(huì )看到五個(gè)消息框——真實(shí)糟透了!此外,在對 DDV的調用中,我不想將正則表達式寫(xiě)死在代碼中。但我拒絕 DDX/DDV 的主要理由是它太程序化。為了驗證新的域,你不得不添加另外的數據成員以及在DoDataExchange 加更多的代碼,不久這個(gè)函數便膨脹臃腫,就像下面這樣:
DDX_Text(pDX, IDC_FOO,...);DDV_Mumble(pDX, ...)DDX_Text(pDX, IDC_BAR,...);DDV_Bletch(...)... // etc for 14 lines
為什么我非得要墨守成規編寫(xiě)過(guò)程指令來(lái)描述固有的驗證規則呢?我的五條編程最高準則之一是:拒斥程序化代碼。另一個(gè)是:一個(gè)表格勝過(guò)一千行代碼。你肯定猜到我要干什么了。最終,我編寫(xiě)自己的對話(huà)框驗證系統,一個(gè)基于規則的、表格驅動(dòng)的驗證系統。它依靠在DDX 的最上層,但廢掉了 DDV并有好得多的用戶(hù)接口。當然,它還易于使用,借助正則表達式進(jìn)行驗證。所有細節都封裝在一個(gè)類(lèi)中,CRegexForm,你可以在任何 MFC對話(huà)框中使用這個(gè)類(lèi)。


// form/field mapBEGIN_REGEX_FORM(MyRegexForm)RGXFIELD(IDC_ZIP,RGXF_REQUIRED,0)RGXFIELD(IDC_SSN,0,0)RGXFIELD(IDC_PHONE,0,0)RGXFIELD(IDC_TOKEN,0,0)RGXFIELD(IDC_PRIME,RGXF_CALLBACK,0)RGXFIELD(IDC_FAVCOL,0,CMRegex::IgnoreCase)END_REGEX_FORM()
這個(gè)宏定義了一個(gè)靜態(tài)表格,表格描述每個(gè)編輯控制域。大多數情況下你只需要控制 ID,還要有地方放標志和 RegexOptions。例如,在TestForm 中,郵政編碼是必輸域(RGXF_REQUIRED),質(zhì)數(PrimeNumber)輸入域使用回調(稍后會(huì )詳細討論),最喜愛(ài)的專(zhuān)欄作家(IDC_FAVCOL)指定 CMRegex::IgnoreCase,它使得大小寫(xiě)不敏感。

"Zip Code\n^\\d{5}(-\\d{4})?$\n[\\d-]\n##### or #####-####"第一個(gè)子串“ZipCode”是域名。第二個(gè)“^\d{5}(-\d{4})?$”,是用于驗證郵編的正則表達式。(在資源串中必須敲入兩個(gè)反斜線(xiàn),目的是轉義正則表達式中反斜線(xiàn))。第三個(gè)子串是另外一個(gè)正則表達式,用來(lái)描述合法字符。對于郵編來(lái)說(shuō),即是“[\d-]”,意思是允許數字和連字符(hyphen)。如果輸入域無(wú)字符限制,你可以通過(guò)敲入兩個(gè)連續的新行符(“\n\n”意思是空子串)省略 LegalChars檢查。第四個(gè)子串全部為工具提示串。最后你可以提供第五個(gè)子串,如果該輸入域無(wú)效則顯示錯誤信息。對于郵編來(lái)說(shuō),它沒(méi)有錯誤信息,所以CRegexForm 產(chǎn)生一個(gè)默認的信息,形式為“Should be xxx”,xxx 被工具提示替代。“Shouldbe”本身即是另一個(gè)資源串(稍后還要說(shuō)到)。這些子串中,只有第一個(gè)域是必輸域。
為什么用資源串來(lái)保存所有信息,而不直接在域映射中編碼處理呢?首先,將它放在映射中使得代碼很笨拙。把這些亂七八糟的字符串放在不顯眼的地方有利于代碼更整潔。此外宏無(wú)法處理可選參數,根據你所用參數的多少,你需要多個(gè)宏,如:RGXFIELD3、RGXFIELD4 和RGXFIELD5。這樣不是太笨拙了嘛?使用資源串真正的好處在于容易本地化。翻譯者可以翻譯字符串并創(chuàng )建不同的資源DLLs。甚至正則表達式本身都需要翻譯(不同的國家和地域郵編可能是不同的),所以它們也放在資源串中。
此外,再讓我順便指出用正則表達式解析這些子串是多么容易。MFC 有一個(gè) 26 行的專(zhuān)門(mén)用來(lái)解析子串的函數AfxExtractSubString,但 CRegexForm 使用 CMRegex 來(lái)做只要一行代碼!
CString str;str.LoadString(nID);vector<CString> substrs = CMRegex::Split(str, _T("\n")); 現在 substrs[i] 是第 i 個(gè)子串,如果你想知道有多少個(gè)子串,調用 substrs.size()即可。我真的很高興我包裝了Split 函數來(lái)返回 STL vector。
一旦你用 BEGIN/END_REGEX_FORM 定義了自己的域映射并編寫(xiě)了自己的資源串,下一步要做的就是在對話(huà)框中實(shí)例化CRegexForm 并進(jìn)行初始化:
// in OnInitDialogm_rgxForm.Init(MyRegexForm, this,IDS_MYREGEXFORM,MYWM_RGXFORM_MESSAGE);
自然,CRegexForm需要域映射并指向你的對話(huà)框;第二和第三個(gè)參數是另一個(gè)資源串和回調消息ID。與單獨的域字符串一樣,初始串由包含新行符分隔的多個(gè)子串組成。對于TestForm,IDS_MYREGEXFORM 是“Error: %s\nRequired\nShould be: %s\nBadValue”。第一個(gè)子串“Error: %s”是錯誤前綴。CRegexForm 用來(lái)顯示“Error: xxx,”,xxx是實(shí)際的出錯信息。第二個(gè)子串,“Required,”是一個(gè)詞/短語(yǔ),當該域為必輸域(RGXF_REQUIRED)時(shí)使用。第三個(gè)子串“Shouldbe: %s,”我前面已經(jīng)描述過(guò),CRegexForm 用它來(lái)產(chǎn)生出錯信息。“Should be: xxx”中的 xxx是域提示。最后一個(gè)子串,“Bad Value,”CRegexForm可以用它來(lái)放任何信息,當域沒(méi)有提示也沒(méi)有出錯信息時(shí)使用。用戶(hù)是看不到此信息的,因為你肯定會(huì )為每一個(gè)域配一個(gè)提示或出錯信息,對不對?
Init 的最后一個(gè)參數 MYWM_RGXFORM_MESSAGE 是應用程序定義的回調消息 ID,利用它可以使 CRegexForm與你的應用程序溝通并做一些需要過(guò)程代碼來(lái)定制驗證的事情。如果你需要用數學(xué)算法來(lái)驗證你的輸入,你可以在域標志中設置RGXF_CALLBACK,CRegexForm 將在進(jìn)行驗證時(shí)用通知代碼 RGXNM_VALIDATEFIELD方式向對話(huà)框發(fā)送回調消息。TestForm 使用回調來(lái)驗證其 Prime Number 域;具體詳細參見(jiàn)Figure 4。
CRegexForm 用其內部擁有的 CStrings 來(lái)進(jìn)行 DDX,所以你不必為每個(gè)文本域定義對話(huà)框成員。你只要調用CRegexForm 來(lái)傳遞數據即可。
void CMyDialog::DoDataExchange(CDataExchange* pDX){CDialog::DoDataExchange(pDX);m_rgxForm.DoDataExchange(pDX);}當你初始化 CRegexForm 時(shí),它分配一個(gè) protected 類(lèi)型的 FLDINFO結構數組,映射中的每個(gè)域都有一個(gè)這樣的數組。FLDINFO 結構的成員之一是 FLDINFO::val,類(lèi)型為 CString,用來(lái)保存當前的域值。CRegexForm在內部使用以此 CString 作為參數的 DDX_Text。你可以通過(guò)調用 CRegexForm::GetFieldValue 或SetFieldValue 獲取或設置該內部域值,它們都用控制ID來(lái)區分域。
m_rgxForm.SetFieldValue(IDC_ZIP,_T("10025")); CRegexForm 將所有值都當作文本對待,并將它們存儲在 CStrings 中,同時(shí)提供 GetFieldValInt 和GetFieldValDouble 方法來(lái)獲得轉換為 int 或 double 的值。對于其它類(lèi)型,你得自己進(jìn)行轉換——或者你仍可以用 MFCDoDataExchange 中的 DDX 函數。TestForm 有一個(gè) “Populate”按鈕,它調用CRegexForm::SetFieldValue 將樣板數據填充到窗體中,如圖 Figure 3 所示。通常,CRegexForm 使用控制ID 來(lái)區分輸入域。它包含有 GetFieldName、GetFieldHint 和 GetFieldError來(lái)獲取域名、提示和出錯信息——它們都帶有一個(gè)參數就是控制 ID。
到此,我說(shuō)明了如何創(chuàng )建域映射,編寫(xiě)資源串,初始化你的 CRegexForm 以及通過(guò) DDX來(lái)關(guān)聯(lián)。所有這些都是序曲。真正的用戶(hù)輸入驗證是在用戶(hù)按下 OK 鍵后進(jìn)行的:
void CMyDialog::OnOK(){UpdateData(TRUE); // 拷貝屏幕輸入->對話(huà)框int nBad = m_rgxForm.Validate();if (nBad>0) {m_badFields = m_rgxForm.GetBadFields();...} UpdateData 調用 MFC 的 DDX 機制,即調用對話(huà)框的 DoDataExchange。然后 DoDataExchange調用 CRegexForm::DoDataExchange,從而將用戶(hù)輸入拷貝到其內部的 FLDINFO 結構。接著(zhù)CRegexForm::Validate 遍歷輸入域,針對域的正則表達式調用 CMRegex::Match來(lái)驗證每一個(gè)域。如果域輸入無(wú)效,CRegexForm 便在其內部的 FLDINFO 中設置錯誤碼 RGXERR_NOMATCH或者必輸于域如果為空,則設置 RGXERR_MISSING。Validate 返回無(wú)效域數量。如果有無(wú)效域,你可調用CRegexForm::GetBadFields 來(lái)獲得一個(gè)無(wú)效域 IDs 數組(STLvector)。然后你可以遍歷該數組以獲取各個(gè)錯誤嗎和出錯信息。這便是 TestForm 中 CMainDlg 建立其錯誤消息框所做的事情,如Figure 2 所示。如果只有一個(gè)域無(wú)效,CMainDlg 調用 CRegexForm::ShowBadField高亮該輸入域并在反饋窗口顯示出錯信息,如圖 Figure 3 所示。如果所有域都沒(méi)問(wèn)題,TestForm 便顯示一個(gè)消息框展示輸入的值(參見(jiàn)Figure 5)。

聯(lián)系客服