Scrplus Tutorial

© 1996-1999 Lucian Wishcik

Introduction - Why use ScrPlus? - Compiling screensavers

Basics - Minimal screen saver - Using a timer to flash - Local data objects - Using ScrmTimer

Configuration dialogs - Overview of configuration - How ScrPlus helps - About dialog, and ScrPrefs - Modes of configuration dialog - Normal delayed dialogs - Immediate response dialogs - Preview dialogs - Secure screensavers

Programming hints - Bitmaps and sound - Using 8bpp DIBSections - Creating DirectDraw screensavers

Summary

Chapter 1: Introduction

This first chapter explains why you should use ScrPlus, and explains how to set up your compiler to compile screensavers with ScrPlus. Chapter 2: Basics introduces you step-by-step to writing very simple screensavers. Chapter 3: Dialogs explains how the configuration dialog works. And Chapter 5: Programming hints describes some general Windows programming techniques that you may wish to use in your screensavers, such as bitmaps, palettes and sounds.

Each chapter is independent of the others; but within each chapter, later parts of the chapter build upon earlier parts. It would probably be easiest to print out this tutorial and read through it in order, but it would also be possible to read the chapters in any order that you find appealing.



1.1 Why use ScrPlus?

Writing a complete screensaver, that does all that is expected of it, is complicated: you have to manage the Master Password Router, multiple threads, command line interpretation, hot-corners, the System Agent, mouse sensitity, password delays, subclassing property sheets, system menus and lots and lots of undocumented internal Windows API. To write even the most elementary screensaver that is Windows Plus! compliant--one, say, that blacks out the screen--takes the better part of 3000 lines of code.

The same screensaver with ScrPlus takes 30 lines of code.



1.2 Compiling screensavers

To compile a screensaver, you must add the scrplus library and resources to your project:



Chapter 2: Basics

This chapter guides you through the process of creating a minimal screensaver. The screensaver will flash the screen with different grey levels. It is not at all complicated.



2.1 Minimal screen saver

