Creating modules for Doodle's Screen Saver - Part 2

From EDM2
Jump to: navigation, search

Introduction

The purpose of these articles about dssaver is to help people to create new screen saver modules for Doodle's Screen Saver. These articles are meant to be something like a tutorial.

If you want to follow the article, and compile the source code we'll put together here, you should have the source code of dssaver, downloadable from its homepage. You'll not need everything from that package, only the header file for saver modules, called SSModule.h. However, it's much easier if you take the source code of an already existing saver module, and modify that to fit your needs.

Creating a simple module

We're going to create a simple saver module for dssaver in this article. The saver module will imitate the old Lockup facility, so a golden padlock will be moving around the screen.

This module can be done very quickly by modifying the eCSBall module. The modifications required are:

  • Replace eCS logo with image of golden padlock
  • Modify code so it will move in a line instead of jumping all around the screen every 30 seconds

To make all these modifications understandable, I'll start here with describing how the old module works. Then we'll see what functions has to be changed and how, to get a new module from the old one.

The internals of eCSBall module

So, let's go through the eCSBall saver module, to see how the things are implemented in it. The source code can be found at the homepage or the screen saver, in the ZIP file containing the sources. The source code of the eCSBall module is in the folder Modules\eCSBall.

The easy part

First of all, we handle the DLL initialization/termination, so

  • we can save the DLL module handle at initialization time, used to load resources from the DLL, and
  • we can do cleanup in case something would be left initialized when the DLL is being unloaded.

This is done by a special function called LibMain (at least with the Watcom compiler). This is a function with two parameters, first is the module handle, and the second tells if the module is being loaded or unloaded.

We do some initialization and termination work in there:

unsigned _System LibMain(unsigned hmod, unsigned termination)
{
  if (termination)
  {
    // Cleanup!
    if (bRunning)
      SSModule_StopSaving();
  } else
  {
    // Startup!
    hmodOurDLLHandle = (HMODULE) hmod;
    bRunning = FALSE;
  }
  return 1;
}

Now we can go for the screen saver module functions that must be implemented. The easiest one is the SSModule_GetModuleDesc(), which is called by the screen saver core to get information about the saver module. In eCSBall, we have this, which simply fills the given structure, I don't think it requires too much description:

SSMODULEDECLSPEC int SSMODULECALL SSModule_GetModuleDesc(SSModuleDesc_p pModuleDesc)
{
  if (!pModuleDesc)
    return SSMODULE_ERROR_INVALIDPARAMETER;

  // Return info about module!
  pModuleDesc->iVersionMajor = 1;
  pModuleDesc->iVersionMinor = 20;
  strcpy(pModuleDesc->achModuleName, "eCS Ball");
  strcpy(pModuleDesc->achModuleDesc,
         "eComStation logo moving around every 30 seconds.\n"
         "Written by Doodle"
        );

  pModuleDesc->bConfigurable = FALSE;
  pModuleDesc->bSupportsPasswordProtection = TRUE;

  return SSMODULE_NOERROR;
}

