道。道。道

安全特性不等于安全的特性

   :: 首页 :: 联系 :: 聚合  :: 管理

常用链接

搜索

  •  

最新评论

绪论

     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);
        //TODO: 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_CLICKEDnotification.它将由按钮发送给你的窗口类.另外的一种方法是从CContainedWindow<>子类化BUTTON窗口来处理click消息.

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

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

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

 

posted on 2007-01-18 09:49 独孤九剑 阅读(1485) 评论(0)  编辑 收藏 引用 所属分类: C/C++/STL/ATL/WTL