使用 CodeProject - 应用程序的一天 - 第 4 部分,共 5 部分
使用 CodeProject 进行偶尔支持的正确编码方式。
第 4 部分,共 5 部分 - 简介
指向系列其他部分的链接
- 第 1 部分 - 构建基本应用程序并添加网格
- 第 2 部分 - 添加分割窗口和可交换视图
- 第 3 部分 - 添加自定义状态栏和多线程
- 第 5 部分 - 修改系统菜单,添加 XML 配置文件支持,以及清理。
以下文本与第 1 部分相同。如果您尚未阅读过该文章,那么本文对您来说将毫无意义,所以请务必先行阅读。我们在这里等您。如果您已阅读第 1 部分文章,可以跳过这些介绍部分。
本系列文章是我“我们实际使用的代码”系列文章的又一篇。没有不必要的理论讨论,没有技术阐述,也没有因为我独立思考出一切而沾沾自喜。这只是我为了让我们的应用程序运行起来而做的一些事情。本文中的大部分内容基于我从 CodeProject 获取的其他代码,以下内容描述了我正在积极开发的项目基础以及我如何整合来自 CodeProject 的文章和帮助。
抱怨
我已经是 CodeProject 的会员六年多了(截至本文撰写之时),我发现有关文章的一些令人担忧的趋势。首先,文章作者倾向于发布一篇文章,随着时间的推移,作者基本上就放弃了这篇文章,而对提问者的回复要么是作者的沉默,要么是像“我不再使用这种/那种语言进行编程了”这样的回复。说实话,您也不能责怪他们。我使用的许多文章都已有三四年历史了,我理解程序员需要继续前进,而这通常意味着完全放弃旧代码。
另一方面是那些下载给定文章相关源代码和示例的人。很多时候,有人会在一篇文章中提出一个问题,这个问题与文章本身完全无关,但主题与文章的某个方面相关。例如,我发表了一篇关于动态构建菜单的文章。最近,有人在那篇文章中发布了一条消息,询问如何为他们动态构建的菜单添加 winhelp。然后还有一些人遇到了文章中的问题(真实或想象的),并期望别人为他们解决。这些人真让我恼火。毕竟,我们都应该是程序员。
那么,本文的要点是什么?
本文的重点在于说明我在过去六年里从 CodeProject 获取的代码片段、类和技术的实际应用,包括为了适应我有时异想天开的需求而进行的变通。很多时候,我会使用 VC++ 论坛来提问,以帮助我理解一篇文章,或者调整文章的代码以供自己使用。
假设
本文的原始版本最初是一个详细的教程,描述如何使用 IDE 以及其他无聊的项目。过了一段时间,我意识到这会增加文章本身的巨大开销。此外,我开始对这一切感到厌倦,并且我清楚地看到我的写作质量因此开始下降。
唯一的解决方案是重新开始,并假设您(用户)已经具备了 VS2005 IDE 的工作知识,尤其是关于创建 VC++/MFC 应用程序。这样,我们就可以更多地讨论重要内容,而不是浪费时间讨论您应该已经知道的内容。我还假设您对 MFC 有扎实的工作知识。我并不是说您必须是专家,但我假设您可以在 MFC 项目中操作,而不会被 CMainFrame 的复杂性所困扰。
其他
在文章中,您会发现“编码注意事项”。这些只是描述了我编码的方式以及原因。它们绝对不是凭空想象的要求,但它们通常涉及代码的可读性和可维护性。我相信你们中的许多人都有自己的做事方式,但请尽量减少有关这些问题的评论。毕竟,本文与风格无关。
编码完整演示应用程序的总过程只需要一个小时左右(如果您事先知道所有步骤)。编写这个系列文章花费了我几天时间,所以不要被它的长度吓倒。
本文的 HTML 和图像包含在项目下载中,但不包含漂亮的 CodeProject 格式。如果您能在心理上处理好这一点,您可以直接参考此 HTML 文件继续您的编程。
最后,我知道有些人会因为是我写的就简单地给我打 1 分。我请求您成熟、专业地投票,并在投票时将您的政治观点留到论坛上。请记住,您投票的是文章,而不是作者。
我们已经完成了什么
在本系列文章的第 1 部分中,我们完成了创建 MFC SDI 应用程序并添加 MFC 网格控件使其视图更有趣的步骤。在第 2 部分中,我们添加了一个分割窗口以及在分割窗格之一中交换视图的功能。在第 3 部分中,我们添加了一个自定义状态栏类和一些简单的多线程功能,这些功能可以更新状态栏窗格的内容。
更多实际需求
我们的实际应用程序的主要目的是通过悬挂在天花板上的 60 英寸等离子显示屏向急诊室工作人员展示“全景”(我知道——似乎总有人能得到所有酷炫的玩具 :))。由于这只不过是一个信息显示,因此不期望也不需要用户交互。如果需要更改内容,用户可以使用我们刚刚完成的客户端应用程序。
除了用户交互部分,大屏幕应用程序与客户端应用程序是相同的。因此,我们将客户端应用程序的整个显示部分移到一个扩展 DLL 中,然后将该 DLL 链接到客户端和大屏幕应用程序。
本文讨论了创建扩展 DLL 以及创建使用它的应用程序的过程。
关于第 4 部分文章内容的一些话
由于第 4 部分中我们要添加的内容的性质,我将不讨论将内容放在哪里,而是更多地讨论我做了什么以及为什么。如果您想查看所有内容,可以解压第 1、2 或 3 部分的代码。
总的来说
为了不搞乱现有代码,我们将在当前解决方案中创建三个新项目
- SDIDisplay - 我们的“大屏幕”应用程序。此应用程序将没有菜单。
- SDIClient - 我们的客户端应用程序。此应用程序将有菜单。
- SDIViews - 我们的扩展 DLL。此 DLL 将被两个应用程序使用。
创建扩展 DLL
请注意,我假设您知道如何进行所有这些步骤而无需指导,因此说明可能显得 sparse。如果您有疑问,请提出。
- 创建一个名为 SDIViews 的新 MFC DLL 项目。在提示选择 DLL 类型时,请确保选中 MFC Extension DLL 复选框。
- 更改以下属性
- Configuration Properties | General | Character set = Multi-byte character set。
- 将 SDIMultiApp1 中使用的 **additional include directories** 设置复制到此新项目中。
- 将以下文件从原始项目文件夹 (SDIMultiApp1) 复制到此新项目的文件夹
- DlgOne.*
- DlgTwo.*
- InfoView.*
- PrimaryView.*
- SecondaryView.*
- 将这八个文件添加到您的项目中。
- 这是一个扩展 DLL,因此我们需要导出一些 MFC 类。实际上,我们要导出视图类。在视图类的头文件中,在“class”和类名之间添加以下内容 - `
__declspec(dllexport)
` - 如下所示class __declspec(dllexport) CInfoView : public CScrollView
- 我们不再需要在该项目中包含旧应用程序的头文件,因此在每个复制的 CPP 文件中,删除此行
#include "SDIMultiApp1.h"
- 由于上一步,我们不再能访问项目的资源,因此在所有复制过来的类的头文件中,添加此行
#include "resource.h"
- 将 MFC 网格控件文件添加到项目中。
- 接下来,我们需要复制要在视图类中处理的菜单项的资源 ID。为了更方便您,以下是要放入您的 resource.h 文件中的行
#define ID_VIEW_PRIMARYVIEW 32771 #define ID_VIEW_SECONDARYVIEW 32772 #define ID_SAMPLESTUFF_DIALOGONE 32773 #define ID_SAMPLESTUFF_DIALOGTWO 32774
此时,请验证您是否可以编译新项目。如果不能,我可能忘记列出了一步或多步,或者您做错了什么,请回过头来纠正。我们还没有完成这个项目,但现在是验证我们有一个可行项目的好时机。我们还将跳过并创建我们的一个新应用程序项目。
将客户端和显示项目添加到解决方案
本质上,客户端应用程序的外观应该与原始应用程序完全相同。唯一区别是它将使用我们刚刚创建的扩展 DLL。使用以下步骤创建这两个应用程序项目。
- 创建一个新的 MFC SDI 应用程序(使用第 1 部分作为项目设置的指南)。第一个应用程序命名为“SDIClient”。第二个应用程序命名为“SDIDisplay”。
- 设置项目依赖项,使其需要 SDIViews 项目。这将告诉编译器在编译此项目之前必须编译此 DLL,并且在运行应用程序时,DLL 将被隐式加载(如果未加载 DLL,应用程序将无法运行)。
- 将其他包含目录更改为以下内容(确保同时为 release 和 debug 配置执行此操作)
.,../include,../MFCGridControl_2-25,../CodeProject,../SDIViews
我不知道为什么,尽管客户端应用程序中没有引用 MFC 网格控件,但您必须将该源代码的路径包含在其他包含目录设置中。
- 在 SDIClient.CPP 文件中的 `InitInstance()` 函数中,找到并更改 `CSingleDocTemplate` 构造函数调用,使其如下所示
pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSDIClientDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CPrimaryView));
这将使我们能够在继续之前测试 DLL。
- 将此项目设置为“Startup Project”。这将导致编译器编译此项目及其依赖项。
- 将 CodeProject 文章文件(用于状态栏、线程和分割窗口)添加到此新项目中。
- 编译并运行客户端应用程序。它的外观应该与本文第 1 部分结尾的屏幕截图完全相同。
此时,程序的功能略有不同。让我们先处理旧的内容,然后先处理 SDIClient 应用程序。
SDIClient 功能
如果您还记得,SDIClient 应用程序的外观应该与我们在第 1 部分到第 3 部分中构建的原始 SDIMultiApp1 程序完全相同。幸运的是,复制这些功能的过程主要是一个将代码从该应用程序复制/粘贴到 SDIClient 的过程。在下面的所有说明中,当我说明更改/删除/添加时,我指的是文件在 SDIClient 中的副本。
- 在 SDIClient.CPP 中
- 在 `InitInstance()` 函数中,将 `pDocTemplate` 构造函数调用更改为再次使用 `CSDIClientView` 类,如下所示
pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSDIClientDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CSDIClientView)); // <-------
- 在 `InitInstance()` 函数中,复制/粘贴我们的线程/视图函数调用(此代码应位于 `InitInstance()` 函数的末尾/附近)
if (!((CMainFrame*)m_pMainWnd)->PrepareViews()) { return FALSE; } if (((CMainFrame*)m_pMainWnd)->CreateTimerThread()) { ((CMainFrame*)m_pMainWnd)->StartTimerThread(); } ((CMainFrame*)m_pMainWnd)->CreateActionThreads();
- 在 `InitInstance()` 函数中,将 `pDocTemplate` 构造函数调用更改为再次使用 `CSDIClientView` 类,如下所示
- 在 MainFrm.H 中
- 将文件的全部内容复制/粘贴到 SDIClient 项目的 _MainFrm.h_ 中。
- 删除以下行
afx_msg void OnSamplestuffDialogone(); afx_msg void OnSamplestuffDialogtwo();
- 将行 `
#include "SDIMultiApp1View.h"
` 更改为 `#include "SDIClientView.h"
`。
- 在 MainFrm.CPP 中
-
- 首先,将文件的全部内容复制/粘贴到 SDIClient 项目的 MainFrm.CPP 中。
- 将行 `
#include "SDIMultiApp1.h"
` 更改为 `#include "SDIClient.h"
`
- 注释掉以下行(稍后我们将把它们复制/粘贴到别处)
#include "DlgOne.h" #include "DlgTwo.h"
...和...ON_COMMAND(ID_SAMPLESTUFF_DIALOGONE, &CMainFrame::OnSamplestuffDialogone) ON_COMMAND(ID_SAMPLESTUFF_DIALOGTWO, &CMainFrame::OnSamplestuffDialogtwo)
...和...//------------------------------------------------------------- //------------------------------------------------------------- void CMainFrame::OnSamplestuffDialogone() { PauseTimers(); CDlgOne dlg; dlg.DoModal(); ContinueTimers(); } //------------------------------------------------------------- //------------------------------------------------------------- void CMainFrame::OnSamplestuffDialogtwo() { CDlgTwo dlg; dlg.DoModal(); }
- 在 `OnCreateClient`(函数末尾附近)中,更改以下行
m_mainSplitter.CreateView(0, 0, RUNTIME_CLASS(CSDIMultiApp1View), CSize(cr.Width(), nHeight), pContext);
...更改为m_mainSplitter.CreateView(0, 0, RUNTIME_CLASS(CSDIClientView), CSize(cr.Width(), nHeight), pContext);
编码注意事项 此时,我意识到创建一个只包含线程类的文件夹会更有效率,因为它们是我们在构建的所有应用程序共有的。我已经为这两个新项目以及我们最初构建的应用程序完成了这项工作。在本文的示例项目中查找 `ThreadClasses` 文件夹。 - 在解决方案文件夹中创建一个名为 ThreadClasses 的新文件夹。
- 将以下文件从 SDIMultiApp1 项目复制到 ThreadClasses 文件夹,然后将文件添加到项目中
- TimersTHread.*
- ThreadActionBase.*
- ThreadActionLong.*
- ThreadActionShort.*
- EDTimerStruct.h
- 在 SDIClientView.h 中,添加行 `
#include "SDIClientDoc.h"
`。- 在 stdafx.h 中,添加行 `
#include "Constants.h"
`
- 接下来,我们需要复制菜单资源。由于我们的所有项目都在同一个解决方案中,这是一个相对简单的复制/粘贴步骤。只需打开 SDIMultiApp1 项目的菜单资源,然后打开 SDIClient 项目的菜单资源,您可以从一个复制/粘贴到另一个。
- 然后我们需要复制对话框资源。同样,这是一个简单的复制/粘贴过程。
- 在 stdafx.h 中,添加行 `
-
再次回到 SDIViews 项目
我们必须为适用的视图添加消息处理程序。在我们的示例中,我们将只在 `CPrimaryView` 类中添加处理程序。不幸的是,IDE 不允许我通过“属性”视图添加消息处理程序,所以我必须手动添加。还记得我们在向 SDIClient 项目添加功能时注释掉的那些行吗?这正是我们注释掉而不是删除它们的原因。回到 SDIClient 项目,复制粘贴显示对话框的消息处理程序代码。确保将所有 `CMainFrame` 的引用更改为 `CPrimaryView`。
编码注意事项 |
我们不必将对话框放入 SDIViews DLL 中,但我想说明一下我们在实际应用程序中遇到的一些问题。我相信有些优雅(但晦涩)的方法可以解决这个问题,但我们时间紧迫,而且这种方法虽然笨拙,但也能很好地工作。 |
- 我们还需要添加一个函数,通过该函数我们可以传递定时器线程指针。将以下内容添加到 SDIViews.H 文件
#include "TimersTHread.h" class __declspec(dllexport) CPrimaryView : public CView { private: CTimersThread* m_pTimersThread; public: /// Sets the timerthread pointer so we can pause/restart from here void SetTimersThreadPtr(CTimersThread* pThread) { m_pTimersThread = pThread; };
…并在 CPP 文件中添加以下内容m_pTimersThread = NULL;
- 我们现在需要将 `CTimersThread` 文件添加到项目中(记住,它们在 ThreadClasses 文件夹中),并将 ThreadClasses 文件夹添加到 **Additional include directories** 设置中。
- 接下来,我们需要将 `PauseTimers` 和 `ContinueTimers` 函数从 SDIClient 项目的 `CMainFrame` 复制/粘贴到 `CPrimaryView` 类中。
编码注意事项 |
在某个时候,您会问自己为什么我没有创建一个包含定时器支持的基类视图。首先,我们的应用程序不需要这样做,因为只有一个视图需要处理任何菜单命令,因为大多数菜单命令只影响主视图。 另一种方法是使用多重继承,并创建一个处理线程指针并将此功能添加到继承它的任何类中的类。事实上,我们现在就来做这件事。这是一种将通用功能添加到多个现有类的便捷方法。 重要提示:这是 MFC 应用程序中为数不多的使用多重继承的情况之一。之所以可以这样做,是因为 CThread(CTimersThread 的基类)不是从 MFC 类派生的。 |
- 创建一个新的 C++ 类(**不是 MFC 类**),名为 `CTimersTHreadMgr`。在单击“Finish”按钮之前,选中 **Virtual destructor** 复选框
- 当 IDE 显示头文件时,将其更改为如下所示
#pragma once #include "TimersTHread.h" class CTimersThreadMgr { private: CTimersThread* m_pTimersThread; public: CTimersThreadMgr(void); virtual ~CTimersThreadMgr(void); /// Sets the timerthread pointer so we can pause/restart from here void SetTimersThreadPtr(CTimersThread* pThread) { m_pTimersThread = pThread; }; void PauseTimers(); void ContinueTimers(); };
- 现在,打开该类的 CPP 文件并用以下代码替换其内容
#include "StdAfx.h" #include "TimersThreadMgr.h" CTimersThreadMgr::CTimersThreadMgr(void) { m_pTimersThread = NULL; } CTimersThreadMgr::~CTimersThreadMgr(void) { } //---------------------------------------------------------------------- // Pauses the timers thread //---------------------------------------------------------------------- void CTimersThreadMgr::PauseTimers() { // make sure we have a valid pointer if (m_pTimersThread) { m_pTimersThread->Pause(); } } //---------------------------------------------------------------------- // Un-pauses the timers thread //---------------------------------------------------------------------- void CTimersThreadMgr::ContinueTimers() { // make sure we have a valid pointer if (m_pTimersThread) { m_pTimersThread->Continue(); } }
- 接下来,在 CPrimaryView.H 中注释掉(或删除)以下行
CTimersThread* m_pTimersThread; void SetTimersThreadPtr(CTimersThread* pThread) { m_pTimersThread = pThread; };
- 在 SDIClient MainFrmp.CPP 的 `CreateTimersThread()` 函数中(在 `
if (m_pTimersThread)
` 代码块的末尾)添加以下行GetPrimaryView()->SetTimersThreadPtr(m_pTimersThread);
编译并运行 SDIClient 程序。同样,如果我在这里遗漏了一两个步骤,您应该能够找出是什么。请随时告知我,我将更新文章。示例应用程序应该可以正常编译,因此您可以跳过上面的所有废话,选择更简单的方式。:)
运行应用程序时,您应该会看到一个功能(和视觉上)与我们在本文系列第 1 部分到第 3 部分中创建的应用程序完全相同的应用程序。
SDIDisplay 功能
此应用程序与 SDIClient 应用程序略有不同。此应用程序中**不**存在以下功能
- 没有分割窗口
- 没有通过菜单进行用户交互
但是,此应用程序确实显示了主视图(网格),并且它确实利用定时器进行周期性更新。尽管我们没有分割窗口需要处理,但我仍然添加了相同的基本基础结构来支持它——以防经理决定我们需要添加此类功能。此应用程序也将使用 SDIViews DLL,因为我们的 `CPrimaryView` 在那里。
本质上,我们需要对 SDIDisplay 进行与 SDIClient 相同的更改,但有两个例外。为避免重复自己,并为您节省大量阅读时间,我将只介绍主要区别。
- 在 _SDIDisplay.CPP_ 中,我们在 `pDocTemplate` 构造函数调用中使用 `CPrimaryView`。
pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSDIDisplayDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CPrimaryView));
- 我们还需要在调用 PrepareViews() 之前调用一个新函数
// delete the menu (this app doesn't need one) ((CMainFrame*)m_pMainWnd)->DeleteMenu();
- 在 MainFrm.CPP 中,我们有一个 `PrepareViews()` 函数的修改版本。由于我们不必处理多个视图或分割窗口,因此工作起来要容易得多。我们只需要确保拥有正确的活动视图。
//----------------------------------------------------------------------- //----------------------------------------------------------------------- bool CMainFrame::PrepareViews() { bool bResult = true; CPrimaryView* pView = (CPrimaryView*)GetActiveView(); if (pView) { } else { bResult = false; } return bResult; }
同样,这也不是真的必要,但我们希望这两个应用程序看起来尽可能相似,以便于维护和通用参考。
- 接下来,我们将以下函数添加到 MainFrm.CPP 中
//----------------------------------------------------------------------- //----------------------------------------------------------------------- void CMainFrame::DeleteMenu() { ::SetMenu(this->GetSafeHwnd(), NULL); ::DestroyMenu(m_hMenuDefault); } //------------------------------------------------------------------------ //------------------------------------------------------------------------ void CMainFrame::AddMenu() { CMenu hNewMenu; hNewMenu.LoadMenu(IDR_MAINFRAME); SetMenu(&hNewMenu); m_hMenuDefault = hNewMenu.GetSafeHmenu(); }
这些函数允许我们控制菜单的显示。我们之所以有一个 `AddMenu()` 函数,是因为需要有一个菜单附加到应用程序才能关闭它(MFC 期望它存在)。
- 您可能已经猜到,我们需要在重写的 `OnClose()` 处理程序中添加一个对 `AddMenu` 的调用。
- 最后,我们需要隐藏工具栏窗口。只需在调用 `m_wndToolbar.Create()` 后立即将以下行添加到 OnCreate 函数中
m_wndToolBar.ShowWindow(SW_HIDE);
最终结果
此时,我们将应用程序分解为以下离散部分
- SDIViews.DLL - 一个共享的 MFC 扩展 DLL,包含我们的视图和非菜单用户界面组件
- SDIClient.EXE - 一个可执行文件,它隐式链接 `SDIViews.DLL`,通过可交换视图分割窗口提供对所有视图的访问,以及一些对话框来演示暂停和继续定时器线程。
- SDIDisplay.EXE - 一个可执行文件,它隐式链接 `SDIViews.DLL`,提供对 `CPrimaryView` 的访问,并省略了任何形式的可用菜单式用户界面组件。是的,应用程序向导提供的标准文件和编辑命令的部分仍然存在,但它们什么也不做。
以下是您应该看到的一些屏幕截图


下一步?
在第 5 部分中,我们将执行“我的天哪,他们给了我们一大堆新需求”的舞蹈。我们将清理一些遗留问题(例如应用程序标题栏中的“Untitled”一词),通过修改工具栏来增强应用程序,添加另外几个视图,添加打印支持,并增强 CFlatSplitterWnd 以绘制自定义垂直分割条。
第 4 部分结束
由于本文篇幅较长,我决定将其分为几个部分。如果网站编辑按照我的要求操作,所有后续部分都应位于网站的同一代码部分。每个部分都有自己的源代码,因此在阅读后续部分时,请确保下载该部分源代码(除非您要手动完成文章中所述的所有内容)。
为了保持一致性(和理智),请对所有部分进行投票,并以相同的方式投票。这有助于将文章保留在同一部分。感谢您的理解。