The next function in the list of mandatory functions from the previous article, is SSModule_Configure(). This is the easiest part, because this module can not be configured (as it's also described in the previous function by setting pModuleDesc->bConfigurable to FALSE), so it only has to return this information:

SSMODULEDECLSPEC int SSMODULECALL SSModule_Configure(HWND hwndOwner, char *pchHomeDirectory)
{
  // This stuff here should create a window, to let the user
  // configure the module. The configuration should be read and saved from/to
  // a private config file in pchHomeDirectory, or in INI files.

  return SSMODULE_ERROR_NOTSUPPORTED;
}

The hard part

And here comes the harder part, doing the real work.

The SSModule_StartSaving() function is the one which has to start the screen saver module, and then return if it could start it or not. It is described in the previous article, that this function should start a new thread which will do the real saving, wait for it, to see if that thread can set up its job or not, and return success or failure.

The start parameters have to be passed to the new thread (saver thread), so it will know if it has to subclass the parent window (if bPreviewMode is TRUE), or if it has to create a new window (if bPreviewMode is FALSE), and it will know that parent window, too.

The preview flag is saved into a global variable, because it will be needed from other places too (like the window procedure), so only one parameter remains, the hwndParent, which will be passed to the thread as the thread initializer.

The syncronization between the saver thread and this function is done via a global variable, called iSaverThreadState, which is set to a constant of STATE_UNKNOWN initially, and which is set by the saver thread to STATE_RUNNING if it could set up everything, or to STATE_STOPPED_ERROR, if it has been stopped, and could not set up the required things.

SSMODULEDECLSPEC int SSMODULECALL SSModule_StartSaving(HWND hwndParent, char *pchHomeDirectory, int bPreviewMode)
{
  // This is called when we should start the saving.
  // Return error if already running, start the saver thread otherwise!

  if (bRunning)
    return SSMODULE_ERROR_ALREADYRUNNING;

  iSaverThreadState = STATE_UNKNOWN;
  bOnlyPreviewMode = bPreviewMode;
  tidSaverThread = _beginthread(fnSaverThread,
                                0,
                                1024*1024,
                                (void *) hwndParent);

  if (tidSaverThread<=0)
  {
#ifdef DEBUG_LOGGING
    AddLog("[SSModule_StartSaving] : Error creating screensaver thread!\n");
#endif
    // Error creating screensaver thread!
    return SSMODULE_ERROR_INTERNALERROR;
  }

  // Wait for saver thread to start up!
  while (iSaverThreadState==STATE_UNKNOWN) DosSleep(32);
  if (iSaverThreadState!=STATE_RUNNING)
  {
#ifdef DEBUG_LOGGING
    AddLog("[SSModule_StartSaving] : Something went wrong in screensaver thread!\n");
#endif

    // Something wrong in saver thread!
    DosWaitThread(&tidSaverThread, DCWW_WAIT);
    return SSMODULE_ERROR_INTERNALERROR;
  }

  // Fine, screen saver started and running!
  bRunning = TRUE;
  return SSMODULE_NOERROR;
}

The only question is what does this saver thread look like.

As it was described before, the saver thread is the one which creates the new window or subclasses the old one, and has the message loop for that window, so, it should run in a bit higher priority than others, to be able to quickly server messages arriving to the window from the Presentation Manager.

void fnSaverThread(void *p)
{
  HWND hwndParent = (HWND) p;
  HWND hwndOldFocus;
  HWND hwndOldSysModal;
  HAB hab;
  QMSG msg;
  ULONG ulStyle;

  // Set our thread to slightly more than regular to be able
  // to update the screen fine.
  DosSetPriority(PRTYS_THREAD, PRTYC_REGULAR, +5, 0);

  hab = WinInitialize(0);
  hmqSaverThreadMsgQueue = WinCreateMsgQueue(hab, 0);

After this, it checks if it has to run in preview mode or as a real saver (remember the global variable bOnlyPreviewMode, set by the SSModule_StartSaving()?), and does different things according to the result. First the easier part, when it has to run as a preview window:

  if (bOnlyPreviewMode)
  {
    PFNWP pfnOldWindowProc;
    // We should run in preview mode, so the hwndParent we have is not the
    // desktop, but a special window we have to subclass.
    pfnOldWindowProc = WinSubclassWindow(hwndParent,
                                         (PFNWP) fnSaverWindowProc);

    // Initialize window proc (simulate WM_CREATE)
    WinSendMsg(hwndParent, WM_SUBCLASS_INIT, (MPARAM) NULL, (MPARAM) NULL);
    // Also make sure that the window will be redrawn with this new
    // window proc.
    WinInvalidateRect(hwndParent, NULL, FALSE);

    iSaverThreadState = STATE_RUNNING;

#ifdef DEBUG_LOGGING
    AddLog("[fnSaverThread] : Entering message loop (Preview)\n");
#endif

    // Process messages until WM_QUIT!
    while (WinGetMsg(hab, &msg, 0, 0, 0))
      WinDispatchMsg(hab, &msg);

As you can see, it simply subclasses the given parent window with the fnSaverWindowProc, which will be our window procedure. Then tells the window proc that it's alive, so sends a special, private message (WM_SUBCLASS_INIT) to the window, which is now subclassed, so it will be got by the fnSaverWindowProc function. This makes every initialization to the subclassed window that would be made if the window would have been created originally (it will load the bitmap from the DLL, and initialize the initial position and speed of the padlock, but more about it later...).

The WinInvalidateRect() function will invalidate the subclassed window, effectively forcing it to be redrawn, but as it's not subclassed, it's our fnSaverWindowProc which will get the WM_PAINT message, not the old one.

If all this is done, the saver thread notifies the SSModule_StartSaving() function that everything is set up, so sets the global variable iSaverThreadState to STATE_RUNNING, which will result in the SSModule_StartSaving() function returning with SSMODULE_NOERROR.

Then the message loop starts, and our subclassed window is alive.

When the screen saver core wants us to stop saving, it calls SSModule_StopSaving(), which will send a WM_QUIT to our window. That will break out of the message loop, so our thread procedure can continue with cleaning up the subclassing:

    // Uinitialize window proc (simulate WM_DESTROY)
    WinSendMsg(hwndParent, WM_SUBCLASS_UNINIT, (MPARAM) NULL, (MPARAM) NULL);
    // Undo subclassing
    WinSubclassWindow(hwndParent,
		       pfnOldWindowProc);
    // Also make sure that the window will be redrawn with the old
    // window proc.
    WinInvalidateRect(hwndParent, NULL, FALSE);

    iSaverThreadState = STATE_STOPPED_OK;
  } else

Sending another private message to our window procedure, so it can clean up the resources allocated in WM_SUBCLASS_INIT (effectively, releasing the padlock bitmap from memory), then undo the subclassing by restoring the old window procedure of the window.

The invalidation of the window is done so that the old window procedure can have a chance to restore the contents of the window to the old one. Then the global variable iSaverThreadState is set to notify the SSModule_StopSaving() function that the saving has been stopped.

It wasn't that complicated, was it? :) Well, here comes the more complicated part, anyway.

If the saver thread has to start real saving, then it has to create a window, which will not release the mouse and the keyboard, and which is on top of everything else. For this, we have to create a system modal window (so the user will not be able to switch away, and the mouse and keyboard focus will stay at our window), and we have to make the window always-on-top (it's a special undocumented window flag).

  {
    // We should run in normal mode, so create a new window, topmost, and everything else...
    WinRegisterClass(hab, (PSZ) SAVERWINDOW_CLASS,
                     (PFNWP) fnSaverWindowProc,
                     CS_SIZEREDRAW | CS_CLIPCHILDREN | CS_CLIPSIBLINGS, 0);

    hwndOldFocus = WinQueryFocus(HWND_DESKTOP);

    // Create the saver output window so that it will be the child of
    // the given parent window.
    // Make window 'Always on top' because we'll be in real screensaver mode!
    ulStyle = WS_VISIBLE | WS_TOPMOST;
    hwndSaverWindow = WinCreateWindow(HWND_DESKTOP, SAVERWINDOW_CLASS, "Screen saver",
                                      ulStyle,
                                      0, 0,
                                      (int) WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN),
                                      (int) WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN),
                                      HWND_DESKTOP,
                                      HWND_TOP,
                                      0x9fff, // Some ID....
                                      NULL, NULL);
    if (!hwndSaverWindow)
    {
#ifdef DEBUG_LOGGING
      AddLog("[fnSaverThread] : Could not create window!\n");
#endif
      // Yikes, could not create window!
      iSaverThreadState = STATE_STOPPED_ERROR;
    } else
    {
      // Cool, window created!

      // Make sure nobody will be able to switch away of it!
      // We do this by making the window system-modal, and giving it the focus!
      hwndOldSysModal = WinQuerySysModalWindow(HWND_DESKTOP);
      WinSetSysModalWindow(HWND_DESKTOP, hwndSaverWindow);
      WinSetFocus(HWND_DESKTOP, hwndSaverWindow);

      iSaverThreadState = STATE_RUNNING;

#ifdef DEBUG_LOGGING
      AddLog("[fnSaverThread] : Entering message loop (Real)\n");
#endif
      // Process messages until WM_QUIT!
      while (WinGetMsg(hab, &msg, 0, 0, 0))
        WinDispatchMsg(hab, &msg);

Note, that the window is created with the very same window procedure (fnSaverWindow) that was used for the preview mode. This simplifies things very well, one has to write only one window proc, which will work for both preview mode and real saving.

When the saving has to be closed, a WM_QUIT will be posted to the window, which will break the loop. The cleanup is very easy after this, simply everything has to be undone. :)

This includes undoing the system modalling by setting the old system modal window back to system modal, destroying our window to free its resources, and setting back the focus to the old window which had the focus before. This last thing is not so important, but makes things much more comfortable, because if the screen saving has been started and stopped, then the user doesn't have to click to the entry field or whatever place he were before the screen saving, he can continue his work from where he left.

      // Undo system modalling
      WinSetSysModalWindow(HWND_DESKTOP, hwndOldSysModal);
      // Window closed, so destroy every resource we created!
      WinDestroyWindow(hwndSaverWindow);
#ifdef DEBUG_LOGGING
      AddLog("[fnSaverThread] : STATE_STOPPED_OK\n");
#endif
      // Restore focus to old window
      WinSetFocus(HWND_DESKTOP, hwndOldFocus);
      iSaverThreadState = STATE_STOPPED_OK;
    }
  }

