Creating 32-Bit Screen Savers with Visual C++ and MFC

Nigel Thompson
Microsoft Developer Network Technology Group

December 4, 1994

Click to open or copy the files in the POP3 sample application for this technical article.

Click to open or copy the files in the Animate library for this technical article.

Abstract

This article describes how to create a 32-bit screen saver for Microsoft® Windows NT™ or Windows™ 95 using Visual C++™ version 2.0 and the Microsoft Foundation Class Library (MFC). The POP3 sample, which accompanies this article, uses simple animation techniques to create a really average screen saver.

Introduction

Let’s come right out in the open and be honest—most screen savers are really screen burners. There, I said it. OK, now we can get on with creating a screen saver that does whatever you want.

A quick tour of the Win32® software development kit (SDK) documentation reveals that writing a screen saver involves creating some simple code to handle a few messages and linking to the SCRNSAVE library. Having done that, you have a screen saver. But what if you want to use Microsoft Foundation Class Library (MFC) classes to create your masterpiece? At first sight, it looks tough because the SCRNSAVE library normally provides all the startup code, and if we want to use MFC classes, we need to let MFC provide its own startup or nothing much is going to work.

A more in-depth look at the Win32 SDK documentation reveals that we also apparently need to export certain functions from our application in order to install and run it from the Control Panel. This all sounds like it’s going to be very messy with an MFC architecture underneath.

Well, as it turns out, we don’t need to use the SCRNSAVE library at all—in fact, creating a screen saver is simply a matter of creating a regular application that has some command line parsing, a special entry in its string table, and .SCR as its file extension. Given those few simple facts, creating a screen saver with MFC doesn’t seem like it will be such a tough job after all, does it?

Creating the Basic Framework

No, that’s basic, not Basic. If you want a Basic framework for a screen saver, that’s in the “Creating Screen Savers with Visual Basic®” class right down the hall. In this classroom, we're creating the basic framework for an MFC-based screen saver.

What we need is a really cut-down MFC framework onto which we can bolt our own specific window drawing code (the essence of the screen saver). Here are the steps you need to go through to get the project started:

  1. Use the Visual C++™ Class Wizard to create a simple single-document interface (SDI) application with no toolbars, OLE, ODBC, printing, and so forth.

  2. Build the application to make sure everything is OK.

  3. Remove the document, view, and main frame classes from the project. Delete their associated .CPP and .H files.

  4. Add the CSaveWnd class files (SAVEWND.CPP and SAVEWND.H) to the project.

  5. If you want to do animation, add the CAnimWnd files (ANIMWND.CPP and ANIMWND.H) to your project.

  6. Derive your own window class from CSaveWnd (or CAnimWnd if you want to do animation). The new class need not do anything yet.

  7. Modify the InitInstance function in the project's main .CPP file (for example, POP3.CPP) to create a window of your own class.

The last step requires a bit more information.

How Screen Savers Are Run

When Microsoft Windows™ starts a screen saver, either its configuration dialog box is displayed or the screen saver is activated. Windows tells the screen saver what to do by passing a command-line parameter. Table 1 lists the possible command line parameters and what they mean.

Table 1. Command-Line Parameters

Parameter Meaning
/s, -s or s Start in screen-saver mode.
/c, -c or c Show configuration dialog box with whatever window is currently active as the parent window.
(None) Show the configuration dialog box with no parent window.

The purpose of the /c option is to disable the application that invoked the screen saver (for example, the application from the Control Panel) while the screen saver's Setup dialog box is active. This is necessary so that the user can't click the Setup button in the Control Panel and get another copy of the screen saver running.

Having looked at the startup options, we can now see how the application starts up and what the InitInstance function has in it (with some of the comments taken out to conserve space):

BOOL CPop3App::InitInstance()
{
    if (((!strcmpi(m_lpCmdLine,"/s") 
    || !strcmpi(m_lpCmdLine,"-s")) 
    || !strcmpi(m_lpCmdLine,"s"))) {
        // Run as screen saver.
        CMyWnd* pWnd = new CMyWnd;
        pWnd->Create();
        m_pMainWnd = pWnd;
       return TRUE;
    } else {
        // Run the configuration dialog box.
        if(((!strcmpi(m_lpCmdLine,"/c") 
           || !strcmpi(m_lpCmdLine,"-c")) 
           || !strcmpi(m_lpCmdLine,"c"))) {
            // Run config with current window as parent.
            DoConfig(CWnd::GetActiveWindow());
        } else {
            // Run the config dialog box with no parent.
            DoConfig(NULL);
        }
        return FALSE;
    }
}

