(Text version)
Borland Delphi
[ City Zoo | Announcements | Articles | Tips & Tricks | Bug List | FAQ | Sites ]


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:

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.

® Delphi is a registered trademark of Borland International.
[ Home Page | What's New | About CITY ZOO | Borland Delphi | About the Authors | INDEX ]


keeper@mindspring.com
Copyright © 1995 Mark R. Johnson. This is a CITY ZOO production.
Last revised September 4, 1995.