Ok, before stopping the saver thread, we only have to destroy the remaining resources we still have:

  WinDestroyMsgQueue(hmqSaverThreadMsgQueue);
  WinTerminate(hab);

  _endthread();
}

That's for the saver thread. From this point, all the tricks are over, the only limit is you imagination (and your dexterity, of course), what you can do in a window.

For completeness, let's see the window proc of the eCSBall module, which moves a logo around the screen randomly every 30 seconds!

The function starts with the initialization part, which is common both for preview mode and real saving mode. The only difference is that the mouse pointer should be hidden in real saving mode, otherwise everything is the same: loading the bitmap from the DLL, and starting a timer to get WM_TIMER messages every 30 seconds, from where we can set new position of the image. Note that to decide if we're in preview mode or not, the global variable bOnlyPreviewMode is used, which was set by the SSModule_StartSaving() function before.

MRESULT EXPENTRY fnSaverWindowProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
{
  SWP swpDlg, swpParent;
  HWND hwndDlg;
  int rc;

  switch( msg )
  {
    case WM_SUBCLASS_INIT:
    case WM_CREATE:
      // Any initialization of the window and variables should come here.

      // Randomize
      srand(time(NULL));

      // Initial image position
      bImagePositionValid = FALSE;

      // Get bitmap
      internal_LoadBitmapResource(hwnd);

      ulAnimationTimerID = WinStartTimer(WinQueryAnchorBlock(hwnd), hwnd,
					  ANIMATION_TIMER_ID, // Timer ID
					  30000);  // Let the image have new position every 30 secs

      // Hide mouse pointer, if we're in real screen-saving mode!
      if (!bOnlyPreviewMode)
        WinShowPointer(HWND_DESKTOP, FALSE);
      break;

The uninitialization of the window is not complicated, either. It releases the bitmap it has loaded at initialization time, stops the timer, and shows the mouse pointer if it was hidden.

    case WM_SUBCLASS_UNINIT:
    case WM_DESTROY:
      // All kinds of cleanup (the opposite of everything done in WM_CREATE)
      // should come here.

      // Stop timer
      WinStopTimer(WinQueryAnchorBlock(hwnd), hwnd,
		    ulAnimationTimerID);
      // Drop bitmap
      internal_DestroyBitmapResource();

      // Restore mouse pointer, if we're in real screen-saving mode!
      if (!bOnlyPreviewMode)
	 WinShowPointer(HWND_DESKTOP, TRUE);

      break;

There is a window message WM_ADJUSTWINDOWPOS, which is sent to a window if its position, size, or Z-order changes. This is the one what we handle in order to stay on top at all costs, if we're screensaving. Note that it's only done if the window is not in preview mode.

What it does is that it checks the SWP structure it gets in MP1, and filters it so that this window will have the topmost Z-order every time. It also updates wht window style (using WinSetWindowBits()) every time, to make sure that this window remains the topmost window.

    case WM_ADJUSTWINDOWPOS:
      if (!bOnlyPreviewMode)
      {
	 SWP *pSWP;

	 // The following is required so that this window will be on
        // top of the xCenter window, evenif that is set to be always on top!

	 // Real screensaving, here we should stay on top!
        // Set WS_TOPMOST flag again!
	 WinSetWindowBits(hwnd, QWL_STYLE, WS_TOPMOST, WS_TOPMOST);

	 pSWP = (SWP *) mp1;
	 pSWP->hwndInsertBehind = HWND_TOP;
        pSWP->fl |= SWP_ZORDER;
      }
      break;

The next message to handle is WM_PAINT, which is sent when we should draw into our window. This is the place where we should draw the black background, and the eCSBall logo onto it. The only thing which complicates this process is that, if we're in preview mode, we have to scale down the logo, so the preview window will really look like a scaled down version of the full screen window.

    case WM_PAINT:
      {
	 HPS hpsBeginPaint;
	 RECTL rclRect, rclImage;
        SWP swpWindow;

	 if (!bImagePositionValid)
	   internal_MakeRandomImagePosition(hwnd);

	 hpsBeginPaint = WinBeginPaint(hwnd, NULLHANDLE, &rclRect);

        WinQueryWindowRect(hwnd, &rclRect);
        // Fill with black
        WinFillRect(hpsBeginPaint, &rclRect, CLR_BLACK);

	 if (hbmImage)
	 {
 	   if (!bImagePositionValid)
	     internal_MakeRandomImagePosition(hwnd);

	   if (bOnlyPreviewMode)
	   {
	     WinQueryWindowPos(hwnd, &swpWindow);
	     internal_CalculateDestImageSize(hwnd);
	     rclImage.xLeft = ((long long) ptlImageDestPos.x) * swpWindow.cx / (int) WinQuerySysValue(HWND_DESKTOP, SV_CXSCREEN);
	     rclImage.yBottom = ((long long) ptlImageDestPos.y) * swpWindow.cy / (int) WinQuerySysValue(HWND_DESKTOP, SV_CYSCREEN);
	     rclImage.xRight = rclImage.xLeft + sizImageDestSize.cx;
	     rclImage.yTop = rclImage.yBottom + sizImageDestSize.cy;

            WinDrawBitmap(hpsBeginPaint, hbmImage,
                          NULL,
                          (PPOINTL) (&rclImage),
                          CLR_NEUTRAL, CLR_BACKGROUND,
                          DBM_STRETCH);
	   } else
	   {
            WinDrawBitmap(hpsBeginPaint, hbmImage,
                          NULL,
                          &ptlImageDestPos,
                          CLR_NEUTRAL, CLR_BACKGROUND,
                          DBM_NORMAL);
	   }
	 }

	 WinEndPaint(hpsBeginPaint);
        return (MRESULT) FALSE;
      }

We've started a timer in the initialization part, which results in WM_TIMER messages every 30 seconds. When this message arrives with our timer ID, we set a flag that a new image position will have to be calculated, and invalidate the window, so it will get a WM_PAINT to be redrawn with the new position. This is all we have to handle in our window proc right now.

    case WM_TIMER:
      if (((SHORT)mp1)==ANIMATION_TIMER_ID)
      {
	 // Timer, so make new image position
        bImagePositionValid = FALSE;
	 WinInvalidateRect(hwnd, NULL, FALSE);
      }
      break;
    default:
      break;
  }
  return WinDefWindowProc( hwnd, msg, mp1, mp2 );
}

