How To: Screen Savers in Win95
Posted: (EET/GMT+2)
How To: Screen Savers in Win95
Level: Intermediate/AdvancedEver 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;
The procedure for running a screen saver in full screen is something
like this:
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:
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:
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:
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:
To make the screen saver complete, I give you some more details.
First, global variables:
Tip: Use Val... ;-)
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.
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.
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.
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.
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.
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.
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!