65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WTL SDI 应用程序中切换视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (10投票s)

2007 年 9 月 19 日

11分钟阅读

viewsIcon

61540

downloadIcon

1624

一篇解释如何在 WTL SDI 应用程序中切换视图的文章。

Screenshot - SDIMultiView.gif

引言

最近我一直在开发 Windows Mobile 应用程序,其中一个我正在使用 WTL 构建。我之所以选择 WTL 而不是 MFC 或 .NET Compact Framework,是因为后两者的速度、大小和依赖性限制。

我从一个 SDI(单文档界面)WTL 向导生成的应用程序开始,添加了一些派生自控件的窗口和一些对话框窗口(窗体视图),然后才意识到我必须找到一种方法,让 SDI 框架能够动态加载和卸载子窗口,就像在 MFC 或 .NET 应用程序中一样——说实话,在 MFC 中也并没有容易多少。

MDI(多文档界面)当然是一个选项,通常情况下是这样,但 WTL 的 MDI 框架不支持 Windows Mobile/CE。和大多数事情一样,也有变通的方法(这里有一个例子)。但即使 MDI 可以勉强实现,我也很不愿费这么大力气让一种架构去做另一种架构(SDI)本身应该做的事情。

本文演示了两种可以用来实现这一目标的技术:在 SDI 应用程序中动态切换视图。我确信还有其他方法可以做到这一点,但这是我使用的两种方法。上面,我已包含了完整的源代码和可运行的示例。我希望这篇文章能为至少一个人节省自己摸索出这些问题的麻烦。

关于技术

  • 技术 1:第一种方法涉及按需销毁和重新创建视图实例。这是这两种方法中比较简单的一种,当你不在意销毁和重新创建窗口对象时,它效果很好。但是,在移动设备或受限设备上开发时,或者在你希望在选择之间保留视图时,你可能不想支付重复销毁和重新创建窗口的成本,或者每次用户更改视图时都重新初始化它们。
  • 技术 2:我将介绍的第二种方法展示了如何按需创建视图,然后通过直接使用 Win32 函数 SetWindowLongPtr 更改内部标识符来在后续选择之间保留它们。(根据 MSDN,SetWindowLong 现在已弃用)

背景

默认的 WTL 向导生成的 SDI 应用程序在单个父“框架”内有一个客户端(client)CWindow 派生的窗口或“视图”,该框架通常继承自 CFrameWindowImpl

{关于 WTL 的组织和使用更全面的讨论,请参阅 Michael Dunn 在 CodeProject 上的优秀系列文章——特别是 “MFC 程序员的 WTL,第二部分 - WTL GUI 基类”。}

这种设计理念在 MDI 中也基本得以延续,其中每个视图都有一个拥有它的子框架(也就是说,它是“一对一”关系,而不是“一对多”关系)。

需要明确的是,WTL 不实现任何类似于 MFC 的文档/视图模型的东西,所以无论我在上面还是在别处提到“视图”,我都仅仅是指应用程序主框架内的子窗口(即窗口、对话框或包装的子控件)。

在很多情况下,获得切换视图的能力的最佳方法是顺势而为,只需将您的应用程序构建为 MDI 应用程序。即便如此,正如上面提到的,也有 MDI 不可用或不理想的情况。回到 SDI,主框架在一个名为 m_hWndClient 的公共变量中存储其子视图的句柄(HWND)。

不该做什么

了解框架如何存储指向其子视图的引用,你可能会首先想通过将新子窗口重新分配给框架的 m_hWndClient,然后更新布局来解决问题。

// Somewhere within your frame class -- Doesn't work!!!

this->m_hWndClient = m_hWndNewView; // m_hWndNewView is a handle to the 

                                       // view I want to switch to

UpdateLayout();

不幸的是,这行不通,主要是因为框架不知道你刚刚给它的窗口的句柄。

“技巧”

这个问题的解决技巧(如果有什么技巧的话)在于理解 Windows 隐式地引用了框架窗口内第一个非控件条的“窗格”,而这恰好就是子视图。我也应该提到,这也是为什么你在 MFC 和 WTL 中都需要做同样的事情来切换视图的原因。

在 MFC 中,这个窗格被标识为 AFX_IDW_PANE_FIRST。如果你查看 ATL(atlres.h),你会找到一个类似命名的定义,叫做 ATL_IDW_PANE_FIRST。但它们的值都一样,为“0xE900”。

正如我上面暗示的那样,你可以销毁当前的子“视图”,创建新视图,然后重新分配新视图的句柄(隐式设置第一个窗格 ID)——技术 #1;或者你可以显式地更改两个视图的 ID,使得当前视图的 ID 不再是 ATL_IDW_PANE_FIRST,然后使用一些直接的窗口调用将此 ID 分配给新视图。(我马上就会向你展示如何做。)

有趣的是,技术 #1 不需要切换 ID——所以我猜测当你创建第二个视图时,它的内部 ID 肯定不同,框架或 Windows 会将 ID 重置为“0xE900”。如果你不切换 ID,只是创建第二个视图并以框架作为其父 HWND,只要第一个视图存在,框架就会继续将其视为子视图。我将留给更懂 Windows 的人进一步解释。

技术 #1:销毁与重新创建视图

我第一次看到这项技术是在 Chris Sell 的白皮书《WTL 使 UI 编程如虎添翼 - 第二部分》中(你可以在 www.sellsbrothers.com 上找到它。在 BitmapView 示例中寻找一个名为 TogglePrintPreview() 的函数)。

我将使用一个略有不同的版本,以便我的示例代码与我提供的演示和源代码匹配。我还对其进行了扩展,以便能够支持任意数量的视图。步骤大致可分为:

  1. 创建新视图
  2. 将新视图的 HWND 传递给框架的 m_hWndClient
  3. 显示新视图
  4. 销毁旧视图
  5. 更新窗口
  6. 可选:更新你的框架的 PreTranslateMessage 方法,以包含你新视图的覆盖
// View is just an enum that makes it more convenient to address views. 

// There's a defined VIEW enum for each view/dialog class you want

// to be able to switch to.

// 

// You could accomplish the same thing with simple integers, member windows 

// handles, or whatever else distinguishes the requested and current views. 


enum VIEW {BASIC, DIALOG, EDIT, NONE};

// Member views

CBasicView m_view; // Basic view derived from wizard

CEditView m_edit; // Basic dialog derived from wizard

CBasicDialog m_dlg; // Basic edit control view derived from wizard


...

void SwitchView(VIEW view)
{
    // Pointers to old and new views

    CWindow *pOldView, *pNewView;

    // Get current window/view

    pOldView = GetCurrentView(); // Defined below


    // Get/create requested view

    pNewView = GetNewView(view); // Defined below


    // Check if requested view is current view or default

    if(!pOldView || !pNewView || (pOldView == pNewView))
        return; // Nothing to do

    
    // Show/Hide

    pOldView->ShowWindow(SW_HIDE); // Hide the old

    pNewView->ShowWindow(SW_SHOW); // Show the new window


    // Delete the old view

    pOldView->DestroyWindow();

     // Ask frame to update client

    UpdateLayout();
}

GetCurrentView() 是一个辅助函数,它将 m_hWndClient 与每个视图的句柄进行比较,然后返回匹配的视图,并将其转换为 CWindow*。如下所示:

// Helper method to get current view ~ MFC GetActiveView() not available!

CWindow* GetCurrentView()
{
    if(!m_hWndClient)
        return NULL;

    if(m_hWndClient == m_view.m_hWnd)
        return (CWindow*)&m_view;
    else if(m_hWndClient == m_dlg.m_hWnd)
        return (CWindow*)&m_dlg;
    else if(m_hWndClient == m_edit.m_hWnd)
        return (CWindow*)&m_edit;
    else
        return NULL;
}

GetNewView(VIEW view) 是一个辅助函数,它返回请求的视图,并将其转换为 CWindow*。在此过程中,它会在必要时创建视图对象,并将其句柄分配给框架的 m_hWndClient。如下所示:

// Helper method to get/create new view

CWindow* GetNewView(VIEW view)
{
    CWindow* newView = NULL;
    // Now set requested view

    switch(view)
    {
    case BASIC:
        // If doesn't exist, create it and set reference to frame's 

        // m_hWndClient

        if(m_view.m_hWnd == NULL)
            m_view.Create(m_hWnd);
        m_hWndClient = m_view.m_hWnd;
        newView = (CWindow*)&m_view;
        break;
    case DIALOG:
        if(m_dlg.m_hWnd == NULL)
            m_dlg.Create(m_hWnd);
        m_hWndClient = m_dlg.m_hWnd;
        newView = (CWindow*)&m_dlg;
        break;
    case EDIT:
        if(m_edit.m_hWnd == NULL)
            m_edit.Create(m_hWnd);
        m_hWndClient = m_edit.m_hWnd;
        newView = (CWindow*)&m_edit;
        break;
    }
    return newView;
}
  • 根据迄今为止的讨论,上面的函数应该相当容易理解。SwitchView(VIEW view) 首先调用 GetCurrentView() 来获取当前视图的引用。
  • 然后它调用 GetNewView(VIEW view) 来获取请求视图的引用,并在必要时创建它。它还将新视图的句柄传递给框架的 m_hWndClient
  • 如果新视图或旧视图为 NULL,或者它们相等——这意味着用户要求将当前视图切换到自身——它将不做任何操作。
  • SwitchView(VIEW view) 然后隐藏**旧**视图并显示**新**视图。
  • 最后,它销毁旧视图。最后一步隐式地将新视图的内部 ID 更改为 ATL_IDW_PANE_FIRST。

如前所述,你还应该考虑更新框架的 PreTranslateMessage 覆盖,以确保视图有机会执行它们自己的 PreTranslateMessage 来处理消息。PreTranslateMessage 基本上允许框架和/或你的视图在消息被翻译和分派之前预览消息并对其进行处理。(返回 TRUE 可阻止消息被翻译和分派。)

大多数应用程序不会覆盖 PreTranslateMessage,除非它们需要进行一些特殊的消息处理,例如当它们正在对大量控件进行子类化时。即便如此,WTL 向导也会在你的 CWindowImplCDialogImpl 视图中自动生成 PreTranslateMessage 函数,并添加必要的代码以从主框架的 PreTranslateMessage 路由消息到它们,这也是我考虑必须确保消息从主框架路由到我的视图的另一个原因。

这是我如何修改框架的 PreTranslateMessage 以让我的视图有机会查看消息的:

// Implemented in CMainFrame

virtual BOOL PreTranslateMessage(MSG* pMsg)
{
    if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
    return TRUE;

    if(m_hWndClient != NULL)
    {
        // Call PreTranslateMessage for the current view

        CWindow* pCurrentView = GetCurrentView(); // Get the current view 

        // (cast as a CWindow*) ~ function shown above


        if(m_view.m_hWnd == pCurrentView->m_hWnd)
            return m_view.PreTranslateMessage(pMsg);
        else if(m_dlg.m_hWnd == pCurrentView->m_hWnd)
            return m_dlg.PreTranslateMessage(pMsg);
        else if(m_edit.m_hWnd == pCurrentView->m_hWnd)
            return m_edit.PreTranslateMessage(pMsg);
    }
    return FALSE;
}

我在这里首先确保框架有一个有效的子句柄,然后我调用上面描述的相同的 GetCurrentView() 函数来返回一个 CWindow*。然后我使用该 CWindow* 的 HWND 成员来与我的每个视图进行比较。我这样做是因为我需要视图才能调用视图自己的 PreTranslateMessage。我不能使用 CWindow 来调用它,因为它没有实现 PreTranslateMessage

不用说,你不需要为不实现 PreTranslateMessage 的任何视图包含消息路由。

可能有更优雅的方法来做到这一点,例如通过运行时类型信息 (RTTI)、模板或其他继承、包含形式等。请记住,RTTI 尤其可能是一种昂贵的解决方案,因为它将为每条消息遍历每个对象的继承层次结构。考虑到 PreTranslateMessage 会经过的消息数量,以及我想专注于我试图解决的核心问题,我将把更优雅的解决方案留给读者作为后续练习。

顺便说一句,PreTranslateMessageCMessageFilter 接口中的唯一方法——并且是主框架实现的方法。然而,它既不在 CWindowImpl 的继承层次结构中,也不在 CDialogImpl 的继承层次结构中。这意味着它在两者中都不是隐式可用的,并且不是实现它们的强制要求。

技术 #2:销毁并重新创建视图

这次讨论会短很多,因为大部分准备工作已经完成。此时只需要对 SwitchView 方法进行简单更改,以便在切换之间保留视图而不是销毁它们。如果你回看上面的 SwitchView,请替换:

// Delete the old view

pOldView->DestroyWindow();

…为…

// Change the current view's ID so it isn't the first child w/in the frame

 pOldView->SetWindowLongPtr(GWL_ID, 0); 

// Make the new view the frame's first pane/child

pNewView->SetWindowLongPtr(GWL_ID, ATL_IDW_PANE_FIRST);

…就是这样!如上所述,框架使用第一个窗格 ID 来更新其客户端视图,因此你需要将当前视图的 GWL_ID 更改为非 ATL_IDW_PANE_FIRST 的值,然后将新视图的 GWL_ID 更改为 ATL_IDW_PANE_FIRST

Using the Code

  • 你可以通过简单地用对应的 VIEW 枚举值调用 SwitchView 来在任何地方调用它,例如 SwitchView(BASIC)SwitchView(EDIT)
  • 如果你想使用我的实现,有几件事你需要做:
    • 更新 enum VIEW {} 以包含每个视图的标识符——为它们命名,任何有助于你保持清晰的方式都可以。
    • 在你的主框架中为每个视图添加一个成员变量。例如 CMyView m_myView
    • 更新 GetNewView(VIEW) 中的 switch 语句,为你的每个 VIEW 枚举和视图成员添加一个 case。
    • 更新 GetCurrentView() 以返回指向你每个视图成员的 CWindow* 引用。
    • 可选地更新你的框架的 PreTranslateMessage 方法,以调用你视图自己的 PreTranslateMessage 方法;同时确保在每个视图中也实现 PreTranslateMessage(如果使用 WTL 向导生成它们,这通常是默认完成的)。
  • 在示例代码中,我实际上更新了 SwitchView 的逻辑,使其使用相同的方法来处理这两种情况,因为两者之间的重叠程度很高。在实践中,我认为你通常会选择其中一种方法,但这让你可以在同一个应用程序中拥有这两种选择。实际的更改是:
void SwitchView(VIEW view, BOOL bPreserve = FALSE) // Destroy by default

{
    ...
    if(bPreserve)
    {    
        // Use Technique #2

    }
    else
    {
        // Use Technique #1

    }        
}

总结

请将本文视为一个起点。陪同本文的代码还有很多可以改进的地方,但我认为这些地方对于我的主要主题来说不是必不可少的,或者由于时间和篇幅的限制我没有进一步探讨。我可能会进行的一些具体改进包括:

  • 创建并存储视图为指针(这正是我自己实践中的做法)——这当然需要更多的细心和对代码的少量修改,因为堆栈和指针语义之间存在差异。一个例子是在成员比较时,如在 PreTranslateMessage 中。你必须确保在检查成员 HWND 之前有一个有效的指针,否则你可能会遇到 ASSERT 错误。例如…
CWindow* CPocketMDFrame::GetCurrentView()
{
    if(!m_hWndClient)
        return NONE;

    if((m_pView) && (m_hWndClient == m_pView->m_hWnd))
        return (CWindow*)m_pView;
    ...
}
  • 将视图存储在 CWindow* 的数组中——这样做可以帮助你摆脱对我 VIEW 枚举的依赖,但会增加一些复杂性;由你选择。
  • 可能创建一个接口类或模板来整合一些行为,并创建一个通用的视图接口。
  • 实现一个轻量级的文档/视图式架构,这将使处理包含的视图变得更简单,可能会使用 观察者设计模式。
  • 可能实现一个 访问者(设计模式)来使框架的 PreTranslateMessage 到视图的路由更清晰。

版权和许可

本文为版权材料,(c) 2007 Tim Brooks。本文经过研究和编写,旨在帮助他人受益于我的知识和经验,正如我也从无数他人那里受益一样。如果你想翻译本文,请给我发邮件告知。我想了解此文章的派生信息,并能在本文及其他地方引用这些翻译。

本文附带的演示代码已发布到公共领域。然而,本文并非公共领域。如果你在自己的应用程序中使用代码,我将非常感谢你给我发邮件告知——但我并不强制要求。最后,在你的源代码中注明出处也会很受欢迎,但同样不是强制要求的。

历史

2007 年 9 月 17 日 - 文章首次发布

© . All rights reserved.