So if the command line says to run as a screen saver, a new CMyWnd object is created as the main window, and the screen saver runs. In the other cases, the configuration dialog box is shown by the DoConfig function:

void CPop3App::DoConfig(CWnd* pParent)
{
    CConfigDlg dlg(pParent);

    // Set up the current parameters.
    dlg.m_iBackground = GetProfileInt("Config",
                                      "Background",
                                      0);

    // Do the dialog.
    if (dlg.DoModal() != IDOK) return;

    // Save the new parameters.
    WriteProfileInt("Config",
                    "Background",
                    dlg.m_iBackground);
}

This is a little simplistic, and I’ve been rather bad in putting literal strings directly in the calls to GetProfileInt and WriteProfileInt, but I wanted to make what’s going on really obvious. As you can see, the current state is read from the .INI file and used to initialize the dialog box. If the user clicks OK to close the dialog box, the new values are saved in the .INI file. Remember that when the application runs as a screen saver, it will always have to get this information from the .INI file, so there is no point in keeping it in variables in the application itself.

A Screen Saver Window Class

All screen savers use basically the same type of window—a pop-up window that is the full size of the screen, has no borders or caption, and is on top of all the other windows. This pop-up window exists so long as the user doesn’t touch the mouse or hit a key. If you create a screen saver using the SCRNSAVE library, you get all this functionality built in. Inasmuch as we aren’t using the SCRNSAVE library, we need to handle all this stuff ourselves. To make life a little simpler (well, more organized at least), I put all the essential elements of a screen saver window into a single class (CSaveWnd) derived from CWnd. So to create your own screen saver window, you can simply derive it from CSaveWnd.

CSaveWnd has three member functions: Create, PostNcDestroy, and WindowProc. The Create function creates (wow, really?) the window:

BOOL CSaveWnd::Create()
{
    // Register a class with no cursor.
    const char* pszClassName 
        = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_SAVEBITS|CS_DBLCLKS,
                              ::LoadCursor(AfxGetResourceHandle(),
                                           MAKEINTRESOURCE(IDC_NULLCURSOR)));

    // Create the window.
    return CWnd::CreateEx(WS_EX_TOPMOST,
                          pszClassName,
                          "",
                          WS_POPUP | WS_VISIBLE,
                          0, 0,
                          ::GetSystemMetrics(SM_CXSCREEN),
                          ::GetSystemMetrics(SM_CYSCREEN),
                          NULL,
                          NULL);
}

Note that the application must provide the IDC_NULLCURSOR used here. This is simply a cursor with nothing visible in it. Using NULL here results in your window having an hourglass cursor.

The PostNcDestroy function is required to do some cleanup work because the MFC application framework is a bit untidy:

void CSaveWnd::PostNcDestroy() 
{
    // We must delete the window object ourselves because the
    // app framework doesn't do this.
    delete this;
}

And finally, the WindowProc function handles all the keyboard and mouse events we care about, as well as a few other messages that might affect the screen saver (for example, cause it to close). Most of this code was taken directly from the Control Panel sources that are used to create the SCRNSAVE library.

