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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (36投票s)

2007 年 1 月 27 日

CPOL

17分钟阅读

viewsIcon

70154

downloadIcon

669

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

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

指向系列其他部分的链接

迟到的说明: 我不知道这是否重要,但我使用的是安装了 Service Pack 1 的 VS2005。

本系列文章是我“代码真正有用”系列文章的又一篇。没有不必要的理论讨论,没有技巧的阐述,也没有因为我独自想出所有东西而沾沾自喜。这只是我为了让我们的一个应用程序正常运行而做的一堆事情。本文中的大部分内容都基于我从 CodeProject 获取的其他代码,接下来的内容将描述我正在积极开发的项目的基础以及我如何整合从 CodeProject 获取的文章和帮助。

抱怨

(截至撰写本文时)我已在 CodeProject 注册超过六年,我发现关于文章的一些令人担忧的趋势。首先,文章作者倾向于发布一篇文章,随着时间的推移,作者基本上会放弃这篇文章,而回复帖子的人会得到作者的沉默,或者得到类似“我不再编写这种/那种语言的代码了”之类的回复。老实说,你也不能责怪他们。我使用的许多文章都有三到四年的历史了,我理解程序员需要前进,而这通常意味着完全放弃旧代码。

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

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

本文的整个重点在于说明过去六年我从 CodeProject 获得的代码片段、类和技术的实际应用,包括为了适应我有时奇特的要求而进行的变通。很多时候,我会使用 VC++ 论坛来提问,以帮助我理解一篇文章,或者调整文章的代码供我自己使用。

假设

本文的最初版本最初是一篇详细的教程,描述如何使用 IDE 以及其他不重要的东西。过了一段时间,我意识到这会给文章的篇幅带来巨大的开销。除此之外,我开始厌倦这一切,我清楚地看到我的写作质量因此开始下降。

唯一的解决办法就是重新开始,并假设您(用户)对 VS2005 IDE 有实际的了解,尤其是在创建 VC++/MFC 应用程序方面。这样,我们就可以更多地讨论重要的事情,而不是费力地讨论您应该知道的事情。我还假设您对 MFC 有良好的实际了解。我并不是说您必须是专家,但我假设您可以在 MFC 项目中进行操作,而不会因为 CMainFrame 的复杂性而感到困惑。

其他

在文章中,您会发现“编码注释”。这些只是描述了我编写代码的方式以及我为什么这样做。它们绝不是凭空想象的要求,但它们通常与代码的可读性和可维护性有关。我相信你们许多人都有自己的做事方式,但请尽量减少关于这些问题的评论。毕竟,本文与风格无关。

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

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

最后,我知道有些人会因为是我写的而给我的东西投 1 票。我请求您成熟、专业地投票,将政治留给肥皂箱。请记住,您是在为文章投票,而不是为作者投票。

我的实际应用程序

该应用程序允许医院急诊科监控急诊室患者和床位的状态。该应用程序分为四个组件:

  • 一个“大屏幕”应用程序,它允许用户进行极少的交互(没有菜单,但有几个加速键可用),并显示在悬挂在天花板上的 50 英寸等离子屏幕上。
  • 一个“客户端”应用程序,允许多个用户在自己的屏幕上操作数据。在客户端应用程序中所做的更改最终会更新到“大屏幕”。
  • 一个数据访问 MFC 扩展 DLL。此 DLL 包含所有数据库访问功能。
  • 一个视图管理器 MFC 扩展 DLL。所有对话框和视图都包含在这里。
  • 所有这些都连接到一个 Oracle 数据库。

以下是我们创建的实际应用程序的一些屏幕截图。请记住,您可能注意到的许多视觉元素都由需求决定,不一定是我愿意实现某些代码方面的方式。了解这一点,请克制住批评感知到的设计方面的冲动。

这是客户端终端模块

这是静态显示模块(此模块显示在悬挂在天花板上的 50 英寸等离子电视上)

我们的示例应用程序

构建示例应用程序为我提供了一个重构大部分应用程序的机会,省略了不再使用的代码,并重新组织代码以便于维护。示例应用程序将演示以下设计元素:
  • 一个多项目解决方案。
  • 使用共享 MFC DLL 的 MFC 应用程序。
  • 使用 MFC 文档/视图体系结构的 SDI 应用程序。
  • 一个水平自定义平滑分割窗口。
  • 在一个分割窗格中切换视图。
  • 演示了在应用程序本身中使用视图以及从扩展 DLL 使用视图。
  • 可切换的视图包括网格视图和 GDI 绘图视图。
  • 从应用程序中移除菜单。
  • 创建一个控制一个或多个计时器的计时器线程。
  • 创建响应计时器线程发布的计时器事件的工作线程。
  • 设置自定义状态栏。
  • 修改系统菜单。
  • 在 XML 文件中加载/保存程序设置。
  • 多重继承。

