WTL体系结构

绪论

     WTL最终来了,而且提供了我所希望的功能.我在WTL Bytesize(译文)的文章列出WTL主要特征.在本文中,我将描述一下WTL的体系结构,同时我会给出一些简单的例子来演示如何使用它的那些特征.希望能够对您有所帮助.

WTL应用程序的类型

     WTL有好几种应用程序类型,供您在AppWizard选取.

 

     下表对这些应用程序进行了描述. 这种弹性构成了WTL体系结构的一部分.

应用程序类型 描述
SDI Application 单文本界面 – 只有一个窗口
Multiple Threads SDI 单个进程拥有一个或多个窗口
MDI Application 多文本界面 – 在框架内,您可以有零个或多个子窗口
Dialog Based 基于对话框模版

     你可能还是首次听说多线程SDI应用程序,但是不用担心,它的概念很容易理解.一个多线程SDI程序启动后它会有一个窗口, 窗口显示了一个文档. 当你想要程序要再创建一个文档时,问题就出现了--SDI程序只能显示一个文档.为了解决这个问题,多线程SDI创建了另一个SDI窗口.看起来是一个新的实例在运行,实际上它不过是原来的进程创建了一个新的窗口,并把它依附到进程的一个新线程. IE的新建窗口就是这样做的.

     除了多线程SDI,所有这些应用程序都可以作为COM服务器, 并且应用程序向导(AppWizard)为此提供了一个选项.另外应用程序向导还可以让你指定该程序是否主持ActiveX控件.令人费解的是,不同的程序类型,选取"Host ActiveX Controls"的地方不同.除对话框应用程序外的其他类型在第一页上选取,而对话框类型却放到第二页.

     第二页的其他选项,对对话框程序以外的类型都是可用的.它们让你指定程序是否需要工具条(toolbar),状态条(status bar)和视窗口(View Window).

     如果选取了"Toolbar"选项,你可以通过"Rebar"选择是否将工具条放入IE Rebar控件中. 如果你选取了Rebar, 你就可以通过框架窗口(frame window)的成员m_hWndToolBar(后边会有详细的描述)来访问它.你可以按照你的意愿,在里边加入其他的工具条. 选取了"Rebar"后, 你可以决定是否选取"Command Bar". Command bar很像CE的command bar控件.只是WTL是用一个类来实现,而在CE, command bar是一个系统窗口类(system window class). Command bar非常有用,它能够把窗口也加入到工具条中去. 如果你选取了这个选项, 工具条和菜单都将被当做toolbar来实现.这使菜单项也可以有关联的图标,并且当你移动鼠标到一个菜单项上时,该菜单项会被置成高亮.从Office 97以来, Office软件的菜单都具有上述特征.

     第二页还有指定程序是否使用视的选项(多半你想要使用), 同时你可以决定这些视如何实现. 下表列出了所有可选的视.

描述
Generic Window 一个简单的窗口. 此类窗口允许程序员编写WM_PAINT消息的处理函数. 适用于需要直接进行paint的文档.
Form 这类视具有一个对话框模版.适用于带ActiveX 控件的窗口. 应用程序来操作这些控件.
List Box 这个视是个list box.它最简单的形式意味着可以通过调用AddString() 方法来添加字符串.
Edit 这个视是个edit control. 本质上,它提供了一个像Notepad一样的程序.
List View 这个视是个list view 通用控件.用这个控件来显示相关的项(比如, 控制面板是一个Explorer主持的List View, 所有的项都是控制面板applet).
Tree View 这个视是个tree view 通用控件. 这个适用于具有层次关系的数据,比如,可以用它来显示数据库的schema. 顶层分支为表和存储过程,次级的分支为表中的字段.
Rich Edit 这个视是个rich edit 控件,像WordPad.
HTML Page 这个视主持了一个IE Web Browser 控件. 它把主持的一个web page当成一个视.

     本文的例子需要一个对话框模版,同时还需要菜单,因此Form view是个理想的选择.

 

