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






4.58/5 (8投票s)
2002年5月16日
10分钟阅读

192178

2402
一个用 Win32 API 编写的 Code Project 屏幕保护程序。
引言
好吧,当时有一个屏幕保护程序比赛。起初,它只针对托管代码,但作为叛逆者,我决定继续用非托管代码编写屏幕保护程序;至少我可以学点新东西。我称之为我的“正常”屏幕保护程序,因为我(叛逆者)决定不使用负面形容词“非托管”。我不会在这里长篇大论(我将留到“观点”部分),但我将展示我参加屏幕保护程序比赛的作品(幸运的是,比赛最终也允许普通作品参赛)。
首先,这是屏幕保护程序的截图,显示了CodeProject的Logo和所有三个XML Feed(48K)
这不会是一个完整的屏幕保护程序教程,因为这里有其他关于如何编写屏幕保护程序的文章,而且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,如下图所示
在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_rcSpaceForLogo
。g_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)
源代码映射
这是源代码文件列表及其中的函数。
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()
- 读取新闻闪报。