收集组件

示例不包含与我使用的文章相关的所有代码,而是仅包含我在自己的项目中实现的那些部分。

包含我从 CodeProject 下载的所有代码,但对于那些想遵循我相同步骤的人,或者如果您只是想以不同于我的方式组织您的源代码文件夹,这里有一个用于创建项目的文章列表:

通常,我会将 CodeProject 的代码放在一个名为“CodeProject”的根项目文件夹中(正如我在这些文章中所做的那样),以便于 a) 查看我正在使用的非我的代码,以及 b) 在作者发布新内容时更新该代码。我知道你们大多数人可能都有自己的做事方式,所以重新排列以符合您的喜好应该很容易。

我坚信使用项目属性中的“附加包含目录”设置,因此如果您在示例中移动了内容,请记住在项目中删除并重新添加文件,然后在调试和发布中更改“附加包含目录”属性,然后再重新编译。如果我从 CodeProject 的东西派生新类,我通常只是将派生类文件放入项目的文件夹中。

入门 - 创建一个解决方案

首先,我们需要创建一个新的解决方案。为什么?因为我们将创建多个相互关联的项目,将它们全部打包在一个解决方案中比处理几个独立的项目更方便。将解决方案名称设置为“SDIMultiSplit”。

接下来,我们将向解决方案中添加一个新项目。既然我们讨论的是 VC++ 和 MFC,我们需要添加的就是这类项目。这是一系列屏幕截图,显示了创建项目时需要使用的设置。





有点无聊,但我们很快就会解决这个问题。我们已经准备好开始添加有趣的东西了。

让视图更有趣

由于应用程序向导已经提供了一个视图,我们将让它做一些比仅仅显示一个大白框更有趣的事情。请按照以下步骤操作。

将 MFC 网格控件添加到项目中

  • 如果您还没有,请下载 MFC 网格控件。您只需要“源代码”(99kb 下载)。我使用的是 2.25 版本,但 2.26 beta 版本也可用。在那里,您可能想浏览一下文章,以获取有关如何“使其正常工作”的有用提示。文章中的示例有点稀疏,但提供了一个示例项目,其中说明了一些更有趣的功能。它是投票最多的文章之一(截至撰写本文时已获得 790 票,并且还在不断增加),并且评分高达 4.81。
  • 由于其文件数量和复杂性,我们将把网格控件的源代码放在自己的文件夹中。在您的解决方案文件夹中创建一个新文件夹,名为 MFCGridControl_2-25。这样做的另一个原因是,您可以轻松地将 2.25 代码替换为更新的 2.26 代码,而不会危及可用的代码库。将文件解压到此新文件夹。
  • 在做任何事情之前,有一个小错误需要修复。在尝试让我的父视图类响应网格控件的通知消息时,我发现 GVN_ENDLABELEDIT 并不总是传递给父视图。问题在于,如果您开始编辑一个单元格,但没有更改任何内容,然后停止编辑,网格控件就不会发送 GVN_ENDLABELEDIT 消息,因为单元格的内容没有改变。我通过用这个稍微修改过的函数替换 GridCtrl.cpp 中的 OnEndEditCell 函数来修复它。您将需要此修复,因此这是该函数的​​新版本:
    void CGridCtrl::OnEndEditCell(int nRow, int nCol, CString str)
    {
    /*
        // this is the original version of the function
        CString strCurrentText = GetItemText(nRow, nCol);
        if (strCurrentText != str)
        {
            SetItemText(nRow, nCol, str);
            if (ValidateEdit(nRow, nCol, str) && 
                SendMessageToParent(nRow, nCol, GVN_ENDLABELEDIT) >= 0)
            {
                SetModified(TRUE, nRow, nCol);
                RedrawCell(nRow, nCol);
            }
            else
            {
                SetItemText(nRow, nCol, strCurrentText);
            }
        }
    
        CGridCellBase* pCell = GetCell(nRow, nCol);
        if (pCell)
            pCell->OnEndEdit();
    */
    
        CString strCurrentText = GetItemText(nRow, nCol);
        if (ValidateEdit(nRow, nCol, str)) 
        {
            if (strCurrentText != str)
            {
                strCurrentText = str;
                SetItemText(nRow, nCol, strCurrentText);
                SetModified(TRUE, nRow, nCol);
                RedrawCell(nRow, nCol);
            }
            else
            {
                SetItemText(nRow, nCol, strCurrentText);
            }
        }
        CGridCellBase* pCell = GetCell(nRow, nCol);
        if (pCell)
        {
            pCell->OnEndEdit();
        }
        SendMessageToParent(nRow, nCol, GVN_ENDLABELEDIT);
    }
    
  • 接下来,我们必须将所有这些文件添加到我们的项目中。打开您的解决方案资源管理器窗格,右键单击树中的 **SDIMultiApp1** 项,选择 **添加 | 现有项...**,然后浏览到 MFCGridControl_2-25 文件夹(或您命名的任何名称),选择所有文件,然后单击 **添加** 按钮。目前可以安全地忽略“实验性升级”文件夹中的文件。

    如果您好奇,可以打开解决方案资源管理器窗格,查看项目中列出的新文件。

  • 使用网格控件的最后一个准备步骤是将该文件夹添加到项目设置中。右键单击树中的 **SDIMultiApp1** 项,然后在后续上下文菜单的底部选择 **属性**。

    在“属性”对话框中,展开 **配置属性** 树项,然后展开 C++ 树项,并将以下文本添加到 **附加包含目录** 设置中(如上图鼠标指针所指)。

        .,../MFCGridControl_2-25

    请注意,此字符串中显示的第一​​个文件夹是“.”(单点)。这告诉编译器在查找包含文件之前先在项目文件夹中查找。实际上,您应该按照您希望它们被搜索的精确顺序将文件夹添加到此设置中。这是包含项目文件的重要方面,不应忽略。

    最后,确保您将相同的字符串添加到所有必需的项目配置(调试和发布)中。

