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

Mike 的普通 Code Project 屏幕保护程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (8投票s)

2002年5月16日

10分钟阅读

viewsIcon

192178

downloadIcon

2402

一个用 Win32 API 编写的 Code Project 屏幕保护程序。

引言

好吧,当时有一个屏幕保护程序比赛。起初,它只针对托管代码,但作为叛逆者,我决定继续用非托管代码编写屏幕保护程序;至少我可以学点新东西。我称之为我的“正常”屏幕保护程序,因为我(叛逆者)决定不使用负面形容词“非托管”。我不会在这里长篇大论(我将留到“观点”部分),但我展示我参加屏幕保护程序比赛的作品(幸运的是,比赛最终也允许普通作品参赛)。

首先,这是屏幕保护程序的截图,显示了CodeProject的Logo和所有三个XML Feed(48K)

 [screen saver - 9K]

这不会是一个完整的屏幕保护程序教程,因为这里有其他关于如何编写屏幕保护程序的文章,而且C语言的MSDN屏幕保护程序编写文档也相当不错。我将重点讨论我如何使用Web服务以及程序中的其他一些技术细节。变量名(希望如此!)足够具有描述性,以便您轻松理解代码。

此屏幕保护程序的系统要求是任何Win32桌面操作系统,以及Internet Explorer 5或更高版本。

快速入门指南

下载二进制文件,并将SCR文件解压缩到您的windows\system(适用于9x/Me)或windows\system32(适用于NT/2000/XP)目录中。右键单击桌面,然后单击“属性”。单击“屏幕保护程序”选项卡。在“屏幕保护程序”组合框中,选择“Mike's CP screen saver”或“MikesCPSaver”,以其中一个显示为准。(它应该显示“Mike's CP screen saver”,但是,在某些Windows版本上,它会显示文件名“MikesCPSaver”。)单击“设置”按钮,查看可用的各种选项,这些选项控制屏幕上显示的信息、使用的字体和使用的图形。

请注意,目前,与Web服务的通信发生在主线程上,因此如果您看到黑屏几秒钟,请不要惊慌。一旦所有数据都从Web服务下载完毕,屏幕保护程序图形就会出现。

关于编译代码的说明:演示项目设置为链接Microsoft提供的静态库scrnsave.lib。在Platform SDK的最新版本中,此lib使用sehprolg.obj中的代码,因此该OBJ文件也包含在链接器选项中。如果您有一个较旧的Platform SDK,其中不包含该OBJ文件,只需将其从链接器选项中删除即可。

屏幕保护程序初始化

主窗口过程ScreenSaverProc()在响应WM_CREATE消息时执行初始化。它检查由scrnsave.lib中的代码设置的fChildPreview变量,以确定屏幕保护程序是否正在从“显示”控制面板小程序中以预览模式运行,并使用InternetGetConnectedState()函数检查计算机是否已连接到Internet。OnCreate()读取屏幕保护程序选项,初始化一些GDI对象(位图、画笔等),并启动一个驱动屏幕保护程序其余部分的计时器。

在预览模式下绘图

当屏幕保护程序在预览模式下运行时,它会用Code Project橙色填充预览窗口,并在窗口中的随机位置绘制一个小小的Code Project Logo,如下图所示

 [Preview window - 8K]

OnTimer()的开头,我们获取屏幕保护程序窗口的绘图设备上下文,然后检查当前时间。变量g_tmNextBitmapDraw存储一个time_t值,表示我们应该绘制Logo的下一个时间。(您会注意到这个程序中有很多全局变量——这是因为没有C++类来更容易地组织事情。)如果到了绘制Logo的时间,我们就调用DrawCPLogo(),重置g_tmNextBitmapDraw,然后返回。

