How to Make a Windows Screen Saver in Delphi Copyright © 1995, 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.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.