连接网格控件

“使其正常工作”相当简单明了,特别是因为我们只是为了举例而显示网格。
  • 打开 SDIMultiApp1View.h 文件并添加以下代码:
    #include "GridCtrl.h"
    
    class CSDIMultiApp1View : public CView
    {
    private:
        CGridCtrl* m_pGridCtrl;
        
    public:
        // give other parts of the program a method for retrieving the grid
        // control. You may not need this right away, but it sure will be 
        // convenient when it's there because that's one less file you have 
        // to check out of source control to change later.
        /// Retrieves the current gridcontrol pointer
        CGridCtrl* GetGridControl() { return m_pGridCtrl; };

    编码注意事项
    当我手动向类添加内容时,我通常会将其添加到类定义的顶部,因为 VS2005 喜欢在底部添加消息处理程序,而随着时间的推移,这会显得很难看。良好的代码可读性意味着良好的代码可维护性 - 始终考虑那些跟随您的人。我还提供了 **在编写代码时** 的大量注释,因为我经常忘记我为什么这样做,即使在我写完代码后仅一个小时。我提供了大量注释,因为我经常忘记我为什么这样做,即使在我写完代码后仅一个小时。

    另外请注意,最后一个注释以 3 个斜杠开头。这是 Intellisense 使用函数时提供文本作为注释的触发器。这对代码新手来说非常方便。我建议您也这样做。
  • 现在,打开您的 SDIMultiApp1View.cpp 文件,并将以下行添加到构造函数中:
        m_pGridCtrl = NULL;

    然后将以下代码添加到析构函数中:

        if (m_pGridCtrl)
        {
            delete m_pGridCtrl;
        }
        m_pGridCtrl = NULL;
    

    编码注意事项
    我知道,我**不必**将指针设置为 NULL,但我老了,在我看来,老方法是最好的方法。

接下来的几个步骤涉及向视图类添加消息处理程序。Microsoft 使 MFC 程序员很难有效地利用 IDE。通过 IDE 添加消息处理程序是这些限制中最令人讨厌的限制之一。

要向 CCmdTarget 派生的类(CView 是其中之一)添加消息处理程序(除了手动添加),您必须将编辑光标放在消息映射宏的开始行或结束行上,如下所示:

完成此操作后,您可以打开 **属性** 窗格,您将看到此工具栏:

嗯,**有时**您会看到该工具栏。VS2005(是的,即使安装了 SP1)有时也会拒绝在属性窗格中显示工具栏。如果您遇到这种情况,请尝试关闭 IDE 并重新打开它(我不能保证这每次都会成功)。

无论如何,有三个按钮是您感兴趣的。单击闪电按钮(工具提示显示“事件”)将显示资源文件中定义的事件 ID。事件按钮旁边的按钮是消息按钮。单击此按钮将显示您正在处理的类所有合适的*Windows*消息。系列中的最后一个按钮是覆盖按钮,它允许您激活覆盖函数,例如 OnExitInstanceOnInitialUpdate 等。

我们感兴趣的是 **消息** 按钮,所以我们继续。

  • 打开属性窗格,然后单击属性工具栏中的 **消息** 按钮。您将看到一个 Windows 消息列表。向下滚动列表直到找到 WM_SIZE。单击消息 ID 旁边的空白字段,然后在下拉列表中选择 OnSize。IDE 将添加所有必要的代码来处理该消息,但我们必须更改它,所以让函数看起来像这样(只需复制/粘贴函数的内容):
    void CSDIMultiApp1View::OnSize(UINT nType, int cx, int cy)
    {
        CView::OnSize(nType, cx, cy);
        if (m_pGridCtrl->GetSafeHwnd())
        {
            CRect rect;
            GetClientRect(rect);
            m_pGridCtrl->MoveWindow(rect);
        }
    }
    
  • 使用上述 WM_SIZE 消息的方法为 WM_ERASEBACKGROUND 添加消息处理程序,并将 IDE 生成的代码更改为如下所示:
    BOOL CSDIMultiApp1View::OnEraseBkgnd(CDC* pDC)
    {
        return TRUE;
    }
    
  • OnCmdMsg 添加一个*覆盖*处理程序。请记住,您必须使用“覆盖”按钮(图标看起来像一个小框)来添加覆盖。将 IDE 生成的代码更改为如下所示:
    BOOL CSDIMultiApp1View::OnCmdMsg(UINT nID, int nCode, void* pExtra, 
                                     AFX_CMDHANDLERINFO* pHandlerInfo)
    {
       if (m_pGridCtrl && IsWindow(m_pGridCtrl->m_hWnd))
        {
            if (m_pGridCtrl->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
            {
                return TRUE;
            }
        }
        return CView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
    }
    
  • OnInitialUpdate 添加一个*覆盖*处理程序。请记住,您必须使用“覆盖”按钮(图标看起来像一个小框)来添加覆盖。将 IDE 生成的代码更改为如下所示:
    void CSDIMultiApp1View::OnInitialUpdate()
    {
        CView::OnInitialUpdate();
    
        CRect rect;
        GetClientRect(rect);
        m_pGridCtrl = new CGridCtrl(0, 0, 10, 1, 1);
        m_pGridCtrl->Create(rect, this, IDC_GRID_CTRL);
        UpdateWindow();
    }

编码注意事项
您也可以尝试使用类视图来添加消息处理程序,但由于某种原因,我从未养成这种习惯。
  • 在您的解决方案目录中创建一个名为 Includes 的新文件夹。
  • 在项目文件夹中创建一个名为 Constants.h 的新文件,并插入以下代码:
    #pragma once
    
    #define IDC_GRID_CTRL            49000
    

    我们将在本系列文章的第 2 部分和第 3 部分返回到此文件,届时我们将为程序添加更多功能。

  • 将以下行添加到项目 stdafx.h 文件的底部:
    #include "Constants.h"

    编码注意事项

    我知道,每次更改此文件都会导致完整的程序重新构建,但我有几个原因这样做;a) 我们的应用程序太小了,在编译时间上不会有影响,b) 它非常方便,因为项目中的所有 CPP 文件都必须 #include stdafx.h,并且在 MFC 项目中创建新类时,此包含是默认执行的(为我节省了本文系列的打字时间)。

  • 将新的 Includes 文件夹添加到您的“附加包含目录”项目设置中。您的新条目应如下所示:
        .,../Includes,../MFCGridControl_2-25
  • 可选步骤 - 由于我们的视图不需要 OnDraw 函数,因此我选择将其在代码中的出现次数降到最低。 CView 是一个抽象类,因此迫使我们覆盖 OnDraw。但是,网格控件负责自己的绘图,而且实际上没有理由保留 OnDraw。您真正需要做的就是从视图的 CPP 文件中删除该函数,并将视图头文件中的函数声明更改为以下内容:
    virtual void OnDraw(CDC* pDC) {};

    这有助于保持 CPP 文件整洁。