程序线程

     跟ATL一样,WTL程序也需要一个_Module全局变量来保存全局数据,方便应用级代码访问.在WTL中,这个变量是CAppModuleCServerAppModule的实例,后者在程序同时作为一个COM服务器时用到.每个应用程序具有一个或者多个UI线程.WTL使用两种方式来管理这些线程.

     如果应用程序只有一个UI线程(除了多线程SDI以外,其他程序类型默认只有一个UI线程),线程调用全局函数run():

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
     CMessageLoop theLoop;
     _Module.AddMessageLoop(&theLoop);
     CMainFrame wndMain;
     if (wndMain.CreateEx() == NULL)
     {
         ATLTRACE(_T("Main window creation failed!//n"));
         return 0;
     }
     wndMain.ShowWindow(nCmdShow);
     int nRet = theLoop.Run();
     _Module.RemoveMessageLoop();
     return nRet;
}

     线程的消息循环包含在CMessageLoop内部.函数创建了一个CMessageLoop实例, 把它放入全局的消息循环映射(message loop map)数组. 以线程ID为索引,线程中运行的其他的代码可以访问到这个实例. 消息循环对象包含了message filter和idle handler. 运行在这个UI线程的UI元件(UI element)可以有它自己的idle handler,在线程的消息队列为空时运行译注:通过CMessageLoop::AddIdleHandler()把这个UI元件加入到CMessageLoop的idle handler 数组中. CMessageLoop::Run()包含了UI线程的主消息映射(main message map).下边是它的伪代码:

MSG m_msg;
int CMessageLoop::Run()
{
     for (;;)
     {
         while (!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE))
             DoIdleHandlers();
         bRet = ::GetMessage(&m_msg, NULL, 0, 0);
         if(bRet == -1)
             continue; 
         else if(!bRet)
             break;
         if (!DoMessageFilters(&m_msg))
         {
             ::TranslateMessage(&m_msg);
             ::DispatchMessage(&m_msg);
         }
     }
     return (int)m_msg.wParam;
}


     可以看到,这个函数推动着消息队列. 没有消息时, 运行注册到线程的idle hander. 如果在队列中检测到消息,把它取出来,传给每个message filter. 如果消息没有被这些函数处理,它将按照通常的方式,发送到目标窗口.

     如果程序有超过一个的UI线程,可以用WTL的线程管理器,多线程SDI就是这样做的. 主线程作为一个管理者线程,它会为每个新窗口创建一个新的新线程. 主要流程如下:

int nRet = m_dwCount;
DWORD dwRet;
while(m_dwCount > 0)
{
     dwRet = ::MsgWaitForMultipleObjects(m_dwCount, m_arrThreadHandles,
         FALSE, INFINITE, QS_ALLINPUT);
     if(dwRet >= WAIT_OBJECT_0 && dwRet <= (WAIT_OBJECT_0 + m_dwCount - 1))
         RemoveThread(dwRet - WAIT_OBJECT_0);
     else if(dwRet == (WAIT_OBJECT_0 + m_dwCount))
     {
         ::GetMessage(&msg, NULL, 0, 0);
         if(msg.message == WM_USER)
             AddThread(_T(""), SW_SHOWNORMAL);
     }
}


那些线程句柄放在一个数组中. 线程通过AddThread()加入到数组(同时启动线程), RemoveThread()从数组移走. wait语句在两种情况下会被打断: 线程死亡(将线程从数组中移出) 或线程收到了WM_USER消息(一个线程在一个新线程里新建了一个窗口). 线程管理者为程序中的一个类,因此可以在循环中加入自己的message handler, 比如,当程序有不止一种窗口类型时. 创建一个新的窗口非常简单,只需在任意一个窗口中调用:

::PostThreadMessage(_Module.m_dwMainThreadID, WM_USER, 0, 0L);