There are some internal functions not described here, like the internal_LoadBitmapResource(), but they can be checked in the original source code, if you're interested.

The not-so-hard part

Now, we're over the hard part. Of course, there are some other functions remaining, what we have to implement. One of them gives the module the ability to stop. :)

The SSModule_StopSaving() is called when the module has to stop. It's done by posting a WM_QUIT message to the saver window so the message processing loop of the saver thread will break, and the saver thread will do the cleanup.

SSMODULEDECLSPEC int SSMODULECALL SSModule_StopSaving(void)
{
  // This is called when we have to stop saving.
  if (!bRunning)
    return SSMODULE_ERROR_NOTRUNNING;

  // Notify saver thread to stop!
  if (bOnlyPreviewMode)
  {
    // In preview mode, which means that there is no window created, but the
    // window was subclassed. So we cannot close the window, but we have to
    // post the WM_QUIT message into the thread's message queue to notify it
    // that this is the end of its job.
    WinPostQueueMsg(hmqSaverThreadMsgQueue, WM_QUIT, (MPARAM) NULL, (MPARAM) NULL);
  }
  else
  {
    // Close saver window
    WinPostMsg(hwndSaverWindow, WM_QUIT, (MPARAM) NULL, (MPARAM) NULL);
  }

  // Wait for the thread to stop
  DosWaitThread(&tidSaverThread, DCWW_WAIT);
  // Ok, screensaver stopped.
  bRunning = FALSE;
  return SSMODULE_NOERROR;
}

This is done, too, fine. What remains is the function to ask password from the user (remember that we reported that password protection is supported by this module!), and the function to tell the user if a wrong password has been entered.

For these, we send special messages to the window, so the window procedure will handle these cases.

SSMODULEDECLSPEC int SSMODULECALL SSModule_AskUserForPassword(int iPwdBuffSize, char *pchPwdBuff)
{
  // This is called when we should ask the password from the user.
  if (!bRunning)
    return SSMODULE_ERROR_NOTRUNNING;

  if ((iPwdBuffSize<=0) || (!pchPwdBuff))
    return SSMODULE_ERROR_INVALIDPARAMETER;

  return (int) WinSendMsg(hwndSaverWindow, WM_ASKPASSWORD,
			   (MPARAM) pchPwdBuff,
			   (MPARAM) iPwdBuffSize);
}

SSMODULEDECLSPEC int SSMODULECALL SSModule_ShowWrongPasswordNotification(void)
{
  // This is called if the user has entered a wrong password, so we should
  // notify the user about this, in a way that fits the best to our screensaver.

  if (!bRunning)
    return SSMODULE_ERROR_NOTRUNNING;

  return (int) WinSendMsg(hwndSaverWindow, WM_SHOWWRONGPASSWORD,
			   (MPARAM) NULL,
			   (MPARAM) NULL);
}

