BCB 与 WTL (二)

话接上回我们的第一个WTL程序, 现在我们一步步的往下看这段代码:
#include  < atlbase.h >
#include 
< atlapp.h >

CAppModule _Module;

#include 
< atlwin.h >
#include 
< atlframe.h >
#include 
< atlcrack.h >
#include 
< atlmisc.h >
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, 但编译器接受它,没办法,它最大嘛。我们只好先消化一下这种编程模式:

 
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");
}

上面这段代码会打印出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; 
}

从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

这个文件要保存好,以后一直要用的。

最后把这个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 "

注意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()   
};

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;
}

好,可以编译执行了, 运行后是不是已经有菜单,图标和标题啦?
另外我用_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  */
}

再编译一遍,大功告成,是如假包换的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 ;
}

再运行的话一个记事本的样子就已经显现出来了,而这个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
{
   ...        
}

查找功能:
要用到"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 ;
}

添加一个消息映射: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;
}


在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;

然后用ResEdit在StringTable里加入一个名为IDS_PANE2的字符串,这个字符串的作用是保证"TStatusBar"的第二个"Panel"有足够的长度,我用了15个'@'字符。

现在记事本底下有一个状态栏了。我们要在"TStatusBar"的第二个"Panel"里显示当前光标的位置,一种方法是用定时器更新,这种方法的缺陷是定时间隔比较难定,短了占CPU时间,长了又更新不及时。好在WTL为我们提供了另一种方案,在空闲时更新,象我们这个程序对CPU来说空闲时间实在是太多了,可以利用利用这个特点。
要支持空闲时操作,我们的CNBNoteBook就要从CIdleHandler继承,这里被人骂得狗血淋头的C++多继承帮了我们的大忙。 修改CNBNoteBook如下:
class  CNBNoteBook:
    
public  CFrameWindowImpl < CNBNoteBook > ,
    
public  CIdleHandler
{
    ...
};
CIdleHandler很简单,只有一个纯虚函数:BOOL OnIdle(), 如果配置完毕,在程序消息队列为空时这个OnIdle就会被调用。
为了让消息队列支持这个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;
}
没什么,只是加了个_Module.AddMessageLoop和_Module.RemoveMessageLoop。

修改CNBNoteBook::OnCreate,在里面加入:
    CMessageLoop  * ml  =  _Module.GetMessageLoop();
    ml
-> AddIdleHandler( this );
好了,现在我们可以为OnIdle写代码了,我们在这里更新菜单状态,当前光标位置:
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;
}
注: 建议大家去下一本<WTL for MFC Programmers>中文版看看,那里讲得比较详细,对BCB同样适用。

最后是我对BCB里使用WTL的一点笔录,

  • 要同时使用VCL和WTL, 在工程选项里加入USING_ATL和STRICT预定义。
  • 如果使用CString出现问题,可以试试工程选项里加入_WTL_USE_CSTRING预定义。
  • 如果要使用CAxWindow,CAxDialogImpl之类的东东,工程选项里加入_ATL_NO_UUIDOF预定义(这一项是BCB编译器的BUG引起的问题)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值