这个循环会一直运行下去,直到所有的UI线程都关闭了. UI线程具有一个thread procedure,它跟单UI线程的Run()方法一样.不过,由于线程管理者使用了MsgWaitForMultipleObjects(), 这意味者最多只能有MAXIMUM_WAIT_OBJECTS-1个UI线程,这也意味着最多只能创建63个窗口. 

框架

     WTL实际上是两类窗口: 框架窗口和视图窗口. 正如名字所暗示的那样, 框架窗口为窗口提供标题栏(caption bar)和边框,你的代码用它来处理工具条(tool bar)和菜单项命令.你看到的程序窗口实际上是视图窗口, 视图覆盖了框架窗口的客户区.客户区是指框架窗口没有被诸如状态条,工具条之类的修饰部件所遮挡的部分.

     线程会创建主框架窗口的一个实例,创建视图的工作由主框架窗口的WM_CREATE消息处理函数完成. 对于SDI程序来说,这个过程很简单. 把视图类的一个实例作为主框架类的一个成员,调用视图类的Create()方法即可.MDI程序稍微有些不同, MDI主框架窗口通过CMDIFrameWindowImpl<>::CreateMDIClient()建立一个名为MDICLIENT的窗口. 这个客户窗口将CMDIChildWindowImpl<>窗口当做它的子窗口,子窗口有一个视图.这也反映了这么一个事实,MDI程序可以具有零个或者多个子窗口,每个都有边框和标题栏.

框架窗口的OnCreate()很有意思,让我看看:

LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
    // create command bar window
     HWND hWndCmdBar = m_CmdBar.Create(m_hWnd, rcDefault,
         NULL, ATL_SIMPLE_CMDBAR_PANE_STYLE);
    // attach menu
     m_CmdBar.AttachMenu(GetMenu());
    // load command bar images
     m_CmdBar.LoadImages(IDR_MAINFRAME);
    // remove old menu
     SetMenu(NULL);
     HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME,
         FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE);
     CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
     AddSimpleReBarBand(hWndCmdBar);
     AddSimpleReBarBand(hWndToolBar, NULL, TRUE);
     CreateSimpleStatusBar();
     m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL,
         WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
         WS_EX_CLIENTEDGE);
     UIAddToolBar(hWndToolBar);
     UISetCheck(ID_VIEW_TOOLBAR, 1);
     UISetCheck(ID_VIEW_STATUS_BAR, 1);
     CMessageLoop* pLoop = _Module.GetMessageLoop();
     pLoop->AddMessageFilter(this);
     pLoop->AddIdleHandler(this);
     return 0;
}

     这是从一个SDI程序拿来的一段代码,该程序有一个基于command bar的工具条和一个状态条. 函数的第一行创建了一个command bar实例,然后对它进行初始化,在其中加入框架窗口的菜单和工具条位图. 这段代码先将菜单取出,把所有的下拉菜单转换为工具条按钮,并将菜单保存在一个变量中,以备后用. 给人的感觉是菜单是由工具条实现的-那我们就把它叫做工具条菜单(menu toolbar)吧. 然后Command Bar将程序工具条的图标装入image list 并将它们的ID保存在数组中. 当点击工具条菜单的按钮时,commandbar会找到对应的子菜单,创建一个弹出菜单. Command bar将子菜单项的ID和它保存的ID进行比较,这些ID跟image list中的工具条按钮图标是相关联的. 如果比较成功, 则将关联的图标加到菜单项上去. 这意味着相同ID的菜单项和工具条按钮具有相同的图标.

接下来, 创建工具条并把它关联到commandbar, 然后创建状态条和视图.可以看到视图的HWND存放在框架窗口的m_hWndClient变量中. 这个窗口句柄在框架窗口的WM_SIZE handler中会用到.当框架窗口改变大小时,它告知视图改变自身,于此同时也要考虑状态条和command bar. 

