使用 CodeProject - 应用程序的一天 - 第 3 部分,共 5 部分
使用 CodeProject 进行偶尔支持的正确编码方式。
第 3 部分(共 5 部分) - 简介
指向系列其他部分的链接
- 第 1 部分 - 构建基本应用程序并添加网格
- 第 2 部分 - 添加分割窗口和可交换视图
- 第 4 部分 - 将视图放入扩展 DLL
- 第 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 部分中,我们将添加一个自定义状态栏类和一些简单的多线程功能,以更新状态栏窗格的内容。
更多实际需求
我的实际应用程序的一组要求是能够定期从数据库请求更新,更新自给定起点以来的经过时间,在状态栏中维护一个时钟,并显示到达上述数据库更新的分钟和秒倒计时。经过时间显示在网格中,其他三项显示在状态栏中。
在任何人指出并嘲笑之前,请允许我提醒您——程序员通常不会被要求就某些功能发表意见。就我而言,我强烈反对在状态栏中显示时钟,因为 a) 任务栏中已经显示了一个,b) 它占用了宝贵的 CPU 周期。我似乎记得“愚蠢的想法”这些词很容易从我嘴里说出来。不管怎样——客户想要什么(这应该给你们所有人一个教训)。继续我们的故事……
在这四个计时器中,只有两个触发了耗时的数据库访问,而且幸运的是,它们位于数据访问层的不同部分。我的方法是创建一个单一的计时器线程,该线程在指定的时间间隔触发事件。所有这些事件都将被发布并由 CMainFrame 类进行过滤,然后 CMainFrame 类会将消息发布给必要的窗口(或启动其他工作线程进行数据库工作)。
关于第 3 部分文章内容的几句话
由于第 3 部分我们将要添加的内容的性质,我不会讨论将代码放在哪里,而是更多地讨论我做了什么以及为什么。如果您想查看完整内容,可以将代码解压覆盖第 1 部分或第 2 部分的代码。
自定义状态栏
在开始编写线程代码之前,我必须有一个可以轻松更新状态栏的机制。我找到了这篇文章 -
- 下载该类,并将以下文件提取到
CodeProject
文件夹中(当然是在我们的解决方案文件夹中)。
- StatusStatic.h
- StatusStatic.cpp
- ExtStatusControlBar.h
- ExtStatusControlBar.cpp
- 将文件添加到项目中。
- 打开 MainFrm.h 并添加以下代码行
#include "StatusStatic.h" #include "ExtStatusControlBar.h" class CMainFrame : public CFrameWnd { private: CExtStatusControlBar m_wndStatusBar; CStatusStatic* m_pLongStatus; CStatusStatic* m_pShortStatus; CStatusStatic* m_pSysTimeStatus;
- 我们还将添加一些辅助函数,以保持代码整洁,因此让我们在此同时添加函数原型。
private: BOOL AddOnePane(CStatusStatic* pPane, UINT nPaneID, CString sDefaultText); BOOL AddStatusPanes();
在 MainFrm.cpp 中,我们需要对应用程序向导提供的代码进行一些修改。
- 可选步骤 - 我的实际应用程序不需要默认指示器,所以我像这样注释掉了它们
static UINT indicators[] = { ID_SEPARATOR, // status line indicator /* ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, */ };
- 接下来,我们必须在
OnCreate()
中添加一些代码来修改状态栏。默认状态栏的高度似乎太短,无法显示带有下降字的字母(如“y”、“g”等)的小写版本。因此,我们必须在状态栏创建后立即对其进行调整。if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } //add these lines m_wndStatusBar.SendMessage(SB_SETMINHEIGHT, 20, 0); m_wndStatusBar.UpdateWindow();
在调用
UpdateWindow()
之后,我们需要添加另一行来实际添加我们的自定义状态栏窗格。// now, let's add our new statusbar panes AddStatusPanes();
- 接下来,我们添加辅助函数来复制 ExtStatusBarCtrl 的功能。我这样做是为了使 OnCreate 函数保持简短和简洁。我从原始代码更改的另一件事是获取状态栏的字体(在
AddOnePane()
中),并将窗格设置为该字体,而不是获取GUI_DEFAULT_FONT
,后者是另一种字体,并且比状态栏使用的字体高一些。另外,请注意关于设置每个窗格的字体以避免不期望的默认设置的注释。//---------------------------------------------------------------------- //---------------------------------------------------------------------- BOOL CMainFrame::AddOnePane(CStatusStatic* pPane, UINT nPaneID, CString sDefaultText, int nPaneWidth) { ASSERT(pPane); ASSERT(nPaneID > 0); DWORD dwFlags = WS_CHILD|WS_VISIBLE|SS_CENTER|SS_CENTERIMAGE| SS_NOPREFIX; CRect ctrlRect(0,0,0,0); BOOL bReturn = FALSE; int nIndex = -1; if (!pPane->Create(sDefaultText, dwFlags, ctrlRect, &m_wndStatusBar, 0)) { return FALSE; } // We MUST set the font for each pane. If we don't, the pane will be // set to 12-point MS Shell Dlg. This should actually be set within // the ExtStatusBarControl class, but I wanted to illustrate the need // to do this. pPane->SetFont(m_wndStatusBar.GetFont()); bReturn = m_wndStatusBar.AddPane(nPaneID, 1); if (!bReturn) { AfxMessageBox(_T("Pane index out of range\nor pane with same ID " + "already exists in the status bar"), MB_ICONERROR); return FALSE; } nIndex = m_wndStatusBar.CommandToIndex(nPaneID); if (nIndex == -1) { return FALSE; } m_wndStatusBar.SetPaneWidth(nIndex, nPaneWidth); m_wndStatusBar.AddPaneControl(pPane, nPaneID, true); return TRUE; } //------------------------------------------------------------------------ //------------------------------------------------------------------------ BOOL CMainFrame::AddStatusPanes() { m_pSysTimeStatus = new CStatusStatic; if (m_pSysTimeStatus) { if (!AddOnePane(m_pSysTimeStatus, ID_STATUSPANE_TIME, " 00/00/0000 00:00 ", 110)) { if (m_pSysTimeStatus) { delete m_pSysTimeStatus; m_pSysTimeStatus = NULL; } return FALSE; } } else { return FALSE; } m_pLongStatus = new CStatusStatic; if (m_pLongStatus) { if (!AddOnePane(m_pLongStatus, ID_STATUSPANE_LONGUPDATE, " Nextg Long Update - 00:00 ", 150)) { delete m_pLongStatus; m_pLongStatus = NULL; return FALSE; } } else { return FALSE; } m_pShortStatus = new CStatusStatic; if (m_pShortStatus) { if (!AddOnePane(m_pShortStatus, ID_STATUSPANE_SHORTUPDATE, " Next Short Update - 00:00 ", 150)) { delete m_pShortStatus; m_pShortStatus = NULL; return FALSE; } } else { return FALSE; } m_pLongRunning = new CStatusStatic; if (m_pLongRunning) { if (!AddOnePane(m_pLongRunning, ID_STATUSPANE_LONGRUNNING, "", 90)) { delete m_pLongRunning; m_pLongRunning = NULL; return FALSE; } } else { return FALSE; } m_pShortRunning = new CStatusStatic; if (m_pShortRunning) { if (!AddOnePane(m_pShortRunning, ID_STATUSPANE_SHORTRUNNING, "", 90)) { delete m_pShortRunning; m_pShortRunning = NULL; return FALSE; } } else { return FALSE; } return TRUE; }
- 最后,我们需要将我们的状态栏窗格 ID 添加到
Constants.h
文件中#define ID_STATUSPANE_TIME 49100 #define ID_STATUSPANE_LONGUPDATE 49101 #define ID_STATUSPANE_SHORTUPDATE 49102 #define ID_STATUSPANE_LONGRUNNING 49103 #define ID_STATUSPANE_SHORTRUNNING 49104
此时,运行应用程序时,它应该看起来像这样。

如果将应用程序窗口调整得更小,您会注意到状态栏在右下角会失去调整大小的抓手,并且自定义窗格的右侧开始被裁剪。请参见下图。

这是因为状态栏的提示窗格(显示“Ready”的地方)已达到其最小尺寸。在我的实际应用程序中,用户可能永远不会这样做,所以对我来说这不是什么大问题。如果对您来说是这样,我将其留给程序员作为一项练习来解决这个问题。
如果应用程序无法编译
编译应用程序时,您可能会收到一个错误,提示编译器找不到 afximpl.h
。要解决此问题,请在 VC++ 包含目录的末尾添加以下行(在工具 | 选项...下)
$(VCInstallDir)atlmfc\src\mfc
计时器线程 (CTimerThread)
我的实际应用程序需要计时器来定期访问数据库,以及更新显示器的各个部分。我决定使用线程计时器是一个不错的选择,原因有几点。
WM_TIMER
消息不保证会发送到您的应用程序,因为它们是优先级最低的窗口消息。
- 计时器线程允许在不打扰用户的情况下对您可以做什么进行更多控制。
- 如果可用,您可以将线程分配给另一个 CPU。
对于我的基类,我选择了 Dominik Filipp 的 CThread 类。这是一个文档齐全且相对简单的类,可以添加到您的项目中。您应该参考 CodeProject 上的 CThread
文章,以便更熟悉该类的运作方式,并熟悉本文将使用的术语。
- 下载
CThread
源代码,并将 Thread.CPP 和 Thread.h 文件提取到您的项目文件夹。同样,我选择将文件放入CodeProject
文件夹。 - 将文件添加到您的项目中。
因为 CThread
类是抽象类,所以您必须创建一个派生自它的新类。对于我们的计时器线程,我们需要它成为一个“可通知”线程,因为它从程序启动到程序结束都在运行。
我们将支持多个计时器,因此我在线程类中实现了一个 CTypedPtrArray
,其中包含使线程能够按指定间隔执行正确操作所需的信息。思路是创建线程,将有关每个计时器的信息传递给线程,然后启动线程。
编码注意事项 |
许多人对使用 MFC 集合类存在问题。就我个人而言,我更喜欢使用它们,因为它们比 STL 提供了更干净的接口。此外,这是一篇 MFC 文章,我更希望使用框架的一部分。 |
我们的大部分工作将在 CMainFrame
中完成。首先,我创建了一个辅助函数,该函数实际创建并设置计时器线程。
BOOL CMainFrame::CreateTimerThread() { // In my application, the settings are held in a XML file. For // purposes of example, we'll just hard code the intervals to // expedite the article. int nLongInterval = 300000; // 5 minutes int nShortInterval = 15000; bool bAllowLongUpdate = true; bool bAllowShortUpdate = true; if (!bAllowLongUpdate) { nLongInterval = 0; } if (!bAllowShortUpdate) { nShortInterval = 0; } // create the thread - all timer messages will be sent to this // object, and if necessary, reflected to the current view(s). m_pTimersThread = new CTimersThread((void*)this, 0); if (m_pTimersThread) { m_pTimersThread->SetInterval(1000); // having these two timers seems redundant since they both fire // at the same time - see about combining them m_pTimersThread->SetTimer(TIMER_DATETIME, 0, 1000, "Current Date/Time", " %m/%d/%Y %H:%M ", m_pSysTimeStatus ); m_pTimersThread->SetTimer(TIMER_ELAPSED, UDM_TIMER_ELAPSED, 1000, "Patient's Elapsed Time", "", NULL ); m_pTimersThread->SetTimer(TIMER_LONG, UDM_TIMER_LONG, nLongInterval, "Long Update", " Next Long Update - %02d:%02d ", m_pLongStatus); m_pTimersThread->SetTimer(TIMER_SHORT, UDM_TIMER_SHORT, nShortInterval, "Short Update", " Next Short Update - %02d:%02d ", m_pShortStatus); // sanity checks - if we don't want automatic updates, or if // something went wrong when ewe retrieved the intervals, the // timers need to be turned off to avoid unexpected/undesireable // events. if (!bAllowLongUpdate || nLongInterval == 0) { m_pTimersThread->EnableTimer(TIMER_LONG, false); } if (!bAllowShortUpdate || nShortInterval == 0) { m_pTimersThread->EnableTimer(TIMER_SHORT, false); } } return (m_pTimersThread != NULL); }
即使计时器线程专门用于更新状态栏窗格,您仍然可以设置一个计时器,该计时器除了向父窗口发送消息外,不做其他任何事情。TIMER_ELAPSED
计时器就是一个很好的例子。请注意,调用 SetTimer for that timer 的状态窗格值为 NULL(函数调用中的最后一个参数)。
您还可以创建不向父窗口发送消息但更新状态栏窗格的计时器。这种计时器的一个例子是 TIMER_DATETIME
。请注意,函数调用中的第二个参数是 0
。
使用计时器线程的第一步是在应用程序类的 InitInstance()
函数中调用此函数。如果 create 函数返回 TRUE,我们就可以启动线程。代码看起来是这样的。
BOOL CSDIMultiApp1App::InitInstance() { ... // all of our programmer-added code will be going at the end of this // function put this AFTER the call to PrepareViews() if (((CMainFrame*)m_pMainWnd)->CreateTimerThread()) { ((CMainFrame*)m_pMainWnd)->StartTimerThread(); } }
计时器线程甚至可以暂停和继续。示例应用程序允许您从菜单中测试这一点。如果单击 Sample Stuff | Dialog One (pauses the timers),计时器线程将被暂停,并且将显示一个对话框。为了进行比较,还有一个 Dialog Two 菜单项,允许计时器线程在显示对话框时继续运行。
这是 CTimersThread
的关键函数 - CheckInterval()
。每次计时器“滴答”时都会调用此函数。通常,这每秒一次,但可以通过 CTimersThread::SetInterval()
函数更改滴答间隔。
void CTimersThread::CheckInterval() { EDTIMER* pTimer = NULL; for (int i = 0; i < m_nTimerCount; i++) { pTimer = m_tpaTimers.GetAt(i); if (!pTimer) { continue; } // see if the timer is enabled if (!pTimer->bEnabled) { pTimer->nElapsed = 0; continue; } // update the elapsed time pTimer->nElapsed += m_nTimerInterval; // see if we need to update the status bar if (pTimer->pStatusPane) { CString sText = ""; if (!pTimer->sFormat.IsEmpty()) { switch (pTimer->nTimerID) { case TIMER_LONG : case TIMER_SHORT : sText = GetTimeLeft(pTimer); break; case TIMER_DATETIME: sText = COleDateTime::GetCurrentTime().Format(pTimer->sFormat); break; case TIMER_ELAPSED : break; } } if (pTimer->pStatusPane && m_bCanContinue) { pTimer->pStatusPane->SetWindowText(sText); } } if (pTimer->nElapsed < pTimer->nInterval) { continue; } pTimer->nElapsed = 0; if (pTimer->nMsgID <= 0) { continue; } switch (pTimer->nTimerID) { case TIMER_LONG : ::SendMessage(m_pParentWnd->GetSafeHwnd(), pTimer->nMsgID, 0, 0); break; case TIMER_SHORT : ::PostMessage(m_pParentWnd->GetSafeHwnd(), pTimer->nMsgID, 0, 0); break; case TIMER_ELAPSED : ::SendMessage(m_pParentWnd->GetSafeHwnd(), pTimer->nMsgID, 0, 0); break; case TIMER_DATETIME : // no message to process break; } } }
上面的函数调用 GetTimeLeft()
函数对经过的时间进行一些计算,并返回一个格式化的字符串,表示计时器线程触发指定消息之前剩余的时间。
CString CTimersThread::GetTimeLeft(EDTIMER* pTimer) { CString sResult = ""; int nMins = 0; int nSecs = 0; if (pTimer->bEnabled) { div_t dt; int nRemaining = pTimer->nInterval - pTimer->nElapsed; dt = div(nRemaining, 1000); nRemaining = dt.quot; dt = div(nRemaining, 60); nMins = dt.quot; nSecs = dt.rem; sResult.Format(pTimer->sFormat, nMins, nSecs); } else { sResult = "ERR"; } return sResult; }
要根据您的需求修改此类,应该是一件很简单的事情。
动作线程
实现了动作线程是为了让我能够执行耗时的操作,而不会阻碍用户使用程序的能力。这些动作线程是在响应 CreateTimersThread()
函数中指定的计时器线程消息时启动的。
再次,我使用了 Dominik Filipp 的 CThread
类作为基础。由于动作线程的执行方式都相同,我首先创建了一个名为 CThreadActionBase
的基类,其中包含线程处理函数的以下版本
DWORD CThreadActionBase::ThreadHandler() { BOOL bCanContinue = TRUE; int nIncomingCommand; do { WaitForNotification(nIncomingCommand); switch (nIncomingCommand) { case CThread::CMD_INITIALIZE: HandleCommandImmediately(CThread::CMD_RUN); break; case CThread::CMD_RUN: PerformTask(); bCanContinue = FALSE; break; case CThread::CMD_PAUSE: SetActivityStatus(CThread::THREAD_PAUSED); break; case CThread::CMD_STOP: bCanContinue = FALSE; break; default: break; }; } while (bCanContinue); // when the thread is done, send this message (if specified) if (m_pOwnerWnd && m_nMsgID > 0) { ::PostMessage(m_pOwnerWnd->GetSafeHwnd(), m_nMsgID, 0, 0); } return 0; // ... if Thread task completion OK }
我使线程“可通知”,以便在不立即启动的情况下创建线程。这个决定也减少了线程完成时删除和需要时重新创建线程的开销。在此应用程序中,这些线程响应计时器线程消息而运行,因此这是一个主要的 CPU 周期节省器(快速说十遍 :))。
接下来,我添加了一个纯虚函数 PerformTask()
。基类在线程启动时调用此函数,并且因为这是一个纯虚函数,所以我们的基类是抽象类,意味着您不能实例化该类的对象,而必须从它派生一个新类。在我们的例子中,我创建了两个类 - CThreadActionLong
和 CThreadActionShort
。这两个类除了重写的 PerformTask
函数外,不包含任何其他内容。
为了说明功能,每个动作线程都设置为休眠不同的秒数。在代码中添加了两个额外的状态栏窗格,以向用户提供视觉反馈。
运行程序
运行程序时,您会注意到状态栏现在更新了状态栏窗格。当动作线程启动时,最初为空的状态窗格将指示动作线程正在运行。您还会注意到,即使在动作线程运行时,计时器线程也会返回到倒计时。

下一步?
在第 4 部分中,我们将把所有视图代码移到一个扩展 DLL 中,并创建两个使用此扩展 DLL 的新应用程序。
第 3 部分结束
由于本文篇幅较长,我决定将其分成几部分。如果网站编辑按照我的要求操作,所有后续部分应该都在网站的同一个代码部分。每个部分都有其自己的源代码,因此在阅读后续部分时,请确保下载该部分对应的源代码(除非您正在手动执行文章中概述的所有内容)。
为了保持一致性(和理智),请对所有部分进行投票,并以相同的方式投票。这有助于将文章保留在同一部分。感谢您的理解。
CExtStatusControlBar,由 Dmitriy Yakovlev 编写。这篇文章提供了一个非常好的扩展状态栏类,但我只需要它提供的一小部分功能。以下是将它包含在我们的示例项目中的步骤。