void OnTimer ( HWND hwnd )
{
HDC    dc = GetDC ( hwnd );
bool   bRedrawCorners = false;
time_t tmCurrentTime = time(NULL);
 
    SaveDC ( dc );
 
    // In preview mode, just check to see if we need to redraw the logo.

    // (That's the only thing we draw in preview mode.)

    if ( g_bPreviewMode )
        {
        if ( tmCurrentTime > g_tmNextBitmapDraw )
            {
            DrawCPLogo ( dc, true );
            g_tmNextBitmapDraw = tmCurrentTime + g_tmBitmapDrawInterval;
            }
 
        RestoreDC ( dc, -1 );
        ReleaseDC ( hwnd, dc );
        return;
        }

这是DrawCPLogo()的代码。它用背景色填充Logo的旧位置以将其擦除,然后选择一个新的随机位置并在此位置绘制Logo。g_rcSpaceForLogo是一个RECT,存储Logo的可用空间。在预览模式下,它的尺寸与整个预览窗口相同。X和Y坐标使用rand()函数选择,并进行调整,以确保Logo完全在屏幕内。

void DrawCPLogo ( HDC dc, bool bEraseOldLogo )
{
HDC dcMem;
 
    // If the logo is already on the screen, erase it by filling its 

    // coordinates with the background color.

    if ( bEraseOldLogo )
        FillRect ( dc, &g_rcLastBitmapDraw, g_hbrBackgroundBrush );
 
    dcMem = CreateCompatibleDC ( dc );
    SelectObject ( dcMem, g_hbmLogo );
 
    // Pick a random location for the logo, staying within the screen and

    // the free space area (as kept by g_rcSpaceForLogo).

    g_rcLastBitmapDraw.left = rand() % ( g_uScrWidth - g_lBmWidth );
    g_rcLastBitmapDraw.top = g_rcSpaceForLogo.top + rand() 
        % ( g_rcSpaceForLogo.bottom - g_rcSpaceForLogo.top - g_lBmHeight );
    g_rcLastBitmapDraw.right = g_rcLastBitmapDraw.left + g_lBmWidth;
    g_rcLastBitmapDraw.bottom = g_rcLastBitmapDraw.top + g_lBmHeight;
 
    BitBlt ( dc, g_rcLastBitmapDraw.left, g_rcLastBitmapDraw.top,
             g_lBmWidth, g_lBmHeight, dcMem, 0, 0, SRCCOPY );
 
    DeleteDC ( dcMem );
}

显示新闻闪报

如果屏幕保护程序不在预览模式下,它会检查是否到了查找新闻闪报的时间,或者,如果新闻闪报正在显示,是否可以将其从屏幕上移除。如果新闻闪报已显示,屏幕保护程序会检查g_tmRemoveNewsflashTime变量,如果当前时间晚于g_tmRemoveNewsflashTime,则会隐藏气球提示,并设置bRedrawCorners标志,以便OnTimer()后面的代码将绘制XML Feed。

    // Is it time to show/remove the newsflash?

    if ( g_bConnectedToNet )
        {
        if ( g_bNewsflashOnScreen )
            {
            if ( tmCurrentTime > g_tmRemoveNewsflashTime )
                {
                TOOLINFO ti = { sizeof(TOOLINFO) };

                ti.hwnd = hwnd;
                ti.uId  = (UINT) hwnd;

                SendMessage ( g_hwndTooltip, TTM_TRACKACTIVATE, 
                     FALSE, (LPARAM) &ti );

                g_bNewsflashOnScreen = false;
                bRedrawCorners = true;
                }
            else
                {
                RestoreDC ( dc, -1 );
                ReleaseDC ( hwnd, dc );
                return;
                }
            }

如果到了检查新闻闪报的时间,屏幕保护程序就会这样做。如果新闻闪报文本非空,屏幕保护程序就会调用DrawNewsFlash()来显示一个大的Bob Logo,并在气球提示中显示新闻闪报。

        else if ( tmCurrentTime > g_tmNextNewsflashUpdate )
            {
            if ( Websvc_GetNewsflash() && !g_sNewsflash.empty() )
                {
                DrawNewsflash ( dc, hwnd );
                g_tmRemoveNewsflashTime = tmCurrentTime + 
                       g_tmNewsflashShowTime;
                g_bNewsflashOnScreen = true;
                }

            if ( 0 == g_tmNewsflashUpdateInterval )
                {
                // If this is the first time getting the newsflash, 

                // also get the

                // # of minutes we should wait 

                // before getting the newsflash again.

                if ( !Websvc_GetNewsflashUpdateInterval() )
                    g_tmNewsflashUpdateInterval = 30*60;    
                          // default to 30 min

                }

            g_tmNextNewsflashUpdate = tmCurrentTime + 
                     g_tmNewsflashUpdateInterval;

            RestoreDC ( dc, -1 );
            ReleaseDC ( hwnd, dc );
            return;
            }
        }

这是我们第一次接触Web服务。Websvc_GetNewsflash()函数与Web服务通信并检索新闻闪报。该函数列示在下面;最终结果是g_sNewsflash变量(它是一个std::string)被填充了新闻闪报文本。

它首先创建一个XML文档,并从Web服务URL初始化它。

bool Websvc_GetNewsflash()
{
USES_CONVERSION;
LPCTSTR szNewsflashURL = _T(
  "https://codeproject.org.cn/webservices/latest.asmx/GetNewsflash?");

    g_sNewsflash.erase();

try
{
MSXML::IXMLDOMDocumentPtr pDoc;
MSXML::IXMLDOMElementPtr pRootNode;

    // Create an XML document, and turn off async mode so 

    // that load() runs synchronously.

    if ( FAILED(pDoc.CreateInstance ( __uuidof(MSXML::DOMDocument), NULL )))
        return false;

    pDoc->async = VARIANT_FALSE;

    pDoc->load ( _variant_t(szNewsflashURL) );

IXMLDOMDocument::load()的一个优点是它可以处理所有HTTP通信。这也是屏幕保护程序神奇地兼容代理和防火墙的原因——我让微软完成了繁重的工作!设置async标志为FALSE很重要,因为我不想在整个文档下载并解析完成之前让load()返回。

新闻闪报XML只有一个标签,<string>,其内部文本包含新闻闪报。我首先通过documentElement属性获取文档的根节点,然后读取其文本,该文本随后存储在g_sNewsflash中。大的try/catch块用于捕获MSXML包装器抛出的异常,以防XML解析过程中出现问题。

    // Get the root node - a <string> tag that is either empty, or contains

    // the newsflash text.

    pRootNode = pDoc->documentElement;

    g_sNewsflash = (LPCTSTR) pRootNode->text;
}
catch (...)
{
    return false;
}

    return true;
}

在屏幕保护程序模式下绘图

在屏幕保护程序模式下,OnTimer()将当前时间与三个Feed的每个Feed对应的time_t变量进行比较。每个time_t值对应一个Feed,如果当前时间晚于某个变量的值,那么就是时候刷新该Feed了。这是获取新文章列表的代码

    // If we're showing latest articles, see if it's time to get the list of

    // articles from CP.

    if ( g_bConnectedToNet  &&  g_bShowNewestArticles  &&  
          tmCurrentTime > g_tmNextArticleListUpdate )
        {
        // Grab latest articles from the CP web service

        Websvc_GetLatestArticles();
        bRedrawCorners = true;

        // If this is the first time getting the article list, also get the

        // # of minutes we should wait before getting the list again.

        if ( 0 == g_tmArticleListUpdateInterval )
            {
            if ( !Websvc_GetArticleListUpdateInterval() )
                g_tmArticleListUpdateInterval = 20*60; // default to 20 min

            }

        // Calculate the next time that we'll get the list again.

        g_tmNextArticleListUpdate = tmCurrentTime + 
              g_tmArticleListUpdateInterval;
        }

g_tmNextArticleListUpdate存储我们应该从Web服务获取文章列表的下一个time_t值。当当前时间超过该值时,我们调用Websvc_GetLatestArticles()来获取列表。如果这是第一次获取文章列表,我们还会获取更新间隔,它告诉我们获取文章列表的频率。之后,g_tmNextArticleListUpdate被设置为下一次更新的时间。

稍后我将回到Websvc_GetLatestArticles()。现在,让我们跳到执行实际绘图的代码。bRedrawCorners标志指示我们是否应该重绘这三个列表。如果该标志为true,我们就会擦除屏幕并重置用于跟踪Logo可用空间的RECT

    // If we're going to redraw the corner items, erase the

    // window now and reset

    // the RECT that keeps track of the space available for the CP logo.

    if ( bRedrawCorners )
        {
        EraseWindow ( hwnd );
        SetRect ( &g_rcSpaceForLogo, 0, 0, g_uScrWidth, g_uScrHeight );
        }

接下来,如果显示时钟的选项开启,我们就会绘制时钟。

    // If the option to show the clock is on, draw the

    // clock now.  (We always do 

    // this because this function gets called about once per second.)

    if ( g_bShowClock )
        DrawClock ( dc, hwnd );

接下来,如果bRedrawCorners为true,我们就会绘制用户想要查看的Feed。

    // If all the corners are being redrawn this time through, draw the

    // items that the user wants to see.

    if ( bRedrawCorners )
        {
        if ( g_bShowNewestLoungePosts )
            DrawLounge ( dc, hwnd );

        if ( g_bShowNewestArticles )
            DrawLatestArticles ( dc, hwnd );

        if ( g_bShowNewestComments )
            DrawLatestComments ( dc, hwnd );
        }

最后,我们检查是否到了重绘Code Project Logo的时间。

    // If the corners were redrawn (which meant the screen was erased),

    // or it's time to move the logo, draw the logo now.

    if ( bRedrawCorners || tmCurrentTime > g_tmNextBitmapDraw )
        {
        DrawCPLogo ( dc, !bRedrawCorners  &&  0 != g_tmNextBitmapDraw );
        g_tmNextBitmapDraw = tmCurrentTime + g_tmBitmapDrawInterval;
        }

我将在这里介绍DrawLatestArticles()函数;绘制角落文本的其他三个函数也非常相似。DrawLatestArticles()读取全局字符串g_sLatestArticles,在其前面添加一个标题,然后调用DrawTextInCorner()来执行实际的绘图。

void DrawLatestArticles ( HDC dc, HWND hwnd )
{
string sText;

    sText = _T("NEWEST ARTICLES:\n") + g_sLatestArticles;
    SetTextColor ( dc, g_crCPOrange );
    DrawTextInCorner ( dc, hwnd, sText.c_str(), g_eLatestArticlesCorner );
}

DrawTextInCorner()执行两项操作:当然是绘制文本,并更新g_rcSpaceForLogog_rcSpaceForLogo是一个RECT,用于跟踪未被任何文本占用的空间;这是Logo将要绘制的空间。DrawTextInCorner()首先计算绘制文本时文本将占用的空间。

void DrawTextInCorner ( HDC dc, HWND hwnd, LPCTSTR szText, ECorner corner )
{
RECT rc;

    SetTextAlign ( dc, TA_LEFT | TA_TOP | TA_NOUPDATECP );

    // Calculate the rect needed to draw the text.

    GetWindowRect ( hwnd, &rc );
    rc.bottom = rc.top;
    DrawText ( dc, szText, -1, &rc, DT_CALCRECT | DT_NOPREFIX );

在调用DrawText()之后,rc存储了文本周围的边界矩形。接下来,我们根据corner值进行切换,并使用DrawText()的正确组合标志,将文本放置在屏幕的正确位置。这是绘制左上角的代码。

    // Draw the text & update the global RECT that holds the space

    // available for the logo.

    switch ( corner )
        {
        case topleft:
            DrawText ( dc, szText, -1, &rc, DT_LEFT | DT_NOPREFIX );
            g_rcSpaceForLogo.top = max(rc.bottom, g_rcSpaceForLogo.top);
        break;

rc.bottom存储文本占用的最底部坐标,因此这也是Logo可用的最顶层坐标。g_rcSpaceForLogo.top相应地进行调整。对于底部角落,rc需要向下移动,使其底部值与屏幕高度(g_uScrHeight)相同。

        case bottomleft:
            rc.top = g_uScrHeight - rc.bottom;
            rc.bottom = g_uScrHeight;
            DrawText ( dc, szText, -1, &rc, DT_LEFT | DT_NOPREFIX );
            g_rcSpaceForLogo.bottom = min(rc.top, g_rcSpaceForLogo.bottom);
        break;

其他两个角落的绘图代码是相似的。

获取文章列表

Websvc_GetLatestArticles()函数从Web服务读取最新的文章列表。由于文章列表是更复杂的XML,我将介绍解析它的代码。Websvc_GetLatestArticles()检索列表,并将文章标题和作者姓名填充到全局字符串g_sLatestArticles中。它首先创建Web服务的URL。

bool Websvc_GetLatestArticles()
{
USES_CONVERSION;
LPCSTR szArticleBriefsFormat = 
  "http://.../latest.asmx/GetLatestArticleBrief?NumArticles=";
std::stringstream strm;
std::string sURL;
static bool s_bFirstCall = true;

    strm << szArticleBriefsFormat << g_nNumArticlesToShow << std::ends;
    sURL = strm.str();
    g_sLatestArticles.erase();

接下来,我们创建一个XML文档并从Web服务URL加载它。

try
{
MSXML::IXMLDOMDocumentPtr pDoc;
MSXML::IXMLDOMElementPtr pRootNode;
MSXML::IXMLDOMNodeListPtr pNodeList;
long l, lSize;

    // Create an XML document, and turn off async mode 

    // so that load() runs synchronously.

    if ( FAILED(pDoc.CreateInstance ( __uuidof(MSXML::DOMDocument), 
          NULL, CLSCTX_INPROC_SERVER )))
        return false;

    pDoc->async = VARIANT_FALSE;

    pDoc->load ( _variant_t(sURL.c_str()) );

和之前一样,我们从文档的根节点开始。这次,我们使用根节点的childNodes属性获取子节点列表,即<ArticleBrief>标签。

    // Get the root node - an <ArrayOfArticleBrief> 

    // tag that holds a list of

    // <ArticleBrief> tags, one per article.

    pRootNode = pDoc->documentElement;
    pNodeList = pRootNode->childNodes;

我们进入一个循环,读取我们感兴趣的每个标签的两个字段——标题和作者。循环首先在下一个<ArticleBrief>节点上获取一个IXMLDOMElement接口。使用该接口,我们读取两个属性(它们本身是子元素)。getElementsByTagName()返回一个元素列表,但我们知道列表中只有一个属性,所以我们通过列表的item属性按索引访问它。

    // For each <ArticleBrief> tag, grab the fields we're 

    // interested in - Title

    // and Author.

    for ( l = 0, lSize = pNodeList->length; l < lSize; l++ )
        {
        MSXML::IXMLDOMNodePtr pNode;
        MSXML::IXMLDOMElementPtr pArticleBriefElt, pTitleElt, pAuthorElt;
        _bstr_t bsTitle, bsAuthor;

        try
            {
            // Get the next <ArticleBrief> node and QI for 

            // its IXMLDOMElement interface.

            pNode = pNodeList->item[l];
            pNode->QueryInterface ( &pArticleBriefElt );

            pTitleElt = pArticleBriefElt->getElementsByTagName ( 
                _bstr_t("Title") )->item[0];
            pAuthorElt = pArticleBriefElt->getElementsByTagName ( 
                _bstr_t("Author") )->item[0];

接下来,我们将标题和作者姓名保存到单独的变量中,然后将文章信息添加到g_sLatestArticles字符串中。

            bsTitle = pTitleElt->text;
            bsAuthor = pAuthorElt->text;

            if ( l > 0 )
                g_sLatestArticles += '\n';

            // Add the title and author to the global string that holds the 

            // list of articles.

            g_sLatestArticles += (LPCTSTR) bsTitle;
            g_sLatestArticles += _T(" (");
            g_sLatestArticles += (LPCTSTR) bsAuthor;
            g_sLatestArticles += ')';
            }
        catch (...)
            {
            // Do nothing, just continue to the next tag in the array.

            }
        }

由于作者姓名可能包含HTML标签,我们移除这些标签,以便在屏幕保护程序中只显示纯文本。这很简单——我们搜索'<''>',并删除这些字符以及它们之间的所有内容。

    // Strip out HTML tags from the feed - the author names come down as they

    // are stored on the server, funky HTML tags included.

string::size_type nLessThan, nGreaterThan;

    while ( (nLessThan = g_sLatestArticles.find ( '<' )) != string::npos )
        {
        nGreaterThan = g_sLatestArticles.find ( '>', nLessThan+1 );
        
        if ( string::npos == nGreaterThan )
            break;

        g_sLatestArticles.erase ( nLessThan, nGreaterThan - nLessThan + 1 );
        }
}
catch (...)
{
    return false;
}

    return true;
}

选项对话框

屏幕保护程序选项对话框非常简单。它主要是无聊的控件设置以及选项的读写。一个重要部分是在标准的屏幕保护程序函数RegisterDialogClasses()中——该函数调用InitCommonControls(),以便在Windows XP上启用主题。ScreenSaverConfigureDialog()是对话框过程。

这是对话框的截图(13K)

 [options thumbnail - 15K]

源代码映射

这是源代码文件列表及其中的函数。

ConfigDlg.cpp

  • RegisterDialogClasses() - 调用InitCommonControls()以在配置对话框中启用主题。
  • ScreenSaverConfigureDialog() - 配置对话框的对话框过程。
    OnChooseFont() - 处理“选择字体”按钮。

CPSaver.cpp

  • ScreenSaverProc() - 屏幕保护程序的主窗口过程。
  • OnCreate() - 处理WM_CREATE,执行初始化。
  • OnTimer() - 处理WM_TIMER,驱动所有绘图。
  • OnDestroy() - 处理WM_DESTROY,释放资源并将一些数据保存到注册表中。
  • EraseWindow() - 擦除整个屏幕保护程序窗口。
  • DrawNewsflash() - 在屏幕中央绘制一个大的Bob Logo,并弹出一个气球提示显示新闻闪报。

globals.cpp

  • ReadSettings() - 从注册表中读取选项,使用键HKCU\software\The Code Project\Mike's Normal Screen Saver
  • InitGDIObjects() - 初始化各种GDI对象。
  • SaveListLengths() - 将每个文章/帖子列表的最大长度存储在注册表中。
  • SaveSettings() - 将选项保存到注册表中。
  • DrawTextInCorner() - 在屏幕的给定角落绘制一个字符串。
  • DrawClock() - 绘制时钟,如果计算机未连接到网络,则显示离线通知。
  • DrawCPLogo() - 绘制Code Project Logo。
  • DrawLounge() - 绘制最新的Lounge话题列表。
  • DrawLatestArticles() - 绘制最新的文章列表。
  • DrawLatestComments() - 绘制最新的讨论论坛话题列表。

websvc.cpp

  • Websvc_GetArticleListUpdateInterval() - 读取请求文章列表更新之间应该等待的分钟数。
  • Websvc_GetLatestArticles() - 读取最新的文章列表。
  • Websvc_GetMaxNumArticles() - 读取Web服务将返回的最新文章列表的最大长度。
  • Websvc_GetCommentsUpdateInterval() - 读取请求论坛话题更新之间应该等待的分钟数。
  • Websvc_GetLatestComments() - 读取最新的论坛话题列表。
  • Websvc_GetMaxNumComments() - 读取将由Web服务返回的论坛话题列表的最大长度。
  • Websvc_GetLoungeUpdateInterval() - 读取请求Lounge话题更新之间应该等待的分钟数。
  • Websvc_GetLatestLoungePosts() - 读取最新的Lounge话题列表。
  • Websvc_GetMaxNumLoungePosts() - 读取将由Web服务返回的Lounge话题列表的最大长度。
  • Websvc_GetNewsflashUpdateInterval() - 读取请求新闻闪报更新之间应该等待的分钟数。
  • Websvc_GetNewsflash() - 读取新闻闪报。
© . All rights reserved.