在下来的三行(从调用UIAddToolBar()开始) 用来显示在运行时会改变状态的UI项(UI item).文章后面还会重提这个话题. 最后,访问消息循环(message loop), 你应该还记得该消息循环存放在一全局数组中. GetMessageLoop() 取得当前线程的消息循环,加入框架窗口的message filter和idle handler, 分别默认是PreTranslateMessage()OnIdle().

框架窗口继承于以下类:

class CMainFrame : 
     public CFrameWindowImpl<CMainFrame>, 
     public CUpdateUI<CMainFrame>, 
     public CMessageFilter, 
     public CIdleHandler


后两个抽象类宣称了框架窗口类实现了PreTranslateMessage()OnIdle(). 从CUpdateUI<>继承表示框架类支持UI update map.

 

视图

视图窗口看起来显得很简单:

class CMyView : public CWindowImpl<CMyView>
{
public:
     DECLARE_WND_CLASS(NULL)
     BOOL PreTranslateMessage(MSG* pMsg)
     {
         pMsg;
         return FALSE;
     }
     BEGIN_MSG_MAP(CMyView)
         MESSAGE_HANDLER(WM_PAINT, OnPaint)
     END_MSG_MAP()
     LRESULT OnPaint(UINT, WPARAM, LPARAM, BOOL&)
     {
         CPaintDC dc(m_hWnd);
        //TOD Add your drawing code here
         return 0;
     }
};


上面是一个SDI程序的视图类. 多线程SDI和MDI的视图类在本质上也跟这个一样,但他们没有PreTranslateMessage()方法. SDI程序就是使用这个函数,赶在框架类处理消息之前把消息抓住. PreTranslateMessage()在SDI的框架类中的实现是,直接将消息转发给视图类. 

这里显示的视图实际上没有做什么工作.你应该自己在OnPaint()函数中加入画出文档内容的代码.如果需要支持输入,如鼠标的点击和键盘的按键,你应该加入相应消息处理函数到类和映射中. 可以看到这个窗口是从CWindowImpl<>继承下来的,如果你想让它基于一个Win32控件的话,就应该从定义在AtlCtrls.h文件中某个WTL类继承.

如果想在基于CWindowImpl<>的类里加上滚动条,那么你应该把基类换成CScrollWindowImpl<>,同时把消息链给它:

class CMyView : public CScrollWindowImpl<CMyView>
{
public:
     typedef CScrollWindowImpl<CMyView> parent;
     BEGIN_MSG_MAP(CMyView)
         CHAIN_MSG_MAP(parent)
     END_MSG_MAP()
     void DoPaint(CDCHandle dc)
     {
     }
};

基类保证窗口具备滚动条,并提供滚动条消息的默认处理.视图类不再有WM_PAINT的处理函数,因为它已被CScrollWindowImpl<>处理.根据滚动条的位置,CScrollWindowImpl<>画出视图相对应的部分. 取而代之的是,在你的类里实现DoPaint(),在这里你需要画出整个视图.如果你想指定滚动的范围,大小或起点,你需要加上处理WM_CREATE消息的函数,把这些初始化代码放到里边.

正如我先前所提到的,框架窗口会改变视图窗口的大小,以使它客户区未被状态条和工具条覆盖的部分为视图所填充. 在大多数情况下,这样就够了.但是当你想要一个具有Windows Explorer样子的程序时,该怎么办呢? Windows Explorer的窗口包含了一个tree view 和一个list view,还有两者之间的分割条. WTL的解决方案很简单:使用splitter窗口!

为此你需要改变一下框架窗口,让它创建splitter窗口的一个实例作为它的视图. 例如, 在你的框架类里有如下的数据成员:

CSplitterWindow m_view;
CTreeViewCtrl m_tree;
CListViewCtrl m_list;


你可以在OnCreate()创建一个splitter窗口:

// get the frame client rect, so that we set the splitter initial size
// and we can get the splitter bar in the centre
RECT rect;
GetClientRect(&rect);
m_hWndClient = m_view.Create(m_hWnd, rect,
     NULL, WS_CHILD | WS_VISIBLE);
m_tree.Create(m_view, rcDefault, NULL,
     WS_CHILD | WS_VISIBLE | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT,
     WS_EX_CLIENTEDGE);
m_list.Create(m_view, rcDefault,
     NULL, WS_CHILD | WS_VISIBLE | LVS_REPORT, WS_EX_CLIENTEDGE);
m_view.SetSplitterPanes(m_tree, m_list);
m_view.SetSplitterPos();


Splitter窗口如同一个视图,将框架窗口作为它的父窗口. 在这段代码里,我将框架窗口客户区的实际大小传给了splitter窗口. 我也可以在这里使用 rcDefault,因为一旦框架窗口创建完成,框架窗口就会转发WM_SIZE消息给splitter. 这样splitter可以马上改变自身的大小来填充框架. 然而,当我准备使用不带参数的SetSplitterPos(),把分割条设置于窗口中线时,出现了问题.Splitter窗口使用它的大小来决定中线的位置,由于rcDefault告诉窗口它的大小是0(因此中线的位置也是0),从而意味着分割条将出现在z最左边,将左窗口隐藏了起来.

创建了splitter窗口后,你需要创建那些你想要分割的窗口了.它们将作为splitter窗口的子窗口被创建.最后你将这些子窗口通过SetSplitterPanes()加到splitter窗口中去,并确定分割条的位置所在.

UI Update

菜单项可以被设置为有效或无效,可以带check记号或着像radio按钮一样,在一组菜单项中同时有且只有一个能被check.此外,菜单项还可以带图标和文字. 所有的这些状态都可以在运行时根据程序中的某个值进行改变.工具条在某种程度上可以看做是菜单的易见形态,因为它们的按钮可以个别地,或者作为一组的一部分被置成有效或无效,推入推出. UI update机制允许你指定哪些UI元件(UI element)的状态可以在运行时改变. WTL使用如下的UI update映射来实现这一功能:

