摘要:討論Active Template Library (ATL) 3.0中的一些類,這些類圍繞著Windows API建立了一個面向對象的編程框架,使用這個框架,可以簡化Microsoft® Windows®編程并且只需要很少的系統開銷。內容包括:考察對窗口做了簡單封裝的CWindow類;使用CWindowImpl進行消息處理和消息映射;使用ATL中的對話框類以及擴展現有窗口類的功能的方法。
簡介:雖然Active Template Library (ATL)主要是為了支持COM開發而設計的,但它確實包含了很多可用于窗口設計的類。這些窗口類和ATL中的其它類一樣,都是基于模版的,并且只需要花費很少系統開銷。這篇文章就向我們演示了使用ATL創建窗口和對話框并進行消息處理的基本方法。
這篇文章假設讀者熟悉C++語言和Windows程序設計;但是并不一定要求讀者具有COM方面的知識。
CWindow:在ATL窗口類中,CWindow是最基本的。這個類對Windows API進行了面向對象的包裝,它封裝了一個窗口句柄,并提供一些成員函數來操作它,這些函數包裝了相應的Windows API。
標準的Windows程序設計看起來象這樣:
HWND hWnd = ::CreateWindow( "button", "Click me",
WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
::ShowWindow( hWnd, nCmdShow );
::UpdateWindow( hWnd );
使用ATL中的CWindow類后,等效代碼如下:
CWindow win;
win.Create( "button", NULL, CWindow::rcDefault, "Click me",
WS_CHILD );
win.ShowWindow( nCmdShow );
win.UpdateWindow();
我們應該在我們的大腦中我們應該保持這樣一個概念:ATL的窗口對象與Windows系統中的窗口是不同的。Windows系統中的窗口指的是操作系統中維持的一塊數據,操作系統靠這塊數據來操作屏幕上的一塊區域。而一個ATL窗口對象,是CWindow類的一個實例,它是一個C++對象,它的內部沒有保存任何有關屏幕區域或者窗口數據結構的內容,只保存了一個窗口的句柄,這個句柄保存在它的數據成員m_hWnd中,CWindow對象和它在屏幕上顯示出來的窗口就是靠這個句柄聯系起來的。
理解了ATL中的窗口對象和Windows系統中窗口的區別,就更加容易理解CWindow對象的構造與窗口的創建是兩個分開的過程。我們再看看前面的代碼,就會發現,首先是一個CWindow對象被構造:
CWindow win;
然后創建它的窗口:
win.Create( "button", NULL, CWindow::rcDefault, "Click me",
WS_CHILD );
我們也可以構造一個CWindow對象,然后把它和一個已經存在的窗口關聯起來,這樣我們就可以通過CWindow類的成員函數來操作這個已經存在的窗口。這種方法非常有用,因為CWindow類提供的函數都是封裝好了的,用起來很方便,比如CWindow類中的CenterWindow, GetDescendantWindow等函數用起來就比直接使用Windows API方便得多。
HWND hWnd = CreateWindow( szWndClass, "Main window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
// 下面的方法中可以任選一種:
// CWindow win( hWnd ); // 通過構造函數關聯
// 或
// CWindow win;
// win = hWnd; // 通過賦值操作符關聯
// 或
// CWindow win;
// win.Attach( hWnd ); // 使用Attach()方法關聯
win.CenterWindow(); // 現在可以使用win對象來代替hWnd進行操作
win.ShowWindow( nCmdShow );
win.UpdateWindow();
CWindow類也提供了一個HWND操作符,可以把CWindow類的對象轉化為窗口句柄,這樣,任何要求使用HWND的地方都可以使用CWindow類的對象代替:
::ShowWindow( win, nCmdShow ); // 此API函數本來要求HWND類型的參數
CWindow類使得對窗口的操作更簡單,而且不會增加系統開銷——它經過編譯和優化后的代碼與使用純API編程的代碼是等價的。
不幸的是,CWindow類不能讓我們自己決定窗口如何響應消息。當然,我們可以使用CWindow類提供的方法來使一個窗口居中或隱藏,甚至可以向一個窗口發送消息,但是當窗口收到消息后怎么處理則取決于創建這個窗口時使用的窗口類,如果我們是創建的是”button”類的窗口,那么它的表現就象個按鈕,如果用”listbox”類創建,那它就具有跟列表框相同的行為,使用CWindow類我們沒有辦法改變這點。幸好,ATL為我們提供了另外一個類CWindowImpl,它允許我們指定窗口的新行為。
CWindowImpl:CWindowImpl類是從CWindow類派生的,所以我們依然可以使用CWindow類中的成員函數,但是CWindowImpl類的功能更強大,它允許我們指定窗口怎樣處理消息。在傳統的窗口編程中,如果我們要處理窗口消息,我們必須使用窗口函數;但是使用ATL,我們只需要在我們的ATL窗口類中定義一個消息映射。
首先,從CWindowImpl類派生自己的窗口類,如下:
class CMyWindow : public CWindowImpl
{
注意,我們自己的類名必須作為一個模版參數傳遞給CWindowImpl類。
然后在類的定義里面定義如下的消息映射:
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_PAINT,OnPaint)
MESSAGE_HANDLER(WM_CREATE,OnCreate)
MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
END_MSG_MAP()
下面這句
MESSAGE_HANDLER(WM_PAINT,OnPaint)
的意思是,當WM_PAINT消息到達時,將調用CMyWindow::OnPaint成員函數。
最后就是定義處理消息的函數了,如下:
LRESULT OnPaint(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnCreate(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnDestroy(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
}; // CmyWindow
這些函數中的參數意義為:第一個是消息ID,中間的兩個參數的意義取決于消息類型,第四個參數是一個標志,用它來決定這個消息是已經處理完了還是需要進一步的處理。關于這些參數,我們在Message Map小結有更詳細的討論。
當窗口收到一個消息,它將從消息映射表的頂部開始查找匹配的消息處理函數,因此把最常用的消息放在消息映射表的前面是個不錯的注意。如果沒有找到匹配的消息處理函數,則這個消息被發送到默認的窗口過程進行處理。
ATL的消息映射表封裝了Windows的消息處理過程,它比傳統的窗口函數中的大量switch分支或者if語句看起來更加直觀。
要創建一個基于CWindowImpl派生類的窗口,請調用CWindowImpl類的Create方法:
CMyWindow wnd; // 構造一個 CMyWindow 類的對象
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );
注意,CWindowImpl類的Create方法與CWindow類的Create方法略有不同,在CWindow類的Create中,我們必須指定一個注冊了的窗口類,但是CWindowImpl則不同,它創建一個新的窗口類,因此,不需要為它指定窗口類。
一個簡單而完整的示例:
這篇文章中的大部分示例都只是代碼片段,但是下面列出的是一個完整的Hello world的示例程序。雖然我們使用的是ATL,但是沒有涉及到COM,因此在使用Visual C++®建立項目的時候,我們選擇Win32® application而不是ATL COM:
在stdafx.h文件中,加入下面幾行:
#include <atlbase.h>
extern CComModule _Module;
#include <atlwin.h>
在hello.cpp文件中,寫如下代碼:
#include "stdafx.h"
CComModule _Module;
class CMyWindow : public CWindowImpl<CMyWindow> {
BEGIN_MSG_MAP( CMyWindow )
MESSAGE_HANDLER( WM_PAINT, OnPaint )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnPaint( UINT, WPARAM, LPARAM, BOOL& ){
PAINTSTRUCT ps;
HDC hDC = GetDC();
BeginPaint( &ps );
TextOut( hDC, 0, 0, _T("Hello world"), 11 );
EndPaint( &ps );
return 0;
}
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ){
PostQuitMessage( 0 );
return 0;
}
};
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int )
{
_Module.Init( NULL, hInstance );
CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) ){
TranslateMessage( &msg );
DispatchMessage( &msg );
}
_Module.Term();
return msg.wParam;
}
在這個示例程序中,CmyWindow是從CWindowImpl派生的,它的消息映射捕獲了兩個消息WM_PAINT和WM_DESTROY,當收到WM_PAINT消息時,它的成員函數OnPaint處理這個消息并在窗口上輸出“Hello world”,當收到WM_DESTROY消息時,也就是當用戶關閉這個窗口的時候,調用OnDestroy函數處理這個消息,在OnDestroy函數中調用PostQuitMessage來結束消息循環。
WinMain函數中創建了一個CmyWindow類的實例并實現了一個標準的消息循環。(有一些地方,我們必須遵循ATL的規范,比如在這里我們必須使用_Module。)
消息映射:
有三組用于消息映射的宏,他們分別是:
- 窗口消息映射宏,用于所有的窗口消息(如WM_CREATE、WM_PAINT等);
- 命令消息映射宏,專用于WM_COMMAND消息(比如由控件或菜單發出的消息);
- 通知消息映射宏,專用于WM_NOTUFY消息(通常由通用控件發出此消息,比如工具欄控件或列表視圖控件)
窗口消息映射宏:
有兩個窗口消息映射宏,他們分別是:
- MESSAGE_HANDLER
- MESSAGE_RANGE_HANDLER
第一個宏將一個特定的消息映射到相應的處理函數;第二個宏將一組消息映射到一個處理函數。消息處理函數都要求具有如下的原形:
LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
其中,參數uMsg是消息標識,wParam和lParam是兩個附加與消息的參數,(他們的具體意義取決與消息類別。)
消息處理函數使用bHandled來標志消息是否已經被完全捕獲,如果bHandled被設置成FALSE,程序將繼續在消息映射表的后續部分查找這個消息的其它處理函數。這個特性使得我們對一個消息使用多個處理函數成為可能。什么時候需要對一個消息使用多個處理函數呢?可能是在對多個類鏈接時,也可能是我們只想對一個消息做出響應但是并不真正捕獲它。在處理函數被調用之前,bHandled被置為TRUE,所以如果我們不在函數的結尾顯式地將它置為FALSE,則消息映射表的后續部分不會被繼續查找,也不會有其它的處理函數被調用。
命令消息映射宏:
命令消息映射宏只處理命令消息(WM_COMMAND消息),但是它能讓我們根據消息類型或者發送命令消息的控件ID來指定消息處理函數。
- COMMAND_HANDLER映射一個特定控件的一條特定消息到一個處理函數;
- COMMAND_ID_HANDLER映射一個特定控件的所有消息到一個處理函數;
- COMMAND_CODE_HANDLER映射任意控件的一個特定消息到一個處理函數;
- COMMAND_RANGE_HANDLER映射一定范圍內的控件的所有消息到一個處理函數;
- COMMAND_RANGE_CODE_HANDLER映射一定范圍內的控件的一條特定消息到一個處理函數。
命令消息處理函數應該具有如下的原形:
LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
其中,參數wNotifyCode代表消息代碼,wID代表發送消息的控件的ID,hWndCtl代表發送消息的控件的窗口句柄,bHandled的意義如前所述。
通知消息映射宏:
通知消息映射宏用來處理通知消息(WM_NOTUFY消息),它根據通知消息的類型和發送通知消息的控件的不同將消息映射到不同的處理函數,這些宏與前面講的命令消息映射宏是等價的,唯一的不同就是它處理的是通知消息而不是命令消息。
- NOTIFY_HANDLER
- NOTIFY_ID_HANDLER
- NOTIFY_CODE_HANDLER
- NOTIFY_RANGE_HANDLER
- NOTIFY_RANGE_CODE_HANDLER
通知消息處理函數都需要如下的原形:
LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
其中,參數idCtrl代表發送通知消息的控件的ID,參數pnmh是指向一個NMHDR結構的指針,bHandled的意義如前所述。
通知消息包含了一個指向消息細節的結構的指針,例如,當一個列表視圖控件發送一個通知消息,這個消息就包含了一個指向NMLVDISPINFO結構的指針,所有類似于NMLVDISPINFO的結構都包含一個NMHDR結構的頭,pnmh就指向這個頭,如果需要訪問這種結構中頭部以外的其它數據成員,可以將pnmh轉化成相應類型的指針。
例如,我們如果要處理列表視圖控件發出的LVN_ENDLABELEDIT通知消息,我們可以把下面這行代碼放到消息映射表中:
NOTIFY_HANDLER( ID_LISTVIEW, LVN_ENDLABELEDIT, OnEndLabelEdit)
這個通知消息附帶的額外信息包含在一個NMLVDISPINFO結構中,因此,消息處理函數看起來應該象下面這個樣子:
LRESULT OnEndLabelEdit(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
// The item is -1 if editing is being canceled.
if ( ((NMLVDISPINFO*)pnmh)->item.iItem == -1) return FALSE;
...
可以看出,pnmh指針被轉化成NMLVDISPINFO*類型,以便訪問頭部結構以外的數據。
為現有的窗口類添加功能:有許多向現有的窗口添加功能的方法。如果這個類是ATL窗口類,我們可以從這個窗口類派生自己的類,就象Base Class Chaining中描述的一樣。這種方法主要是一個C++類的繼承加上一點消息映射的鏈接。
如果我們想擴展一個預定義的窗口類(如按紐類或列表框類)的功能,我們可以超類化它。就是創建一個基于這個預定義類的新類,并在消息映射表中添加消息映射以增強它的功能。
有些時候,我們需要改變一個已經存在的窗口實例的行為,而不是一個窗口類——或許我們要讓一個對話框上的編輯框做點什么特別的事情。在這種情況下,我們可以寫一個新的ATL窗口類,并子類化這個已經存在的編輯框。任何本該發送到這個編輯框的消息都會先被發送到這個子類的對象。
另外一種可選的方法:我們也可以讓這個編輯框成為一個被包含的窗口,所有發送到這個編輯框的消息都會經過它的容器窗口;我們可以在這個容器窗口中為這個被包含的窗口實現特殊的消息處理。
最后的一種方法就是消息反射,當一個窗口收到一個消息后不處理它,而是反射給發送這個消息的窗口自己處理,這種技術可以用來創建自包含的控件。
基類消息鏈(Base Class Chaining):如果我們已經有一些實現了特定功能的ATL窗口類,我們可以從它們派生新類以充分利用繼承的優點。比如:
class CBase: public CWindowImpl< CBase >
// simple base window class: shuts down app when closed
{
BEGIN_MSG_MAP( CBase )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& )
{
PostQuitMessage( 0 );
return 0;
}
};
class CDerived: public CBase
// derived from CBase; handles mouse button events
{
BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
END_MSG_MAP()
LRESULT OnButtonDown( UINT, WPARAM, LPARAM, BOOL& )
{
ATLTRACE( "button down\n" );
return 0;
}
};
// in WinMain():
...
CDerived win;
win.Create( NULL, CWindow::rcDefault, "derived window" );
可是,上面的代碼有一個問題。當我們在調試模式下運行這個程序,一個窗口出現了,如果我們在這個窗口中單擊,“button down”將出現在輸出窗口中,這是CDrived類的功能,可是,當我們關閉這個窗口的時候,程序并不退出,盡管CBase類處理了WM_DESTROY消息并且CDrived類是從CBase類派生的。
Why?因為我們必須明確地將一個消息映射表鏈接到另外一個。如下:
BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
CHAIN_MSG_MAP( CBase ) // 鏈接到基類
END_MSG_MAP()
現在,任何在CDrived類中沒有被處理的消息都會被傳到CBase類中。
為什么不自動將派生類的消息映射和它的基類的消息映射鏈接起來呢?這是因為在ATL的體系結構中有很多多重繼承的情況,這種情況下沒有辦法知道究竟應該鏈接到哪個基類,所以只好讓程序員自己來做決定。
可選的消息映射:
消息映射鏈允許多個類同時進行消息處理,同時也帶來了問題:如果我們在多個類中都要響應WM_CREATE消息,但是不同的類需要基類提供不同的處理,怎么辦呢?為了解決這個問題,ATL使用了可選的消息映射:將消息映射表分成很多節,每一節用不同的數字標識,每一節都是一個可選的消息映射表。
// in class CBase:
BEGIN_MSG_MAP( CBase )
MESSAGE_HANDLER( WM_CREATE, OnCreate1 )
MESSAGE_HANDLER( WM_PAINT, OnPaint1 )
ALT_MSG_MAP( 100 )
MESSAGE_HANDLER( WM_CREATE, OnCreate2 )
MESSAGE_HANDLER( WM_PAINT, OnPaint2 )
ALT_MSG_MAP( 101)
MESSAGE_HANDLER( WM_CREATE, OnCreate3 )
MESSAGE_HANDLER( WM_PAINT, OnPaint3 )
END_MSG_MAP()
如上,基類的消息映射表由3節組成:一個默認的消息映射表(隱含的標識為0)和兩個可選的消息映射表(標識為100和101)。
當你鏈接消息映射表時,指定你所希望的方案的標識,如下:
class CDerived: public CBase {
BEGIN_MSG_MAP( CDerived )
CHAIN_MSG_MAP_ALT( CBase, 100 )
END_MSG_MAP()
...
CDrived類的消息映射表鏈接到CBase類中標識號為100的可選節,因此當WM_PAINT到達時,CBase::OnPaint2被調用。
(譯者注:我覺得這種方法不太合乎C++的思想,基類的編寫者不一定總能知道派生自它的類會有哪些需求,而且把所有不同的版本都在基類中實現,基類中無用的代碼量會大大增加。更好的辦法應該是把基類中的消息處理函數聲明為虛函數。總之,我覺得這一小節并不能體現出可選消息映射的真正用途。)
其它類型的鏈:
除了基類消息映射鏈,ATL也提供了成員鏈(member chaining)和動態鏈(dynamic chaining),這些很少使用到的鏈技術超出了我們這篇文章的討論范圍,但是可以簡單提一下。成員鏈允許把消息映射鏈接到一個類的成員變量,動態鏈允許在運行時進行動態鏈接。如果你想了解更多,請參考ATL文檔中的CHAIN_MSG_MAP_DYNAMIC 和CHAIN_MSG_MAP_MEMBER的相關內容。
窗口的超類化:超類化定義一個類,并為預定義的窗口類(如按鈕類或列表框類)添加新的功能,下面的例子超類化一個按鈕,讓這個按鈕在被單擊的時候發出蜂鳴。
class CBeepButton: public CWindowImpl< CBeepButton >
{
public:
DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("Button") )
BEGIN_MSG_MAP( CBeepButton )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown )
END_MSG_MAP()
LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
MessageBeep( MB_ICONASTERISK );
bHandled = FALSE; // alternatively: DefWindowProc()
return 0;
}
}; // CBeepButton
DECLARE_WND_SUPERCLASS宏聲明了這個窗口的類名(“BeepButton”)和被超類化的類名(“Button”)。它的消息映射表只有一個入口項,將WM_LBUTTONDOWN消息映射到OnLButtonDown函數。其余的消息都讓默認的窗口過程處理,除了可以發出蜂鳴外,CbeepButton需要和其它的按鈕表現相同,因此在OnLButtonDown函數的最后,需要將bHandled設置為FALSE,讓默認的窗口過程在OnLButtonDown函數完成后對WM_LBUTTONDOWN消息進行其它的處理。(另外的一種方法是直接調用DefWindowProc函數。)
到目前為止,我們所做的只是定義了一個新類;我們依然需要創建一些真正的CbeepButton窗口,下面的類定義了兩個CbeepButton類型的成員變量,因此,當這個類的窗口被創建時,將會創建兩個CbeepButton類型的子窗口。
const int ID_BUTTON1 = 101;
const int ID_BUTTON2 = 102;
class CMyWindow: public CWindowImpl< CMyWindow, CWindow,
CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE> >
{
CBeepButton b1, b2;
BEGIN_MSG_MAP( CMyWindow )
MESSAGE_HANDLER( WM_CREATE, OnCreate )
COMMAND_CODE_HANDLER( BN_CLICKED, onClick )
END_MSG_MAP()
LRESULT onClick(WORD wNotifyCode, WORD wID, HWND hWndCtl,
BOOL& bHandled)
{
ATLTRACE( "Control %d clicked\n", wID );
return 0;
}
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& )
{
RECT r1 = { 10, 10, 250, 80 };
b1.Create(*this, r1, "beep1", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON1);
RECT r2 = { 10, 110, 250, 180 };
b2.Create(*this, r2, "beep2", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON2);
return 0;
}
}; // CMyWindow
窗口的子類化:子類化允許我們改變一個已經存在的窗口的行為,我們經常用它來改變控件的行為。它的實現機制是插入一個消息映射表來截取發向控件的消息。舉例說明:假設有一個對話框,對話框上有一個編輯框控件,我們想讓這個控件只接受不是數字的字符。我們可以截獲發往這個控件的WM_CHAR消息并拋棄接收到的數字字符。下面的類實現這個功能:
class CNoNumEdit: public CWindowImpl< CNoNumEdit >
{
BEGIN_MSG_MAP( CNoNumEdit )
MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
TCHAR ch = wParam;
if( _T(''0'') <= ch && ch <= _T(''9'') )
MessageBeep( 0 );
else
bHandled = FALSE;
return 0;
}
};
這個類只處理一個消息WM_CHAR,如果這個字符是數字的話,則調用MessageBeep( 0 )并返回,這樣可以有效地忽略這個字符。如果不是數字,則將bHandled設置為FALSE,指明默認的窗口過程這個消息需要進一步處理。
現在我們將子類化一個編輯框控件,以便CnoNumEdit能夠搶先處理發到這個編輯框得消息。(下面得例子用到了CdialogImpl類,這個類我們將在ATL中的對話框類一節中介紹。)在這個例子中,CmyDialog類中用到了一個對話框資源(ID號為IDD_DIALOG1),對話框中有一個編輯框控件(ID號為IDC_EDIT1),當對話框初始化的時候,編輯框經過SubclassWindow而變成一個不接受數字的編輯框:
class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP( CMyDialog )
MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& )
{
ed.SubclassWindow( GetDlgItem( IDC_EDIT1 ) );
return 0;
}
CNoNumEdit ed;
};