Unfortunately, for this to work, we have to insert two more cases into the switch statement of the fnSaverWindowProc.

The first one handles the private message WM_ASKPASSWORD. It shows a dialog window, centered to the screen, waiting for the user to enter the password, and stores the entered password in the buffer passed in the MP1 parameter of the message.

The second one simply shows another dialog window, again, centered to the screen, but this dialog window has no entry field for password, only a button to confirm that the user has read its contents about invalid password.

One thing to note here is that it's not the default dialog procedure which is used for them, but a private one (fnAutoHiderDialogProc), which closes the dialog window if the user does not enter any characters for 10 seconds. This prevents these dialog window to stay on screen and burn into the CRT.

    case WM_ASKPASSWORD:
      {
        // Get parameters
        char *pchPwdBuff = (char *) mp1;
	 int iPwdBuffSize = (int) mp2;

	 // Show mouse pointer, if we're screensaving.
	 if (!bOnlyPreviewMode)
	   WinShowPointer(HWND_DESKTOP, TRUE);

	 hwndDlg = WinLoadDlg(hwnd, hwnd,
			      fnAutoHiderDlgProc,
			      hmodOurDLLHandle,
			      DLG_PASSWORDPROTECTION,
			      NULL);
	 if (!hwndDlg)
	 {
	   // Could not load dialog window resources!
	   if (!bOnlyPreviewMode)
	     WinShowPointer(HWND_DESKTOP, FALSE);
	   return (MRESULT) SSMODULE_ERROR_INTERNALERROR;
	 }

	 // Ok, dialog window loaded!
	 // Initialize control(s)!
	 WinSendDlgItemMsg(hwndDlg, EF_PASSWORD,
			   EM_SETTEXTLIMIT,
			   (MPARAM) (iPwdBuffSize-1),
			   (MPARAM) 0);
	 WinSetDlgItemText(hwndDlg, EF_PASSWORD, "");

	 // Center dialog in screen
	 if (WinQueryWindowPos(hwndDlg, &swpDlg))
	   if (WinQueryWindowPos(hwnd, &swpParent))
	   {
	     // Center dialog box within the screen
	     int ix, iy;
	     ix = swpParent.x + (swpParent.cx - swpDlg.cx)/2;
	     iy = swpParent.y + (swpParent.cy - swpDlg.cy)/2;
	     WinSetWindowPos(hwndDlg, HWND_TOP, ix, iy, 0, 0,
			     SWP_MOVE);
	   }
	 WinSetWindowPos(hwndDlg, HWND_TOP, 0, 0, 0, 0,
			 SWP_SHOW | SWP_ACTIVATE | SWP_ZORDER);

	 // Process the dialog!
	 rc = WinProcessDlg(hwndDlg);

	 if (rc!=PB_OK)
	 {
	   // The user pressed cancel!
          rc = SSMODULE_ERROR_USERPRESSEDCANCEL;
	 } else
	 {
	   // The user pressed OK!
	   // Get the entered password
	   WinQueryDlgItemText(hwndDlg, EF_PASSWORD,
			       iPwdBuffSize,
			       pchPwdBuff);
          rc = SSMODULE_NOERROR;
	 }

	 // Destroy window
	 WinDestroyWindow(hwndDlg);

	 // Hide mouse pointer again, if we're screensaving.
	 if (!bOnlyPreviewMode)
	   WinShowPointer(HWND_DESKTOP, FALSE);

	 return (MRESULT) rc;
      }

    case WM_SHOWWRONGPASSWORD:
      // Show mouse pointer, if we're screensaving.
      if (!bOnlyPreviewMode)
	 WinShowPointer(HWND_DESKTOP, TRUE);

      hwndDlg = WinLoadDlg(hwnd, hwnd,
                           fnAutoHiderDlgProc,
                           hmodOurDLLHandle,
                           DLG_WRONGPASSWORD,
                           NULL);
      if (!hwndDlg)
      {
	 // Could not load dialog window resources!

	 if (!bOnlyPreviewMode)
          WinShowPointer(HWND_DESKTOP, FALSE);

        return (MRESULT) SSMODULE_ERROR_INTERNALERROR;
      }

      // Ok, dialog window loaded!

      // Center dialog in screen
      if (WinQueryWindowPos(hwndDlg, &swpDlg))
        if (WinQueryWindowPos(hwnd, &swpParent))
        {
          // Center dialog box within the screen
          int ix, iy;
          ix = swpParent.x + (swpParent.cx - swpDlg.cx)/2;
          iy = swpParent.y + (swpParent.cy - swpDlg.cy)/2;
          WinSetWindowPos(hwndDlg, HWND_TOP, ix, iy, 0, 0,
                          SWP_MOVE);
        }
      WinSetWindowPos(hwndDlg, HWND_TOP, 0, 0, 0, 0,
                      SWP_SHOW | SWP_ACTIVATE | SWP_ZORDER);

      // Process the dialog!
      rc = WinProcessDlg(hwndDlg);

      // Destroy window
      WinDestroyWindow(hwndDlg);

      // Hide mouse pointer again, if we're screensaving.
      if (!bOnlyPreviewMode)
        WinShowPointer(HWND_DESKTOP, FALSE);

      return (MRESULT) SSMODULE_NOERROR;

