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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (15投票s)

2007 年 1 月 27 日

CPOL

14分钟阅读

viewsIcon

44240

downloadIcon

448

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

第 3 部分(共 5 部分) - 简介

指向系列其他部分的链接

以下文本与第 1 部分完全相同。如果您还没有阅读过该文章,那么这篇文章对您来说将毫无意义,所以请务必先补上。我们在这里等您。如果您已阅读第 1 部分文章,您可以跳过这些介绍部分

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

抱怨

我已经是 CodeProject 的会员六年多了(截至本文撰写时),并且我逐渐发现了一些关于文章的令人担忧的趋势。首先,文章作者倾向于发布一篇文章,随着时间的推移,作者基本上就放弃了这篇文章,而对提问的人,他们要么得到作者的沉默,要么得到像“我不再编写这种/那种语言了”这样的回复。实话实说,您不能责怪他们。我使用的许多文章都有三四年了,我理解程序员需要前进,而这通常意味着完全放弃旧代码。

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

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

这篇文章的全部重点在于说明我在过去六年中从 CodeProject 获得的实际代码片段、类和技术的用途,包括为我有时古怪的需求调整代码的变通方法。很多时候,我会使用 VC++ 论坛来提出一个问题,以帮助我理解一篇文章,或者修改文章中的代码供我自己使用。

假设

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

唯一的解决方案是重新开始,并假设您,用户,已经掌握了 VS2005 IDE 的工作知识,特别是其在创建 VC++/MFC 应用程序方面的应用。这样,我们可以更多地讨论重要的事情,而不是在您应该已经知道的事情上浪费时间。我还假设您对 MFC 也有良好的工作知识。我并不是说您必须成为专家,但我假设您可以在 MFC 项目中自如地移动,而不会被 CMainFrame 的复杂性所困扰。

其他

在文章中,您会找到“编码注意事项”。这些只是描述了我编码的方式以及为什么这样做。它们绝不是想象中的必需品,但它们通常涉及代码的可读性和可维护性。我确信你们许多人都有自己的做事方式,但请尽量减少关于这些问题的评论。毕竟,这篇文章不是关于风格的。

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

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

最后,我知道有些人会因为这是我写的而只给我打 1 分。我要求您成熟、专业,并在投票时将您的政治观点限制在自己的地方。请记住,您是在为文章投票,而不是为作者投票。

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()。基类在线程启动时调用此函数,并且因为这是一个纯虚函数,所以我们的基类是抽象类,意味着您不能实例化该类的对象,而必须从它派生一个新类。在我们的例子中,我创建了两个类 - CThreadActionLongCThreadActionShort。这两个类除了重写的 PerformTask 函数外,不包含任何其他内容。

为了说明功能,每个动作线程都设置为休眠不同的秒数。在代码中添加了两个额外的状态栏窗格,以向用户提供视觉反馈。

运行程序

运行程序时,您会注意到状态栏现在更新了状态栏窗格。当动作线程启动时,最初为空的状态窗格将指示动作线程正在运行。您还会注意到,即使在动作线程运行时,计时器线程也会返回到倒计时。

下一步?

在第 4 部分中,我们将把所有视图代码移到一个扩展 DLL 中,并创建两个使用此扩展 DLL 的新应用程序。

第 3 部分结束

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

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

CExtStatusControlBar,由 Dmitriy Yakovlev 编写。这篇文章提供了一个非常好的扩展状态栏类,但我只需要它提供的一小部分功能。以下是将它包含在我们的示例项目中的步骤。
© . All rights reserved.