话接上回我们的第一个WTL程序, 现在我们一步步的往下看这段代码:
atlapp.h是WTL的基本头文件,它定义了下面用到的CAppModule,但在它之前如果不包含atlbase.h编译器不干,这方面它说了算。(另外atlbase.h里也确实有不少好东西,如CComPtr,CComQIPtr,CComBSTR,CRegKey,CComAutoCriticalSection等)
atlframe.h定义了我们要用到时的CFrameWindowImpl, 同样在它之前必须包含atlwin.h,而且编译器说atlwin.h需要_Module变量 ~!@#$%^& ...,只好先定义这个CAppModule _Module了。
atlcrack.h定义了一系列如MSG_WM_PAINT的消息处理宏, atlmisc.h则定义了CRect,CSize,CString等常用的类。(没错,就是传说中的CString,不过不建议用,有BCB里老出问题,有谁能发现其中根源不要忘了说一声啊)
接着是我们定义的CMyWin类
class CMyWin : public CFrameWindowImpl<CMyWin>{
...
};
不得不承认这种模板定义比较BT, 但编译器接受它,没办法,它最大嘛。我们只好先消化一下这种编程模式:
上面这段代码会打印出Hello C1和Hello C2. 很象多态是不是? 注意看我们基类里的Print可不是virtual的,这完全是Base<C1>和Base<C2>里的模板起的作用。这样做的优点是它消除了多态的缺点(关于多态的缺点,网上一找一大堆)。
DECLARE_FRAME_WND_CLASS_EX("MainWClass",0,CS_HREDRAW|CS_VREDRAW,COLOR_WINDOW) 宏用来帮助定义WNDCLASSEX(参考上一回里的SDK代码中的InitApplication函数)。 第一第三个参数对照着一看就明白。第二个参数是一个资源ID号, 如果是一个有效的资源ID, WTL会为我们载入图标,光标,菜单,标题文字等给CMyWin, 因为我们没有加入任何资源,所以写了个0。 最后一个参数是背景色。
也可以使用简单一点的DECLARE_FRAME_WND_CLASS,它只需要前两个参数,就象这样: DECLARE_FRAME_WND_CLASS("MainWClass",0)
接下来是一个消息映射宏,和我们VCL里的消息映射很象: 由BEGIN_MSG_MAP_EX() 和 END_MSG_MAP() 加是 中间的一堆消息映射组成。
对于系统已经定义了的消息,我们只需在消息名称前加MSG_即可,如MSG_WM_PAINT 对应 WM_PAINT , MSG_WM_CREATE 对应 WM_CREATE, MSG_WM_SIZE 对应 WM_SIZE等等...
对于我们自己定义的消息,WTL有一个泛化的一个消息映射宏:MESSAGE_HANDLER
然而我们不得不面对WTL资料奇缺的问题,微软官方不对其提供文档(不知道以后会不会)。要看MSG_WM_PAINT,MESSAGE_HANDLER的定义,我们只能从WTL源码里找,在BCB里输入MSG_WM_PAINT,然后右击它并选择Find Declaration,BCB会为我们找到它的定义:
从MSG_WM_PAINT的定义里我们知道了我们可以写下这样的代码:
...
MSG_WM_PAINT(OnPaint)
...
void OnPaint(HDC hdc){
...
}
提示: 我们可以使用Doxygen来生成ATL和WTL的文档,以方便我们查阅。 只是Doxygen的使用方法不在本文范围之列。
对比SDK与WTL的代码,细心的读者一定发现了我们没有实现WM_DESTROY的映射,但程序却能正常退出?消息映射里除了MSG_之外还有一个CHAIN_MSG_MAP(...), 它的作用是把我们没处理的消息传给其它类(这个类必须继承自CMessageMap,以后我们再说这个)来处理。CFrameWindowImpl也是从CMessageMap继承来的,它实现了WM_DESTROY的消息处理,所以我们用CHAIN_MSG_MAP(CFrameWindowImpl<CMyWin>)命令把余下的消息都扔给它来做。
呼~~接下来就是WinMain了
我们很早之前就定义的_Module在这里终于用上了, 这个_Module相当于我们整个程序流程的管家, 在所有WTL成员动手之前,先要它调用Init(),最后还得要它收尾Term().
CAppModule的Init函数有三个参数, 第一和第三一般用在写COM组件上,我们平时用不到。关键是第二个参数:Instance, WTL组件里需要LoadIcon,LoadBitmap之类的资源都会用到这个Instance.
然后就是我们的WTL窗体了,对照SDK代码,明显是API CreateWindow和ShowWindow的C++包装.
接下来就是一个消息循环类:CMessageLoop, 它的Run函数里就一消息循环(当然比我们之前写的SDK那个复杂那么一点点)。
注: _Module.Init和_Module.Term之间用{}对包裹起来是为了在_Module.Term()之前保证CMyWin和CMessageLoop正确析构。
对比一下WTL代码和VCL代码会发现 CAppModule + CMessageLoop 和我们VCL里的Application有很多相似之处哦。
这个CFrameWindowImpl模板类自然对应于我们的TForm啦, 我们的CMyWin从它这里继承。
现在,对WTL的程序已经有一个总体上的了解了, 我们开始写第二个WTL程序:一个NB记事本。
起手式和第一个一样, File->New->Other... 选Console Wizard, 里面除了C++, 什么也不选。
这次我们要用到之间说的ResEdit了, 打开ResEdit,新建一个RC文件, 加入Icon, Menu, 和一个String Table, 里面加入一句标题。 使Icon,Menu及标题字符串的ID号相同(我把它取名为IDR_MAINFORM)。
保存后打开RC文件,把#include <commctrl.h>改成#include <commctrl.r>, 我们自己建一个名为commctrl.r的文件,内容如下:
这个文件要保存好,以后一直要用的。
最后把这个RC文件加入到我们的BCB工程里去。
新建一个名为Unit1.h的文件,包含文件和之前的WTL程序类似:
注意CAppModule _Module改成了extern CAppModule _Module,因为_Module是在Unit1.cpp里定义的。 另外加了几个头文件,atlctrls.h和atlctrlx.h包含了一堆Windows控件的C++封装(如Edit,Button等),atldlgs.h则封装了一堆对话框(如打开对话框,字体对话框等)
先写个框架:
Unit1.cpp和之前的那个代码一样:
好,可以编译执行了, 运行后是不是已经有菜单,图标和标题啦?
另外我用_T宏来定义字符串 来和Unicode兼容, 我们的BCB当然也能编译Unicode程序, 在BCB的主菜单Project->Options->Directories/Conditionals页, 在Conditionals里添加UNICODE和_UNICODE两项。
再编译一遍,出错啦? 嗯,我忘了一件事,BCB自带的crtdbg.h有个低级BUG,我们来修改一下:
打开crtdbg.h,找到第54行, 把整个__ASSERTE_Helper函数改成:
再编译一遍,大功告成,是如假包换的Unicode窗体。
现在应该加入"TMemo",嗯,而且应该是"alClient"的,它要撑满整个窗体。
开始,在class CNBNoteBook 里定义一个CEdit变量,我把它取名为m_edtText,这个CEdit单行时是"TEdit",多行时就是"TMemo"了。
这个CEdit最好的建立时机应该在主窗体刚建立之后,我们来映射这个主窗体的WM_CREATE消息。
在BEGIN_MSG_MAP_EX下面加入:MSG_WM_CREATE(OnCreate) ,并在class CNBNoteBook 里定义一个LRESULT OnCreate(LPCREATESTRUCT lpCS);函数, 内容:
再运行的话一个记事本的样子就已经显现出来了,而这个m_edtText的"alClient"是怎么做到的呢? 看OnCreate里很重要的一句话: this->m_hWndClient = m_edtText, CFrameWindowImpl会在改变大小时自动调整m_hWndClient关联的窗口大小。 另外它还有m_hWndStatusBar和m_hWndToolBar, 都是HWND类型, 分别用于管理"TStatusBar","TToolBar/TCoolBar/TControlBar"。
OnCreate里还有一句SetMsgHandled(false);,它的作用是告诉窗体处理完OnCreate后继续调用默认的WM_CREATE处理函数。如果不写,它默认是SetMsgHandled(true),即不进行默认消息处理。
现在就剩下菜单处理了,菜单太多,这里只取几个说说:
保存功能:
要用到"TSaveDialog",在WTL里"TOpenDialog/TSaveDialog"合并成一个"CFileDialog",调用方法如下:
查找功能:
要用到"TFindDialog",在WTL里"TFindDialog/TReplaceDialog"合并成了"CFindReplaceDialog",并且使用方法与BCB里的区别很大:它是非模态窗体,靠发送消息(CFindReplaceDialog::GetFindReplaceMsg())与窗体通信。调用方法:
添加一个消息映射:MESSAGE_HANDLER(CFindReplaceDialog::GetFindReplaceMsg(), OnFindMsg),并在class CNBNoteBook 里定义一个LRESULT OnFindMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled);函数, 内容:
在Find的消息处理里,我们可以从lParam里取回CFindReplaceDialog,然后跟据CFindReplaceDialog的方法:GetFindString(),GetReplaceString(),SearchDown(),FindNext(),MatchCase(),MatchWholeWord(),ReplaceCurrent(),ReplaceAll(),IsTerminating()提供的信息做你想你的事情。
现在我们再给记事本加入一个"TStatusBar", 我们前面提到过的CFrameWindowImpl的成员变量m_hWndStatusBar要派上用场了。
在class CNBNoteBook 里定义一个CMultiPaneStatusBarCtrl类:m_StatusBar, 修改OnCreate函数,添加如下语句:
然后用ResEdit在StringTable里加入一个名为IDS_PANE2的字符串,这个字符串的作用是保证"TStatusBar"的第二个"Panel"有足够的长度,我用了15个'@'字符。
现在记事本底下有一个状态栏了。我们要在"TStatusBar"的第二个"Panel"里显示当前光标的位置,一种方法是用定时器更新,这种方法的缺陷是定时间隔比较难定,短了占CPU时间,长了又更新不及时。好在WTL为我们提供了另一种方案,在空闲时更新,象我们这个程序对CPU来说空闲时间实在是太多了,可以利用利用这个特点。
要支持空闲时操作,我们的CNBNoteBook就要从CIdleHandler继承,这里被人骂得狗血淋头的C++多继承帮了我们的大忙。 修改CNBNoteBook如下:
CIdleHandler很简单,只有一个纯虚函数:BOOL OnIdle(), 如果配置完毕,在程序消息队列为空时这个OnIdle就会被调用。
为了让消息队列支持这个OnIdle,我们还得做这些事:
修改WinMain如下:
没什么,只是加了个_Module.AddMessageLoop和_Module.RemoveMessageLoop。
修改CNBNoteBook::OnCreate,在里面加入:
好了,现在我们可以为OnIdle写代码了,我们在这里更新菜单状态,当前光标位置:
注: 建议大家去下一本<WTL for MFC Programmers>中文版看看,那里讲得比较详细,对BCB同样适用。
最后是我对BCB里使用WTL的一点笔录,
#include
<
atlbase.h
>
#include < atlapp.h >
CAppModule _Module;
#include < atlwin.h >
#include < atlframe.h >
#include < atlcrack.h >
#include < atlmisc.h >
#include < atlapp.h >
CAppModule _Module;
#include < atlwin.h >
#include < atlframe.h >
#include < atlcrack.h >
#include < atlmisc.h >
atlframe.h定义了我们要用到时的CFrameWindowImpl, 同样在它之前必须包含atlwin.h,而且编译器说atlwin.h需要_Module变量 ~!@#$%^& ...,只好先定义这个CAppModule _Module了。
atlcrack.h定义了一系列如MSG_WM_PAINT的消息处理宏, atlmisc.h则定义了CRect,CSize,CString等常用的类。(没错,就是传说中的CString,不过不建议用,有BCB里老出问题,有谁能发现其中根源不要忘了说一声啊)
接着是我们定义的CMyWin类
class CMyWin : public CFrameWindowImpl<CMyWin>{
...
};
不得不承认这种模板定义比较BT, 但编译器接受它,没办法,它最大嘛。我们只好先消化一下这种编程模式:
template
<
class
T
>
class
Base
{
public:
void DoPrint(){
T* pT = (T*)this; //强制转换this指针为T类型
pT->Print(); //调用T::Print();
}
protected:
void Print(){
cout<< "Hello Base" <<endl;
}
} ;
class C1 : public Base < C1 > {
public:
void Print(){
cout<< "Hello C1" << endl;
}
} ;
class C2 : public Base < C2 > {
public:
void Print(){
cout<< "Hello C2" << endl;
}
} ;
main() {
C1 c1;
C2 c2;
c1.DoPrint();
c2.DoPrint();
system("pause");
}
public:
void DoPrint(){
T* pT = (T*)this; //强制转换this指针为T类型
pT->Print(); //调用T::Print();
}
protected:
void Print(){
cout<< "Hello Base" <<endl;
}
} ;
class C1 : public Base < C1 > {
public:
void Print(){
cout<< "Hello C1" << endl;
}
} ;
class C2 : public Base < C2 > {
public:
void Print(){
cout<< "Hello C2" << endl;
}
} ;
main() {
C1 c1;
C2 c2;
c1.DoPrint();
c2.DoPrint();
system("pause");
}
上面这段代码会打印出Hello C1和Hello C2. 很象多态是不是? 注意看我们基类里的Print可不是virtual的,这完全是Base<C1>和Base<C2>里的模板起的作用。这样做的优点是它消除了多态的缺点(关于多态的缺点,网上一找一大堆)。
DECLARE_FRAME_WND_CLASS_EX("MainWClass",0,CS_HREDRAW|CS_VREDRAW,COLOR_WINDOW) 宏用来帮助定义WNDCLASSEX(参考上一回里的SDK代码中的InitApplication函数)。 第一第三个参数对照着一看就明白。第二个参数是一个资源ID号, 如果是一个有效的资源ID, WTL会为我们载入图标,光标,菜单,标题文字等给CMyWin, 因为我们没有加入任何资源,所以写了个0。 最后一个参数是背景色。
也可以使用简单一点的DECLARE_FRAME_WND_CLASS,它只需要前两个参数,就象这样: DECLARE_FRAME_WND_CLASS("MainWClass",0)
接下来是一个消息映射宏,和我们VCL里的消息映射很象: 由BEGIN_MSG_MAP_EX() 和 END_MSG_MAP() 加是 中间的一堆消息映射组成。
对于系统已经定义了的消息,我们只需在消息名称前加MSG_即可,如MSG_WM_PAINT 对应 WM_PAINT , MSG_WM_CREATE 对应 WM_CREATE, MSG_WM_SIZE 对应 WM_SIZE等等...
对于我们自己定义的消息,WTL有一个泛化的一个消息映射宏:MESSAGE_HANDLER
然而我们不得不面对WTL资料奇缺的问题,微软官方不对其提供文档(不知道以后会不会)。要看MSG_WM_PAINT,MESSAGE_HANDLER的定义,我们只能从WTL源码里找,在BCB里输入MSG_WM_PAINT,然后右击它并选择Find Declaration,BCB会为我们找到它的定义:
#define
MSG_WM_PAINT( func ) if (uMsg == WM_PAINT)
{
SetMsgHandled(TRUE);
func((HDC)wParam);
lResult = 0;
if(IsMsgHandled())
return TRUE;
}
#define MESSAGE_HANDLER( msg, func ) if(uMsg == msg)
{
bHandled = TRUE;
lResult = func(uMsg, wParam, lParam, bHandled);
if(bHandled)
return TRUE;
}
{
SetMsgHandled(TRUE);
func((HDC)wParam);
lResult = 0;
if(IsMsgHandled())
return TRUE;
}
#define MESSAGE_HANDLER( msg, func ) if(uMsg == msg)
{
bHandled = TRUE;
lResult = func(uMsg, wParam, lParam, bHandled);
if(bHandled)
return TRUE;
}
从MSG_WM_PAINT的定义里我们知道了我们可以写下这样的代码:
...
MSG_WM_PAINT(OnPaint)
...
void OnPaint(HDC hdc){
...
}
提示: 我们可以使用Doxygen来生成ATL和WTL的文档,以方便我们查阅。 只是Doxygen的使用方法不在本文范围之列。
对比SDK与WTL的代码,细心的读者一定发现了我们没有实现WM_DESTROY的映射,但程序却能正常退出?消息映射里除了MSG_之外还有一个CHAIN_MSG_MAP(...), 它的作用是把我们没处理的消息传给其它类(这个类必须继承自CMessageMap,以后我们再说这个)来处理。CFrameWindowImpl也是从CMessageMap继承来的,它实现了WM_DESTROY的消息处理,所以我们用CHAIN_MSG_MAP(CFrameWindowImpl<CMyWin>)命令把余下的消息都扔给它来做。
呼~~接下来就是WinMain了
我们很早之前就定义的_Module在这里终于用上了, 这个_Module相当于我们整个程序流程的管家, 在所有WTL成员动手之前,先要它调用Init(),最后还得要它收尾Term().
CAppModule的Init函数有三个参数, 第一和第三一般用在写COM组件上,我们平时用不到。关键是第二个参数:Instance, WTL组件里需要LoadIcon,LoadBitmap之类的资源都会用到这个Instance.
然后就是我们的WTL窗体了,对照SDK代码,明显是API CreateWindow和ShowWindow的C++包装.
接下来就是一个消息循环类:CMessageLoop, 它的Run函数里就一消息循环(当然比我们之前写的SDK那个复杂那么一点点)。
注: _Module.Init和_Module.Term之间用{}对包裹起来是为了在_Module.Term()之前保证CMyWin和CMessageLoop正确析构。
对比一下WTL代码和VCL代码会发现 CAppModule + CMessageLoop 和我们VCL里的Application有很多相似之处哦。
这个CFrameWindowImpl模板类自然对应于我们的TForm啦, 我们的CMyWin从它这里继承。
现在,对WTL的程序已经有一个总体上的了解了, 我们开始写第二个WTL程序:一个NB记事本。
起手式和第一个一样, File->New->Other... 选Console Wizard, 里面除了C++, 什么也不选。
这次我们要用到之间说的ResEdit了, 打开ResEdit,新建一个RC文件, 加入Icon, Menu, 和一个String Table, 里面加入一句标题。 使Icon,Menu及标题字符串的ID号相同(我把它取名为IDR_MAINFORM)。
保存后打开RC文件,把#include <commctrl.h>改成#include <commctrl.r>, 我们自己建一个名为commctrl.r的文件,内容如下:
#ifndef WC_HEADER
#define WC_HEADER "SysHeader"
#endif
#ifndef WC_LISTVIEW
#define WC_LISTVIEW "SysListView"
#endif
#ifndef WC_TREEVIEW
#define WC_TREEVIEW "SysTreeView"
#endif
#ifndef WC_COMBOBOXEX
#define WC_COMBOBOXEX "ComboBoxEx32"
#endif
#ifndef WC_TABCONTROL
#define WC_TABCONTROL "SysTabControl"
#endif
#ifndef WC_IPADDRESS
#define WC_IPADDRESS "SysIPAddress32"
#endif
#ifndef WC_PAGESCROLLER
#define WC_PAGESCROLLER "SysPager"
#endif
#ifndef WC_NATIVEFONTCTL
#define WC_NATIVEFONTCTL "NativeFontCtl"
#endif
#ifndef WC_BUTTON
#define WC_BUTTON "Button"
#endif
#ifndef WC_STATIC
#define WC_STATIC "Static"
#endif
#ifndef WC_EDIT
#define WC_EDIT "Edit"
#endif
#ifndef WC_LISTBOX
#define WC_LISTBOX "ListBox"
#endif
#ifndef WC_COMBOBOX
#define WC_COMBOBOX "ComboBox"
#endif
#ifndef WC_SCROLLBAR
#define WC_SCROLLBAR "ScrollBar"
#endif
#ifndef WC_LINK
#define WC_LINK L"SysLink"
#endif
#ifndef RT_MANIFEST
#define RT_MANIFEST 24
#endif
#define WC_HEADER "SysHeader"
#endif
#ifndef WC_LISTVIEW
#define WC_LISTVIEW "SysListView"
#endif
#ifndef WC_TREEVIEW
#define WC_TREEVIEW "SysTreeView"
#endif
#ifndef WC_COMBOBOXEX
#define WC_COMBOBOXEX "ComboBoxEx32"
#endif
#ifndef WC_TABCONTROL
#define WC_TABCONTROL "SysTabControl"
#endif
#ifndef WC_IPADDRESS
#define WC_IPADDRESS "SysIPAddress32"
#endif
#ifndef WC_PAGESCROLLER
#define WC_PAGESCROLLER "SysPager"
#endif
#ifndef WC_NATIVEFONTCTL
#define WC_NATIVEFONTCTL "NativeFontCtl"
#endif
#ifndef WC_BUTTON
#define WC_BUTTON "Button"
#endif
#ifndef WC_STATIC
#define WC_STATIC "Static"
#endif
#ifndef WC_EDIT
#define WC_EDIT "Edit"
#endif
#ifndef WC_LISTBOX
#define WC_LISTBOX "ListBox"
#endif
#ifndef WC_COMBOBOX
#define WC_COMBOBOX "ComboBox"
#endif
#ifndef WC_SCROLLBAR
#define WC_SCROLLBAR "ScrollBar"
#endif
#ifndef WC_LINK
#define WC_LINK L"SysLink"
#endif
#ifndef RT_MANIFEST
#define RT_MANIFEST 24
#endif
这个文件要保存好,以后一直要用的。
最后把这个RC文件加入到我们的BCB工程里去。
新建一个名为Unit1.h的文件,包含文件和之前的WTL程序类似:
#define
_WIN32_IE 0x0501
#define _WIN32_WINNT 0x0501
#include < atlbase.h >
#include < atlapp.h >
extern CAppModule _Module;
#include < atlwin.h >
#include < atlframe.h >
#include < atlmisc.h >
#include < atlcrack.h >
#include < atlctrls.h >
#include < atlctrlx.h >
#include < atldlgs.h >
#include " resource.h "
#define _WIN32_WINNT 0x0501
#include < atlbase.h >
#include < atlapp.h >
extern CAppModule _Module;
#include < atlwin.h >
#include < atlframe.h >
#include < atlmisc.h >
#include < atlcrack.h >
#include < atlctrls.h >
#include < atlctrlx.h >
#include < atldlgs.h >
#include " resource.h "
注意CAppModule _Module改成了extern CAppModule _Module,因为_Module是在Unit1.cpp里定义的。 另外加了几个头文件,atlctrls.h和atlctrlx.h包含了一堆Windows控件的C++封装(如Edit,Button等),atldlgs.h则封装了一堆对话框(如打开对话框,字体对话框等)
先写个框架:
class
CNBNoteBook :
public CFrameWindowImpl < CNBNoteBook > {
public :
DECLARE_FRAME_WND_CLASS(_T( " TNBNoteBook " ),IDR_MAINFORM)
BEGIN_MSG_MAP_EX(CNBNoteBook)
CHAIN_MSG_MAP( CFrameWindowImpl < CNBNoteBook > )
END_MSG_MAP()
};
public CFrameWindowImpl < CNBNoteBook > {
public :
DECLARE_FRAME_WND_CLASS(_T( " TNBNoteBook " ),IDR_MAINFORM)
BEGIN_MSG_MAP_EX(CNBNoteBook)
CHAIN_MSG_MAP( CFrameWindowImpl < CNBNoteBook > )
END_MSG_MAP()
};
Unit1.cpp和之前的那个代码一样:
#include
"
Unit1.h
"
CAppModule _Module;
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
int nRet;
_Module.Init(NULL,hInstance);
{
CNBNoteBook mywin;
mywin.CreateEx();
mywin.ShowWindow(nCmdShow);
CMessageLoop MsgLoop;
nRet = MsgLoop.Run();
}
_Module.Term();
return nRet;
}
CAppModule _Module;
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
int nRet;
_Module.Init(NULL,hInstance);
{
CNBNoteBook mywin;
mywin.CreateEx();
mywin.ShowWindow(nCmdShow);
CMessageLoop MsgLoop;
nRet = MsgLoop.Run();
}
_Module.Term();
return nRet;
}
好,可以编译执行了, 运行后是不是已经有菜单,图标和标题啦?
另外我用_T宏来定义字符串 来和Unicode兼容, 我们的BCB当然也能编译Unicode程序, 在BCB的主菜单Project->Options->Directories/Conditionals页, 在Conditionals里添加UNICODE和_UNICODE两项。
再编译一遍,出错啦? 嗯,我忘了一件事,BCB自带的crtdbg.h有个低级BUG,我们来修改一下:
打开crtdbg.h,找到第54行, 把整个__ASSERTE_Helper函数改成:
__inline
int
__ASSERTE_Helper(
char
*
expr,
char
*
file,
int
line)
{
char msg[ 256 * 2 ];
::wsprintfA(msg, " %s failed - %s/%d " ,expr, file, line);
/* throw (msg); */
_ErrorExit(msg);
return 0 ; /* Never really gets here */
}
{
char msg[ 256 * 2 ];
::wsprintfA(msg, " %s failed - %s/%d " ,expr, file, line);
/* throw (msg); */
_ErrorExit(msg);
return 0 ; /* Never really gets here */
}
再编译一遍,大功告成,是如假包换的Unicode窗体。
现在应该加入"TMemo",嗯,而且应该是"alClient"的,它要撑满整个窗体。
开始,在class CNBNoteBook 里定义一个CEdit变量,我把它取名为m_edtText,这个CEdit单行时是"TEdit",多行时就是"TMemo"了。
这个CEdit最好的建立时机应该在主窗体刚建立之后,我们来映射这个主窗体的WM_CREATE消息。
在BEGIN_MSG_MAP_EX下面加入:MSG_WM_CREATE(OnCreate) ,并在class CNBNoteBook 里定义一个LRESULT OnCreate(LPCREATESTRUCT lpCS);函数, 内容:
LRESULT CNBNoteBook::OnCreate(LPCREATESTRUCT lpCS)
{
DWORD EditStyle =
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS |
WS_TABSTOP | WS_HSCROLL | WS_VSCROLL |
ES_LEFT | ES_MULTILINE | ES_NOHIDESEL;
m_edtText.Create( * this ,rcDefault,NULL,EditStyle,WS_EX_CLIENTEDGE);
this -> m_hWndClient = m_edtText;
SetMsgHandled( false );
return 0 ;
}
{
DWORD EditStyle =
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS |
WS_TABSTOP | WS_HSCROLL | WS_VSCROLL |
ES_LEFT | ES_MULTILINE | ES_NOHIDESEL;
m_edtText.Create( * this ,rcDefault,NULL,EditStyle,WS_EX_CLIENTEDGE);
this -> m_hWndClient = m_edtText;
SetMsgHandled( false );
return 0 ;
}
再运行的话一个记事本的样子就已经显现出来了,而这个m_edtText的"alClient"是怎么做到的呢? 看OnCreate里很重要的一句话: this->m_hWndClient = m_edtText, CFrameWindowImpl会在改变大小时自动调整m_hWndClient关联的窗口大小。 另外它还有m_hWndStatusBar和m_hWndToolBar, 都是HWND类型, 分别用于管理"TStatusBar","TToolBar/TCoolBar/TControlBar"。
OnCreate里还有一句SetMsgHandled(false);,它的作用是告诉窗体处理完OnCreate后继续调用默认的WM_CREATE处理函数。如果不写,它默认是SetMsgHandled(true),即不进行默认消息处理。
现在就剩下菜单处理了,菜单太多,这里只取几个说说:
保存功能:
要用到"TSaveDialog",在WTL里"TOpenDialog/TSaveDialog"合并成一个"CFileDialog",调用方法如下:
CFileDialog fd(
FALSE, // TRUE for FileOpen, FALSE for FileSaveAs
_T( " .txt " ), // 默认后缀名
_T( " Hello.txt " ), // 默认文件名
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, // 对话框标记,参考MSDN里OPENFILENAME结构的Flags成员变量定义
_T( " Text Files(*.txt)/0*.txt/0All Files(*.*)/0*.*/0 " ), // Filter, 和BCB不同的是它以'/0'分隔,以两个'
0'结束
);
if (IDOK == fd.DoModal(m_hWnd)) // fd.DoModal()相当于BCB里的OpenDialog->Execute(),可以加入一个HWND参数作为它的Owner窗体,如果点击确定,返回IDOK
{
...
}
FALSE, // TRUE for FileOpen, FALSE for FileSaveAs
_T( " .txt " ), // 默认后缀名
_T( " Hello.txt " ), // 默认文件名
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, // 对话框标记,参考MSDN里OPENFILENAME结构的Flags成员变量定义
_T( " Text Files(*.txt)/0*.txt/0All Files(*.*)/0*.*/0 " ), // Filter, 和BCB不同的是它以'/0'分隔,以两个'
0'结束
);
if (IDOK == fd.DoModal(m_hWnd)) // fd.DoModal()相当于BCB里的OpenDialog->Execute(),可以加入一个HWND参数作为它的Owner窗体,如果点击确定,返回IDOK
{
...
}
查找功能:
要用到"TFindDialog",在WTL里"TFindDialog/TReplaceDialog"合并成了"CFindReplaceDialog",并且使用方法与BCB里的区别很大:它是非模态窗体,靠发送消息(CFindReplaceDialog::GetFindReplaceMsg())与窗体通信。调用方法:
LRESULT CNBNoteBook::OnMuFind(WORD wCode, WORD wID, HWND hwnd, BOOL
&
bHandled)
{
CFindReplaceDialog * pfd = new CFindReplaceDialog; // FindDialog必须用new的方法,否则就得自己继承并用一个空函数重载OnFinalMessage
CWindow fp = pfd -> Create(
TRUE, // TRUE for Find, FALSE for FindReplace
_T( "" ), // 初始要找的字符串
NULL, // 初始替换的字符串
FR_DOWN | FR_HIDEWHOLEWORD, // 对话框标记,参考MSDN里的FINDREPLACE
* this ); // Owner窗体
fp.ShowWindow(SW_NORMAL);
return 0 ;
}
{
CFindReplaceDialog * pfd = new CFindReplaceDialog; // FindDialog必须用new的方法,否则就得自己继承并用一个空函数重载OnFinalMessage
CWindow fp = pfd -> Create(
TRUE, // TRUE for Find, FALSE for FindReplace
_T( "" ), // 初始要找的字符串
NULL, // 初始替换的字符串
FR_DOWN | FR_HIDEWHOLEWORD, // 对话框标记,参考MSDN里的FINDREPLACE
* this ); // Owner窗体
fp.ShowWindow(SW_NORMAL);
return 0 ;
}
添加一个消息映射:MESSAGE_HANDLER(CFindReplaceDialog::GetFindReplaceMsg(), OnFindMsg),并在class CNBNoteBook 里定义一个LRESULT OnFindMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled);函数, 内容:
LRESULT CNBNoteBook::OnFindMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL
&
bHandled)
{
CFindReplaceDialog *pfd = CFindReplaceDialog::GetNotifier(lParam); // 从lParam里取回CFindReplaceDialog 对象
if(pfd->IsTerminating()) return 0; // 正在退出
if(pfd->FindNext()) // 按了FindNext按钮
{
int Start=0,End=0;
m_edtText.GetSel(Start,End);
HLOCAL hMem = m_edtText.GetHandle(); // 为了内存资源及效率考虑,使用GetHandle的方法取得Edit内的文本
LPCTSTR pszMem = (LPCTSTR)::LocalLock(hMem);
int P = Find(pszMem,pfd->GetFindString(),pfd->SearchDown()?End : Start-1,pfd->SearchDown(),pfd->MatchCase());
::LocalUnlock(hMem);
if(P>=0) m_edtText.SetSel(P,P+lstrlen(pfd->GetFindString()),true);
}
return 0;
}
{
CFindReplaceDialog *pfd = CFindReplaceDialog::GetNotifier(lParam); // 从lParam里取回CFindReplaceDialog 对象
if(pfd->IsTerminating()) return 0; // 正在退出
if(pfd->FindNext()) // 按了FindNext按钮
{
int Start=0,End=0;
m_edtText.GetSel(Start,End);
HLOCAL hMem = m_edtText.GetHandle(); // 为了内存资源及效率考虑,使用GetHandle的方法取得Edit内的文本
LPCTSTR pszMem = (LPCTSTR)::LocalLock(hMem);
int P = Find(pszMem,pfd->GetFindString(),pfd->SearchDown()?End : Start-1,pfd->SearchDown(),pfd->MatchCase());
::LocalUnlock(hMem);
if(P>=0) m_edtText.SetSel(P,P+lstrlen(pfd->GetFindString()),true);
}
return 0;
}
在Find的消息处理里,我们可以从lParam里取回CFindReplaceDialog,然后跟据CFindReplaceDialog的方法:GetFindString(),GetReplaceString(),SearchDown(),FindNext(),MatchCase(),MatchWholeWord(),ReplaceCurrent(),ReplaceAll(),IsTerminating()提供的信息做你想你的事情。
现在我们再给记事本加入一个"TStatusBar", 我们前面提到过的CFrameWindowImpl的成员变量m_hWndStatusBar要派上用场了。
在class CNBNoteBook 里定义一个CMultiPaneStatusBarCtrl类:m_StatusBar, 修改OnCreate函数,添加如下语句:
m_StatusBar.Create(
*
this
);
int pPanes[ 2 ] = {ID_DEFAULT_PANE,IDS_PANE2}; // ID_DEFAULT_PANE是WTL定义的,自动填充完剩下的状态栏空间
m_StatusBar.SetPanes(pPanes, 2 ,FALSE);
m_hWndStatusBar = m_StatusBar;
int pPanes[ 2 ] = {ID_DEFAULT_PANE,IDS_PANE2}; // ID_DEFAULT_PANE是WTL定义的,自动填充完剩下的状态栏空间
m_StatusBar.SetPanes(pPanes, 2 ,FALSE);
m_hWndStatusBar = m_StatusBar;
然后用ResEdit在StringTable里加入一个名为IDS_PANE2的字符串,这个字符串的作用是保证"TStatusBar"的第二个"Panel"有足够的长度,我用了15个'@'字符。
现在记事本底下有一个状态栏了。我们要在"TStatusBar"的第二个"Panel"里显示当前光标的位置,一种方法是用定时器更新,这种方法的缺陷是定时间隔比较难定,短了占CPU时间,长了又更新不及时。好在WTL为我们提供了另一种方案,在空闲时更新,象我们这个程序对CPU来说空闲时间实在是太多了,可以利用利用这个特点。
要支持空闲时操作,我们的CNBNoteBook就要从CIdleHandler继承,这里被人骂得狗血淋头的C++多继承帮了我们的大忙。 修改CNBNoteBook如下:
class
CNBNoteBook:
public CFrameWindowImpl < CNBNoteBook > ,
public CIdleHandler
{
...
};
public CFrameWindowImpl < CNBNoteBook > ,
public CIdleHandler
{
...
};
为了让消息队列支持这个OnIdle,我们还得做这些事:
修改WinMain如下:
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
int
nCmdShow)
{
int nRet;
_Module.Init(NULL,hInstance);
{
CMessageLoop MsgLoop;
_Module.AddMessageLoop( & MsgLoop);
CNBNoteBook mywin;
mywin.CreateEx();
mywin.ShowWindow(nCmdShow);
nRet = MsgLoop.Run();
_Module.RemoveMessageLoop();
}
_Module.Term();
return nRet;
}
{
int nRet;
_Module.Init(NULL,hInstance);
{
CMessageLoop MsgLoop;
_Module.AddMessageLoop( & MsgLoop);
CNBNoteBook mywin;
mywin.CreateEx();
mywin.ShowWindow(nCmdShow);
nRet = MsgLoop.Run();
_Module.RemoveMessageLoop();
}
_Module.Term();
return nRet;
}
修改CNBNoteBook::OnCreate,在里面加入:
CMessageLoop
*
ml
=
_Module.GetMessageLoop();
ml -> AddIdleHandler( this );
ml -> AddIdleHandler( this );
BOOL CNBNoteBook::OnIdle()
{
// 更新StatusBar
int Start = 0 ,End = 0 ;
m_edtText.GetSel(Start,End);
int nRow = m_edtText.LineFromChar(Start);
int nCol = Start - m_edtText.LineIndex(); // 获得当前行列号,从VCL源码里参考来的:)
CString S; // 传说中的CString,其实不是很建议在BCB里使用.
S.Format(_T( " %d 行, %d 列, 已选择 %d 个字符 " ),nRow,nCol,End - Start);
m_StatusBar.SetPaneText(IDS_PANE2,S);
// 更新剪切板信息
CMenuHandle MainMenu = this -> GetMenu();
MainMenu.EnableMenuItem(IDM_UNDO, m_edtText.CanUndo() ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_CUT, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_COPY, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_DELETE, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_FIND, m_edtText.GetWindowTextLength() > 0 ? MF_ENABLED : MF_GRAYED);
::OpenClipboard(NULL); // 如果剪切板里有数据的话菜单Paste为Enabled
BSTR hText = (BSTR)::GetClipboardData(CF_UNICODETEXT);
MainMenu.EnableMenuItem(IDM_PASTE, hText != NULL ? MF_ENABLED : MF_GRAYED);
::CloseClipboard();
return TRUE;
}
{
// 更新StatusBar
int Start = 0 ,End = 0 ;
m_edtText.GetSel(Start,End);
int nRow = m_edtText.LineFromChar(Start);
int nCol = Start - m_edtText.LineIndex(); // 获得当前行列号,从VCL源码里参考来的:)
CString S; // 传说中的CString,其实不是很建议在BCB里使用.
S.Format(_T( " %d 行, %d 列, 已选择 %d 个字符 " ),nRow,nCol,End - Start);
m_StatusBar.SetPaneText(IDS_PANE2,S);
// 更新剪切板信息
CMenuHandle MainMenu = this -> GetMenu();
MainMenu.EnableMenuItem(IDM_UNDO, m_edtText.CanUndo() ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_CUT, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_COPY, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_DELETE, End != Start ? MF_ENABLED : MF_GRAYED);
MainMenu.EnableMenuItem(IDM_FIND, m_edtText.GetWindowTextLength() > 0 ? MF_ENABLED : MF_GRAYED);
::OpenClipboard(NULL); // 如果剪切板里有数据的话菜单Paste为Enabled
BSTR hText = (BSTR)::GetClipboardData(CF_UNICODETEXT);
MainMenu.EnableMenuItem(IDM_PASTE, hText != NULL ? MF_ENABLED : MF_GRAYED);
::CloseClipboard();
return TRUE;
}
最后是我对BCB里使用WTL的一点笔录,
- 要同时使用VCL和WTL, 在工程选项里加入USING_ATL和STRICT预定义。
- 如果使用CString出现问题,可以试试工程选项里加入_WTL_USE_CSTRING预定义。
- 如果要使用CAxWindow,CAxDialogImpl之类的东东,工程选项里加入_ATL_NO_UUIDOF预定义(这一项是BCB编译器的BUG引起的问题)。