To be good players, the SSModule_PauseSaving() and SSModule_ResumeSaving() functions are also implemented. They are the ones which are called when the saver module should temporary stop or resume all of its CPU-intensive tasks, because the monitor has been turned off or turned back on.

SSMODULEDECLSPEC int SSMODULECALL SSModule_PauseSaving(void)
{
  // This is called when the system wants us to pause saving, because
  // a DPMS saving state has been set, and no need for CPU-intensive stuffs.

  if (!bRunning)
    return SSMODULE_ERROR_NOTRUNNING;

  return (int) WinSendMsg(hwndSaverWindow, WM_PAUSESAVING, (MPARAM) NULL, (MPARAM) NULL);
}

SSMODULEDECLSPEC int SSMODULECALL SSModule_ResumeSaving(void)
{
  // This is called when the system wants us to resume saving, because
  // a DPMS state has been ended, and we should continue our CPU-intensive stuffs.

  if (!bRunning)
    return SSMODULE_ERROR_NOTRUNNING;

  return (int) WinSendMsg(hwndSaverWindow, WM_RESUMESAVING, (MPARAM) NULL, (MPARAM) NULL);
}

As you can see, it again passes the job to the window procedure, so we have to extend that with two new cases for WM_PAUSESAVING and WM_RESUMESAVING.

The only CPU-intensive stuff of eCSBall module is the moving of the logo, so, we stop the timer which makes our logo move, when we have to pause saving, and restart the timer when we have to resume saving.

    case WM_PAUSESAVING:
      // We should pause ourselves
      if (ulAnimationTimerID)
      {
	 WinStopTimer(WinQueryAnchorBlock(hwnd), hwnd,
		      ulAnimationTimerID);
	 ulAnimationTimerID = 0; // Note that we're paused!
      }
      return (MRESULT) SSMODULE_NOERROR;

    case WM_RESUMESAVING:
      // We should resume screen saving
      if (!ulAnimationTimerID)
      {
	 ulAnimationTimerID = WinStartTimer(WinQueryAnchorBlock(hwnd), hwnd,
					    ANIMATION_TIMER_ID, // Timer ID
					    30000);  // Let the image have new position every 30 secs
      }
      return (MRESULT) SSMODULE_NOERROR;

That's all in general for the eCSBall module. The only remaining parts which are not described here are the RC file (describing the dialog window resources and containing the bitmaps), the makefile and the linker file. All these are available in the ZIP file containing the full source code.

Changing the eCSBall module to get the Padlock module

And now the quick part, describing what to replace in this module to get a new module, in which a padlock will move around.

Renaming the files

First of all, it's a good idea to make a copy of the Modules\eCSBall folder, let's name that Modules\Padlock. We should also rename all the files in the new Padlock folder to replace the eCSBall to Padlock in their name.

Modifying the makefile

Then we should modify the makefile, because the name of the source files have been changed.

There are three lines containing eCSBall, all these have to be replaced to contain Padlock. These lines are:

  • dllname=Padlock : This tells the name of the DLL we want to get, and the name of the linker file to get this DLL (the linker file is Padlock.lnk in this case).
  • object_files=Padlock.obj : This is a list of object files the DLL will be linked from. In our case, this list consists of only one object file, the Padlock.obj, which will be created by the compiler by compiling the Padlock.c file.
  • rcname=Padlock-Resources : This tells the name of the RC file we have to pass on to the resource compiler to compile and add to the final DLL. This resourc file contains the dialog window descriptions and the name of the padlock BMP file.

Modifying the linker file

The linker file (Padlock.lnk) also has to be modified, because it contains a list of object files to link together, and it also contains the internal name of the DLL. So, you should change every eCSBall string to Padlock in the linker file.

It's also a good idea to change the DESCRIPTION line too, describing the DLL file (this string will be appended to the end of the DLL in clear text form by the linker).