The following code is a complete screen saver. It just blacks out the screen.
  #define WIN32_LEAN_AND_MEAN
  #define STRICT
  #include 
  #include "scrplus.h"

  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { switch (msg)
    { case WM_ERASEBKGND:
      { HDC hdc=(HDC) wParam; RECT rc; GetClientRect(hwnd,&rc);
        FillRect(hdc,&rc,GetStockObject(BLACK_BRUSH);
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }

  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { return ScrExecute(hInstance,MySaverProc); }

ScrExecute(hInstance,MySaverProc) is all you need put in your WinMain. It will figure out what to do--be it create a preview window, pop up a configuration dialog or run the screen saver full screen--and does it. We give it the address of the window procedure for our screen saver window. Note that it does not use DefWindowProc(..): rather, it uses DefScreenSaverProc(..). It responds to the WM_ERASEBKGND message because the default background brush is NULL.

And that's it! Your first complete screensaver! You could even install it into the Windows directory now if you wanted. If you did, you'd see it did all of the following:



2.2 Using a timer to flash

The above screen saver is kind of dull. Make it a little more interesting: have it flash the screen different colours. This code is ready created for you in the 'Blank.scr' example that comes in the ScrPlus package.
  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { static int  i;
    static UINT idTimer;
    switch (msg)
    { case WM_ERASEBKGND: ....
      case WM_CREATE: { idTimer=SetTimer(hwnd,1,200,NULL); i=0; } break;
      case WM_DESTROY: { if (idTimer) KillTimer(hwnd,uTimer); } break;
      case WM_TIMER:
      { HDC hdc=GetDC(hwnd); RECT rc; GetClientRect(hwnd,&rc);
        if (i++<=4) FillRect(hdc,&rc,GetStockObject(i)); else i=0;
        ReleaseDC(hwnd,hdc);
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }

There is nothing special about this: it is just a standard Windows programming technique. We create a timer in response to WM_CREATE, destroy it in response to WM_DESTROY, and repaint the window whenever the timer clicks.

If you copied this one into the windows directory and selected it from the control panel, you'd see that the animated preview window worked correctly. Wow!



2.3 Local data objects

It's kind of ugly to use static variables. This section describes a standard Windows way to do the same sort of thing as above, but without using static variables.
  class TMyData { public:
    TMyData();
    int  i;
    UINT idTimer;
  };
  TMyData::TMyData() {i=0; idTimer=0;}

  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,GWL_USERDATA);
    if (mydata==NULL && msg!=WM_CREATE) return DefScreenSaverProc(hwnd,msg,wParam,lParam);
    switch (msg)
    { ...
      case WM_CREATE:
      { TMyData *mydata=new TMyData();
        SetWindowLong(hwnd,GWL_USERDATA,(LONG)mydata);
        mydata->idTimer=SetTimer(hwnd,1,200,NULL);
      } break;
      case WM_DESTROY:
      { if (mydata->idTimer!=0) KillTimer(hwnd,mydata->idTimer);
        SetWindowLong(hwnd,GWL_USERDATA,0);
        delete mydata;
      } break;
      ...
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }

There's nothing special about this. It's just good programming practice. A new class is created at window-start and deleted and window-end, and the window stores a pointer to it. This technique has been introduced mainly as a foundation for later, more advanced examples.



2.4 Using ScrmTimer

The above two examples used WM_TIMER messages for their animation. But WM_TIMERs have major problems. If you set them too fast, then they clog up your message queue and you can't do anything. If you set them too slow, then your screensaver doesn't run as quickly as you'd like. If you use a callback, the period can not go as fast as you want. The best solution is hideously complicated and uses multiple threads and separate message queues, and you don't want to know about how it works. Fortunately, you don't have to! ScrPlus does all of this itself, and so it can generate for you very fast timer messages without any of the problems listed above:
  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { switch (msg)
    { case SCRM_TIMER:
      { ... // just like WM_TIMER above
        return 15;
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }

All you have to do is this:

The differences between SCRM_TIMER and WM_TIMER might not seem very significant; but when you get onto advanced features, like having preview windows in your configuration dialog, you'll find that they dramatically improve speed and responsiveness.

You have learnt how to create basic screensavers with local data objects and with SCRM_TIMERs. Later on in Programming hints we return to more advanced features in a screensaver window such as sound and bitmaps. But first, a look at the screensaver configuration and about dialogs.



Chapter 3: Configuration Dialogs

ScrPlus provides the first 'general' tab of the settings dialog, but providing an additional 'options' tab is up to you. In particular you have to to manage the controls, manage the registry, and manage the interraction between the dialog and the screensaver. This chapter starts with an explanation of what ScrPlus provides and how to use it. Then, since it is up to you to manage your own dialogs, some suggestions and ready-made code are given illustrating how you could do it. If you are happy with just the general tab, then you can safely ignore this entire chapter.

The configuration property-sheet has an icon at its top left. You should supply this icon, with its identification number ID_APP. If you fail to supply an icon, the default ScrPlus icon will be used.

To add your own page, you must supply the dialog-proc for your options page as an argument to ScrExecute in your WinMain:

  int WINAPI Winmain(HISNTANCE hInstance,HINSTANCE,LPSTR,int)
  { return ScrExecute(hInstance,MySaverProc,MyConfigProc);

This tutorial only shows how to add a single 'options' tab to the screensaver config dialog, and how to include your own about dialog that has no special behaviour apart from its OK and Cancel buttons. If you wish to do anything different such as adding more than one tab to the config dialog, replacing it totally so it doesn't even have the property sheet, or putting in an About dialog that has special behaviour, then you will have to use ScrExecuteEx. Read the ScrPlus Reference for more details.



3.1 Overview of configuration

Obviously you need some way of communication between your settings dialog and your screensaver, and you need a place to store your settings. You use the registry for both tasks. The best place for you to store settings is
\HKEY_CURRENT_USER\Software\CompanyName\ProductName.

ScrPlus introduces a new feature to screensavers called ActiveConfig. While the screensaver is running full-screen, the user is able to press the control key or click the right mouse button, and the user is able to alter the settings with the screensaver still running! This cuts away the tedious chore of opening up the Display Properties, clicking on 'Settings', changing them, clicking on 'OK' and finally clicking on 'Preview' to see how it looks. If you wish to use ActiveConfig, then you must enable it explicitly at the start of your program. About Dialog, and ScrPrefs has details on this.

The Registry is used both as a permament store of the settings, and sometimes (if it is needed) as a transfer buffer.



3.2 How ScrPlus helps

ScrPlus has three features built in to help with managing the configuration dialog:

The general page contains stuff for corner hot-spots, mouse sensitivity, password delay, mute and to allow the user to enable or disable ActiveConfig. You can read the current settings at any time with the following code:

  TScrGeneralSettings sgs;
  ScrGetGeneralSettings(&sgs);
  // Fields in the structure include mute, mousethreshold,
  // activeconfig, passworddelay: see scrplus.h for details
In addition, if something on the general page has changed (eg. if the user clicked on 'mute'), then a SCRM_CHANGE message gets sent to your screensaver informing it of the change. More on this below.

An About dialog box is standard on Plus screensaver, and can be reached via the system menu of the configuration dialog. ScrPlus will look in your screensaver for a dialog with resource id DLG_ABOUT. If it finds it, it will use this for the about dialog; if you have not included such a dialog, then it defaults to an 'about' dialog that describes ScrPlus itself.

SCRM_CHANGE is the instrument which enables the registry to be used as a transfer buffer. Suppose that your page in the configuration dialog has an HWND hOptionsPage. This means that the property sheet itself has HWND hPropertySheet=GetParent(hOptionsPage) and that the parent of the property sheet has HWND hChangeWindow=GetParent(hpropertySheet). It is this parent of the property sheet that is of interest.

  #define SCRCHANGE_MYDATA 102

  // following code would be inside your dialog procedure
  TScrChangeRouter cr;
  cr.hsrcwnd=hPropertySheet; cr.lParam=(LPARAM)mydata;
  cr.code=SCRCHANGE_MYDATA | SCRCHANGE_FROMAPPLY | SCRCHANGE_TOBOTH;
  HWND hChangeWindow=GetParent(GetParent(hOptionsPage));
  SendMessage(hChangeWindow,SCRM_CHANGE,0,(LPARAM)&cr);
In screensavers like the 'Science' screensaver, it is typical for changes to apply immediately to the preview of the screensaver in the configuration dialog, but to apply to the full-screen saver window only when you click on Apply. To do this, you would respond to a change in any control with the code
SCRCHANGE_MYDATA | SCRCHANGE_FROMCONTROL | SCRCHANGE_TOPREVIEW
and you would respond to a click on Apply with the code
SCRCHANGE_MYDATA | SCRCHANGE_FROMAPPLY | SCRCHANGE_TOBOTH.

These notifications get sent as messages to MySaverProc:

  UINT msg      =  SCRM_CHANGE

  WPARAM wParam =  SCRCHANGE_MYDATA | SCRCHANGE_FROMxxx | SCRCHANGE_TOxxx
  LPARAM lParam =  cr.lParam
For instance, your screensaver window might wish to handle the built-in code SCRCHANGE_GENERAL. Suppose that your screensaver had sound, and that whenever the user clicked on OK or Apply and had turned sound on or off, you wanted to be aware of this change. (Note: in the SCRCHANGE_GENERAL code, cr.lParam points to a TScrGeneralSettings structure.
  case SCRM_CHANGE:
  { if (wParam & SCRCHANGE_GETCODE)==SCRCHANGE_GENERAL
     &&(wParam & SCRCHANGE_GETFROM)==SCRCHANGE_FROMAPPLY)
    { TScrGeneralSettings *sgs=(TScrGeneralSettings *)lParam;
      ShouldMakeSound=sgs->mute;
    }
  } break;



3.3 About dialog, and ScrPrefs

The standard Plus! way for a user to get an about box for your screensaver is via the system menu on the property sheet. If you have in your screensaver a dialog with resource DLG_ABOUT, then this dialog will be displayed; if you do not, then the standard ScrPlus dialog will be displayed.

If you do not want any 'about' dialog at all, then you can disable it completely right at the start of your application, using ScrPrefs.

  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { TScrPrefs *p=ScrCreatePrefs();
    p->DisableAbout=true;
    p->AllowActiveConfig=true;
    p->AllowMute=false;
    ScrSetPrefs(p);
    return ScrExecute(hInstance,MySaverProc,MyConfigProc);
  }
This example also illustrates how to use the ScrPrefs to enable ActiveConfig and to disable Mute. Normally, checkboxes for 'Mute' and 'ActiveConfig' appear in the general page of the screensaver config dialog. If either of AllowMute or AllowActiveConfig is set to false (to indicate that this is a feature that your screensaver simply does not support) then the check-boxes vanish.

Note that, if you do ScrCreatePrefs, then you should follow it with ScrSetPrefs. This is because the first call allocates a block of memory, and the second deallocates it. Failure to do this would result in a memory leak.



3.4 Modes of configuration dialog

ActiveConfig, introduced above, introduces one complexity about the handling of changes to the settings. And if you have a configuration dialog which has a preview window inside it, like the Science screensaver that came with ScrPlus, you face additional complexities. It is up to you the programmed to decide how to work with these, and how to program it. As an aid, the following three sections describe three typical approaches and indicates how to program them. In increasing order of complexity:

Note that the descriptions and code given below are just one way of working with dialogs and with ScrPlus. You are free to use any alternatives you feel appropriate.



3.5 Normal delayed dialogs

The normal way to run a dialog is 'Normal Delayed'. If you have enabled ActiveConfig, and the dialog happens to be running as an active-config dialog, then the screensavers settings only change when you click on OK or Apply. If you have not enabled ActiveConfig then then it just works as you would expect. The example 'Blobs.scr' that comes with the ScrPlus development kit illustrates this approach: you might wish to look at its source code. Essentially, we set up the dialog in response to WM_INITDIALOG, and we read its values in response to PSN_APPLY.

There is an interesting thing to note. In these examples, the TMyData class will have several functions:

You should appreciate that a new instance of TMyData will be created in each place that needs it: for the full-screen window, for the configuration dialog, and for any preview windows. This means that you might conceptually have three or more instances of it existing in the same application at any one time.

Note! If you have the old win32 headers, without goodies like property sheets, then it'll be kind of hard for you to program property sheets. If you're really desparate, then look at the 'flame' example which does a couple of #defines at the start to get the necessary constants even if it is using the old headers.

  // This code illustrates a screensaver with an option for
  // initial blanking out of the screen
 
  #define ID_BLANK  110
  #define SCRCHANGE_MYDATA  2

  class TMyData { public:
    TMyData();
    //
    bool blank;
    //
    bool GetControls(HWND);
    void SetControls(HWND);
    void ReadRegistry();
    void WriteRegistry();
    void NotifyChange(HWND);
  };

  // Constructor: puts the default values into the object.
  // These will be the values used when the screensaver is first
  // run on a new computer
  //
  TMyData::TMyData()
  { blank=true;
  }

  // GetControls: reads the current state of the dialog into
  // the TMyData object. Returns 'true' if there have been
  // any changes.
  //
  bool TMyData::GetControls(HWND hdlg)
  { bool ischanged=false;
    bool newblank=IsDlgButtonChecked(hdlg,ID_BLANK);
    if (newblank!=blank) ischanged=true; blank=newblank;
    return ischanged;
  }

  // SetControls: writes the control values into the dialog
  //
  void TMyData::SetControls(HWND hdlg)
  { CheckDlgButton(hdlg,ID_BLANK,blank);
  }


  // ReadRegistry: Loads the configuration from the registry
  //
  void TMyData::ReadRegistry()
  { LONG res; HKEY skey; DWORD val,valsize,valtype;
    res=RegOpenKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,KEY_ALL_ACCESS,&skey);
    if (res!=ERROR_SUCCESS) return;
    // 'blank'
    valsize=sizeof(val); res=RegQueryValueEx(skey,"Blank",0,&valtype,(LPBYTE)&val,&valsize);
    if (res==ERROR_SUCCESS) blank=val;
    RegCloseKey(skey);
  }

  // WriteRegistry: stores the configuration into the registry
  //
  void TMyData::WriteRegistry()
  { LONG res; HKEY skey; DOWRD val; DWORD disp;
    res=RegCreateKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,NULL,
                       REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&skey,&disp);
    // We use CreateKey not OpenKey, so the first time the saver is run 
    if (res!=ERROR_SUCCESS) return;
    // 'blank'
    val=blank;
    RegSetValueEx(skey,"DoLines",0,REG_DWORD,(CONST BYTE*)&val,sizeof(val));
    RegCloseKey(skey);
  }
 
  // NotifyChange: (only needed if you have enabled ActiveConfig)
  // If running as an active-config dialog, NotifyChange informs
  // the screensaver window that the settings have changed
  //
  void TMyData::NotifyChange(HWND hOptionsPage)
  { UINT code=SCRCHANGE_MYDATA | SCRCHANGE_FROMAPPLY | SCRCHANGE_TOFULL;
    HWND hPropertySheet=GetParent(hOptionsPage);
    HWND hChangeWindow =GetParent(hPropertySheet);
    TScrChangeRouter cr;
    cr.hsrcwnd=hPropertySheet; cr.lParam=(LPARAM)this; cr.code=code;
    SendMessage(hChangeWindow,SCRM_CHANGE,0,(LPARAM)&cr);
  }


  // MyConfigProc: dialog-procedure for the Options page.
  //
  BOOL WINAPI MyConfigProc(HWND hwnd,UINT msg,WPARAM,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,DWL_USER);
    if (mydata==NULL && msg!=WM_INITDIALOG) return false;
    switch (msg) {
    case WM_INITDIALOG:
    { TMyData *mydata=new TMyData();
      SetWindowLong(hwnd,DWL_USER,(LONG)mydata);
      mydata->ReadRegistry();
      mydata->SetControls(hwnd);
      return true;
    }
    case WM_DESTROY:
    { delete mydata;
      SetWindowLong(hwnd,DWL_USER,0);
      return true;
    }
    case WM_NOTIFY:
    { LPNMHDR nmh=(LPNMHDR) lParam;
      UINT code=nmh->code;
      switch (code)
      { case PSN_APPLY:
        { mydata->GetControls(hwnd);
          mydata->WriteRegistry();
          mydata->NotifyChange(hwnd);
          return true;
        }
      }
    }
    return false;
  }


  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,GWL_USERDATA);
    if (mydata==NULL && msg!=WM_CREATE) return DefScreenSaverProc(hwnd,msg,wParam,lParam);
    switch (msg)
    { case WM_CREATE:
      { TMyData *mydata=new TMyData();
        mydata->ReadRegistry();
        SetWindowLong(hwnd,GWL_USERDATA,(LONG)mydata);
      } break;
      case WM_ERASEBKGND: 
      { if (blank)
        { HDC hdc=(HDC) wParam; RECT rc; GetClientRect(hwnd,&rc);
          FillRect(hdc,&rc,GetStockObject(BLACK_BRUSH);
        }
      } break;
      case WM_DESTORY:
      { delete mydata;
        SetWindowLong(hwnd,GWL_USERDATA,0);
      } break;
      case SCRM_TIMER:
      { // do some animation
        return 100;
      }
      // The following message SCRM_CHANGE is only important if
      // we have enabled ActiveConfig
      case SCRM_CHANGE:
      { if (wParam & SCRCHANGE_GETCODE)==SCRCHANGE_MYDATA)
        { delete mydata;
          mydata=new TMyData();
          mydata->ReadRegistry();
          SetWindowLong(hwnd,GWL_USERDATA,(long)mydata);
          InvalidateRect(hwnd,NULL,true);
        }
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }


  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { TScrPrefs *p=ScrCreatePrefs();
    p->AllowActiveConfig=true;
    ScrSetPrefs(p);
    return ScrExecute(hInstance,MySaverProc,MyConfigProc);
  }


  ----------- and also the dialog resource
  DLG_SCRNSAVECONFIGURE DIALOG 0, 0, 237, 220
  STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU
  CAPTION "Options"
  FONT 8, "Helv" 
  {
    CONTROL "&Blank screen at start", ID_BLANK, "button",
            BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 183, 95, 12
  }

Here is a summary of the important points:



3.6 Immediate response dialogs

In the previous section you learnt one way to program delayed dialogs, which took effect when you clicked 'OK'. This section describes how to program 'Immediate Response' dialogs, which take effect as soon as you make a change. (This is useful only during ActiveConfig). The essential points are these:

  // This code illustrates an immediate-mode screensaver
  // with a flag for the size of blobs on screen
 
  #define ID_BIGBLOBS  110
  #define SCRCHANGE_MYDATA  2

  class TMyData { public:
    TMyData();
    //
    bool BigBlobs;
    bool markBigBlobs;
    //
    bool GetControls(HWND);
    void SetControls(HWND);
    void ReadRegistry();
    void WriteRegistry();
    void NotifyChange(HWND);
    void Mark();
    bool Reset();
  };

  // Constructor: puts the default values into the object.
  //
  TMyData::TMyData()
  { BigBlobs=true;
  }

  // Mark: remembers the current values of the object
  //
  void TMyData::Mark()
  { markBigBlobs=BigBlobs;
  }

  // Reset: returns to the remembered values. Returns
  // true or false for whether or not that involves a change
  //
  bool TMyData::Reset()
  { bool ischanged=false;
    if (BigBlobs!=markBigBlobs) ischanged=true;
    BigBlobs=markBigBlobs;
    return ischanged;
  }

  // GetControls: reads the current state of the dialog
  //
  bool TMyData::GetControls(HWND hdlg)
  { bool ischanged=false;
    bool newBigBlobs=IsDlgButtonChecked(hdlg,ID_BIGBLOBS);
    if (newBigBlobs!=BigBlobs) ischanged=true;
    BigBlobs=newBigBlobs;
    return ischanged;
  }

  // SetControls: writes the control values into the dialog
  //
  void TMyData::SetControls(HWND hdlg)
  { CheckDlgButton(hdlg,ID_BIGBLOBS,BigBlobs);
  }


  // ReadRegistry: Loads the configuration from the registry
  //
  void TMyData::ReadRegistry()
  { LONG res; HKEY skey; DWORD val,valsize,valtype;
    res=RegOpenKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,KEY_ALL_ACCESS,&skey);
    if (res!=ERROR_SUCCESS) return;
    // 'bigblobs'
    valsize=sizeof(val); res=RegQueryValueEx(skey,"BigBlobs",0,&valtype,(LPBYTE)&val,&valsize);
    if (res==ERROR_SUCCESS) BigBlobs=val;
    RegCloseKey(skey);
  }

  // WriteRegistry: stores the configuration into the registry
  //
  void TMyData::WriteRegistry()
  { LONG res; HKEY skey; DOWRD val; DWORD disp;
    res=RegCreateKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,NULL,
                       REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&skey,&disp);
    // We use CreateKey not OpenKey, so the first time the saver is run 
    if (res!=ERROR_SUCCESS) return;
    // 'BigBlobs'
    val=BigBlobs;
    RegSetValueEx(skey,"DoLines",0,REG_DWORD,(CONST BYTE*)&val,sizeof(val));
    RegCloseKey(skey);
  }
 
  // NotifyChange: inform the full saver that changes have been made
  //
  void TMyData::NotifyChange(HWND hOptionsPage)
  { UINT code=SCRCHANGE_MYDATA;
    HWND hPropertySheet=GetParent(hOptionsPage);
    HWND hChangeWindow =GetParent(hPropertySheet);
    TScrChangeRouter cr;
    cr.hsrcwnd=hPropertySheet; cr.lParam=(LPARAM)this; cr.code=code;
    SendMessage(hChangeWindow,SCRM_CHANGE,0,(LPARAM)&cr);
  }


  // MyConfigProc: dialog-procedure for the Options page.
  //
  BOOL WINAPI MyConfigProc(HWND hwnd,UINT msg,WPARAM,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,DWL_USER);
    if (mydata==NULL && msg!=WM_INITDIALOG) return false;
    switch (msg) {
    case WM_INITDIALOG:
    { TMyData *mydata=new TMyData();
      SetWindowLong(hwnd,DWL_USER,(LONG)mydata);
      mydata->ReadRegistry();
      mydata->Mark();
      mydata->SetControls(hwnd);
      return true;
    }
    case WM_DESTROY:
    { delete mydata;
      SetWindowLong(hwnd,DWL_USER,0);
      return true;
    }
    case WM_COMMAND:
    { HWND hctrlwnd=(HWND) lParam;
      if (hctrlwnd==NULL) return false;
      bool ischanged=mydata->GetControls(hwnd);
      if (ischanged) mydata->NotifyChange(hwnd);
      return true;
    }
    case WM_NOTIFY:
    { LPNMHDR nmh=(LPNMHDR) lParam;
      UINT code=nmh->code;
      switch (code)
      { case PSN_APPLY:
        { mydata->Mark();
          mydata->WriteRegistry();
          // We don't even have to NotifyChange
          return true;
        }
        case PSN_RESET:
        { // revert back to old values
          bool ischanged=mydata->Reset();
          if (ischanged) mydata->NotifyChanges(hwnd);
          return true;
        }
      }
    }
    return false;
  }


  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,GWL_USERDATA);
    if (mydata==NULL && msg!=WM_CREATE) return DefScreenSaverProc(hwnd,msg,wParam,lParam);
    switch (msg)
    { case WM_CREATE:
      { TMyData *mydata=new TMyData();
        mydata->ReadRegistry();
        SetWindowLong(hwnd,GWL_USERDATA,(LONG)mydata);
      } break;
      case WM_ERASEBKGND: 
      { HDC hdc=(HDC) wParam; RECT rc; GetClientRect(hwnd,&rc);
        FillRect(hdc,&rc,GetStockObject(BLACK_BRUSH);
      } break;
      case WM_DESTORY:
      { delete mydata;
        SetWindowLong(hwnd,GWL_USERDATA,0);
      } break;
      case SCRM_TIMER:
      { // do something with blobs
        RECT rc; GetWindowRect(hwnd,&rc); int width=rc.right-rc.left;
        int rad=random(width/20);
        int x=random(width), y=random(width);
        HDC hdc=GetDC(hwnd);
        LOGBRUSH lb; lb.lbStyle=BS_SOLID; lb.lbColor=random(4); lb.lbHatch=0;
        HGDIOBJ holdbrush=SelectObject(hdc,CreateBrushIndirect(&lb));
        Ellipse(hdc,x-rad,y-rad,x+rad,y+rad);
        holdbrush=SelectObject(hdc,holdbrush);
        DeleteObject(holdbrush);
        return 15;
      }
      // This version of SCRM_CHANGE uses lParam instead of reading
      // the registry
      case SCRM_CHANGE:
      { if (wParam & SCRCHANGE_GETCODE)==SCRCHANGE_MYDATA)
        { TMyData *newdata=(TMyData *)lParam;
          mydata->bigblobs=newdata->bigblobs;
        }
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }


  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { TScrPrefs *p=ScrCreatePrefs();
    p->AllowActiveConfig=true;
    ScrSetPrefs(p);
    return ScrExecute(hInstance,MySaverProc,MyConfigProc);
  }


  ----------- and also the dialog resource
  DLG_SCRNSAVECONFIGURE DIALOG 0, 0, 237, 220
  STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU
  CAPTION "Options"
  FONT 8, "Helv" 
  {
    CONTROL "Use &big blobs", ID_BIGBLOBS, "button",
            BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 183, 95, 12
  }

Here is a summary of the important points:



3.7 Preview dialogs

A 'preview dialog' is one with a little monitor inside the configuration dialog. This looks the best, since it gives the user a chance to see how her changes will look before she commits to them. It is also the most complicated, combining aspects of the previous two styles: changes in the preview control are instantaneous, and (if you are running ActiveConfig) then changes in the full-screen window only happen when upon clicking OK or Apply. The 'Sperm.scr' example that comes with ScrPlus uses this approach. The essential points are these:

  // This code illustrates an preview-mode screensaver
  // with a flag for the size of blobs on screen
 
  #define ID_BIGBLOBS  110
  #define SCRCHANGE_MYDATA  2

  class TMyData { public:
    TMyData();
    //
    bool BigBlobs;
    bool markBigBlobs;
    //
    bool GetControls(HWND);
    void SetControls(HWND);
    void ReadRegistry();
    void WriteRegistry();
    void NotifyChange(HWND,bool fromcontrol);
    void Mark();
    bool Reset();
  };

  // Constructor: puts the default values into the object.
  //
  TMyData::TMyData()
  { BigBlobs=true;
  }

  // Mark: remembers the current values of the object
  //
  void TMyData::Mark()
  { markBigBlobs=BigBlobs;
  }

  // Reset: returns to the remembered values. Returns
  // true or false for whether or not that involves a change
  //
  bool TMyData::Reset()
  { bool ischanged=false;
    if (BigBlobs!=markBigBlobs) ischanged=true;
    BigBlobs=markBigBlobs;
    return ischanged;
  }

  // GetControls: reads the current state of the dialog
  //
  bool TMyData::GetControls(HWND hdlg)
  { bool ischanged=false;
    bool newBigBlobs=IsDlgButtonChecked(hdlg,ID_BIGBLOBS);
    if (newBigBlobs!=BigBlobs) ischanged=true;
    BigBlobs=newBigBlobs;
    return ischanged;
  }

  // SetControls: writes the control values into the dialog
  //
  void TMyData::SetControls(HWND hdlg)
  { CheckDlgButton(hdlg,ID_BIGBLOBS,BigBlobs);
  }


  // ReadRegistry: Loads the configuration from the registry
  //
  void TMyData::ReadRegistry()
  { LONG res; HKEY skey; DWORD val,valsize,valtype;
    res=RegOpenKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,KEY_ALL_ACCESS,&skey);
    if (res!=ERROR_SUCCESS) return;
    // 'bigblobs'
    valsize=sizeof(val); res=RegQueryValueEx(skey,"BigBlobs",0,&valtype,(LPBYTE)&val,&valsize);
    if (res==ERROR_SUCCESS) BigBlobs=val;
    RegCloseKey(skey);
  }

  // WriteRegistry: stores the configuration into the registry
  //
  void TMyData::WriteRegistry()
  { LONG res; HKEY skey; DOWRD val; DWORD disp;
    res=RegCreateKeyEx(HKEY_CURRENT_USER,"Software\\Lu\\TestSaver",0,NULL,
                       REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&skey,&disp);
    // We use CreateKey not OpenKey, so the first time the saver is run 
    if (res!=ERROR_SUCCESS) return;
    // 'BigBlobs'
    val=BigBlobs;
    RegSetValueEx(skey,"DoLines",0,REG_DWORD,(CONST BYTE*)&val,sizeof(val));
    RegCloseKey(skey);
  }


 
  // NotifyChange: inform the saver windows that changes have occured.
  // If the change was from a control, then we route the message only
  // to preview windows; if the change was from the Apply button, then
  // we route the message only to the full-screen window
  //
  void TMyData::NotifyChange(HWND hOptionsPage, bool fromcontrol)
  { UINT code;
    if (fromcontrol)
      code=SCRCHANGE_MYDATA | SCRCHANGE_FROMCONTROL | SCRCHANGE_TOPREVIEW;
    else
      code=SCRCHANGE_MYDATA | SCRCHANGE_FROMAPPLY   | SCRCHANGE_TOFULL;
    HWND hPropertySheet=GetParent(hOptionsPage);
    HWND hChangeWindow =GetParent(hPropertySheet);
    TScrChangeRouter cr;
    cr.hsrcwnd=hPropertySheet; cr.lParam=(LPARAM)this; cr.code=code;
    SendMessage(hChangeWindow,SCRM_CHANGE,0,(LPARAM)&cr);
  }


  // MyConfigProc: dialog-procedure for the Options page.
  //
  BOOL WINAPI MyConfigProc(HWND hwnd,UINT msg,WPARAM,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,DWL_USER);
    if (mydata==NULL && msg!=WM_INITDIALOG) return false;
    switch (msg) {
    case WM_INITDIALOG:
    { TMyData *mydata=new TMyData();
      SetWindowLong(hwnd,DWL_USER,(LONG)mydata);
      mydata->ReadRegistry();
      mydata->Mark();
      mydata->SetControls(hwnd);
      ScrCreatePreview(GetDlgItem(hwnd,ID_MONITOR));
      return true;
    }
    case WM_DESTROY:
    { delete mydata;
      SetWindowLong(hwnd,DWL_USER,0);
      return true;
    }
    case WM_COMMAND:
    { HWND hctrlwnd=(HWND) lParam;
      if (hctrlwnd==NULL) return false;
      bool ischanged=mydata->GetControls(hwnd);
      if (ischanged) mydata->NotifyChange(hwnd,true);
      return true;
    }
    case WM_NOTIFY:
    { LPNMHDR nmh=(LPNMHDR) lParam;
      UINT code=nmh->code;
      switch (code)
      { case PSN_APPLY:
        { mydata->Mark();
          mydata->WriteRegistry();
          mydata->NotifyChange(hwnd,false);
          return true;
        }
        case PSN_RESET:
        { // revert back to old values
          bool ischanged=mydata->Reset();
          if (ischanged) mydata->NotifyChanges(hwnd,false);
          return true;
        }
      }
    }
    return false;
  }


  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { TMyData *mydata=(TMyData *)GetWindowLong(hwnd,GWL_USERDATA);
    if (mydata==NULL && msg!=WM_CREATE) return DefScreenSaverProc(hwnd,msg,wParam,lParam);
    switch (msg)
    { case WM_CREATE:
      { TMyData *mydata=new TMyData();
        mydata->ReadRegistry();
        SetWindowLong(hwnd,GWL_USERDATA,(LONG)mydata);
      } break;
      case WM_ERASEBKGND: 
      { HDC hdc=(HDC) wParam; RECT rc; GetClientRect(hwnd,&rc);
        FillRect(hdc,&rc,GetStockObject(BLACK_BRUSH);
      } break;
      case WM_DESTORY:
      { delete mydata;
        SetWindowLong(hwnd,GWL_USERDATA,0);
      } break;
      case SCRM_TIMER:
      { // do something with blobs
        RECT rc; GetWindowRect(hwnd,&rc); int width=rc.right-rc.left;
        int rad=random(width/20);
        int x=random(width), y=random(width);
        HDC hdc=GetDC(hwnd);
        LOGBRUSH lb; lb.lbStyle=BS_SOLID; lb.lbColor=random(4); lb.lbHatch=0;
        HGDIOBJ holdbrush=SelectObject(hdc,CreateBrushIndirect(&lb));
        Ellipse(hdc,x-rad,y-rad,x+rad,y+rad);
        holdbrush=SelectObject(hdc,holdbrush);
        DeleteObject(holdbrush);
        return 15;
      }
      // This version of SCRM_CHANGE uses lParam instead of reading
      // the registry
      case SCRM_CHANGE:
      { if (wParam & SCRCHANGE_GETCODE)==SCRCHANGE_MYDATA)
        { TMyData *newdata=(TMyData *)lParam;
          mydata->bigblobs=newdata->bigblobs;
        }
      } break;
    }
    return DefScreenSaverProc(hwnd,msg,wParam,lParam);
  }


  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { TScrPrefs *p=ScrCreatePrefs();
    p->AllowActiveConfig=true;
    ScrSetPrefs(p);
    return ScrExecute(hInstance,MySaverProc,MyConfigProc);
  }


  ----------- and also the dialog resource
  DLG_SCRNSAVECONFIGURE DIALOG 0, 0, 237, 220
  STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU
  CAPTION "Options"
  FONT 8, "Helv" 
  {
    CONTROL "Use &big blobs", ID_BIGBLOBS, "button",
            BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 183, 95, 12
  }

Here is a summary of the important points:



3.8 Secure screensavers

A common request is to change the password mechanism for your screensaver, perhaps so it accesses some central password database or so it is more secure. You cannot change the password function of any pre-written screensavers: you can only change it for your own. The code below illustrates how.

  #include 
  #include 

  LRESULT CALLBACK MySaverProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
  { switch (msg)
    { case WM_ERASEBKGND:
      { HDC hdc=(HDC) wParam; RECT rc; GetClientRect(hwnd,&rc);
        FillRect(hdc,&rc,GetStockObject(BLACK_BRUSH);
      } break;
      case SCRM_VERIFYPW:
      { // Put your own password verification box here
        return TRUE; // if the user got the password ok, or FALSE otherwise
      }
    }
  }

  BOOL CALLBACK MyChangePassword(HWND hwnd)
  { // pop up your change-password dialog box here, as a child of hwnd
    return TRUE;
  }

  int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int)
  { TScrPrefs *p=ScrCreatePrefs();
    p->AlwaysRequirePassword=true;
    ScrSetPrefs(p);
    return ScrExecute(hInstance,MySaverProc,NULL,NULL,MyChangePassword);
  }

One thing used in this screensaver was the global preference flag 'AlwaysRequirePassword'. (There is an equivalent 'NeverRequirePassword). This means that as soon as the screensaver has been activated, to get out of it will require a password. Normal behaviour is for the user to specify a password delay, only after which time would the password be required. You might want to require a password always if you are writing a screensaver for security in a particularly critical situation. If you happened to want the screensaver configuration dialog to be accessible without requiring a password, then you can set the flag CheckPasswordForActiveConfig=false.

Also see 5.9 Preventing ctrl-alt-delete and alt-tab.



Chapter 4: Programming hints

You have learnt everything you need to know about writing screensavers. But you will probably want to include some flashy effects, using bitmaps and sound and palettes &c. This section gives short answers to a few common Windows questions.



4.1 Bitmaps and sound

Playing a sound is remarkably easy. First, you will probably want to check whether the user has disabled sound. The section How ScrPlus helps above included some sample code for this. The code for playing a sound resource is simply as below. The 'Boing.scr' example that comes with the ScrPlus development kit demonstrates this feature.
  if (ShouldPlaySound)
  { PlaySound(MAKEINTRESOURCE(WAV_BOING),hInstance,
              SND_RESOURCE | SND_ASYNC | SND_NODEFAULT);
  }

  ------- and in your .rc file
  WAV_BOING WAVE "boing.wav"

If you are running on a true-colour display (16bpp or above), or if you have low-colour bitmaps (16 colours or below) then you don't need to bother about palettes. Generally you would load the bitmap resource or file right at the start, in response to WM_CREATE in your screensaver window, and destroy it at WM_DESTROY.

  // Following code called in response to WM_CREATE. Use exactly the
  // same call with slightly different parameters to load a .bmp file.
  //
  hBitmap=(HBITMAP)LoadImage(hInstance,MAKEINTRESOURCE(BMP_BOING),
                             IMAGE_BITMAP,0,0,0);
  BITMAP bmp; GetObject(hBitmap,sizeof(bmp),&bmp);
  Width=bmp.bmWidth; Height=bmp.bmHeight;


  // Following code called in response to WM_DESTROY.
  //
  DeleteObject(hBitmap);


  // Following code to draw the bitmap. We assume that 'hdc' is the
  // HWND of the screensaver window, obtained with hdc=GetDC(hwnd);
  // or by the WM_PAINT method.
  //
  hMemDC=CreateCompatibleDC(hdc);
  hOldBitmap=SelectObject(hMemDC,hBitmap);
  BitBlt(hdc,0,0,Width,Height,hMemDC,0,0,SRCCOPY);
  SelectObject(hMemDC,hOldBitmap);
  DeleteDC(hMemDC);
If the cases above did not hold, and if you do have to worry about palettes (or if had an 8bpp bitmap and wanted to do palette animation) then you'd have to obtain the bitmap's palette and respond to WM_QUERYNEWPALETTE and WM_PALETTECHANGED as in the next section.



4.2 Using 8bpp DIBSections

If you want to do fast animation, you might well want access directly to the bytes inside a bitmap. You might also want to use palette animation on an 8bpp bitmap. This section describes how to do both. The example screensavers 'scotch', 'static' and 'ghosts' do this.

Displaying an 8bpp DIBSection is a seriously non-trivial task. You have to respond to WM_QUERYNEWPALETTE and WM_PALETTECHANGED; you have to select palettes here and there; if you want to redefine 254 colours (ie. all the available ones) then you have to respond to WM_ACTIVATEAPP by snitching the static colours from the system. And there's lots lots more.

To make this easier, 'scotch', 'static' and 'ghosts' use a module called 'Bits'. The following code would create an 8bpp DIBSection and have it displayed in the window.

  TBits *bits=new TBits();
  bits->DipPainter.PositionAtPoint(0,0);
  bits->EnableSubclass();
  bits->Create(hwnd,false,1,200,150,8,true);
and you don't have to do anything more! The Bits module also does a couple of other things, including support for DirectDraw. You could use Bits yourself, or look at it to see how it works. Here is another example of it: TBits *bits=new TBits(); TBits *loadbits=new TBits(); loadbits->CreateDibFromFile(NULL,"c:\\art\\lu.bmp"); bits->DibPainter.PositionAtPoint(0,0); bits->EnableSubclass(); for (int i=0; i<256; i++) { bits->LogPal.Entries[i].peRed=i; bits->LogPal.Entries[i].peGreen=i; bits->LogPal.Entries[i].peBlue=i; } bits->ChangePalette(); bits->CreateDibResample(hwnd,loadbits->surface[0],250,100,8,true); delete loadbits;



4.3 Creating DirectDraw screensavers

Writing a screensaver in a different graphics mode can be attractive, because the reduced resolution might speed up your processing. Or you might want to use other features of DirectDraw. There are a number of considerations.

Two example screensavers that come with ScrPlus demonstrate how to do this. 'Static' draws a full-screen of static which it colour-cycles, and is relatively basic. 'Scotch' has transparent tarten grey diagonals sliding across the screen and demonstrates how to take a snapshot of the screen before you start, and to manipulate it. 'Ghosts' has static but loads in a sequence of bitmaps and fades them into the static.



Chapter 5: Summary

Hopefully now you know how to write Windows Plus! screensavers with ScPlus! This tutorial has covered