MFC App to Screen Saver: the Easy Way

Introduction

I have a regular multi-document MFC app that does interactive animation in a view. I've often thought that, if the animation could be done on auto-pilot, it would make a nice screen saver. If it's fun to watch when you're operating it, it might also be fun when you're not.

But, it seemed like a big job. I figured a screen saver involved some sort of secret setup and special main window, with no documents or views or frame windows, and I'd have to figure out a way to draw without these. I always got stuck here, because the data lives in documents and gets drawn by views, so it didn't seem possible to get along without them.

Then, I discovered this amazing fact: Any app can be a screen saver. If you copy a runfile to windows\system32 and change the extension from exe to scr, it shows up in the screen saver list; you can choose it, and it comes up. It's not full screen and doesn't behave right, but there it is. Another useful fact: When the system fires up a screen saver, it passes a special command-line argument specifying the mode of operation—normal, preview, or properties dialog.

That means your app can look at the command line, see it's being called as a screen saver, and flip into "screen saver mode" where it creates a different type of main window, and avoids interaction with the user. The main part of the job is adding some code to InitInstance. Not much else has to change.

One consequence of the approach is that your installer can take a single runfile, copy it into two places with two different extensions, and the customer gets a dessert topping AND a floor wax.

This article describes the steps in the process. I'm not providing any downloadable code, just what's in the text.

Step 1: Steal Some Code

Download the handy MFC screensaver framework posted on CodeGuru by "chensu:" http://www.codeguru.com/cpp/misc/misc/screensavers/article.php/c397/

Everything I know about screen savers I learned from this project, and I used chunks of the code as described below. If you build it and put a little flourish in OnPaint, you'll have a genuine screen saver and a better idea of how it works.

I linked in three sets of cpp/h files from chensu: drawwnd, saver (converted from a CWinApp class to some helper functions), and saverwnd.

Step 2: Modify InitInstance

What InitInstance has to do after creating the main frame window is look for a special command line, and if it sees it, create a new main window. This is shown here:

// globals or app members
CMyView *myview = 0;
CWnd *mainFrameWnd = 0;
bool bScreenSaverMode = false;

