Home | Blog | Publications | Photos | Services | About | Contact

Archived document: "How To: Screen Savers in Win95"

Note: This article is a verbatim copy of my original article written in 1996-1998. This article has not been updated to reflect the technology of today.

Original URL: http://www.dystopia.fi/~janij/techinfo/scrsav95.htm


How To: Screen Savers in Win95

Ever wanted to create your own, personal screen saver with Delphi? After reading this "How To" you should be able to do it!

The Win32 SDK is very sparse in helping you to create your own, well behaving screen savers. Of course, creating an application which draws simple images on the screen is almost trivial, but integrating with the shell (preview windows, control-alt-del protection, passwords, and so on) is a bit harder.

In this tutorial we create a simple Win95 style screen saver using Borland Delphi 2.0. While trying to make the screen saver fast loading and small, we don't use Delphi forms or components. This results in small EXE file size (about 20k). Also, you learn how to use the registry and dialog boxes in the old and bare API style.

Before we begin, one note about this project. As said, we aren't going to use VCL (Visual Component Library) or any forms. As such, you should make sure that you don't have the default units, like Forms, Graphics, etc. listed in the uses clauses of your project and units. This is very important because otherwise bizarre effects will occur. Also, when you create a new project, Delphi will automatically display a form. To remove this unwanted form, open the Project Manager, select the form from the list and then press the red minus sign to remove it.

That's it about the notes. Lets begin!

Technically, screen savers are normal EXE files (with .SCR extenstion), which are controlled through command line parameters. For example, if user wants to configure the saver, Windows runs your saver with the '-c' command line parameter. We start creating your saver by creating a "main" function, which looks like this:

Procedure RunScreenSaver;
Var S : String;
Begin
   S := ParamStr(1);
   If (Length(S) > 1) Then Begin
   Delete(S,1,1); { delete first char - usally "/" or "-" }
   S[1] := UpCase(S[1]);
   End;
   LoadSettings; { load settings from registry }
   If (S = 'C') Then RunSettings
   Else If (S = 'P') Then RunPreview
   Else If (S = 'A') Then RunSetPassword
   Else RunFullScreen;
End;  

Because we need to create a small preview window and a full screen window, it is best to use a single window class to handle the drawing. To be well-behaving, we also need to use multiple threads. This is because, firstly, the saver should not stop running even if something "heavy" is happening, and secondly, we don't need to use timers. Actually using multiple threads is quite simple, so don't worry about that.

The procedure for running a screen saver in full screen is something like this:

Procedure RunFullScreen;
Var
   R : TRect;
   Msg : TMsg;
   Dummy : Integer;
   Foreground : hWnd;
   
Begin
   IsPreview := False;
   MoveCounter := 3;
   Foreground := GetForegroundWindow;
   While (ShowCursor(False) > 0) do ;
   GetWindowRect(GetDesktopWindow,R);
   CreateScreenSaverWindow(R.Right-R.Left,R.Bottom-R.Top,0);
   CreateThread(nil,0,@PreviewThreadProc,nil,0,Dummy);
   SystemParametersInfo(spi_ScreenSaverRunning,1,@Dummy,0);
   While GetMessage(Msg,0,0,0) do Begin
     TranslateMessage(Msg);
     DispatchMessage(Msg);
   End;
   SystemParametersInfo(spi_ScreenSaverRunning,0,@Dummy,0);
   ShowCursor(True);
   SetForegroundWindow(Foreground);
End;

Firstly, we initialized some global variables (described later), hide the mouse cursor (multiple times, so it actually gets hidden), create the saver window, start a message loop, and eventually, stop the application. Note that it is important to notify Windows that this is a screen saver: thus the call to SystemParametersInfo (this disables control-alt-del booting so don't forget your password). To create our screen saver window (which, of course, is the size of the screen - thus the GetWindowRect(GetDesktopWindow) call) we do:

Function CreateScreenSaverWindow(Width,Height : Integer;
   ParentWindow : hWnd) : hWnd;
Var WC : TWndClass;
Begin
   With WC do Begin
     Style := cs_ParentDC;
     lpfnWndProc := @PreviewWndProc;
     cbClsExtra := 0;
     cbWndExtra := 0;
     hIcon := 0;
     hCursor := 0;
     hbrBackground := 0;
     lpszMenuName := nil;
     lpszClassName := 'MyDelphiScreenSaverClass';
     hInstance := System.hInstance;
   End;
   RegisterClass(WC);
   If (ParentWindow <> 0) Then
     Result := CreateWindow('MyDelphiScreenSaverClass','MySaver',
       ws_Child Or ws_Visible or ws_Disabled,0,0,
       Width,Height,ParentWindow,0,hInstance,nil)
   Else Begin
     Result := CreateWindow('MyDelphiScreenSaverClass','MySaver',
       ws_Visible or ws_Popup,0,0,Width,Height,
       0,0,hInstance,nil);
     SetWindowPos(Result,hwnd_TopMost,0,0,0,0,
       swp_NoMove or swp_NoSize or swp_NoRedraw);
   End;
   PreviewWindow := Result;
End;

This is how windows are created using Windows API calls. I've stripped error checking, but usually you won't need them, especially in this kind of application. In case we need to create a preview window (ParentWindow <> 0), we create a disabled window. This is very important because otherwise the preview window will receive unwanted messages, and behaves strangely. Also note that the full screen window is placed as the topmost window (using SetWindowPos) so that it will always be on top of any other window.

Now you might wonder how we did get the parent handle of preview window. Actually, this is quite simple: Windows simply passes this window handle in the command line when needed. Thus:

Procedure RunPreview;
Var
   R : TRect;
   PreviewWindow : hWnd;
   Msg : TMsg;
   Dummy : Integer;
   
Begin
   IsPreview := True;
   PreviewWindow := StrToInt(ParamStr(2));
   GetWindowRect(PreviewWindow,R);
   CreateScreenSaverWindow(R.Right-R.Left,R.Bottom-R.Top,PreviewWindow);
   CreateThread(nil,0,@PreviewThreadProc,nil,0,Dummy);
   While GetMessage(Msg,0,0,0) do Begin
     TranslateMessage(Msg);
     DispatchMessage(Msg);
   End;
End;

As you can see, the window handle is the second parameter (after "-p"). Our preview window will simply become a disabled child window of this window.

To actually "run" the screen saver - ie. make something visual happen - we need a thread. This is created with the CreateThread call above. The actual thread procedure is like this:

Function PreviewThreadProc(Data : Integer) : Integer; StdCall;
Var R : TRect;
Begin
   Result := 0;
   Randomize;
   GetWindowRect(PreviewWindow,R);
   MaxX := R.Right-R.Left;
   MaxY := R.Bottom-R.Top;
   ShowWindow(PreviewWindow,sw_Show);
   UpdateWindow(PreviewWindow);
   Repeat
     InvalidateRect(PreviewWindow,nil,False);
     Sleep(30);
   Until QuitSaver;
   PostMessage(PreviewWindow,wm_Destroy,0,0);
End;

The thread simply forces a redraw to our window, sleeps for a while, and redraws something again. Note that the function named a bit strangely: actually it is used with the "full screen" window too, not just with the preview window.

In Windows, something is painted in response to a WM_PAINT message, which is always sent to a window, not to a thread. To handle this message, we need a window procedure, which is something like this:

Function PreviewWndProc(Window : hWnd;
   Msg,WParam,LParam : Integer)
   : Integer; StdCall;
Begin
   Result := 0;
   Case Msg of
     wm_NCCreate : Result := 1;
     wm_Destroy : PostQuitMessage(0);
     wm_Paint : DrawSingleBox; { paint something }
     wm_KeyDown : QuitSaver := AskPassword;
     wm_LButtonDown,
     wm_MButtonDown,
     wm_RButtonDown,
     wm_MouseMove : Begin
         If (Not IsPreview) Then Begin
           Dec(MoveCounter);
           If (MoveCounter <= 0) Then
             QuitSaver := AskPassword;
         End;
       End;
     Else Result := DefWindowProc(Window,Msg,WParam,LParam);
   End;
End;

Notice how we response to the WM_NCCREATE message. Result must be equal to one for the window to be created. Also note the ignoring of mouse messages for the first few times. This is important so that the saver won't stop immediately after is has been started. Imagine for example the user pressing the "Test" button in the shell. Note also that there is no need to check if we get key presses in a preview window: remember how it was created as disabled, so we won't get key down messages.

Otherwise if mouse is moved, button clicked or a key was pressed, we ask the use a password, and if it is OK, we return true to eventually end the saver, like this:

Function AskPassword : Boolean;
Var
   Key : hKey;
   D1,D2 : Integer; { two dummies }
   Value : Integer;
   Lib : THandle;
   F : TVSSPFunc;
   
Begin
   Result := True;
   If (RegOpenKeyEx(hKey_Current_User,'Control Panel\Desktop',0,
     Key_Read,Key) = Error_Success) Then Begin
     D2 := SizeOf(Value);
     If (RegQueryValueEx(Key,'ScreenSaveUsePassword',nil,@D1,
       @Value,@D2) = Error_Success) Then Begin
       If (Value <> 0) Then Begin
         Lib := LoadLibrary('PASSWORD.CPL');
         If (Lib > 32) Then Begin
           @F := GetProcAddress(Lib,'VerifyScreenSavePwd');
           ShowCursor(True);
           If (@F <> nil) Then Result := F(PreviewWindow);
           ShowCursor(False);
           MoveCounter := 3; { reset again if password was wrong }
           FreeLibrary(Lib);
         End;
       End;
     End;
     RegCloseKey(Key);
   End;
End;   

This also demonstrates using registry in the API level. Also note how we dynamically load the password provider functions using LoadLibrary. Remember functional types? TVSSFunc is defined as:

Type
   TVSSPFunc = Function(Parent : hWnd) : Bool; StdCall;  

Now almost everything is ready, except the configuration dialog. This is easy:

Procedure RunSettings;
Var Result : Integer;
Begin
   Result := DialogBox(hInstance,'SaverSettingsDlg',0,@SettingsDlgProc);
   If (Result = idOK) Then SaveSettings;
End;  

The difficult part is to create the dialog script (remember: we aren't using Delphi forms here!). I did this using 16-bit Resource Workshop (came with Turbo Pascal for Windows). I saved the file as a script (text) file, and the compiled it with BRCC32:

SaverSettingsDlg DIALOG 70, 130, 166, 75
     STYLE WS_POPUP | WS_DLGFRAME | WS_SYSMENU
     CAPTION "Settings for Boxes"
     FONT 8, "MS Sans Serif"
     BEGIN
   DEFPUSHBUTTON "OK", 5, 115, 6, 46, 16
   PUSHBUTTON "Cancel", 6, 115, 28, 46, 16
   CTEXT "Box &Color:", 3, 2, 30, 39, 9
   COMBOBOX 4, 4, 40, 104, 50, CBS_DROPDOWNLIST | CBS_HASSTRINGS
   CTEXT "Box &Type:", 1, 4, 3, 36, 9
   COMBOBOX 2, 5, 12, 103, 50, CBS_DROPDOWNLIST | CBS_HASSTRINGS
   LTEXT "Boxes Screen Saver for Win32 Copyright © 1996 Jani
   Järvinen.", 7, 4, 57, 103, 16,
   WS_CHILD | WS_VISIBLE | WS_GROUP
   END

Almost as easy is to make the dialog box actually work using a dialog procedure (similar to a window procedure):

Function SettingsDlgProc(Window : hWnd;
   Msg,WParam,LParam : Integer) : Integer; StdCall;
Var S : String;
Begin
   Result := 0;
   Case Msg of
   wm_InitDialog : Begin
       { initialize the dialog box }
       Result := 0;
     End;
   wm_Command : Begin
       If (LoWord(WParam) = 5) Then
       EndDialog(Window,idOK)
       Else If (LoWord(WParam) = 6) Then
       EndDialog(Window,idCancel);
     End;
   wm_Close : DestroyWindow(Window);
   wm_Destroy : PostQuitMessage(0);
   Else Result := 0;
   End;
End;  

After user has chosen some settings to our saver, we need to save them. We use the registry (once again):

Procedure SaveSettings;
Var
   Key : hKey;
   Dummy : Integer;
   
Begin
   If (RegCreateKeyEx(hKey_Current_User,
     'Software\SilverStream\SSBoxes',
     0,nil,Reg_Option_Non_Volatile,
     Key_All_Access,nil,Key,
     @Dummy) = Error_Success) Then Begin
     RegSetValueEx(Key,'RoundedRectangles',0,Reg_Binary,
       @RoundedRectangles,SizeOf(Boolean));
     RegSetValueEx(Key,'SolidColors',0,Reg_Binary,
       @SolidColors,SizeOf(Boolean));
     RegCloseKey(Key);
   End;
End;  

Almost similary, loading the keys is done like this:

Procedure LoadSettings;
Var
   Key : hKey;
   D1,D2 : Integer; { two dummies }
   Value : Boolean
Begin
   If (RegOpenKeyEx(hKey_Current_User,
     'Software\SilverStream\SSBoxes',0,
     Key_Read,Key) = Error_Success) Then Begin
     D2 := SizeOf(Value);
     If (RegQueryValueEx(Key,'RoundedRectangles',nil,
         @D1,@Value,@D2) = Error_Success) Then Begin
       RoundedRectangles := Value;
     End;
     If (RegQueryValueEx(Key,'SolidColors',nil,@D1,@Value,
         @D2) = Error_Success) Then Begin
       SolidColors := Value;
     End;
     RegCloseKey(Key);
   End;
End;

Easy, isn't it? We also need to let the user to set a password to the saver. I honestly don't know why this has been left to the applications developer - why couldn't Windows set the password? Nonetheless, we set the password the "Win95" way, like this:

Procedure RunSetPassword;
Var
   Lib : THandle;
   F : TPCPAFunc;

Begin
   Lib := LoadLibrary('MPR.DLL');
   If (Lib > 32) Then Begin
     @F := GetProcAddress(Lib,'PwdChangePasswordA');
     If (@F <> nil) Then F('SCRSAVE',StrToInt(ParamStr(2)),0,0);
     FreeLibrary(Lib);
   End;
End;

We dynamically load the (undocumented) library MPR.DLL, which has a function to set the screen saver password, so we don't need to worry about that. TPCPAFund is defined as:

Type
   TPCPAFunc = Function(A : PChar; Parent : hWnd;
   B,C : Integer) : Integer; StdCall;

(Don't ask me what the parameters B and C are for.) Now the only thing we need consider is the funniest part of all: drawing the graphics. I'm not a graphics guru, so you won't see any Goraud shaded polygons rotating in real time. I just made it plain simple: we draw some boxes.

Procedure DrawSingleBox;
Var
   PaintDC : hDC;
   Info : TPaintStruct;
   OldBrush : hBrush;
   X,Y : Integer;
   Color : LongInt;

Begin
   PaintDC := BeginPaint(PreviewWindow,Info);
   X := Random(MaxX); Y := Random(MaxY);
   If SolidColors Then
   Color := GetNearestColor(PaintDC,RGB(Random(255),Random(255),Random(255)))
   Else Color := RGB(Random(255),Random(255),Random(255));
   OldBrush := SelectObject(PaintDC,CreateSolidBrush(Color));
   If RoundedRectangles Then
   RoundRect(PaintDC,X,Y,X+Random(MaxX-X),Y+Random(MaxY-Y),20,20)
   Else Rectangle(PaintDC,X,Y,X+Random(MaxX-X),Y+Random(MaxY-Y));
   DeleteObject(SelectObject(PaintDC,OldBrush));
   EndPaint(PreviewWindow,Info);
End;

Every tried similar things with Delphi? Oh, it is so simple... And the above is pretty straightforward.

To make the screen saver complete, I give you some more details. First, global variables:

Var
   IsPreview : Boolean;
   MoveCounter : Integer;
   QuitSaver : Boolean;
   PreviewWindow : hWnd;
   MaxX,MaxY : Integer;
   RoundedRectangles : Boolean;
   SolidColors : Boolean;

Next the actual project source code (.dpr). Pretty simple, huh?

program MySaverIsGreat;
uses
   Utility; { defines all routines }

{$R SETTINGS.RES}

begin
   RunScreenSaver; 
end.

Oh, before I forget: If you use SysUtils in your project (StrToInt is defined there) you get a bigger EXE than the promised 20k. If you want a 20k EXE, you can't use SysUtils so you need to write your own StrToInt routine. I'm not going to give you the code to do a "string to int" conversion, so I leave it to you as homework!

Tip: Use Val... ;-)

 

› Publications - Archive