For your information, the FILE line(s) of the linker file tell the linker the object files to link together, and the NAME line of the linker file tells the linker the internal module name of the DLL file.

Modifying the resources

We want to replace the eCS logo with our padlock image. To do this, the padlock image (Padlock.BMP) has to be copied into this directory, and the Logo*.BMP files can be deleted, as we don't need them anymore.

Also, we have to replace every link to the old bitmap files with a link to the new padlock image file.

We'll not need two bitmap IDs, so

  • Delete the #define BM_ECSBALL2 265 line from the Padlock-Resources.h file
  • Rename the BM_ECSBALL to BM_PADLOCK in the Padlock-Resources.h file

Now, it's time to tell the resource compiler that we want it to include the Padlock.BMP file into the final binary file, not the logo bitmaps. This can be done by modifying the Padlock-Resources.rc file:

  • Change the name of the resource ID include file to Padlock-Resources.h from eCSBall-Resources.h
  • Delete the BITMAP BM_ECSBALL2 "<path and filename>" entry from the end of the file
  • Change the BITMAP BM_ECSBALL "<path and filename>" entry to BITMAP BM_PADLOCK "Padlock.BMP"
  • Delete the CONTROL BM_ECSBALL2... entry of the RC file
  • Change the CONTROL BM_ECSBALL, BM_ECSBALL, 14,... line to CONTROL BM_PADLOCK, BM_PADLOCK, 14,...

If all this is done, you've successfully renamed the resource files to Padlock, and modified it to have only one bitmap instead of two, and that bitmap will be the Padlock.BMP file.

Modifying the source code

And now we should modify the source code.

First of all, our resource IDs are not in eCSBall-Resources.h anymore, so we have to change the #include statement at the beginning of the Padlock.c file to include the new Padlock-Resources.h instead.

It's also a good idea to have a unique window class name for every saver module, so the #define of SAVERWINDOW_CLASS should be changed to this:

// Private class name for our saver window
#define SAVERWINDOW_CLASS "PADLOCK_SCREENSAVER_CLASS"

Now, we can go for the real changes.

First of all, change the SSModule_GetModuleDesc() function to report new information about us:

SSMODULEDECLSPEC int SSMODULECALL SSModule_GetModuleDesc(SSModuleDesc_p pModuleDesc)
{
  if (!pModuleDesc)
    return SSMODULE_ERROR_INVALIDPARAMETER;

  // Return info about module!
  pModuleDesc->iVersionMajor = 1;
  pModuleDesc->iVersionMinor = 20;
  strcpy(pModuleDesc->achModuleName, "Bouncing padlock");
  strcpy(pModuleDesc->achModuleDesc,
         "A golden padlock bouncing around the screen.\n"
         "Written by Doodle"
        );

  pModuleDesc->bConfigurable = FALSE;
  pModuleDesc->bSupportsPasswordProtection = TRUE;

  return SSMODULE_NOERROR;
}

The other exported SSModule functions can stay as they are. The only thing to modify is the window procedure.

The window procedure needs the following changes:

  • Speed up timer, so it will send WM_TIMER messages at a much faster rate. This will enable us to change the position of the image much faster, instead of changing it once ever 30 seconds.
  • When processing the WM_PAINT message, and a new image position is required, we should calculate the new image position from current image position and current image speed, instead of randomly selecting a new position.

That's all we need.

Speeding up the timer is quite easy. We only have to change the parameter of WinStartTimer() from 30000 to for example 64. The only thing to remember is that we have to change it in the WM_RESUMESAVING case too, not just at initialization time.

Changing the function to calculate new image position doesn't have any trick either. A new variable has been introduced, which contains the X and Y speed of the image, and the image position is always modified by these amounts. Once the image reaches one side of the screen, the given image speed is changed to something else, and everything can go on.

Compiling, testing, and enjoying the result

The only thing to do now is to run "wmake" in the Padlock folder. If all goes well, that should build the new module, which can be tested with the tester application, which can be found in the Tester folder.

If you've created a new module, always test it with the tester application before trying it with the screen saver core. If you make some mistakes, and the DLL crashes, it can take down the whole WPS, because the screen saver runs in the context of the WPS. It's better to test it with Tester first, so if something goes wrong, it's only the tester which crashes, not the WPS. Once everything goes well, it's safe to copy the DLL to the \OS2\APPS\SSAVER directory, and enjoy it. :)

Conclusion

We've went through the internal structure of the eCSBall saver module, and we've seen how to implement all the required functions. We've also seen that it's quite easy to change the module once the developer knows the internal structure of the module, and the new module is not too different from the old one.