现在,编译代码。您可能会注意到编译时出现一串警告。这是因为我使用的网格控件版本尚未针对 VS2005 进行修改。这不会影响应用程序的可靠性,但如果这让您感到困扰,请下载 2.26 beta 版本,在解决方案文件夹中创建一个新文件夹(我建议使用 MFCGridControl_2-26)。如果您将其放在自己的文件夹中,请确保返回项目属性并更改包含目录设置以指向新文件夹(确保调试和发布配置都这样做)。编译后,运行应用程序。您应该看到类似以下内容:

恭喜。您已完成第一阶段 - 创建一个有趣的入门级应用程序。


编码注意事项
在开发过程的这个阶段,我通常会花时间添加注释并将代码组织成部分,将覆盖函数与 Windows 消息处理程序分开,等等。本文提供的示例代码演示了该过程。同样,这只是我为了帮助保持代码可读性而做的事情,并非您的强制要求。

随意尝试网格控件(添加行、使用图像等)。网格控件的演示源代码应该可以帮助您弄清楚如何使用一些更有趣的功能。同时,请务必在网格控件页面上投票。另外,如果您对网格控件的使用有任何疑问,请在网格控件的文章页面上提问。

第 1 部分结束

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

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

历史

2008/04/15:修复了文章中的所有链接。

© . All rights reserved.