BEGIN_UPDATE_UI_MAP(CMainFrame)
     UPDATE_ELEMENT(ID_FILE_SAVERESULTS, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
     UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
     UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()

这个例子指出三个菜单项在运行时有一个状态需要显示,其中的一个, ID_FILE_SAVERESULTS,还有一个工具条按钮跟它相关联. WTL通过建立一个数组来保存这些信息.为此你需要完成两方面的工作:

首先是UI元件的状态. 如果是菜单项, 你可以使用UIEnable()使能该菜单项, UISetCheck()设置check记号, UISetText()改变菜单的文字.如果是工具条按钮,那么你使用UIEnable()使能该按钮, UISetCheck()或者UISetRadio()决定按钮是推入还是推出.下边的代码根据是否有文本被选中,来使能Cut菜单项和工具条按钮:

BOOL bSelected = GetSelected();
UIEnable(ID_EDIT_CUT, bSelected);


你可以把这样的代码放入相应处理函数中(如一个菜单项的状态依赖于另一个菜单项的动作,将它放入后者的处理函数中),或者放入OnIdle()方法,通过检查某个类变量来决定元件的状态.

其次是确定各个UI元件是否都被更新了,为此你需要调用CUpdateUI<>的某个方法将UI元件加入到列表中.主菜单已被自动加入,但是其他的任何菜单和所有的工具条必须分别通过调用UIAddMenuBar()UIAddToolBar()手动加入.

其他还有一堆事情要注意. 首先,设置了工具条的状态后,使用UIUpdateToolBar()以使工具条状态更新. 对于菜单,你不需如此,因为子菜单是动态生成的.UIUpdateMenuBar()这个方法也存在,但是它的作用是把菜单恢复到初始状态,如果你改变过某些项的文字,调用UIUpdateMenuBar()的结果可能不是你所期望的(因为菜单项的文字会变成老的). 

尽管还有一个方法UISetRadio(),但是还没有一个把几个菜单项或者工具条按钮当做radio按钮组(也就是说,有一个而且只有一个被选中)的机制.如果你希望得到这样效果,你必须自己编码,不过它并不难.

 

对话框
ATL的对话框支持一向很好,对此WTL新增了通用对话框的封装. 本质上是为对话框加入了输入验证和回调函数. 比如, 你想在用户改变年Open对话框中的文件夹时有所动作,那么你应该从CFileDialogImpl<>继承一个类,实现OnFolderChange():

class CMyFileDialog : public CFileDialogImpl<CMyFileDialog>
{
public:
     CMyFileDialog(BOOL b) 
         : CFileDialogImpl<CMyFileDialog>(b) { }
     void OnFolderChange(LPOFNOTIFY lpon)
     {
         char strFolder[MAX_PATH];
         if (GetFolderPath(strFolder, sizeof(strFolder)) > 0)
         {
             MessageBox(strFolder);
         }
     }
};

当文件夹的路径改变时,CFileDialogImpl<>调用OnFolderChange().该函数使用基类的GetFolderPath(),来取得新路径.

控件

WTL为所有的Win32和通用控件提供了封装类,包括Windows 2000新加入的. 虽然只是简单的包装,但是它们使这些控件更加容易访问.譬如,你能记清楚从List View读出当前选定项的文字的消息和需要传的参数吗?(实际上, 你需要发送两个消息, 一个是得到选定项的索引,另一个是读出它的文字.) WTL的作者为你完成了这些烦人的工作, 提供了一个简单的封装函数供你使用.

使用这些控件类有两种方法. 如果你的对话框里有一个控件, 你可以将控件的HWND依附到一个封装对象,使用封装类的方法来访问控件.这种方法简化了你读写控件数据和处理notification消息的代码. 

另外的用法是把这些类加到你的视图类的继承层次中去:

class CMyView : public CWindowImpl<CMyView, CListBox>

这表示CWindowImpl<>CListBox继承而来,因此创建的窗口将是一个list box (因为窗口类的名字是通过调用 
CListBox::GetWndClassName()得到的). 另外, ATL的窗口机制会子类化这个窗口,将发给它的消息路由到你的消息映射中去. 它保留了老的窗口函数,这样,你没有处理的消息将由老的窗口函数来处理.当你的视图类从控件类继承时,WTL就会使用这一技术.

在notification消息和子类化这个主题上,有一点很值得指出,那就是当某事件发生时,绝大多数窗口控件都会发送notification消息给它们的父窗口.让你窗口来处理这些notification消息要比子类化一个已存在控件窗口(或子类化一个已存在的类,然后建立一个实例),从而在控件之前取得消息好得多. 譬如, 你想处理按钮的click事件,你所需要做的只是处理BN_CLICKED notification.它将由按钮发送给你的窗口类.另外的一种方法是从CContainedWindow<>子类化BUTTON窗口来处理click消息. 

我之所以说这个是因为一个知名的ATL鼓吹者给我一份代码里就是这么做的.他的代码取得一个简单的按钮click事件所花的时间是别人的3到4倍,因为他子类化了按钮控件,而不是简单的处理BN_CLICKED notification.

WTL还提供了一些新的控件,在win32中没有对等者. 你已经看到过一个 -- command bar, 实际上还有其他一些非常有用类:

类   描述
CBitmapButton 这是一个用位图替代标题的按钮.你可以提供一个image list,里边包含按钮在正常状态,失效, 推入和鼠标落在按钮上的图表.
CHyperLink 让你建立一个static控件,它代表一个hyperlink,这样当用户点击它时,默认的web浏览器打开该链接.
CWaitCursor 这不过是在它的构造函数中把鼠标图标改成等待状态,而在析构函数中还原.
CCheckListViewCtrl 在每一项边上都有一个check box的list box.
CMultiPaneStatusBarCtrl 具有多个pane的状态条

你可能感兴趣的:(WTL体系结构)