by Jim Karabatsos - GUI Computing
It seemed like such a simple request.
"I'd just like a custom screen saver that displays a background bitmap and plays an AVI over the top of it. Any time today will do…"
I did try to duck it. "Can't we just have the application decide it's been inactive and go into advertising mode?" I said, smugly.
"No, the 'application' is a browser so we can't do that. It has to be a screen saver. Win95, SVGA. Go for it."
Oh well, it looked like I'd have to write it after all. No problem. I mean, how hard can a screen saver be, anyway?
Plenty hard, as it turns out. I guess it depends on whether you are a perfectionist or not. First things first. Let's look up the IScreenSaver interface in Win95. Hey, where is it. Waddayamean there's no COM interface for screen savers? Bummer. OK, let's look at the messages and API calls we need. Hmmm. None of those either. What's going on here?
It took me quite a while trawling the on-line resources to discover a help file written by some kind soul documenting the undocumented interface to screen savers. It's a marvellous interface - it's a command line. I kid you not. Here's how it works.
First, you write a program and compile it to an EXE. You rename it so it has a .SCR extension and place it in the Windows System directory. Then it will appear in the Screen Properties dialog - and that's where the fun begins.
When the user clicks on the name of your screen saver in the list box, the screen properties dialog needs to tell your program to display itself in the little preview window inside that dialog. So it launches your application and passes the handle to that little preview window (as a "/P " followed by a string of ASCII digits) on the command line. I kid you not! You need to grab that window handle and draw on it. Oh, and just to make life interesting, the way you know you should stop is by hooking into the message queue for that window and trapping a WM_CLOSE message. Sounds like a fun thing to do in VB.
Then there's the configuration mode - you know, that dialog your screen saver pops up when it needs to let the user define how many toasters they want. Here's what happens. Remember, your application is already running, doing its thing in that little preview window which it has subclassed anyway to trap the WM_CLOSE message so it knows when to stop. So of course the logical thing to do would be for the properties dialog to send a custom message to that window so your program could intercept it and know that it should display the configuration screen. So of course that is not what happens. Instead, your program is terminated (using the WM_CLOSE scenario) and launched again with a command line parameter of "/C" (or theoretically "-C" for all the Unix die-hards, although I have not yet seen that sent. Oh, and if the user right-clicks on the SCR file in Explorer and selects "Configure" from the context menu, your screen saver is launched with and empty command line, so you are supposed to assume that an empty command line is the same as "/C".
But wait, there's more. When the time comes to actually show your screen saver (because the system decides it's time, or because the user chooses to preview the screen saver full-screen) then your screen saver is launched yet again, this time with "/S" in the command line.
And finally, you need to handle the "/A" command line, which relates to passwords and is basically your cue to call "PwdChangePasswordA" in MPR.DLL, another finely documented little piece of work.
Lots of fun.
Anyway, here's what I came up with. If it seems a little disjointed, it's because I was re-doing bits and pieces as I was going along and as I discovered more of the magic keys to the kingdom.
First, let me say that it is possible to create a screen saver in VB5. It's not even all that hard. The problem I had with VB5 was more related to what I was trying to show in the screen saver than the framework itself. In fact, I did have a screen saver going in VB5 at one stage but I was using an undocumented OCX that some web site had deposited on my disk and I had no idea what the licensing arrangements were (or even who to approach to sort them out). That, coupled with the 1.3M of run-time support files made me swap over to Delphi fairly quickly.
First things first. You don't do ANYTHING until you know what you are supposed to do. So the FormCreate event for the main form looks like this:
procedure TfrmMain.FormCreate(Sender: TObject); begin ShowingScreenSaver := False; DetermineMode; Case ScreenSaverMode of ssmConfig : ShutDown := DoConfig; ssmDisplay : ShutDown := DoDisplay; ssmPreview : ShutDown := DoPreview; ssmPassword : ShutDown := DoPassword; ssmPrevRunning : ShutDown := True; ssmUnknown : ShutDown := True; end; If ShutDown Then Close; end;
ScreenSaverMode is a global variable set by DetermineMode, a function in a utility unit that determines what needs to be done and returns one of the enumerated values ssmConfig .. ssmUnknown. We'll look at that in just a moment, but just before we do, you can see how we use the return value to select what we are going to do. The DoConfig, DoDisplay, DoPreview and DoPassword methods return True if they want the app to terminate or False if they want the app to continue.
OK, here's DetermineMode:
function DetermineMode: TScreenSaverMode; var ParamStr1: String; ParamStr2: String; begin if DebugMode then begin ParamStr1 := DebugParam1; ParamStr2 := DebugParam2; end else begin ParamStr1 := ParamStr(1); ParamStr2 := ParamStr(2); end; Result := ssmUnknown; ScreenSaverMode := Result; BMPFile := ReadRegString(HKey_Local_Machine, 'Software\GUI\ScreenSaver\Settings', 'BMPFile',''); AVIFile := ReadRegString(HKey_Local_Machine, 'Software\GUI\ScreenSaver\Settings', 'AVIFile',''); StartType := UpperCase(ParamStr1); if StartType = '' Then //This will happen when a user StartType := '/C'; //right-clicks the .SCR //file and chooses "configure" case StartType of 'C': begin TargetHandle := FindWindow(nil,'Display Properties'); Result := ssmConfig; ScreenSaverMode := ssmConfig; exit; end; 'S': begin TargetHandle := 0; Result := ssmDisplay; ScreenSaverMode := ssmDisplay; if ExistsPrevious('GUI Screen Saver Main Form')then begin Result := ssmPrevRunning; ScreenSaverMode := ssmPrevRunning; end; exit; end; 'P': begin try TargetHandle := StrToInt(ParamStr2); except TargetHandle := 0; end; if IsWindow(TargetHandle) then begin Result := ssmPreview; ScreenSaverMode := ssmPreview; exit; end; end; 'A': begin try TargetHandle := StrToInt(ParamStr2); except TargetHandle := 0; end; if IsWindow(TargetHandle) then begin Result := ssmPassword; ScreenSaverMode := ssmPassword; exit; end; end; end; end;
Fun, isn't it?
I'll leave you to look at the source code to determine how to handle the configuration dialog and the normal view of the screen saver. Those functions are pretty much specific to the screen saver you are writing anyway, so that DoConfig, for example, just shows a modal dialog which does whatever it needs to do:
function TfrmMain.DoConfig: Boolean; begin with TFrmConfig.Create(self) do try ShowModal finally Free end; Result := True; end;
The interesting one is the preview window. What you need to do is to subclass the window whose handle you were passed and do things to it. Here's how I did it:
var PrevRect : TRect; PreviewCanvas : TCanvas; PreviewImage: TImage; DrawY, DrawX, DrawHeight, DrawWidth : Integer; //This is the window proc for the preview window; //it only implements 4 messages. I don't know if it //is OK to quit on WM_CLOSE, but I had no problems doing so function MyWndProc (Wnd : hWnd; Msg : Integer; wParam : Integer; lParam : Integer) : Integer; far; stdcall; var DC: hDC; PS: TPaintStruct; begin case Msg of WM_QUIT: //---------------------------------------- begin Result := 0; Exit; end; WM_DESTROY, WM_CLOSE: //---------------------------------------- begin PostQuitMessage(0); Result := 0; Exit; end; WM_PAINT: //---------------------------------------- begin DC := BeginPaint(Wnd, PS); FillRect(DC,PrevRect,GetStockObject(BLACK_BRUSH)); StretchBlt(DC,DrawX,DrawY,DrawWidth,DrawHeight, PreviewImage.Canvas.Handle,0,0, PreviewImage.Width, PreviewImage.Height, SRCCOPY); EndPaint(Wnd,PS); Result := 0; Exit; end; end; Result := DefWindowProc(Wnd, Msg, wParam, lParam); end; //This creates the preview window, //and goes into a message loop to keep it running procedure CreatePreview(imgPreview:TImage); var WndClass : TWndClass; DC : hDC; MyWnd : hWnd; Msg : TMsg; const ChildClassName : PChar = 'GUIScreenSaverPreview3'#0; begin //-------------------------make image accessible in the message handler PreviewImage := imgPreview; //-------------------------create a new window class with WndClass do begin style := CS_PARENTDC; lpfnWndProc := @MyWndProc; cbClsExtra := 0; cbWndExtra := 0; hIcon := 0; hCursor := 0; hbrBackground := 0; lpszMenuName := nil; lpszClassName := ChildClassName; end; WndClass.hInstance := hInstance; Windows.RegisterClass (WndClass); //------------------------get some info on parent window GetWindowRect(TargetHandle, PrevRect); PrevRect.Right := PrevRect.Right - PrevRect.Left; PrevRect.Bottom := PrevRect.Bottom - PrevRect.Top; PrevRect.Left := 0; PrevRect.Top := 0; DrawX := 0; DrawY := 0; DrawWidth := PrevRect.Right; DrawHeight := PrevRect.Bottom; if PreviewImage.Width < Screen.Width then begin DrawWidth := (DrawWidth * PreviewImage.Width) div Screen.Width; DrawX := (PrevRect.Right - DrawWidth) div 2; end; if PreviewImage.Height < Screen.Height then begin DrawHeight := (DrawHeight * PreviewImage.Height) div Screen.Height; DrawY := (PrevRect.Bottom - DrawHeight) div 2; end; //---------------------------create the window as child of the // window given in ParamHandle MyWnd := CreateWindow (ChildClassName, 'GUISaver', WS_CHILDWINDOW or WS_VISIBLE, 0, 0, PrevRect.Right, PrevRect.Bottom, TargetHandle, 0, hInstance, nil); //----------------------------get the DC for the new created window DC := GetDC(MyWnd); PreviewCanvas := TCanvas.Create; PreviewCanvas.Handle := DC; //----------------------------go into a message loop // I don't think I can use the // built-in loop here while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; //----------------------------Terminate the preview window. // I don't care about other // resources here, as the app // terminates immediately and // Win95 cleans up after it PreviewCanvas.Free; PreviewCanvas := nil; end;
Shades of C, Batman! If this looks a lot like traditional message loop processing, that's just because it is. Don't let it scare you, however. It really is not that hard. Essentially, I'm creating a new windows class called GUIScreenSaverPreview3 and then creating a window of that class. In the creation process, I am making that window a child of the window whose handle I was passed (identified by TargetHandle in the code).
As soon as the window is created, it starts receiving messages. These messages end up in the windows procedure associated with this windows class, in this case MyWndProc. It is here that I look for and handle the WM_CLOSE message as well as WM_QUIT and WM_DESTROY in a sort of belt-and-braces system. I also handle the WM_PAINT message so I know when I should redraw the image in the preview window, in this case by blasting a copy of the selected background image into the appropriate rectangle so that the user can see the relationship between the bitmap size and the screen size. I did think about showing the AVI in the preview window scaled as necessary - then I lay down until the thought passed. No way was I going to bite off that mouthful!
That's the guts of it. All the source code is available in a downloadable ZIP so you can see how it all hangs together. You probably want to have a look at how the timer works to kill the screen saver (and how it has to ignore at least one mouse move message). You also want to take a look at how the configuration dialog is made a modal child of the properties dialog window and positioned so that it covers the preview window. That's so you don't see that the screen properties dialog has killed your preview screen - I kid you not, this is what happens. Bring up any of the built-in screen saver's configuration dialogs and then move them off the preview window; you'll see that the preview window is just a single colour. Finally, check out DoPassword to see how that all hangs together.
Oh, and look in the project source file to see how the addition of these two lines makes the project not show the main form:
ShowWindow(Application.Handle, SW_HIDE); Application.ShowMainForm := FALSE;
What can I say about this? Writing a screen saver really should not be this hard. It's obvious that this interface was never designed as such, it just sort of grew and took on a life of its own. Personally, I hope I never have to do another one of these again, although I bet I will. Once you do something once, you're the expert.