LRESULT CSaveWnd::WindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    static BOOL     fHere = FALSE;
    static POINT    ptLast;
    POINT           ptCursor, ptCheck;

    switch (nMsg){
    case WM_SYSCOMMAND:
        if ((wParam == SC_SCREENSAVE) || (wParam == SC_CLOSE)) {
            return FALSE;
        }
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    case WM_SETCURSOR:
        SetCursor(NULL);
        break;

    case WM_NCACTIVATE:
        if (wParam == FALSE) {
            return FALSE;
        }
        break;

    case WM_ACTIVATE:
    case WM_ACTIVATEAPP:
        if(wParam != FALSE) break;               
        // Only fall through if we are losing the focus...

    case WM_MOUSEMOVE:
        if(!fHere) {
            GetCursorPos(&ptLast);
            fHere = TRUE;
        } else {
            GetCursorPos(&ptCheck);
            if(ptCursor.x = ptCheck.x - ptLast.x) {
                if(ptCursor.x < 0) ptCursor.x *= -1;
            }
            if(ptCursor.y = ptCheck.y - ptLast.y) {
                if(ptCursor.y < 0) ptCursor.y *= -1;
            }
            if((ptCursor.x + ptCursor.y) > THRESHOLD) {
                PostMessage(WM_CLOSE, 0, 0l);
            }
        }
        break;

    case WM_LBUTTONDOWN:
    case WM_MBUTTONDOWN:
    case WM_RBUTTONDOWN:
        GetCursorPos(&ptCursor);
        ptCursor.x ++;
        ptCursor.y ++;
        SetCursorPos(ptCursor.x, ptCursor.y);
        GetCursorPos(&ptCheck);
        if(ptCheck.x != ptCursor.x && ptCheck.y != ptCursor.y)
        ptCursor.x -= 2;
        ptCursor.y -= 2;
        SetCursorPos(ptCursor.x,ptCursor.y);

    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
        PostMessage(WM_CLOSE, 0, 0l);
        break;
    }
    return CWnd::WindowProc(nMsg, wParam, lParam);
}

The POP3 Sample Application

Why is it called POP3? Because its predecessors were called POP and POPSCR. (I know how you love this irrelevant detail.)

The sample uses some classes from my animation library to move a couple of sprites around the screen. The window class architecture looks like this:

Figure 1. The window architecture of the POP3 sample

If you’re interested in how CAnimWnd works, take a look at one of my other articles, "Creating Programs Without a Standard Windows User Interface Using Visual C++ and MFC."

The CMyWnd class uses two sprites created from bitmap images. It creates a timer and uses the timer ticks to change the position of the sprites on the screen. The configuration dialog box allows the user to select either a black background or the current screen image as the background.

A Note About Palettes

The code in the Animate library is designed to run on 8-bit-per-pixel (bpp) displays. If you’re targeting 4-bpp or 16-bpp displays, you’re going to need to do some extra work. The palette used in the POP3 application is built differently depending on whether the user selects the screen background mode or a black background. If a black background is selected, the palette is built using a color cube, and the sprite images are mapped to that. I could just as easily have used a palette built from the color table in one of the sprites.

If the user selects the current screen as the background image, the palette is built from the current contents of the system palette. If you don’t do this, the screen will change colors as your animation runs. The side effect of using the current system palette is that your animation has to make do with whatever colors it finds in this palette—not exactly the optimal solution. Tricky stuff, palettes.

Here’s the way the palettes get created:

BOOL CMyWnd::Create()
{
    // Create the palette we want to use.
    switch (m_iBackMode) {
    case BACK_SCREEN: {
        HDC hdcScreen = ::GetDC(NULL);
        CDC dcScreen;
        dcScreen.Attach(hdcScreen);
        // Create a palette for the current screen condition.
        // Allocate a log pal and fill it with the color table info.
        LOGPALETTE* pPal = (LOGPALETTE*) malloc(sizeof(LOGPALETTE) 
                         + 256 * sizeof(PALETTEENTRY));
        ASSERT(pPal);
        pPal->palVersion = 0x300; // Windows 3.0
        pPal->palNumEntries = (WORD) 256; // table size
        GetSystemPaletteEntries(hdcScreen, 
                                0,
                                256,
                                pPal->palPalEntry);
        m_Pal.CreatePalette(pPal);
        free(pPal);
        } break;

    case BACK_BLACK:
    default:
        m_Pal.CreateWash();
        m_Pal.SetSysPalColors();
        break;
    }

    if (!CAnimWnd::Create(&m_Pal)) {
        return FALSE;
    }
    
    m_uiTimer = SetTimer(1, 100, NULL);
    return TRUE;                 
}

The Small Print

The final detail in getting your screen saver up and running is to provide the description string that the Control Panel will insert in the screen savers list of the Control Panel application. This string must have an ID value of 1 (one) and must be placed in the application's string table. The file SCRNSAVE.H defines IDS_DESCRIPTION as 1 for this purpose. So you need to add a string to the string table, and give it an ID value of 1.