程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 對話框模板RegexTest

對話框模板RegexTest

編輯:關於VC++

對話框模板

RegexTest

我想用MFC和C++ 創建一個基於對話框的程序(主窗口本身是個對話框)。我不想使用資源(.rc)文件,而是想在內存中動態創建對話框。我在 MSDN 中找到一些線索,但沒有發現代碼例子。我了解到 DLGTEMPLATE和DLGITEMTEMPLATE 結構以及 InitModalIndirect 函數或許可以用來創建模式對話框,但我不知道從何入手。請問如何不依賴資源文件動態創建對話框?

Thomas Zeitlberger

從理論上講,動態創建對話框很簡單,但實際上那樣做很危險。就是內存中創建正確的結構並調用一系列 Indirect 對話框創建函數之一:用CDialog::CreateIndirect 創建非模式對話框,或者用CDialog::InitModalIndirect 創建模式對話框(然後調用DoModal 運行)。它們分別對應著 Win32 API 函數 ::CreateDialogIndirect和::DialogBoxIndirect。不管用什麼方法,你都得在內存中傳遞一個指向對話框模板的指針。

從概念上講,創建對話框模板很簡單,就是在內存中建立並初始化相關結構。其具體細節是有講究的,因為這些結構有點奇奇怪怪,很詭詐,你不得有一點差錯,只要有一個字節的偏差,那麼你的程序便會莫名其妙地垮掉。控件的位置和大小計算也會出現混亂,原因是對話框不使用像素,而是用對話框單位(units),它依賴對話框的字體。

要完整地討論包含所有類型控件的對話框模板不是本專欄所能勝任的。但我可以提供一個簡單的例子,它至少包含一個控件。我寫了一個類:CStringDialog,它顯示一個對話框,請求用戶輸入一個字符串,如圖 Figure 1 所示。

Figure 1 String Dialog

為了使用這個類,你只需實例化然後調用Init和DoModal 即可:

CStringDialog dlg;
dlg.Init(_T("Hi"), _T("Please enter your name:"));
if (dlg.DoModal()==IDOK) {
  CString name = dlg.m_str;
  // do something with it...
}

CStringDialog 的樣子和行為類似於所有基於對話框資源的CDialog 派生類,所不同的是該對話框用其自身模板在內存中動態生成。

那麼對話框模板到底是個什麼東西呢?對話框模板其實就是一個描述對話框的內存結構。這個模板之所以復雜並容易出錯,是因為它並非像 CREATESTRUCT和WNDCLASS 一樣是個定長結構。它是一個變長結構,其中包含有定長結構元素 DLGTEMPLATE 以及 DLGITEMTEMPLATE 結構數組,其每個數組元素對應著一個對話框控件項。DLGTEMPLATE和DLGITEMTEMPLATE 兩者都包含一些跟在 C 結構後面非常很特別的變長域。這些結構如 Figure 2 所示,Figure 3 是整個結構的布局。

Figure 3 對話框模板

對話框模板有點像匯編語言編程手冊中的內容,現在就讓我們穿上蹩腳的工作制服,立即從 DLGTEMPLATE 開始吧。

假設你分配了一塊足夠大的內存來存放整個對話框模板,首先要做的事情就填寫 DLGTEMPLATE 結構域。這一部分不難:

WORD* pTempl = new WORD[1024];
DLGTEMPLATE& dt = *((DLGTEMPLATE*)pTempl);
dt.style = WS_POPUPWINDOW|DS_MODALFRAME|WS_DLGFRAME;
dt.cdit = 3; // # dlg items
dt.x = 100; // in dlg units
// etc.

