Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Mike's Normal Code Project Screen Saver

0.00/5 (No votes)
24 May 2002 1  
A Code Project screen saver written in Win32 API

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):

 [screen saver - 9K]

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:

 [Preview window - 8K]

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 );
 
    // 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;
        }

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 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 );
}

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.

    // 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;
                }
            }

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 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;
            }
        }

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;

    // 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) );

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.

    // 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;
}

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 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 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 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 );
        }

Next, we draw the clock if the option to show the clock is on:

    // 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 );

Next, if bRedrawCorners is true, we draw the feeds that the user wants to see:

    // 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 );
        }

Finally, we check whether it's time to redraw the 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;
        }

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 );

    // Calculate the rect needed to draw the text.

    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:

    // 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 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;

    // 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()) );

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.

    // Get the root node - an <ArrayOfArticleBrief> 

    // tag that holds a list of

    // <ArticleBrief> tags, one per article.

    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 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];

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';

            // 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.

            }
        }

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.

    // 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;
}

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):

 [options thumbnail - 15K]

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here