How To: Screen Savers in Win95

Posted: (EET/GMT+2)

 

How To: Screen Savers in Win95

Level: Intermediate/Advanced

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... ;-)


Questions? Comments? Give feedback. Or just fill in a form.