How to Make
a Windows Screen Saver in Delphi
by Mark R.
Johnson
From time
to time, I see questions asked about how to make a Windows screen saver in
Delphi that can be selected in the Control Panel Desktop. After seeing a few
general responses that only partially answered the question, I decided to give
it a try myself. The code you will see here is the result: a simple Windows
screen saver.
The
complete Delphi source code for this screen saver is available for FTP as
spheres.zip (4K). Before getting into the details of the code, however, I would
like to thank Thomas W. Wolf for the general screen saver tips he submitted to
comp.lang.pascal, which I found helpful in writing this article.
Background
A Windows
screen saver is basically just a standard Windows executable that has been
renamed to have a .SCR filename extension. In order to interface properly with
the Control Panel Desktop, however, certain requirements must be met. In
general, the program must:
maintain
optional settings
provide a
description of itself
distinguish
between active mode and configuration mode
disallow
multiple copies of itself to run
exit when
the user presses a key or moves the mouse
In the
following description, I will try to show how each of these requirements can be
met using Delphi.
Getting
Started
The screen
saver we are going to create will blank the screen and begin drawing shaded
spheres at random locations on the screen, periodically erasing and starting
over. The user will be able to specify the maximum number spheres to draw
before erasing, as well as the size and speed with which to draw them.
To begin,
start a new, blank project by selecting New Project from the Delphi File menu. (Indicate
"Blank project" if the Browse Gallery appears.)
Configuration
Form
The first
thing most people see of a screen saver is its setup dialog. This is where the
user specifies values for options specific to the screen saver. To create such
a form, change the properties of Form1 (created automatically when the new
project was begun) as follows:
BorderIcons [biSystemMenu]
biSystemMenu True
biMinimize False
biMaximize False
BorderStyle bsDialog
Caption Configuration
Height 162
Name CfgFrm
Position poScreenCenter
Visible False
Width 266
We want to
be able to configure the maximum number of spheres drawn on the screen, the
size of the spheres, and the speed with which they are drawn. To do this, add
the following three Labels (Standard palette) and SpinEdits (Samples palette):
(Note: You can select the following text, copy it to the clipboard, and paste
it onto the configuration form to create the components.)
object
Label1: TLabel
Left = 16
Top = 19
Width = 58
Height = 16
Alignment = taRightJustify
Caption = 'Spheres:'
end
object
Label2: TLabel
Left = 41
Top = 59
Width = 33
Height = 16
Alignment = taRightJustify
Caption = 'Size:'
end
object
Label3: TLabel
Left = 29
Top = 99
Width = 45
Height = 16
Alignment = taRightJustify
Caption = 'Speed:'
end
object
spnSpheres: TSpinEdit
Left = 84
Top = 15
Width = 53
Height = 26
MaxValue = 500
MinValue = 1
TabOrder = 0
Value = 50
end
object
spnSize: TSpinEdit
Left = 84
Top = 55
Width = 53
Height = 26
MaxValue = 250
MinValue = 50
TabOrder = 1
Value = 100
end
object
spnSpeed: TSpinEdit
Left = 84
Top = 95
Width = 53
Height = 26
MaxValue = 10
MinValue = 1
TabOrder = 2
Value = 10
end
Finally, we
need three buttons -- OK, Cancel, and Test. The Test button is not standard for
screen saver setup dialogs, but it is convenient and easy to implement. Add the
following three buttons using the BitBtn buttons of the "Additional"
palette:
object
btnOK: TBitBtn
Left = 153
Top = 11
Width = 89
Height = 34
TabOrder = 3
Kind = bkOK
end
object
btnCancel: TBitBtn
Left = 153
Top = 51
Width = 89
Height = 34
TabOrder = 4
Kind = bkCancel
end
object
btnTest: TBitBtn
Left = 153
Top = 91
Width = 89
Height = 34
Caption = 'Test...'
TabOrder = 5
Kind = bkIgnore
end
Once we
have the form layout, we need to add some code to make it work. First, we need
to be able to load and save the current configuration. To do this, we should
place the Spheres, Size, and Speed values into an initialization file (*.INI)
in the user's Windows directory. Delphi's TIniFile object is just the thing for
this.
Switch to
the code view for the Setup form, and add the following uses clause to the implementation
section of the configuration form's unit:
uses
IniFiles;
Then, add
the following procedure declarations to the private section of the TCfgFrm
declaration:
procedure LoadConfig;
procedure SaveConfig;
Now add the
following procedure definitions after the uses clause in the implementation
section:
const
CfgFile = 'SPHERES.INI';
procedure
TCfgFrm.LoadConfig;
var
inifile : TIniFile;
begin
inifile := TIniFile.Create(CfgFile);
try
with inifile do begin
spnSpheres.Value :=
ReadInteger('Config', 'Spheres', 50);
spnSize.Value := ReadInteger('Config', 'Size', 100);
spnSpeed.Value := ReadInteger('Config', 'Speed', 10);
end;
finally
inifile.Free;
end;
end;
{TCfgFrm.LoadConfig}
procedure
TCfgFrm.SaveConfig;
var
inifile : TIniFile;
begin
inifile := TIniFile.Create(CfgFile);
try
with inifile do begin
WriteInteger('Config', 'Spheres',
spnSpheres.Value);
WriteInteger('Config', 'Size',
spnSize.Value);
WriteInteger('Config', 'Speed',
spnSpeed.Value);
end;
finally
inifile.Free;
end;
end;
{TCfgFrm.SaveConfig}
All that
remains for the configuration form is to respond to a few events to properly
load and save the configuration. First, we need to load the configuration
automatically whenever the program starts up. We can use the setup form's
OnCreate event to do this. Double- click the OnCreate field in the events
section of the Object Inspector and enter the following code:
procedure
TCfgFrm.FormCreate(Sender: TObject);
begin
LoadConfig;
end;
{TCfgFrm.FormCreate}
Next,
double-click the OK button. We need to save the current configuration and close
the window whenever OK is pressed, so add the following code:
procedure
TCfgFrm.btnOKClick(Sender: TObject);
begin
SaveConfig;
Close;
end;
{TCfgFrm.btnOKClick}
In order to
simply close the form (without saving) when the Cancel button is pressed,
double-click on the Cancel button and add:
procedure
TCfgFrm.btnCancelClick(Sender: TObject);
begin
Close;
end;
{TCfgFrm.btnCancelClick}
Finally, to
test the screen saver, we will need to show the screen saver form (which we
haven't yet created). Go ahead and double-click on the Test button and add the
following code:
procedure
TCfgFrm.btnTestClick(Sender: TObject);
begin
ScrnFrm.Show;
end;
{TCfgFrm.btnTestClick}
Then add
"Scrn" to the uses clause in the implementation section. Scrn refers
to the screen saver form unit that we will create in the next step. In the
meantime, save this form unit as "Cfg" by selecting Save File As from
the File menu.
Screen
Saver Form
The screen
saver itself will simply be a large, black, captionless form that covers the
entire screen, upon which the graphics are drawn. To create the second form,
select New Form from the File menu and indicate a "Blank form" if
prompted by the Browse Gallery.
BorderIcons []
biSystemMenu False
biMinimize False
biMaximize False
BorderStyle bsNone
Color clBlack
FormStyle fsStayOnTop
Name ScrnFrm
Visible False
To this
form, add a single component -- a timer from the System category of the Delphi
component palette. Set its properties accordingly:
object
tmrTick: TTimer
Enabled = False
OnTimer = tmrTickTimer
Left = 199
Top = 122
end
No other
components will be required for this form. However, we will need to add some
code to handle drawing the shaded spheres. Switch to the code window
accompanying the ScrnFrm form. In the TScrnFrm private section, add the
following procedure declaration:
procedure DrawSphere(x, y, size : integer;
color : TColor);
Now, in the
implementation section of the unit, add the code for this procedure:
procedure
TScrnFrm.DrawSphere(x, y, size : integer; color : TColor);
var
i, dw
: integer;
cx, cy
: integer;
xy1, xy2 : integer;
r, g, b
: byte;
begin
with Canvas do begin
{Fill in the pen & brush settings.}
Pen.Style := psClear;
Brush.Style := bsSolid;
Brush.Color := color;
{Prepare colors for sphere.}
r := GetRValue(color);
g := GetGValue(color);
b := GetBValue(color);
{Draw the sphere.}
dw := size div 16;
for i := 0 to 15 do begin
xy1 := (i * dw) div 2;
xy2 := size -
xy1;
Brush.Color :=
RGB(Min(r + (i * 8), 255), Min(g + (i * 8), 255),
Min(b + (i * 8), 255));
Ellipse(x + xy1, y + xy1, x + xy2, y + xy2);
end;
end;
end; {TScrnFrm.DrawSphere}
As you can see from the code, we are given the (x,y)
coordinates of the top, left corner of the sphere, as well as its diameter and
base color. Then, to draw the sphere, we step through brushes of increasingly
bright color, starting with the given base color. With each new brush, we draw
a smaller filled circle concentric with the previous ones.
You will also notice, however, that the function refers to
another function, Min(). This is not a standard Delphi function, so we must add
it to the unit, above the declaration for DrawSphere().
function Min(a, b : integer) : integer;
begin
if b < a then
Result := b
else
Result := a;
end; {Min}
In order to periodically call the DrawSphere() function, we
must respond to the OnTimer event of the Timer component we added to the
ScrnFrm. Double-click the Timer component on the form and fill in the
automatically created procedure with the following code:
procedure TScrnFrm.tmrTickTimer(Sender: TObject);
const
sphcount : integer
= 0;
var
x, y : integer;
size : integer;
r, g, b : byte;
color : TColor;
begin
if sphcount >
CfgFrm.spnSpheres.Value then begin
Refresh;
sphcount := 0;
end;
Inc(sphcount);
x :=
Random(ClientWidth);
y :=
Random(ClientHeight);
size :=
CfgFrm.spnSize.Value + Random(50) - 25;
x := x - size div
2;
y := y - size div
2;
r := Random($80);
g := Random($80);
b := Random($80);
DrawSphere(x, y,
size, RGB(r, g, b));
end; {TScrnFrm.tmrTickTimer}
This procedure keeps track of the number of spheres that
have been drawn in sphcount, and refreshes (erases) the screen when we have
reached the maximum number. In the meantime, it calculates the random position,
size, and color for the next sphere to be drawn. (Note: The color range is
limited to only the first half of the brightness spectrum in order to provide
greater depth to the shading.)
As you may have noticed, the tmrTickTimer() procedure
references the CfgFrm form to retrieve the configuration options. In order for
this reference to be recognized, add the following uses clause to the
implementation section of the unit:
uses
Cfg;
Next, we will need a way to deactivate the screen saver when
a key is pressed, the mouse is moved, or the screen saver form looses focus.
One way to do this is to create an handler for the Application.OnMessage event
that looks for the necessary conditions to terminate the screen saver.
First, add the following variable declaration to the
implementation section of the unit:
var
crs : TPoint;
This variable will be used to store the original position of
the mouse cursor for later comparison. Now, add the following declaration to
the private section of TScrnFrm:
procedure
DeactivateScrnSaver(var Msg : TMsg; var Handled : boolean);
Add the corresponding code to the implementation section of
the unit:
procedure TScrnFrm.DeactivateScrnSaver(var Msg : TMsg; var
Handled : boolean);
var
done : boolean;
begin
if Msg.message =
WM_MOUSEMOVE then
done :=
(Abs(LOWORD(Msg.lParam) - crs.x) > 5) or
(Abs(HIWORD(Msg.lParam) - crs.y) > 5)
else
done :=
(Msg.message = WM_KEYDOWN) or (Msg.message = WM_ACTIVATE) or
(Msg.message = WM_ACTIVATEAPP) or (Msg.message = WM_NCACTIVATE);
if done then
Close;
end; {TScrnFrm.DeactivateScrnSaver}
When a WM_MOUSEMOVE window message is received, we compare
the new coordinates of the mouse to the original location. If it has moved more
than our threshold (5 pixels in any direction), then we close the screen saver.
Otherwise, if a key is pressed or another window or dialog box takes the focus,
the screen saver closes.
In order for this procedure to go into effect, however, we
need to set the Application.OnMessage property and get the original position of
the mouse cursor. A good place to do this is in the form's OnShow event
handler:
procedure TScrnFrm.FormShow(Sender: TObject);
begin
GetCursorPos(crs);
tmrTick.Interval := 1000 -
CfgFrm.spnSpeed.Value * 90;
tmrTick.Enabled := true;
Application.OnMessage := DeactivateScrnSaver;
ShowCursor(false);
end; {TScrnFrm.FormShow}
Here we also specify the timer's interval and activate it,
as well as hiding the mouse cursor. Most of these things should be undone,
however, in the form's OnHide event handler:
procedure TScrnFrm.FormHide(Sender: TObject);
begin
Application.OnMessage := nil;
tmrTick.Enabled := false;
ShowCursor(true);
end; {TScrnFrm.FormHide}
Finally, we need to make sure that the screen saver form
fills the entire screen when it is shown. To do this add the following code to
the form's OnActivate event handler:
procedure TScrnFrm.FormActivate(Sender: TObject);
begin
WindowState :=
wsMaximized;
end; {TScrnFrm.FormActivate}
Take this opportunity to save the ScrnFrm form unit as
"SCRN.PAS" by selecting Save File from the File menu.
The Screen Saver Description
You can define the text that will appear in the Control
Panel Desktop list of screen savers by adding a {$D text} directive to the
project source file. The $D directive inserts the given text into the module
description entry of the executable file. For the Control Panel to recognize
the text you must start with the term "SCRNSAVE", followed by your
description.
Select Project Source from the Delphi View menu so you can
edit the source file. Beneath the directive "{$R *.RES}", add the
following line:
{$D SCRNSAVE Spheres Screen Saver}
The text "Spheres Screen Saver" will appear in the
Control Panel list of available screen savers when we complete the project.
Active Versus Configuration Mode
Windows launches the screen saver program under two possible
conditions: 1) when the screen saver is activated, and 2) when the screen saver
is to be configured. In both cases, Windows runs the same program. It
distinguishes between the two modes by adding a command line parameter --
"/s" for active mode and "/c" for configuration mode. For
our screen saver to function properly with the Control Panel, it must check the
command line for these switches.
Active Mode
When the screen saver enters active mode (/s), we need to
create and show the screen saver form. We also need create the configuration
form, since it contains all of the configuration options. When the screen saver
form closes, the entire program should then terminate. This fits the definition
of a Delphi Main Form -- a form that starts when the program starts and signals
the end of the application when the form closes.
Configuration Mode
When the screen saver enters configuration mode (/c), we
need to create and show the configuration form. We should also create the
screen saver form, in case the user wishes to test configuration options.
However, when the configuration form closes, the entire program should then
terminate. In this case, the configuration form fits the definition of a Main
Form.
Defining the Main Form
Ideally, we would like to identify ScrnFrm as the Main Form
when a /s appears on the command line, and CfgFrm as the Main Form in all other
cases. To do this requires knowledge of an undocumented feature of the
TApplication VCL object: The Main Form is simply the first form created with a
call to Application.CreateForm(). Thus, to define different Main Forms
according to our run-time conditions, modify the project source as follows:
begin
if (ParamCount >
0) and (UpperCase(ParamStr(1)) = '/S') then begin
{ScrnFrm needs to
be the Main Form.}
Application.CreateForm(TScrnFrm, ScrnFrm);
Application.CreateForm(TCfgFrm, CfgFrm);
end else begin
{CfgFrm needs to
be the Main Form.}
Application.CreateForm(TCfgFrm, CfgFrm);
Application.CreateForm(TScrnFrm, ScrnFrm);
end;
Application.Run;
end.
Just by changing the order of creation, we have
automatically set the Main Form for that instance. In addition, the Main Form
will automatically be shown, despite the fact that we have set the Visible
properties to False for both forms. As a result, we achieve the desired effect
with only minimal code.
(Note: for the if statement to function as shown above, the
"Complete boolean eval" option should be unchecked in the Options |
Project | Compiler settings. Otherwise, an error will occur if the program is
invoked with no command line parameters.)
In order to use the UpperCase() Delphi function, SysUtils
must be included in the project file's uses clause to give something like:
uses
Forms, SysUtils,
Scrn in 'SCRN.PAS'
{ScrnFrm},
Cfg in 'CFG.PAS'
{CfgFrm};
Blocking Multiple Instances
One difficulty with Windows screen savers is that they must
prevent multiple instances from being run. Otherwise, Windows will continue to
launch a screen saver as the given time period ellapses, even when an instance
is already active.
To block multiple instances of our screen saver, modify the
project source file to add the outer if statement shown below:
begin
{Only one instance
is allowed at a time.}
if hPrevInst = 0
then begin
if (ParamCount
> 0) and (UpperCase(ParamStr(1)) = '/S') then begin
...
end;
Application.Run;
end;
end;
The hPrevInst variable is a global variable defined by
Delphi to point to previous instances of the current program. It will be zero
if there are no previous instances still running.
Now save the project file as "SPHERES.DPR" and
compile the program. With that, you should be able to run the screen saver on
its own. Without any command line parameters, the program should default to
configuration mode. By giving "/s" as the first command line
parameter, you can also test the active mode. (See Run | Parameters...)
Installing the Screen Saver
Once you've tested and debugged your screen saver, you are
ready to install it. To do so, simply copy the executable file (SPHERES.EXE) to
the Windows directory, changing its filename extension to .SCR in the process
(SPHERES.SCR). Then, launch the Control Panel, double-click on Desktop, and
select Screen Saver | Name. You should see "Spheres Screen Saver" in
the list of possible screen savers. Select it and set it up.