Introduction
OK, so there was this screen saver contest. At first it was for managed code only, but me being the rebel that I am, decided to go ahead and write a screen saver in unmanaged code; I could at least learn something new. I call this my "normal" screen saver because I (the rebel) decided not to use the negative adjective "unmanaged". I won't get off on a rant here (I'll save that for the Soapbox) but I will present my entry into the screen saver contest (which, lucky me, did eventually allow normal entries as well).
First, a screen shot of the screen saver, showing the CodeProject logo and all three XML feeds (48K):
This won't be a full-on screen saver tutorial, since there are other articles here on how to write screen savers, and the MSDN documentation on writing screen savers in C is pretty good. I will instead discuss how I used the web service and some other technical details in the program. The variable names are (hopefully!) descriptive enough that you can follow the code easily.
The system requirements for this screen saver are any Win32 desktop OS, and Internet Explorer 5 or later.
Quick start guide
Download the binary and extract the SCR file into your windows\system (for 9x/Me) or windows\system32 (for NT/2000/XP) directory. Right-click the desktop and click Properties. Click the Screen Saver tab. In the Screen Saver combo box, pick "Mike's CP screen saver" or "MikesCPSaver," whichever appears there. (It should say "Mike's CP screen saver", however on some versions of Windows it will show the name of the file, "MikesCPSaver".) Click the Settings button to see the various options available, which control the information displayed on the screen, the font used, and the graphics used.
Note that at the moment, the communication with the web service happens on the main thread, so don't be alarmed if you get a black screen for a few seconds. The screen saver graphics will appear once all the data has been downloaded from the web service.
A note about compiling the code: The demo project is set to link with the Microsoft-provided static library scrnsave.lib. In recent builds of the Platform SDK, this lib uses code in sehprolg.obj, so that OBJ file is in the linker options as well. If you have an older Platform SDK that does not include that OBJ file, just remove it from the linker options.
Saver initialization
The main window procedure, ScreenSaverProc()
, does the initialization in response to the WM_CREATE
message. It checks the fChildPreview
variable which is set by the code in scrnsave.lib to determine whether the saver is being run in preview mode from the Display control panel applet, and checks whether the computer is connected to the Internet using the InternetGetConnectedState()
function. OnCreate()
reads the saver options, initializes some GDI objects (bitmaps, brushes, etc.), and starts a timer which drives the rest of the screen saver.
Drawing in preview mode
When the saver is run in preview mode, it fills the preview window with Code Project Orange and draws a small Code Project logo at a random location in the window, as shown here:
At the beginning of OnTimer()
, we get a device context for drawing in the saver window, then check the current time. The variable g_tmNextBitmapDraw
holds a time_t
value representing the next time we should draw the logo. (You'll notice there are a lot of globals in this program - the result of not having C++ classes to make organizing things easier.) If it's time to draw the logo, we call DrawCPLogo()
, reset g_tmNextBitmapDraw
, and return.
void OnTimer ( HWND hwnd )
{
HDC dc = GetDC ( hwnd );
bool bRedrawCorners = false;
time_t tmCurrentTime = time(NULL);
SaveDC ( dc );
if ( g_bPreviewMode )
{
if ( tmCurrentTime > g_tmNextBitmapDraw )
{
DrawCPLogo ( dc, true );
g_tmNextBitmapDraw = tmCurrentTime + g_tmBitmapDrawInterval;
}
RestoreDC ( dc, -1 );
ReleaseDC ( hwnd, dc );
return;
}
Here is the code for DrawCPLogo()
. It fills in the old location of the logo with the background color to erase it, then picks a random new location and draws the logo in that location. g_rcSpaceForLogo
is a RECT
that holds the space available for the logo. In preview mode, this is the same size as the entire preview window. The X and Y coordinates are chosen with the rand()
function, and adjusted so that the logo remains entirely on the screen.
void DrawCPLogo ( HDC dc, bool bEraseOldLogo )
{
HDC dcMem;
if ( bEraseOldLogo )
FillRect ( dc, &g_rcLastBitmapDraw, g_hbrBackgroundBrush );
dcMem = CreateCompatibleDC ( dc );
SelectObject ( dcMem, g_hbmLogo );
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 );
}
Displaying newsflashes
If the saver is not in preview mode, it checks whether it's time to look for a newsflash or, if a newsflash is being displayed, remove it from the screen. If the newsflash is up, the saver checks the g_tmRemoveNewsflashTime
variable and if the current time is later than g_tmRemoveNewsflashTime
, hides the balloon tooltip, and sets the bRedrawCorners
flag so that the code later in OnTimer()
will draw the XML feeds.
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;
}
}
If it's time to check for a newsflash, the saver does so. If the newsflash text is nonempty, the saver calls DrawNewsFlash()
to display a big Bob logo with the newsflash in a balloon tooltip.
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 ( !Websvc_GetNewsflashUpdateInterval() )
g_tmNewsflashUpdateInterval = 30*60;
}
g_tmNextNewsflashUpdate = tmCurrentTime +
g_tmNewsflashUpdateInterval;
RestoreDC ( dc, -1 );
ReleaseDC ( hwnd, dc );
return;
}
}
Here's our first encounter with the web service. The Websvc_GetNewsflash()
function communicates with the web service and retrieves a news flash. The function is listed below; the end result is the g_sNewsflash
variable (which is a std::string
) is filled in with the newsflash text.
It starts by creating an XML document and initializing it from the web service URL.
bool Websvc_GetNewsflash()
{
USES_CONVERSION;
LPCTSTR szNewsflashURL = _T(
"http://www.codeproject.com/webservices/latest.asmx/GetNewsflash?");
g_sNewsflash.erase();
try
{
MSXML::IXMLDOMDocumentPtr pDoc;
MSXML::IXMLDOMElementPtr pRootNode;
if ( FAILED(pDoc.CreateInstance ( __uuidof(MSXML::DOMDocument), NULL )))
return false;
pDoc->async = VARIANT_FALSE;
pDoc->load ( _variant_t(szNewsflashURL) );
One of the nice things about IXMLDOMDocument::load()
is that it handles all the HTTP communication. This is also how the saver magically works with proxies and firewalls - I let Microsoft do the hard work! Setting the async
flag to FALSE
is important, since I don't want load()
to return until the entire document has been downloaded and parsed.
The newsflash XML has only one tag, <string>
, and its inner text contains the newsflash. I first get the root node of the document with the documentElement
property, then read its text, which is then stored in g_sNewsflash
. The big try/catch block catches exceptions thrown by the MSXML wrappers in case something goes wrong in the XML parsing.
pRootNode = pDoc->documentElement;
g_sNewsflash = (LPCTSTR) pRootNode->text;
}
catch (...)
{
return false;
}
return true;
}
Drawing in screen saver mode
In screen saver mode, OnTimer()
checks the current time against time_t
variables for each of the three feeds. Each time_t
value corresponds to a feed, and if the current time is later than a variable's value, it's time to refresh that feed. Here is the code that gets the list of new articles:
if ( g_bConnectedToNet && g_bShowNewestArticles &&
tmCurrentTime > g_tmNextArticleListUpdate )
{
Websvc_GetLatestArticles();
bRedrawCorners = true;
if ( 0 == g_tmArticleListUpdateInterval )
{
if ( !Websvc_GetArticleListUpdateInterval() )
g_tmArticleListUpdateInterval = 20*60;
}
g_tmNextArticleListUpdate = tmCurrentTime +
g_tmArticleListUpdateInterval;
}
g_tmNextArticleListUpdate
holds the time_t
value of the next time we should get the list from the web service. When the current time passes that value, we call Websvc_GetLatestArticles()
to get the list. If this is the first time getting the article list, we also get the update interval, which tells us how often to get the list of articles. After that, g_tmNextArticleListUpdate
is set to the time of the next update.
I will return to Websvc_GetLatestArticles()
later. For now, let's jump ahead to the code that does the actual drawing. The bRedrawCorners
flag indicates whether we should redraw the three lists. If that flag is true, we erase the screen and reset the RECT
that keeps track of the space available for the logo:
if ( bRedrawCorners )
{
EraseWindow ( hwnd );
SetRect ( &g_rcSpaceForLogo, 0, 0, g_uScrWidth, g_uScrHeight );
}
Next, we draw the clock if the option to show the clock is on:
if ( g_bShowClock )
DrawClock ( dc, hwnd );
Next, if bRedrawCorners
is true, we draw the feeds 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 );
}
Finally, we check whether it's time to redraw the Code Project logo:
if ( bRedrawCorners || tmCurrentTime > g_tmNextBitmapDraw )
{
DrawCPLogo ( dc, !bRedrawCorners && 0 != g_tmNextBitmapDraw );
g_tmNextBitmapDraw = tmCurrentTime + g_tmBitmapDrawInterval;
}
I will cover the DrawLatestArticles()
function here; the other three functions that draw text in the corners are quite similar. DrawLatestArticles()
reads the global string g_sLatestArticles
, prepends a header to it, and calls DrawTextInCorner()
to do the actual drawing.
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()
does two things: draws the text of course, and updates g_rcSpaceForLogo
. g_rcSpaceForLogo
is a RECT
that keeps track of the space not occupied by any text; this is the space in which the logo will be drawn. DrawTextInCorner()
starts by calculating the space that the text will occupy when drawn.
void DrawTextInCorner ( HDC dc, HWND hwnd, LPCTSTR szText, ECorner corner )
{
RECT rc;
SetTextAlign ( dc, TA_LEFT | TA_TOP | TA_NOUPDATECP );
GetWindowRect ( hwnd, &rc );
rc.bottom = rc.top;
DrawText ( dc, szText, -1, &rc, DT_CALCRECT | DT_NOPREFIX );
After the DrawText()
call, rc
holds the bounding rectangle around all of the text. Next, we switch on the corner value, and using the right combination of flags to DrawText()
, place the text in the right position on the screen. Here is the code that draws in the top-left corner:
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
holds the bottom-most coordinate occupied by the text, so that's also the top-most coordinate available for the logo. g_rcSpaceForLogo.top
is adjusted accordingly. For the bottom corners, rc
has to be shifted down so its bottom value is the same as the screen height (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;
The drawing code for the other two corners are similar.
Getting the list of articles
The Websvc_GetLatestArticles()
function reads the list of newest articles from the web service. Since the article list is more complex XML, I will cover the code that parses it. Websvc_GetLatestArticles()
retrieves the list and fills in the global string g_sLatestArticles
with the article titles and author names. It starts by creating the URL to the web service:
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();
Next, we create an XML document and load it from the web service URL.
try
{
MSXML::IXMLDOMDocumentPtr pDoc;
MSXML::IXMLDOMElementPtr pRootNode;
MSXML::IXMLDOMNodeListPtr pNodeList;
long l, lSize;
if ( FAILED(pDoc.CreateInstance ( __uuidof(MSXML::DOMDocument),
NULL, CLSCTX_INPROC_SERVER )))
return false;
pDoc->async = VARIANT_FALSE;
pDoc->load ( _variant_t(sURL.c_str()) );
As before, we start at the root node of the document. This time, we get a list of child nodes, the <ArticleBrief>
tags, using the childNodes
property of the root node.
pRootNode = pDoc->documentElement;
pNodeList = pRootNode->childNodes;
We enter a loop that reads the two fields that we're interested in from each tag - title and author. The loop starts by getting an IXMLDOMElement
interface on the next <ArticleBrief>
node. Using that interface, we read the two attributes (which themselves are sub-elements). getElementsByTagName()
returns a list of elements, but we know there will only be one attribute in the list, so we access it via index using the item
property of the list.
for ( l = 0, lSize = pNodeList->length; l < lSize; l++ )
{
MSXML::IXMLDOMNodePtr pNode;
MSXML::IXMLDOMElementPtr pArticleBriefElt, pTitleElt, pAuthorElt;
_bstr_t bsTitle, bsAuthor;
try
{
pNode = pNodeList->item[l];
pNode->QueryInterface ( &pArticleBriefElt );
pTitleElt = pArticleBriefElt->getElementsByTagName (
_bstr_t("Title") )->item[0];
pAuthorElt = pArticleBriefElt->getElementsByTagName (
_bstr_t("Author") )->item[0];
Next we save the title and author name to separate variables, then add the article info to the g_sLatestArticles
string.
bsTitle = pTitleElt->text;
bsAuthor = pAuthorElt->text;
if ( l > 0 )
g_sLatestArticles += '\n';
g_sLatestArticles += (LPCTSTR) bsTitle;
g_sLatestArticles += _T(" (");
g_sLatestArticles += (LPCTSTR) bsAuthor;
g_sLatestArticles += ')';
}
catch (...)
{
}
}
Since author names can contain HTML tags, we remove the tags so that only the plain text will be shown in the saver. This is pretty simple - we search for '<'
and '>'
, and remove those characters and everything in between.
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;
}
The options dialog
The screen saver options dialog is pretty straightforward. It's mostly boring control setup and reading/writing of options. One important part is in the standard screen saver function RegisterDialogClasses()
- that function calls InitCommonControls()
so that themes are enabled on Windows XP. ScreenSaverConfigureDialog()
is the dialog proc.
Here's a screen shot of the dialog (13K):
Source code map
Here is a list of the source files and the functions in each one.
ConfigDlg.cpp
RegisterDialogClasses()
- Calls InitCommonControls()
to enables themes in the config dialog.
ScreenSaverConfigureDialog()
- Dialog proc for the config dialog.
OnChooseFont()
- Handles the Choose Font button.
CPSaver.cpp
ScreenSaverProc()
- Main window proc for the screen saver.
OnCreate()
- Handles WM_CREATE
, does initialization.
OnTimer()
- Handles WM_TIMER
, drives all the drawing.
OnDestroy()
- Handles WM_DESTROY
, releases resources and saves some data to the registry.
EraseWindow()
- Erases the entire saver window.
DrawNewsflash()
- Draws a big Bob in the middle of the screen and pops up a tooltip to show the newsflash.
globals.cpp
ReadSettings()
- Reads options from the registry, using the key HKCU\software\The Code Project\Mike's Normal Screen Saver
.
InitGDIObjects()
- Initializes various GDI objects.
SaveListLengths()
- Stores the max length of each list of articles/posts in the registry.
SaveSettings()
- Saves options to the registry.
DrawTextInCorner()
- Draws a string in a given corner of the screen.
DrawClock()
- Draws the clock and an offline notice if the computer is not connected to the net.
DrawCPLogo()
- Draws the Code Project logo.
DrawLounge()
- Draws the list of latest Lounge topics.
DrawLatestArticles()
- Draws the list of latest articles.
DrawLatestComments()
- Draws the list of latest discussion forum topics.
websvc.cpp
Websvc_GetArticleListUpdateInterval()
- Reads the number of minutes we should wait between asking for article list updates.
Websvc_GetLatestArticles()
- Reads the list of latest articles.
Websvc_GetMaxNumArticles()
- Reads the maximum length of the list of latest articles that will be returned by the web service.
Websvc_GetCommentsUpdateInterval()
- Reads the number of minutes we should wait between asking for forum topic updates.
Websvc_GetLatestComments()
- Reads the list of latest forum topics.
Websvc_GetMaxNumComments()
- Reads the maximum length of the list of forum topics that will be returned by the web service.
Websvc_GetLoungeUpdateInterval()
- Reads the number of minutes we should wait between asking for Lounge topic updates.
Websvc_GetLatestLoungePosts()
- Reads the list of latest Lounge topics.
Websvc_GetMaxNumLoungePosts()
- Reads the maximum length of the list of Lounge topics that will be returned by the web service.
Websvc_GetNewsflashUpdateInterval()
- Reads the number of minutes we should wait between asking for newsflash updates.
Websvc_GetNewsflash()
- Reads the newsflash.