DLGTEMPLATE 結構域是自擴展的,對此我不再做進一步說明。緊跟著該結構後面的域是變長域:菜單,對話框類和標題。每一項都不能超過一個 WORD。它可以是一個空結尾的Unicode 字符串以標示某個 MENU 資源的名字,對話框類名或標題。此外,菜單和類名可以用特殊值 0xFFFF 後跟一個 16位 的ID——即可以是菜單資源的ID,也可以是預定義系統窗口類的序數。在大多數情況下,類名都應該使用0x0000(空串),它告訴 Windows 操作系統使用默認的對話框類(#32770)。多數對話框都沒有菜單,所以菜單也是0x0000(空串)。在代碼中是這樣寫的:

*pTempl++ = 0; // 菜單 (無)
*pTempl++ = 0; // 對話框類 (使用標准的對話框類)

接下來是標題,一個空結尾的Unicode 字符串:

USES_CONVERSION;
LPCWSTR wszText = T2W(_T("My Dialog"));
wcscpy((WCHAR*)pTempl, wszText);
pTempl += wcslen(wszText)+1;

這段代碼支持 Unicode 或者 ASCII,因為定義了 _UNICODE,T2W是一個串轉換宏。不要忘了增加模板指針 pTempl 的增量值,將其指到串後面的下一個 WORD。如果對話框具有 DS_SETFON 式樣,在第四個字段:16位的字體大小後跟 Unicode 字體名,例如:“Verdana”。

最後,我要指出對話框模板有一個擴展版本 DLGTEMPLATEEX,它可以讓你指定更多的域,如字體點數和重量、是否用斜體、字符集、字體名。想了解更多信息請參考文檔。這裡我僅描述一個簡單版本,因為通過在 OnInitDialog 處理例程中調用SetFont 來設置字體是很容易的事情。(對於對話框中的控件項也有一個 DLGITEMTEMPLATEEX 擴展版本)。

講了這麼多 DLGTEMPLATE。下面該看看控件。對話框中的每個控件項都是通過一個模板來描述的(DLGITEMTEMPLATE),其值不能超過一個 DWORD:

pTempl = AlignDWORD(pTempl);
DLGITEMTEMPLATE& it = *((DLGITEMTEMPLATE*)pTempl);
it.x = 0;
it.y = 0;
// etc.

與 DLGTEMPLATE 類似,DLGITEMTEMPLATE 結構後面有三個變長域。即類名,文本和“”創建數據(creation data)。類名也是空結尾的Unicode 字符串指定窗口類名(例如,“SysListView32”或者“MyFancyControl”),或者 0xFFFF 後跟特定的原子碼之一,這些編碼如 Figure 4 所示,它們都用於標准的預定義系統控件。例如,下面的代碼示范了如何創建一個靜態的文本控件:

// class immediately after DLGITEMTEMPLATE
*pTempl++ = 0xFFFF; // next WORD is atom:
*pTempl++ = 0x0082; // static control

類名後面是標題。它既可以是Unicode 字符串,也可以是特定的0xFFFF 後跟 16位的資源 ID。你還可以用0xFFFF + ID 的格式來為某個具備 SS_ICON 或 SS_BITMAP 式樣的靜態控件指定一個圖標或位圖。CStringDialog 使用字符串形式來創建其提示:

USES_CONVERSION;
LPCWSTR wszTest = T2W(_T("My Dialog"));
int maxlen = /* don''t overflow! */
wcsncpy((WCHAR*)pTempl, wszText, maxlen); pTempl += wcslen(wszText)+1;

最後,“creation data”可以是任何你想要的數據。第一個 WORD 是數據長度,如果沒有數據,其值可以是零。Windows 用LPARAM 將一個指向數據的指針傳遞給 WM_INITDIALOG(模式對話框)或者 WM_CREATE(無模式對話框)。在此我不推薦使用創建數據,因為將任何你想要的數據成員添加到對話框類中,並用對話框的構造函數或 OnInitDialog 處理例程初始化它們的做法要容易得多。但你仍然得提供一個 0 WORD 來告訴 Windows 沒有創建數據:

*pTempl++ = 0; // no creation data

一旦你填充完指定的DLGITEMTEMPLATE 數據,便可如法炮制下一個對話框控件,直到完成所有的控件。接著確保 DLGTEMPLATE::cdit 指定正確的控件總數。為了簡化建立對話框模板的過程(使之盡量少出錯)。我寫了一個輔助類,CDlgTemplateBuilder。CStringDialog 用這個類可以一步到位建立對話框:

// in CStringDialog::Init
CDlgTemplateBuilder& dtb = m_dtb;
DLGTEMPLATE* pTempl = dtb.Begin(...);
dtb.AddItem(...);
dtb.AddItem(...);
dtb.AddItem(...);
InitModalIndirect(pTempl, ...);

我已將細節模糊而突出主要思路:調用一次 Begin,然後針對每個控件調用一次 AddItem。CDlgTemplateBuilder::Begin 生成 DLGTEMPLATE 並且每次調用AddItem 生成另一個 DLGITEMTEMPLATE。CDlgTemplateBuilder 在自己的內存緩沖裡生成模板並在每次添加控件時自動增加 DLGTEMPLATE::cdit(控件項數目)。CDlgTemplateBuilder 具備輔助函數 AlignDWORD和AddText 以確保數據對齊和實現正確的字符串轉換。有關細節自己下載源代碼細細琢磨吧。

我前面說過對話框使用對話框單位,而非像素。DLGTEMPLATE和DLGITEMTEMPLATE 兩個都包含 x,y,cx h和 cy 成員來指定對話框或控件項的位置和大小。這些值都是對話框單位。每個水平方向的對話框單位是四分之一的基本單位,而每個垂直方向的對話框單位是八分之一的基本單位。一個基本單位是對話框中一個字符寬度和高度的平均值,並且依賴於對話框的字體。是不是很痛苦啊!沒錯,但憑心而論,這個想法是值得贊譽的:對話框單位使你的對話框外觀獨立於其字體。所以不管你用大字體也好,小字體也好,所有控件的相對位置是不會變的,一切都顯示正常。Windows 有一個特別的函數叫做 MapDialogRect,用來將對話框單位轉換為像素;令人驚訝的是卻沒有反向轉換函數,而這正是生成模板所需要的——但你可以用如下公式:

CSize base = ::GetDialogBaseUnits();
xDlg = MulDiv(xPixel, 4, base.cx);
yDlg = MulDiv(yPixel, 8, base.cy);

對於 CStringDialog 來說,我懶得去做這些事情,而是試驗性找到正確的值顯示出如圖 Figure 1 所示的對話框。更復雜的實現得檢查提示串的長度,或允許調用這指定尺寸。如果處理這些對話框單位讓你頭痛,那麼你就創建大小和位置都是0 的控件得了,然後實現 OnSize 處理例程將控件移到適合的像素位置。你的對話框得從 OnInitDialog 向自身發送 WM_SIZE 消息以確保第一次顯示時控件被正確定位。

最後,我是如何讓 CStringDialog 顯示如 Figure 1 所示的問號的呢?CStringDialog::Init 讓你指定提示圖標。默認是IDI_QUESTION。但 IDI_QUESTION 是一個內建的圖標,不是來自應用程序資源文件的圖標。如果你指定一個對話框模板中的資源 ID,Windows 期望它在資源文件中。那麼我如何讓 Windows 改用系統圖標呢?

當然,話雖如此,CStringDialog 檢查圖標資源 ID,看看值是否大於 IDI_APPLICATION,也就是第一個系統圖標的ID。如果該圖標 ID 在系統 ID 范圍之內,CStringDialog 通過調用::LoadIcon 來加載它,此時 hInstance 置為 NULL(用於系統圖標)並在 數據成員 m_hIcon 中保存加載的HICON。然後 CStringDialog用0xFFFF + nResID 格式(nResID=0)來構造對話框模板。這導致 Windows 創建一個靜態圖標,但並非實際的圖標,然後,CStringDialog 在 OnInitDialog 中才設置實際圖標:

// in CStringDialog::OnInitDialog()
if (m_hIcon) {
  CStatic* pStatic = (CStatic*)GetDlgItem(IDICON);
  pStatic->SetIcon(m_hIcon);
}

這樣一來,你可以將任何 IDI_XXX 形式的圖標 IDs 傳遞給 CStringDialog::Init。你還能用自己的圖標,只要其 ID 小於 IDI_APPLICATION = 32512。具體細節請參考源代碼。

我最近要寫一個正則表達式的DDV 確認程序,正巧你寫了一個(參見 2005 四月刊)。想知道為什麼你要包裝 .NET 庫,這樣無端地添加了許多依賴性(包裝庫累贅),為什麼不用Visual Studio .NET 裡現成而簡潔的正則表達式庫,你只要包含一個頭文件便可以在你的MFC 程序中使用它,atlrx.h?雖然它不是百分百標准的語法,但我寧願用它而不願添加對 .NET 框架的依賴。

Gil Rosin

將我一軍還要朝我拍磚!我甚至都不知道 ATL 有一個 regex 類。Windows 的東西太多,即使你是一個高手,也不一定就知道的那麼全。沒錯,你說得很對,ATL 確實提供了一個 regex 實現!

首先,讓我來更正一下人們關於 .NET 框架的一些印象。我知道很多人在應用程序中添加這樣的依賴性時都非常勉強,因為害怕代碼臃腫,我剛開始也是這樣。但是使用.NET 框架也許並不像你想像的那麼糟。雖然托管應用在啟動時明顯感覺性能問題,但不管你信不信,一旦框架被加載之後,微軟中間語言(MSIL)代碼甚至可以運行得比本機 EXEs 還快。那是因為 JIT (即時)編譯器真的能進行許多性能優化。雖然一些面向老版本 Windows 如 Windows 98 或 Windows NT 的應用不一定有現成的框架環境,你得自己安裝(參見“Using Visual Studio .NET 2003 to Redistribute the .NET Framework”或在 google 上搜索“dotnetfx.exe”),但較新的以及未來的Windows 版本都會將預裝框架環境。隨著 .NET 框架越來越普及,其性能也會得到不斷的改進,調用框架所產生的額外成本(性能和安裝方面)將會降至最小。

對於我的包裝庫,其實要說它的“累贅”,只不過是一層使之能編譯的薄薄糖衣。頂多添加了一個額外的函數調用,因為每個包裝器對象只是一個托管對象句柄。正像我在四月份的文章(“Wrappers: Use Our ManWrap Library to Get the Best of .NET in Native C++ Code”)中所指出的那樣,函數調用對於大多數應用程序來說無關緊要。此外,我之所以選擇 regex,只是以此為例;我的主要目的是創建一種通用機制來包裝任何框架類。最終,如果你使用的是老版本編譯器不支持 /clr,或者出於某種原因想避免使用/clr,那麼你只需要用包裝器即可。要不然就撇開包裝器,直接通過托管擴展調用框架。

現在我已經消除了誤解,我必須承認,當我得知 ATL 模板庫有一個 regex 類後,盡管我的孤陋寡聞使我有些忐忑不安,但我還是十分興奮的。當我收到你的e-mail 後做的第一件事情是將測試程序從使用.NET 庫的ManWrap 移植到 ATL。我想看看是不是很容易做到。我碰到了一些小麻煩,但沒有費什麼周折就解決了。

與 .NET 框架相比,ATL 實現的regex 比較原始,但它在多數情況下表現不錯。ATL 使用兩個模板類:一個是CAtlRegExp,用於操作正則表達式;另一個是CAtlREMatchContext,用於處理匹配。這兩個模板由另一個描述字符集特性(例如,ASCII,WCHAR 或多字節)的類參數化。在實際應用中,你可以將此忽略掉,因為 ATL 模板根據你對 _UNICODE 的設置提供默認的字符集特性 CAtlRECharTraits:

// in atlrx.h
#ifndef _UNICODE
typedef CAtlRECharTraitsA CAtlRECharTraits;
#else
typedef CAtlRECharTraitsW CAtlRECharTraits;
#endif
template <class CharTraits=CAtlRECharTraits>
class CAtlRegExp; // forward declaration

從效果上講,所有的ATL regex 默認使用TCHARs。所以在 ATL 中要創建正則表達式可以這樣寫:

CAtlRegExp<> re;
re.Parse("a+b+");

它將正則表達式解析為內部結構,這樣一來你便可以用CAtlREMatchContext 調用Match 來得到匹配:

CAtlREMatchContext<> mc;
re.Match("aaabbx", &mc);

與框架的regex 類以及其它更成熟的實現相比,CAtlREMatchContext 多少土氣一些。它有一個數據成員 m_Match,類型為 MatchGroup 結構,用於保存匹配的開始和結尾:

struct MatchGroup {
const RECHAR *szStart;
const RECHAR *szEnd;
};

此處 RECHAR 可以是任何在字符集中定義的字符類型;在實際應用中,如果使用的是默認的字符集,則它與 TCHAR 相同。CAtlREMatchContext 還可以在輸入字符串中查找匹配的子分組。調用Match 之後,mc.m_uNumGroups 保存匹配子分組的數目,你可以調用GetMatch(i,...)來獲得第 i 個子分組匹配。ATL 正則表達式的一個不可思議的事情之一是它使用花括弧來表示分組,而不是標准的圓括弧。例如:

CAtlRegExp<> re;
re.Parse("{a+}{b+}");
re.Match("aaabbx", &mc);

這段代碼將找到一個匹配(“aaabb”),兩個分組(“aaa”和“bb”)。ATL 會匹配常規的父分組,但你無法找到單獨的子匹配,除非你使用花括弧。

CAtlRegExp和CAtlREMatchContext 在使用上顯得有些笨拙。例如,為了找到所有匹配,你得用前一次匹配的szEnd 指針作為下一個輸入串的開始重復調用Match。又不是研究火箭,為什麼必須跟蹤狀態才能快速找到想要的所有匹配?於是我用一個簡單的類 CRegex 對細節進行了封裝,使編程更容易一些。主要的頭文件如 Figure 5 所示,Figure 6 是我四月份文章中關於 ManWrap 的RegexTest 程序,已經移植到 ATL。最初的程序是輸入正則表達式和字符串,RegexTest 顯示匹配和分組。它用CRegex 來遍歷匹配:

CRegex re(/* regex */);
re.SetInput(/* input */);
while (re.NextMatch()) {
  int offset=0;
  CString match = re.GetMatch(&offset);
  ...
}

CRegex 構造函數自動調用Parse(你應該檢查 m_err 看看是否出錯),如果你想要單獨分組匹配,可以調用CRegex::GetNumGroups 來獲取當前匹配的分組總數,調用CRegex::GetGroup 獲得每個分組匹配。具體細節如 Figure 7 所示。CRegex 在內部有其自己的CAtlRegExp和CAtlREMatchContext 對象,並對其在輸入串中位置進行跟蹤——不必自己去做!

Figure 6 RegexTest 運行畫面

我還添加了幾個有用的特性,ATL 沒有 regex Replace 函數,但添加一個並不難。有了框架的regex 類,CRegex::Replace 得來全不費功夫,函數靜態,這樣你用不用CRegex 對象調用都可以。例如,RegexTest 的主對話框使用靜態版本來將新行(\n)轉換為回車換行 CRLF(\r\n):

static CString LF2CRLF(LPCTSTR lpsz)
{
  return CRegex::Replace(lpsz,_T("\n"),_T("\r\n"),TRUE);
}

就這麼簡單,如果沒有 CRegex,直接用CAtlRegExp 要數行代碼才能做到。當你在微軟的基本構件塊上花點時間來做高層次的抽象,你就會出效率。這就是分層設計的本質所在。我甚至添加了一個與 .NET 框架類似的動態 Replace 函數,以便能用回調函數來實現動態替換算法,就像我四月份文章中 WordMess 程序所做的那樣。最後,我添加了 Split,以便你能用正則表達式作為分隔符將某個字符串剝離到一個子串數組中。例如:vector<CString> strs = CRegex::Split("one,two,three",",");

它返回有三個子串的向量數組:“one”,“two”,“three”。Split 的實現主要是字符串處理功夫,不過一旦你寫就了代碼,它就是你的並且永遠受用。具體細節去參考源代碼吧。

如果你決定使用ATL 正則表達式或我封裝的CRegex,請謹慎:ATL 版本的正則比表達式是一個簡化的,非標准的實現。我已經提到過它使用花括弧而不是圓括弧。ATL 還使用非標准的元字符(例如,用\b 而不是\s 表示空格),並且你無法在表示范圍時([\a\d]不工作)使用元字符。特殊字符清單參見 Figure 8。請仔細閱讀文檔,提防 bug。復雜的正則表達式並不是總能湊效,你需要 #pragma 來抑制兩個四級編譯器警告(具體細節參見代碼)。但如果你只需要一些基本的模式匹配來對用戶輸入進行有效性檢查,ATL 是可以勝任的。

結束本文之前,我想告訴 C++ fans 們正則表達式不日將肯定成為標准模板庫的一部分。其實現很可能是基於 boost 的實現,你可以從 boost.org 得到它。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved