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

使用 CodeProject - 应用程序的一天 - 第 2 部分,共 5 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (19投票s)

2007 年 1 月 27 日

CPOL

17分钟阅读

viewsIcon

48762

downloadIcon

697

使用 CodeProject 进行偶尔支持的正确编码方式。

第 2 部分,共 5 部分 - 简介

指向系列其他部分的链接

以下文本与第 1 部分相同。如果您尚未阅读该文章,那么本文对您来说将毫无用处,所以请务必先补上。我们在那儿等您。如果您已阅读第 1 部分文章,可以 跳过这些介绍部分

本文系列是我“我们实际使用的代码”系列文章的又一篇。这里没有不必要的理论讨论,没有技巧阐述,也没有因为我独立思考出了所有东西而沾沾自喜。这只是我为了支撑我们的一个应用程序而做的一些事情。本文中的大部分内容都基于我从 CodeProject 获取的其他代码,以下内容描述了我正在积极开发的一个项目的基本情况,以及我如何整合从 CodeProject 获取的文章和帮助。

抱怨

我已在 CodeProject 注册六年多了(截至本文撰写时),并逐渐发现了一些关于文章的令人担忧的趋势。首先,文章作者倾向于发布一篇文章,随着时间的推移,作者基本上就放弃了这篇文章,而回复提问的人要么得不到作者的任何回复,要么得到类似“我不再编写此/彼语言了”的回应。让我们面对现实吧,您也不能责怪他们。我使用的许多文章都有三四年了,我理解程序员需要前进,这通常意味着完全放弃旧代码。

另一方面是那些下载给定文章关联的源代码和示例的人。很多时候,有人会在文章中发布一个问题,这个问題与文章本身毫无关系,但主题与文章的某个方面有关。例如,我曾发布了一篇关于动态构建菜单的文章。最近,有人在那篇文章中留言,询问如何为他们动态构建的菜单添加 winhelp。还有些人遇到了文章中(真实或想象的)问题,并期望别人为他们修复。这些人真的惹恼我了。毕竟,我们在这里都是程序员。

那么,本文的要点是什么?

本文的全部目的是说明在过去六年里,我从 CodeProject 获取的代码片段、类和技术在现实世界中的应用,包括为了适应我有时奇特的需求而进行的变通。很多时候,我会使用 VC++ 论坛来提问,以帮助我理解一篇文章,或者为我自己的使用而修改文章的代码。

假设

本文的原始版本最初是一个详细的教程,描述了如何使用 IDE 以及其他类似的东西。过了一段时间,我意识到这给文章的篇幅造成了巨大的开销。除此之外,我开始对整个事情感到厌烦,而且我清楚地看到我的写作质量因此开始下降。

唯一的解决办法就是重新开始,并假设您(用户)对 VS2005 IDE 具有实际的操作知识,尤其是在创建 VC++/MFC 应用程序方面。这样,我们可以谈论更重要的事情,而不是受困于您应该已经知道的内容。我还假设您对 MFC 有相当的实际操作知识。我并不是说您必须是专家,但我假设您可以在 MFC 项目中进行操作,而不会被 CMainFrame 的复杂性所困扰。

其他

在整篇文章中,您会找到“编码注意事项”。这些只是描述了我编码时的做法以及这样做的原因。它们绝非想象中的要求,但它们通常涉及代码的可读性和可维护性。我相信你们中的许多人都有自己的做事方式,但请尽量减少关于这些问题的评论。毕竟,这篇文章与风格无关。

编写完整的演示应用程序的总过程只需要大约一个小时(如果您提前知道所有步骤)。写这一系列文章花费了我好几天时间,所以不要被它的长度吓倒。

本文的 HTML 和图像包含在项目下载中,但不包含漂亮的 CodeProject 格式。如果您能在心理上处理好这一点,您可以直接参考此 HTML 文件继续您的编程。

最后,我知道有些人会因为是我写的文章就直接给我的内容打 1 分。我请求您成熟、专业地投票,将您的政治倾向限制在“肥皂箱”上。请记住,您投票的是文章,而不是作者。

我们已经完成了什么

在本系列文章的第一部分,我们完成了创建 MFC SDI 应用程序的步骤,并通过添加 MFC Grid Control 使视图更加有趣。在第 2 部分,我们将创建一个可以切换主窗格视图的平面分割窗口。

添加带可切换视图的分割窗口

在 MFC 应用程序中添加分割窗口实际上非常简单,所有工作都在 CMainFrame 类中完成。当然,有一篇 MSDN 文章描述了添加分割窗口的过程,但我讨厌为了弄清楚如何做某事而追逐链接,我打赌您也一样。所以,为了方便起见,我们将跳过基本的分割窗口,直接进入我们真正想要的——平面分割窗口。

在我的实际应用程序中,我只需要一个水平分割窗口,所以本讨论仅限于此要求。此外,我从 Marc Richarme 的 平面分割窗口 文章开始,然后从 Dan Clark 的 多视图分割窗口 文章中添加了大部分代码,以获得可切换视图的功能。这是使用 CodeProject 上的两篇独立文章创建一个专用类的完美示例。由于结合了这两篇文章的代码,您最好使用我在示例项目中提供的代码,除非您只是想自己亲身体验一遍。

在第 1 部分,我们将 MFC Grid Control 添加到了 CSDIMultSpliView 类(由应用程序向导创建)中。虽然这很好,但我们要变得更花哨一些,加入可切换视图。为此,我们需要创建我们将要切换的视图。

创建新视图类 - CPrimaryView

此视图将用于显示网格。

  • 在“解决方案资源管理器”窗格中右键单击 SDIMultiApp1 项目项,然后在上下文菜单中选择 添加 | 类...
  • 在随后的“添加类”对话框中,在“类别”树(对话框左侧)中选择 *MFC*,然后在“模板”列表中(对话框右侧)选择 *MFC 类*。单击“添加”。
  • 在下一个对话框中,指定类名(本示例使用 CPrimaryView),并选择基类。对于我们的示例,我们将使用 CView。单击“完成”。
    • 将与网格控件相关的所有代码从 CSDIMultiSplitView 类移动到 CPrimaryView 类。您应该使用 IDE 在此类中添加适当的覆盖。如果您需要详细说明,请参阅本文系列的第 1 部分。简而言之,您需要覆盖 OnInitialUpdateOnCmdMsg,并为 OnSizeOnEraseBkgnd 添加消息处理程序。只需将代码从 CSDIMultiApp1View 中生成的函数复制到此类中。
    • 可选步骤 - 从 CSDIMultiSplit 类中删除网格控件代码。我们不需要在那里放它,因为该类将简化为一个占位符视图,用于可切换视图。在本文提供的示例应用程序中,我只是将网格控件代码用 ifdef 括起来,这样在编译应用程序时就不会包含它。

创建新视图类 - CSecondaryView

这将是一个简单的 GDI 视图,其中包含报告形式的文本(实际上,它将是一个简单的线条集合,在 for 循环中创建和显示)。此视图也将支持打印(正如您很快会发现的,这并不容易)。

由于您刚刚为上面的类完成了此操作,因此我将不再详细介绍创建新类的过程。当您看到类向导对话框时,类名应为 CSecondaryView,并且它应派生自 CScrollView,如下所示。

这是 OnDraw() 函数的一个新版本,它使视图更具趣味性。不要因为我们在屏幕上显示了大量线条而感到惊讶,因为我们将使用它来测试我们稍后将添加的打印功能。

void CSecondaryView::OnDraw(CDC* pDC)
{
    CDocument* pDoc = GetDocument();

    CFont docFont;
    CFont* pOldFont;

    // construct and select our font
    BuildFont(pDC, &docFont, 11, false, false);
    pOldFont = pDC->SelectObject(&docFont);

    // Get a baseline lineheight value. We add 2 pixels to the calculated 
    // lineheight so we have a little white space betwen lines of text.
    CString sText = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    int nLineHeight = pDC->GetTextExtent(sText).cy + 2;

    // set our x position
    int nMargin = 10;
    // limit how many lines we draw
    int nMaxLines = 70;
    
    // draw the lines
    for (int i = 1; i <= nMaxLines; i++)
    {
        sText.Format("This is line number %02d of %d", i, nMaxLines);
    }
    pDC->SelectObject(pOldFont);
}

为了支持所需的字体,这里是您必须添加到类中的 BuildFont 函数。更敏锐的用户可能会认出这个函数就是我们在 CFlatSplitterWnd 类中使用的那个。

BOOL CSecondaryView::BuildFont(CDC* pDC, CFont* pFont, int nFontHeight, 
                               bool bBold, bool bItalic)
{
    nFontHeight = -MulDiv(nFontHeight, pDC->GetDeviceCaps(LOGPIXELSY), 72);
    CString sFontName = _T("Arial");

    int   nWeight = (bBold) ? FW_BOLD : FW_NORMAL;
    BYTE  nItalic = (bItalic) ? 1 : 0;

    return pFont->CreateFont(nFontHeight, 0, 0, 0, nWeight, nItalic, 0, 0, 
                             DEFAULT_CHARSET,
                             OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, 
                             DEFAULT_QUALITY,
                             DEFAULT_PITCH | FF_DONTCARE, sFontName);
}

创建新视图类 - CInfoView

按照上述相同步骤,创建另一个名为 CInfoView 的新视图类,同样,将基类设置为 CSrollView

这里有一些代码可以使视图看起来更有趣。此代码与我们在 CSecondaryView 类中使用的代码之间的唯一区别在于我们在屏幕上显示的线条数量。由于我们不打算使此视图可打印,因此我们不需要那么多线条来测试视图。

void CInfoView::OnDraw(CDC* pDC)
{
    CDocument* pDoc = GetDocument();

    CFont docFont;
    CFont* pOldFont;

    // construct and select our font
    BuildFont(pDC, &docFont, 11, false, false);
    pOldFont = pDC->SelectObject(&docFont);

    // Get a baseline lineheight value. We add 2 pixels to the calculated 
    // lineheight so we have a little white space betwen lines of text.
    CString sText = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    int nLineHeight = pDC->GetTextExtent(sText).cy + 2;

    // set our x position
    int nMargin = 10;
    // limit how many lines we draw
    int nMaxLines = 20;
    
    // draw the lines
    for (int i = 1; i <= nMaxLines; i++)
    {
        sText.Format("This is line number %02d of %d", i, nMaxLines);
    }
    pDC->SelectObject(pOldFont);
}

最后,也将 BuildFont() 函数的副本放入此类。

继续查看解决方案资源管理器窗格中的相应文件(或者类视图窗格,如果您更喜欢的话)。要完成此视图的基本功能,只需在 OnDraw 中添加一些代码即可填充视图。这将有两个目的——在我们在此视图和网格视图之间切换时提供即时且免费的视觉反馈,并为我们在文章稍后添加打印功能做好准备。现在,这是一个新的 OnDraw 函数。

创建新视图类 - CSecondaryView

按照上述相同步骤,创建另一个名为 CSecondaryView 的新视图类。

创建新视图类 - CInfoView

按照上述相同步骤,创建另一个名为 CInfoView 的新视图类。

这里有一些代码可以使视图看起来更有趣。

实现带可切换视图支持的分割窗口

我们实际应用程序的任务之一是提供一个拆分视图,允许用户在另一个窗口中查看特定类型的数据。唯一真正的要求是分割条始终可见,并且它水平分割主窗口。为了简洁和将所有内容保持在预期的范围内,我们将忽略其他类型的拆分以及水平和垂直拆分组合相关的问题。

除了上述要求外,我还要自己想办法实现它。在 CodeProject 上浏览了一番后,我从 Marc Richarme 的 FlatSplitterWnd 类中找到了一个不错的起点。本质上,这个类提供了一个始终可见的平面分割窗口,而我的工作是让它变得漂亮。我选择将分割条本身设置为黄色,并在条上写上“Info”字样,并加上两个向下的箭头。这样做的目的是方便查看。废话不多说,以下是我为实现此功能所遵循的步骤。

  • 所以,首先我们将 CFlatSplitterWnd 文件添加到我们的项目中。再次,我倾向于将来自外部源的文件放在自己的文件夹中(是的,即使像这种情况一样修改它们),所以下载 FlatSplitterWnd 文章的源代码后,将 FlatSplitterWnd.CPP 和 FlatSplitterWnd.H 文件解压到您选择的文件夹(我将其放在 \SDIMultiSplit\FlatSplit 中)。由于我们在添加 MFC Grid Ctrl 时执行了类似步骤,因此我认为没有理由再次演示如何将文件添加到项目中。只需在“解决方案资源管理器”中右键单击 SDIMultiSplit 项目,在上下文菜单中单击 添加 | 现有...,然后浏览到您放置 FlatSplitterWnd 文件的文件夹,然后单击“添加”。
  • 如果您像我一样将这些文件放在自己的文件夹中,您必须在项目设置的附加包含目录中添加该文件夹。请确保为所有配置都这样做。
  • 现在我们已经将文件放在我们想要的位置,让我们看看我为实现分割条的视觉样式所做的更改。打开 FlatSplitterWnd.CPP,并查看该类的构造函数。
    CFlatSplitterWnd::CFlatSplitterWnd()
    {
        m_cxSplitter = m_cySplitter = 3 + 1 + 1;
        m_cxBorderShare = m_cyBorderShare = 0;
        m_cxSplitterGap = m_cySplitterGap = 3 + 1 + 1;
        m_cxBorder = m_cyBorder = 1;
    }
    

    由于我们需要为条形文本腾出空间,因此我们需要使条形更高,所以我将上面的代码更改为此。

    CFlatSplitterWnd::CFlatSplitterWnd()
    {
        // we need our splitter bar to be really wide/tall to 
        // support the text - jms
        m_cxSplitter    = m_cySplitter    = 15 + 1 + 1;
        m_cxBorderShare = m_cyBorderShare = 0;
        m_cxSplitterGap = m_cySplitterGap = 15 + 1 + 1;
        m_cxBorder      = m_cyBorder      = 1;
    
        // we need this for swappable views
        m_bFirstView    = true;
    }
    
  • 接下来,我们需要修改 OnDrawSplitter 函数。这是关于分割条颜色/样式的所有精彩之处。这是原始函数。
    void CFlatSplitterWnd::OnDrawSplitter(CDC* pDC, ESplitType nType, 
                                          const CRect& rectArg)
    {
        // Let CSplitterWnd handle everything but the border-drawing
        if((nType != splitBorder) || (pDC == NULL))
        {
            CSplitterWnd::OnDrawSplitter(pDC, nType, rectArg);
            return;
        }
    
        ASSERT_VALID(pDC);
    
        pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW), 
                                            GetSysColor(COLOR_BTNHIGHLIGHT));
    }
    

    我的修改的关键在于 ESplitType 参数。有四种可能的值,但我们只对其中一种感兴趣——splitBar。下面显示的 OnDrawSplitter 函数的修改版本包含足够的注释,无需我再做进一步的描述。

        if((nType != splitBorder && nType != splitBar) || (pDC == NULL))
        {
            CSplitterWnd::OnDrawSplitter(pDC, nType, rectArg);
            return;
        }
    
        ASSERT_VALID(pDC);
    
        switch (nType)
        {
            case splitBorder :
                {
                    pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW), 
                                    GetSysColor(COLOR_BTNHIGHLIGHT));
                }
                break;
            case splitBar :
                {
                    // we need to get the window rect so we can center our 
                    // text
                    CRect   wndRect;
                    GetWindowRect(&wndRect);
    
                    // get ready to draw our yellow rectangle
                    CRect   tempRect = rectArg;
    
                    // draw the 3D rectangle (do I really need to do this?)
                    pDC->Draw3dRect(rectArg, GetSysColor(COLOR_BTNSHADOW), 
                                    GetSysColor(COLOR_BTNHIGHLIGHT));
    
                    CFont   docFont;
                    CFont*  pOldFont;
                    CBrush  newBrush;
                    CBrush* pOldBrush;
    
                    // make the brush yellow and select it
                    newBrush.CreateSolidBrush(RGB(255,255,0));
                    pOldBrush = pDC->SelectObject(&newBrush);
    
                    // construct and select our font
                    BuildFont(pDC, &docFont, 9, false, false);
                    pOldFont = pDC->SelectObject(&docFont);
                    
                    // build our title text
                    CString sTitle = "INFO";
                    CSize sz = pDC->GetTextExtent(sTitle);
    
                    // make our rectangle 1 pixel smaller on all sides
                    tempRect.DeflateRect(1,1,1,1);
    
                    // calculate start position for text
                    int xPos = (int)((wndRect.Width() - sz.cx) * 0.50);
    
                    // make the text black, and the background (for drawing 
                    // text) transparent
                    pDC->SetTextColor(RGB(0,0,0));
                    pDC->SetBkMode(TRANSPARENT);
    
                    // draw our rectangle (uses default black pen)
                    pDC->Rectangle(&tempRect);
                    
                    // free the brush resources
                    pDC->SelectObject(pOldBrush);
                    newBrush.DeleteObject();
    
                    // make sure our drawing rectangle is large enough to 
                    // accomodate the text AND the arrowheads, and draw the
                    // text if we can
                    bool bCanDrawText = (xPos - 30 > tempRect.left);
                    if (bCanDrawText)
                    {
                        pDC->TextOut(xPos, tempRect.top, sTitle);
                    }
                    
                    // free the font resource
                    pDC->SelectObject(pOldFont);
    
                    // if we have room to draw the text, then we have room
                    // for the arrowheads as well
                    if (bCanDrawText)
                    {
                        // create a brush for the down-arrow heads
                        newBrush.CreateSolidBrush(RGB(0,0,0));
                        pOldBrush = pDC->SelectObject(&newBrush);
    
                        // adjust the rectangle
                        tempRect.DeflateRect(0,3,0,4);
                        CPoint pts[4];
    
                        // draw the down-pointing arrowheads
                        for (int i = 0; i <=1; i++)
                        {
                            int x = (i == 0) ? tempRect.Width() - sz.cx - 30
                                             : tempRect.Width() + sz.cx + 15;
                            int y = tempRect.top + 2;
                            x = (int)(x * 0.50);
                            pts[0] = CPoint(x,      y);
                            pts[1] = CPoint(x + 10, y);
                            pts[2] = CPoint(x + 5,  y + 5);
                            pts[3] = CPoint(x,      y);
                            pDC->Polygon(pts, 4);
                        }
    
                        // free our resources
                        pDC->SelectObject(pOldBrush);
                        newBrush.DeleteObject();
                    }
                }
                break;
        }
    编码注意事项
    此版本的 CFlatSplitterWnd 仅支持绘制水平分割条。在本系列文章的后面,我们将尝试解决这个问题。

    我还添加了一个函数来构建我们需要的字体。为了尽可能保持 OnDrawSplitter() 函数的整洁,我将其代码移到了自己的函数中,如下所示。

    //----------------------------------------------------------------------
    // Added this function to build a suitable font for the INFO text - jms
    //----------------------------------------------------------------------
    BOOL CFlatSplitterWnd::BuildFont(CDC* pDC, CFont* pFont, int nFontHeight,
                                     bool bBold, bool bItalic)
    {
        // set the font properties
        CString sFontName = _T("Arial");
        int     nWeight   = (bBold) ? FW_BOLD : FW_NORMAL;
        BYTE    nItalic   = (bItalic) ? 1 : 0;
        nFontHeight       = -MulDiv(nFontHeight, 
                                        pDC->GetDeviceCaps(LOGPIXELSY), 72);
        // create the font
        return pFont->CreateFont(nFontHeight, 0, 0, 0, nWeight, nItalic, 0, 
                                 0, DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, 
                                 CLIP_CHARACTER_PRECIS, DEFAULT_QUALITY,
                                 DEFAULT_PITCH | FF_DONTCARE, sFontName);
    }
    
  • 现在,我们将为分割窗口添加可切换视图的支持。对于此功能,我修改了 CFlatSplitterWnd 类,以包含 CodeProject 上另一篇文章的必要代码 - Dan Clark 的 Unlimited number of switchable views within a Splitter window

    由于我们只是从另一篇文章复制/粘贴到我们现有的分割窗口类中,因此无需实际使用 Dan 文章中的任何文件。我们将只讨论我复制/粘贴的函数以及我对这些函数的更改。当然,您也可以选择将此类派生自 CFlatSplitterWnd(反之亦然),但我个人不喜欢太多文件,更不用说耗费 VS2005 的处理能力了(您拥有的文件/类越多,IDE 执行某些函数(如更新 intellisense)所需的时间就越长)。

    AddSwitchableView 函数是从 CMainFrame 的 OnCreateClient() 函数调用的,它为每个您想使其可切换的视图调用一次。当我第一次查看这段代码时,我认为可以进行一些细微的改进,所以这些函数与 Dan 的文章不完全匹配。原始代码的更改已在注释中注明。

    ///////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////
    ////                            Replaceable Views                         
    ////                                                                      
    ////          Source - copied from CodeProject article by Dan Clark       
    ////      https://codeproject.org.cn/splitter/DanCMultiViewSplitter.asp   
    //////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    
    //------------------------------------------------------------------------
    // Adds a view - jms version implements the following changes from the 
    // original code.
    //
    // 1) I think a global variable that indicates the first view works 
    //    better than having the calling function determine the condition.  
    //    I default this variable to true in the constructor.
    //
    // 2) Eliminating the isFirstView parameter also allowed me to eliminate 
    //    the original altID from the parameter list.
    //
    // The two changes described above resulted in a shorter parameter list 
    // and cleaner code.
    //---------------------------------------------------------------------
    bool CFlatSplitterWnd::AddSwitchableView(CRuntimeClass* pView, 
                                             CCreateContext* pContext, 
                                             CRect& size,  
                                             UINT viewID)
    {
    
        CWnd* pWin  = (CWnd*) pView->CreateObject();
        DWORD style = WS_CHILD;
        if (m_bFirstView) 
        {
            style |= WS_VISIBLE;
            m_bFirstView = false;
        }
        pWin->Create(NULL, NULL, style, size , this, viewID, pContext);
        views[pWin] = viewID;
        return true;
    }
    

    SwitchView() 函数实际执行视图切换操作。我更改了大部分注释,并对旧视图的窗口 ID 设置做了一个小的更改。

    //----------------------------------------------------------------------
    // Hides one view and shows another.
    //
    //Note from jms - notice that any splitter pane can hold swappable views.
    //----------------------------------------------------------------------
    bool CFlatSplitterWnd::SwitchView(UINT id, int paneRow, int paneCol)
    {
        CView* pOldView = (CView*) GetPane(paneRow, paneCol); // get current 
                                                              // view
    
        // if we don't have an old view set, get out.
        // if you assert here, you haven't properly initilaized the splitter 
        // window.
        ASSERT(pOldView != NULL);
        if (pOldView == NULL)
        {
            MessageBeep(0);
            return false;
        }
    
        // get the new view specified by the id parameter
        CView* pNewView = (CView*)GetDlgItem(id);
        
        // if the new view is null, get out
        // if you assert here, you haven't properly initilaized the splitter
        // window.
        ASSERT(pNewView != NULL);
        if (pNewView == NULL ) 
        {
            return false;
        }
    
        CFrameWnd* mainWnd = (CFrameWnd*)AfxGetMainWnd();
        // if you assert here, something REALLY bad happend.
        ASSERT(mainWnd != NULL);
        if (mainWnd == NULL) // serious prob
        {
            return false;
        }
        // set the active view to the specified view
        if (mainWnd->GetActiveView() == pOldView)
        {
            mainWnd->SetActiveView(pNewView);
        }
        // show/hide the windows - do it in this order to avoid flashing
        pNewView->ShowWindow(SW_SHOW);
        pOldView->ShowWindow(SW_HIDE);
    
        // set the window id from our view map
        pNewView->SetDlgCtrlID(IdFromRowCol(paneRow, paneCol));
    
        // we need to reset the old views ID so we can look it up again later
        // jms change - since we actually got to this point in the code, it's 
        // safe to assume that the old view is really somewhere in the view map.
        CWnd* pCwnd =(CWnd*)pOldView;
        if (views.find(pCwnd) != views.end())
        {
            UINT oldId = views[pCwnd];
            pOldView->SetDlgCtrlID(oldId);
        }
    
        // clean up
        RecalcLayout();
        pOldView->Invalidate();
        pNewView->Invalidate();
    
        return true;
    }
    
  • GetViewPtr() 函数未从原始版本更改。它只是检索映射到指定 ID 的视图的指针。

    //---------------------------------------------------------------------
    // Gets the window corresponding to the specified ID
    //---------------------------------------------------------------------
    CWnd* CFlatSplitterWnd::GetViewPtr(UINT id, int paneRow, int paneCol)
    {
        map<CWnd*, UINT>::iterator It, Iend = views.end();
        for (It = views.begin(); It != Iend; It++)
        {
            if ((*It).second == id)
            {
                return (*It).first;
            }
        }
        return NULL;
    }
    

    GetIsActiveView() 函数是我自己添加的,用于帮助确定给定视图的活动状态,并从 CMainFrame 调用。

    //-----------------------------------------------------------------------
    // This function is called by CMainFrame OnUpdate functions for the view 
    // selection menu items.
    //-----------------------------------------------------------------------
    BOOL CFlatSplitterWnd::GetIsActiveView(UINT nID, int nPaneRow, 
                                           int nPaneCol)
    {
        CWnd* pOldWnd = GetPane(nPaneRow, nPaneCol);
        CWnd* pNewWnd = GetViewPtr(nID, nPaneRow, nPaneCol);
        return (pOldWnd == pNewWnd);
    }
    

    以下行已添加到 CFlatSplitterWnd.H

    class CFlatSplitterWnd : public CSplitterWnd
    {
    public:
        // from https://codeproject.org.cn/splitter/DanCMultiViewSplitter.asp
        map<CWnd*, UINT> views;
        bool m_bFirstView;
        CWnd* GetViewPtr       (UINT id, int paneRow, int paneCol);
        bool  SwitchView       (UINT id, int paneRow, int paneCol);
        bool  AddSwitchableView(CRuntimeClass* pView, CCreateContext* pContext, 
                                CRect& size,  UINT viewID);
        BOOL  GetIsActiveView  (UINT nID, int nPaneRow, int nPaneCol);
    
  • 最后,我们需要为我们的视图创建 ID。还记得我们在第 1 部分创建的Constants.h 文件吗?打开它,并在网格控件的定义正下方添加以下代码。
    #define IDC_PANE0_SECONDARY_VIEW 49001
    #define IDC_PANE0_PRIMARY_VIEW   49002
    #define IDC_PANE1_PRIMARY_VIEW   49003
    

挂载 CMainFrame

  • 添加 OnCreateClient 的重写。类向导会将此函数放在 CMainFrame 的底部。
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs,
                                    CCreateContext* pContext)
    {
        // TODO: Add your specialized code here and/or call the base class
    
        return CFrameWnd::OnCreateClient(lpcs, pContext);
    }
    

    用以下版本替换该函数。此代码块中的注释应充分说明正在发生的事情。

    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, 
                                    CCreateContext* pContext)
    {
        CRect cr;
        GetWindowRect(&cr);
        // we need to subtract a reasonable value from the returned screen 
        // height so that when we actually create the view, it won't cover 
        // up part of the screen with it's soon-to-be-resized rectangle.
        int nHeight = ::GetSystemMetrics(SM_CYSCREEN) - 100;
    
        // our splitter window will be static
        if (!m_mainSplitter.CreateStatic(this, 2, 1))
        {
            AfxMessageBox("Error setting up splitter window", MB_ICONERROR);
            return FALSE;
        }
    
        // add our replacable views - the view IDs are defined in constants.h
        m_mainSplitter.AddSwitchableView(    RUNTIME_CLASS(CPrimaryView  ), 
                                            pContext, 
                                            CRect(0, 1, cr.Width(), nHeight), 
                                            IDC_PANE0_PRIMARY_VIEW);
        m_mainSplitter.AddSwitchableView(    RUNTIME_CLASS(CSecondaryView), 
                                            pContext, 
                                            CRect(0, 1, cr.Width(), nHeight), 
                                            IDC_PANE0_SECONDARY_VIEW);
    
        // create views in the splitter
        // we need a blank view to start off in the upper pane as the "old 
        // view because part of the initialization of the application is 
        // changing to the proper view. We'll use the view created by the 
        // application wizard.
        m_mainSplitter.CreateView(0, 0, RUNTIME_CLASS(CSDIMultiApp1View), 
                                  CSize(cr.Width(), nHeight), pContext);
        // and for the lower pane, we'll use our CInfoView class.
        m_mainSplitter.CreateView(1, 0, RUNTIME_CLASS(CInfoView), 
                                  CSize(cr.Width(), 0), pContext);
    
        return TRUE;
    }
    
  • 接下来,我们需要添加一些辅助函数,以便将来让我们的生活更轻松。
    CPrimaryView* CMainFrame::GetPrimaryView()
    {
        return (dynamic_cast<CPrimaryView*>(m_mainSplitter.GetPane(0,0)));
    }
    
    CSecondaryView* CMainFrame::GetSecondaryView()
    {
        return (dynamic_cast<CSecondaryView*>(m_mainSplitter.GetPane(0,0)));
    }
    
    CInfoView* CMainFrame::GetInfoView()
    {
        return (dynamic_cast<CInfoView*>(m_mainSplitter.GetPane(1,0)));
    }
    

    如您所见,这些函数仅返回指向函数名称所示的活动视图的指针。由于我们使用的是 dynamic_cast,因此如果返回的视图不是指定的类型(介于 < > 符号之间),则指针将为 NULL。

    现在我们已经编写了所有视图类并实现了分割窗口,我们需要为用户提供一种切换视图的方式。

菜单使其生效

用户可以通过菜单项在主视图和辅助视图之间切换。在 MFC 应用程序中添加菜单非常简单。

  • 在 IDE 的资源视图中,打开资源,展开树直到看到资源类别列表,展开菜单项,然后双击 IDR_MAINFRAME。IDE 中将打开一个新窗口,显示程序的当前菜单。
  • 找到视图项,单击它,然后在“状态栏”项正下方名为“在此处键入”的行上右键单击。
  • 右键单击按钮并从随后的菜单中选择插入分隔符项。菜单中将添加一条分隔线,分隔线下方会出现一个新的“在此处键入”行。
  • 单击新的在此处键入项,然后键入“主视图”。请注意,正下方添加了一个新的“在此处键入”项。
  • 单击新的在此处键入项,然后键入“辅助视图”。
  • 返回并右键单击主视图项,然后从菜单中选择添加事件处理程序...
    • 此时应显示“事件处理程序向导”对话框(见下文)。此对话框允许您一次设置一个事件处理程序。请注意,我们可以设置两个可能的事件。我们都需要它们用于示例,但由于我们一次只能处理一个,因此我们将选择 ON_COMMAND。请务必确保您已选择要添加此处理程序的正确类——它必须是 CMainFrame。您需要重复上一步才能回到这里设置 ON_COMMAND_UI 的处理程序。同样,请确保将处理程序添加到正确的类。
    • 辅助视图菜单项重复最后两步。如果您正确执行了前面的步骤,您应该会在 CMainFrame 类的底部看到以下函数。
      void CMainFrame::OnViewPrimaryview()
      {
      }
      
      void CMainFrame::OnUpdateViewPrimaryview(CCmdUI *pCmdUI)
      {
      }
      
      void CMainFrame::OnViewSecondaryview()
      {
      }
      
      void CMainFrame::OnUpdateViewSecondaryview(CCmdUI *pCmdUI)
      {
      }
      
    • 向新的消息处理函数添加代码,使其切换视图并根据当前活动视图启用/禁用菜单项。
      void CMainFrame::OnViewPrimaryview()
      {
          m_mainSplitter.SwitchView(IDC_PANE0_PRIMARY_VIEW, 0, 0);
      }
      
      void CMainFrame::OnUpdateViewPrimaryview(CCmdUI *pCmdUI)
      {
          pCmdUI->Enable(!m_mainSplitter.GetIsActiveView(
                                            IDC_PANE0_PRIMARY_VIEW,0,0));
      }
      
      void CMainFrame::OnViewSecondaryview()
      {
          m_mainSplitter.SwitchView(IDC_PANE0_SECONDARY_VIEW, 0, 0);
      }
      
      void CMainFrame::OnUpdateViewSecondaryview(CCmdUI *pCmdUI)
      {
          pCmdUI->Enable(!m_mainSplitter.GetIsActiveView(
                                          IDC_PANE0_SECONDARY_VIEW,0,0));
      }

编译应用程序

重新生成解决方案,然后运行应用程序。第一张图片显示了应用程序的初始显示方式。第二张截图显示了单击/拖动分割条时应用程序的外观。第三张图片说明了菜单选择辅助视图,第四张图片显示了辅助视图。

下一步?

在第 3 部分,我们将再次使用 CodeProject 上的文章来添加自定义状态栏类和一些多线程组件。

第 2 部分结束

由于本文篇幅较长,我决定将其分成几部分。如果网站编辑按照我的要求操作,所有后续部分都应该在网站的同一个代码部分。每一部分都有自己的源代码,因此在阅读后续部分时,请务必下载该部分相应的源代码(除非您要手动完成文章中概述的所有内容)。

    为了保持一致性(和理智),请对所有部分进行投票,并以相同的方式投票。这有助于将文章保留在同一部分。感谢您的理解。

© . All rights reserved.