CMyApp::InitInstance()
{
   // usual init, down to creating main frame
   ...
   m_pMainWnd = new CMDIFrameWnd;
   if (!m_pMainWnd->LoadFrame(IDR_MAINFRAME))
      return FALSE;

   // a. check command line for screensaver option
   HWND hSSWnd = 0;
   int screenSaverMode = GetScrModeAndWnd(hSSWnd);
   if (screenSaverMode != 0) 
   {
      // b. perhaps load a document to show in screensaver
      CDocument *newDoc = CreateInvisibleDocument("c:\\mydoc.dat");

      // set global pointer to view, for use elsewhere
      // GetView uses newDoc->GetFirstViewPosition, GetNextView
      myview = *newDoc->GetView();

      // c. save existing main wnd for later deletion
      mainFrameWnd; = m_pMainWnd;

      // d. create the saver window and let it be m_pMainWnd
      m_pMainWnd = RunScreenSaver(scrMode, hSSWnd);

      // e. fit the view to the full screen
      CRect rWnd;
      m_pMainWnd->GetClientRect(rWnd);
      myview->MoveWindow(rWnd);

      // f. we are now in saver mode; skip unneeded parts of init
      bScreenSaverMode = true;
      return TRUE;
   }
   ...
   m_pMainWnd->ShowWindow(m_nCmdShow);

a. Parse command line

GetScrModeAndWnd is a renamed version of chensu's CSaverApp::GetScrMode. His routine takes a hundred lines to do what could be done in about five; it just checks whether the command line is one of the following, and returns an enum value or zero. It also returns a window handle; this is why I renamed it.

  /s     Display screen saver in full-screen window
  /p   Screen saver preview, in a supplied window  
  /c   Put up saver properties dialog
  /a   Change password

b. Create invisible document

This routine is taken from some OLE automation code developed by my coworker Julie Nelson. It creates and loads a normal document without making it visible.

CDocument *CreateInvisibleDocument(LPCTSTR filename)
{
   POSITION pos = AfxGetApp()->GetFirstDocTemplatePosition();
   CDocTemplate* pTemplate = AfxGetApp()->GetNextDocTemplate(pos);
   CDocument *pNewDoc = pTemplate->OpenDocumentFile(filename, FALSE);
   pNewDoc->SendInitialUpdate();
   return pNewDoc;
}

c. Save main wnd pointer

Hang on to the existing main frame pointer before creating a new one. Later, delete it like this:

void CDrawWnd::PostNcDestroy()
{
   if (m_bAutoDelete) {
      mainFrameWnd->SendMessage(WM_CLOSE);
      delete this;
   }
}

d. Create main saver window

RunScreenSaver is a modified version of chensu's CSaverApp::InitInstance. This one returns the created CWnd, which becomes the m_pMainWnd of the application.

CWnd *RunScreenSaver(SCRMODE scrMode, HWND& hWnd)
{
   switch (scrMode)
   case SMSAVER:
      CSaverWnd *const pWnd = new CSaverWnd;
      pWnd->Create();
      return pWnd;

   case SMPREVIEW:
      CWnd *const pWndParent = CWnd::FromHandle(hWnd);
      ...
      return pWnd;

   // rest as in CSaverApp::InitInstance ... other cases return 0

e. Resize view

MoveWindow on your view to make it fill the entire screen.

f. Set mode flag

Set a global flag here for other parts of the code to consult to avoid doing non-screensaver actions.

g. Return early

In my app, the remainder of InitInstance shows the main frame and does startup actions not needed for saver mode, so you bail.

Step 3: Modify OnPaint

In the OnPaint method of chensu's CDrawWnd, rendering the view is done with the same three-line method used by the standard MFC version. In this case, the method belongs to the saver window but the drawing is done by your view.

void CDrawWnd::OnPaint()
{
   CPaintDC dc(this);
   myview->OnPrepareDC(&dc);
   myview->OnDraw(&dc);
}

Step 4: Watch Out for Main Frame Calls

After a day of chasing crashing bugs, I realized my code was littered with calls like this:

((CMainFrame*)AfxGetMainWnd())->DoMainFrameAction();    // wrong!

((CMainFrame*)GetSafeMainFrm())->DoMainFrameAction();

which cause surprises if AfxGetMainWnd is not actually returning a frame window. So, I replaced these with calls to a new routine that returns the actual main frame in all cases:

CMainFrame *GetSafeMainFrm()
{
   if (bScreenSaverMode)
      return (CMainFrame*)mainFrameWnd;
   else
      return (CMainFrame*)AfxGetMainWnd();
}

Step 5: Skip Inappropriate Sections

Screen savers do not normally show dialogs or alerts or interact with the user at all. Much of that is prevented by the draw window; it terminates on keystrokes or mouse moves, so you get no menu events or other user actions. But, you will surely need to go around sprinkling statements like "if (screen saver) return" to avoid various other actions.

Step 6: Install and Test

With the above changes in place, build your normal exe (debug build is okay) and copy it to windows\system32 with an extension of .scr. Right-click your desktop, choose Properties, Screen Saver tab, and find your app in the drop-down. Fire it up and see what happens.

Step 7: Debug

To debug, set the command line to /S, start up the app, and off it goes. You soon find it a nuisance to work on a program that takes up the whole screen and insists on being the front window. It's also a nuisance working with an app that quits as soon as you touch the mouse. But, it doesn't have to be this way.

Start by going into CSaverWnd::Create and eliminating WS_EX_TOPMOST for debug builds. That helps a lot. Then look at the various On<Action> routines in that file. Most terminate the app; in debug builds, they don't have to. You can detect a keystroke and have it call DebugBreak or do something else useful. (Actually, I got carried away with this idea—my screen saver accepts various keystrokes to let the user control the display. This may break some rule of screen savers, but it adds a lot to the user enjoyment.)

You can't usually see trace debug output when the screen is being constantly redrawn. What I did about this was to allocate a strip at the bottom of the screen as a status line; it shows helpful info to the user, and in debug builds, even more helpful info to me.

Step 8. Sell It

I haven't figured this part out yet.



About the Author

Jim Dill

Developer at CambridgeSoft Corp. Hobby: TrainPlayer Software.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds