Mephzara - Andreas Kardin

Software Entwicklung

Was ist ein Screensaver?

Ein Screensaver ist eine normale EXE-Datei (ein Windows-Programm), welche statt der Extension EXE die Extension SCR hat. Also einfach die EXE-Datei nach dem Kompilieren umbenennen - fertig.

Dieses Programm wird nach einer definierten Zeit (Systemsteuerung->Anzeige->Bildschirmschoner) von Windows aufgerufen, wobei die durch Kommandozeilen-Parameter dem Bildschirmschoner mitgeteilt wird, was er zu tun hat.

Parameterauswertung

Dem Bildschirmschoner wird ein Operationscode (Buchstabe) übergeben, der dem Bildschirmschoner mitteilt, was er zu tun hat. Bei gewissen Dingen wird noch ein weiterer Parameter mitgeliefert - ein Windows-Fenster-Handle.

Es gibt folgende Operationen...
  • A - Hier wird der Bildschirmschoner aufgefordert, einen Dialog zur Eingabe des Passwortes zu öffnen. Nach dem 'A' übergibt Windows einem noch ein Fensterhandle. Dies wird für den Passwortdialog benötigt. Siehe "Die Passwortgeschichte".
  • C - Das ist wohl der einfachste Teil. Hier muss man den Einstellungsdialog öffnen - ein stinknormales Fenster - einzige Falle : mehrfacher Start des Programmes. In manchen Situationen kann es vorkommen, dass Windows versucht den Bildschirmschoner mehrmals zu starten. Das passiert vor allem, wenn der Dialog Systemsteuerung->Anzeige offen ist. Um das zu verhindern verwende ich folgende Routine "Mehrfachen Programmstart verhindern"
  • P - Fordert das Programm auf, sich im Preview-Modus (Vorschaumodus) zu starten. Als weiteren Parameter bekommt man eine Integerzahl. Diese Zahl ist das Fensterhandle des Preview-Fensters. Mehr dazu unter "Die Vorschau".
  • S - Mit diesem Parameter startet Windows den Bildschirmschoner im tatsächlichen "Schonermodus". Siehe "Vor und nach und während des Schonens"
  • L - Dieser Parameter wird von mir einfach ignoriert, da er in "freier Wildbahn" auch nicht einfach so vorkommt. Wenn ich mich recht entsinne, sollte hier der Bildschirmschoner in einer Art Testmodus (ähnlich dem Preview-Modus) gestartet werden um Benchmark-Tests oder sowas Aehnliches zu ermöglichen.

Die Parameter werden gleich in der Projekt-Unit, noch bevor irgendeine Form oder das Application-Objekt erstellt wurde, ausgewertet.

Hier erstmal eine kurze Routine, die ich für den Start des Savers brauche. Sie verwendet grundsätzlich PreventToRunTwice um den mehrfachen Start zu verhindern und initialisiert dann alles so, wie es Delphi bei einer normalen Applikation auch machen würde.

Die Parameter enthalten die Formklasse und die Variable, welche das Hauptform dann speichert.

Ich habe den Bildschirmschoner so gebaut, dass sich in den verschiedenen Modi einfach das Hauptformular ändert.

procedure StartApp(InstanceClass: TFormClass; var Reference);
begin
PreventToRunTwice;
Application.Initialize;
Application.Title := 'Particle Systems';
Application.CreateForm(InstanceClass,Reference);
Application.Run;
end;

Hier folgt nun ein Ausschnitt aus dem PSSS-Source, der die Parameterauswertung vornimmt. Keine spannende Geschichte. Zuerst wird mal alles geUPPERCASED (dann muss man sich nur um "grosses" kümmern ;) ein allfälliges '/' oder '-' gelöscht und danach einfach, je nach Parameter, ein anderes Form als Hauptfenster verwendet.

if ParamCount >= 1 then begin
strTmp := UpperCase(ParamStr(1));


if strTmp[1] in ['/', '-'] then Delete(strTmp, 1, 1);


case strTmp[1] of
'A' : SetPassword;
'C' : StartApp(TfrmCfgParticleSystems,frmCfgParticleSystems);
'P' : StartApp(TfrmParticleSystemsPreview,frmParticleSystemsPreview);
'S' : StartApp(TfrmParticleSystems,frmParticleSystems);
'L' : ; // NOP

else StartApp(TfrmCfgParticleSystems,frmCfgParticleSystems);
end;
end else begin
StartApp(TfrmCfgParticleSystems,frmCfgParticleSystems);
end;

Saver-Fenster

Das Fenster eines Bildschirmschoners muss folgende Eigenschaften haben :

  • es muss den ganzen Schirm ausfüllen
  • es darf keinen Rahmen haben
  • es darf keine Titelleiste haben
  • es muss immer das oberste Fenster sein - d.h. alle anderen überdecken

Um das zu erreichen, setzt man im Object Inspector...

  • BorderStyle auf bsNone
  • BorderWidth auf 0
  • alle BorderIcons auf false
  • Caption auf '' (soll heissen "Leerer String" ;)
  • FormStyle auf fsStayOnTop

Damit hat man schon einiges erreicht. Damit's aber wirklich klappt habe ich noch folgende Methode hinzugefügt...

Deklaration
protected                                        
procedure CreateParams(var Params : TCreateParams);override;

Implementierung

procedure TfrmParticleSystems.CreateParams(var Params : TCreateParams);
begin
inherited CreateParams (Params);


Params.Style :=Params.Style or WS_POPUP;
Params.ExStyle:=Params.ExStyle or WS_EX_TOPMOST;
end;

Die Methode bewirkt, dass neben den Delphi-Attributen auch bestimmte eigene Window-Styles gesetzt werden.

Damit die Grösse stimmt, platziert man im FormCreate-Event...

SetBounds(0,0,screen.width,screen.height);
Tip

Um sich dass Leben für's Debuggen einfacher zu machen, empfiehlt es sich, die Grösse und Position des Fensters durch bedingte Kompilierung so zu setzen, dass man zum Debuggen ein kleineres Schoner-Fenster hat, neben dem man bequem durch den Code steppen kann. Fullscreen-Fenster, die immer zuoberst sind, stören doch extrem ;)

Vor dem Schonen

Jetzt haben wir ein schönes Schoner-Fenster und müssen nur noch loslegen. Unter Windows 95, 98 (Me?) sollte man dem System nun mitteilen, dass ein Bildschirmschoner läuft (tja... Windows startet zwar den Schoner, ist aber derart vergesslich, dass man es ihm gleich wieder mitteilen muss :). Das erreicht man mit...

SystemParametersInfo(SPI_SCREENSAVERRUNNING,Word(True),@dummy,0);

Am besten platziert man diese Zeile im FormShow-Event. Sie bewirkt, dass unter den besagten Windows-Versionen die ALT-TAB-Taste gesperrt wird, damit der Benutzer nicht einfach so vom Schoner zu einer anderen Applikation wechseln kann. Dies ist vor allem wichtig, wenn der Passwortschutz aktiv ist - da dieser so kinderleicht umgangen werden könnte. Ich rufe diese Funktion unter jeder Windows-Version auf, obwohl Sie unter den NT-Abkömmlingen keinen Effekt mehr haben soll - wer weiss aber schon, was in den Untiefen von Windows wirklich passiert?

Ach ja! Mauszeiger ausschalten nicht vergessen :

ShowCursor(false);

Auch diese Zeile ist im FormShow gut aufgehoben.

Achtung

Ein immer wieder beliebtes Thema bei Bildschirmschonern ist die Manipulation des aktuellen Bildschirminhaltes. Man "grabbt" sich das Desktop-Fenster und verschmiert, verquirlt, rotiert, faded oder was auch immer anschliessend das Bild.

Unter NT und dessen Abkömmlingen (Windows 2000, XP?) ist es jedoch so, dass Windows einen eigenen Desktop für den Bildschirmschoner öffnet. Wenn man also hier einfach das Desktop-Fenster "grabbt", bekommt man nur das Hintergrundbild ohne Icons und ohne die anderen offenen Fenster, weil der Screensaver-Desktop jungfräulich aufgeräumt und eben nur mit Hintergrundbild daherkommt.

Eine Lösung für das Problem habe ich allerdings noch nicht gefunden, allerdings hab ich sie auch noch nicht gebraucht. Ich werde dies hier zu gegebener Zeit ergänzen.

Waehrend dem Schonen

Jetzt könnte man die "Show" starten. Fast jede Bildschirmschoner-Animation - Slideshows und Filmplayer, die man mithilfe einer Komponenten realisiert und einfach sagt : "Spiel ab", mal ausgenommen - basiert auf dem frame-weisen Zeichnen. D.h. man zeichnet x-mal pro Sekunde ein Bild und das ergibt dann eine Animation.

Um das zu erreichen gibt es im wesentlichen zwei Methoden :

  • man benutzt eine Timerkomponente und stellt das Intervall beispielsweise auf 20 oder 30 Millisekunden und zeichnet dann im Timer-Event. Der Vorteil dieser Methode ist, dass das ganze Timing einfach geregelt wird und auf jedem PC die Animation etwa gleich schnell läuft - jedenfalls nicht zu schnell. Der Nachteil ist, dass die Standard-Timer mit solch niedrigen Intervallen sehr ungenau sind und sehr viel Zeit in den Untiefen von Windows verloren geht (die könnte man ja für seine Killer-Animation gut gebrauchen) - das spielt aber nicht immer so eine grosse Rolle.
  • man baut eine Endlos-Schleife und zeichnet wie wild drauf los. Der Vorteil ist, dass man sich keinen Overhead durch den Timer einhandelt. Der Nachteil ist, dass gar kein Timing vorhanden ist und man möglicherweise viel zu oft ein Frame aufbaut und auf schnellen PCs die Bilder an einem vorüberfliegen.

Ich habe in den ersten Versionen des PSSS mit Methode 1 gearbeitet. Doch der Overhead ist relativ gross und PSSS ist ein Schoner der jeden Taktzyklus gebrauchen kann :) Ich hätte statt eines normalen Timers aber auch einen Windows-Multimedia-Timer verwenden können. Diese Timer haben sehr wenig Overhead und sind viel genauer. Allerdings sind sie nicht gerade einfach in der Handhabung. Da gibt's verschiedene Auflösungen und Genauigkeiten, die von der Hardware abhängen usw.

Darum hab ich mich für Methode 2 entschieden, weil sie keine speziellen Ressourcen beansprucht und recht einfach zu implementieren ist.

Hier nun der Körper dieser Routine :

procedure TfrmParticleSystems.DoAnimation;
begin
fRunning:=true;


while fRunning do begin
DoFrame;


Application.ProcessMessages;
end;
end;

Nun das sieht auf den ersten Blick recht komisch aus. fRunning ist eine Variable vom Typ Boolean und wird im Form als private deklariert. Wir setzten Sie am Anfang der Routine auf true, um nun anzuzeigen, dass wir am animieren sind und damit wir in den nachfolgenden While-Schleife hängen bleiben. Wie kommt das Programm nun aus der Schleife raus? Das geschieht, indem ein anderer Teil des Programmes die Variable fRunning auf false stellt - zum Beispiel, wenn der Benutzer die Maus bewegt. Damit das klappt, muss unbedingt Application.ProcessMessages aufgerufen werden, denn nur so kann der Schoner auf andere Ereignisse reagieren.

Jetzt stellt sich noch die Frage von wo aus DoAnimation aufgerufen werden soll? Ich selbst habe schlechte Erfahrungen damit gemacht, DoAnimation aus dem FormShow-Event aufzurufen, weil einerseits die Ereignisbehandlung von! FormShow? niemals verlassen wird (DoAnimation? wird ja erst wieder verlassen, wenn der Schoner beendet werden soll) und andererseits wegen dem Problem, welches ich in "Die Nachrichtenfalle" näher beschreibe. Um zu erreichen, dass der FormShow-Event komplett behandelt wird und DoAnimation eben danach gestartet wird, habe ich einen One-Shot-Timer verwendet. Ein One-Shot-Timer ist eine ganze normale Timerkomponente, bei welcher aber der Timer, sobald der Timer-Event ausgelöst wurde, gleich wieder abgeschaltet wird - das Timerereignis tritt also genau einmal auf.

procedure TfrmParticleSystems.tmrStartAnimTimer(Sender: !TObject);
begin
tmrStartAnim.Enabled:=false;
DoAnimation;
end;

Innerhalb des FormShow-Events wird die Animation gestartet, indem mit...

tmrStartAnim.Enabled:=true;

...der Timer eingeschaltet wird. Man könnte das auch mittels einer eigenen Windows-Message und einer eigenen Nachrichtenbehandlungsmethode erreichen (ist wohl eleganter als der Timer) - allerdings hat man da nicht die Möglichkeit ein Intervall festzulegen.

Nachrichten-Falle

Diese Kapitel ist einem Umstand gewidmet, der mich fast an den Rand der Verzweiflung gebracht hat. PSSS kann die Bildschirmauflösung für das Schonen ändern und setzt diese nach Programmende wieder zurück. Eigentlich ist das gar keine so grosse Hexerei, wenn man sich mit der API-Funktion ChangeDisplaySettings und ein paar weiteren auseinandersetzt. Allerdings führt das ändern der Bildschirmauflösung dazu, dass MouseMove-Message und noch ein paar andere Nachrichten beim Bildschirmschoner eintrudeln (IIRC eine unmotivierte Close-Message). Diese Nachrichten führen unweigerlich dazu, dass der Bildschirmschoner meint, der Benutzer habe die Maus bewegt und beendet sich sofort.

Auf eine akzeptable Lösung für das Problem, bin ich erst nach einigen Experimenten und Misserfolgen gekommen. Der Trick besteht darin, die Message-Behandlung für eine gewisse Zeit komplett abzuschalten. D.h. alle eingehenden Nachrichten bis zum offiziellen Start des Schoners zu ignorieren. Nun könnte man auf die Idee kommen, einfach alle Event-Handler mit einem "Ignore-It"-Mechanismus auszustatten. Dies ist aber recht mühsam und hat mir nicht gefallen. Stattdessen hab ich eine eigene allesübergreifende Nachrichtensenke eingebaut. Mit...

Application.OnMessage:=MsgFilter;

...kann man eine eigene Routine einklinken, die jede Nachricht abfängt, bevor irgendeine andere Komponente die Nachricht erhält. Die Routine sieht so aus :

procedure TfrmParticleSystems.MsgFilter(var Msg: TMsg;var Handled: Boolean);
begin
case msg.message of
WM_CLOSE : Handled:=true;
WM_MOUSEMOVE : if fMsgFilterOn then Handled:=true;
end;
end;

Sie blockiert mal alle Close-Messages, weil mir Windows unerwünschte Close-Messages geschickt und mir den Schoner so frühzeitig ins Nirwana befördert hat - ach ja, wenn man Handled auf True setzt, gilt die Nachricht als erledigt und niemand sieht mehr etwas von Ihr.

Die MouseMove-Messages hingegen wollen wir nur für eine kurze Zeit blockieren. Darum eine die boolsche Variable fMsgFilterOn. Steht diese auf True, dann werden die MouseMove-Nachrichten gefiltert (d.h. ignoriert), andernfalls ganz normal weitergeleitet.

Ich stelle nun im FormShow-Event den MsgFilter, unmittelbar bevor ich die Bildschirmauflösung ändere, auf True und am Anfang von DoAnimation wieder auf false. Dadurch kann ich die Auflösung ändern, ohne dass der Schoner gleich beendet wird. Beenden des Schoners

Ein Bildschirmschoner soll in der Regel beendet werden, wenn eines der folgenden Ereignisse auftritt :

  • der Benutzer bewegt die Maus
  • der Benutzer betätigt eine Maustaste
  • der Benutzer drückt eine Taste auf der Tastatur

Den ersteren Fall kann man wie folgt behandeln...

procedure TfrmParticleSystems.FormMouseMove(Sender: TObject;Shift: TShiftState; X, Y: Integer);
begin
if (fLastX>0) and (fLastY>0) then begin
if (abs(fLastX-x)>10) or (abs(fLastY-y)>10) then begin
Close;
end;
end;

fLastX:=x;
fLastY:=y;
end;

Das ist ein Event-Handler für MouseMove-Ereignisse. Die Idee dahinter ist, dass der Benutzer die Maus mit einer gewissen Geschwindigkeit bewegen soll, bevor der Schoner sich schliesst - bewegt er sie zu langsam, so war das wahrscheinlich nur ein versehen (hat am Tisch gewackelt). Dazu merkt sich der Schoner in fLastX und fLastY jeweils die Mouse-Koordinate und kann Sie mit der Position beim letzten mal, als der Event aufgetreten ist, vergleichen. Unterscheiden sich diese nun um mehr als 10 Pixel, dann hat der Benutzer die Maus schnell genug bewegt (je höhe die Zahl um so schneller muss die Maus bewegt werden).

Die beiden letzteren Ereignisse sind einfacher. Da muss muss nur Close aufgerufen werden.

Jetzt gibt's da aber noch die ALT-Taste sowie die verschiedenen Windows-Tasten und man möchte doch, dass sich auch in diesem Fall der Schoner schliesst. Also auf und kurz eine eigene Message-Methode definiert :

Deklaration
protected
procedure WMSysKeyDown(var Msg : TWMSysKeyDown);message WM_SYSKEYDOWN;
Implementierung
procedure TfrmParticleSystems.WMSysKeyDown(var Msg : TWMSysKeyDown);
begin
Close;

inherited;
end;

Vorschau

Im Vorschaumodus läuft alles sehr ähnlich wie beim normalen Schonen. Mit der Ausnahme, dass...

  • Mausbewegungen nicht zum Programmabbruch führen dürfen - nur ein normales Close-Ereignis
  • man das Betriebssystem nicht mit SystemParametersInfo über den Start und das Ende des Schoners informieren muss
  • kein Passwortmechanismus verwendet werden muss/darf
  • der Mauszeiger nicht abgeschaltet werden muss/darf

Wie schon weiter oben erwähnt, bekommt der Bildschirmschoner hinter dem 'P' für Preview auch noch eine Integerzahl - das Windows-Handle der Vorschaufensters im Bildschimschoner-Tab von Systemsteuerung->Anzeige. In dieses Fenster müssen wir unseres einpassen, aber dazu später mehr.

Beim Fenster an und für sich gibt es auch ein paar Dinge zu beachten. Ich verwende für das Preview-Fenster folgende überschriebene CreateParams-Methode :

procedure TfrmParticleSystemsPreview.CreateParams(var Params: TCreateParams);
begin
inherited;

with Params do begin
Style :=WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN;
ExStyle :=0;
end;
end;

So, jetzt müssen wir dafür sorgen, dass wir in dem "Fernseher" der Systemsteuerung zu sehen sind. Das macht man mit...

if ParamCount>1 then begin
Windows.SetParent(Handle,StrToInt(ParamStr(2)));
end;

...im Create-Event des Vorschaufensters.

Im Show-Event können wir mit...

if ParamCount>1 then begin
GetWindowRect(StrToInt(ParamStr(2)),r);
end;

...die Grösse der Zeichenfläche ermitteln. Das Ergebnis ist in r (vom Typ TRect) und sollte dazu verwendet werden, die Animation auf diesen Bereich abzustimmen. Nebenbei : ich teste hier sicherheitshalber immer, ob auch genug Parameter da sind, obwohl der Aufruf des Schoners mit 'P' ohne Fensterhandle nicht sinnvoll ist.

Und noch etwas. Mit...

ShowWindow(Application.Handle,SW_HIDE);

...kann man verhindern, dass in der Taskbar der Fenstertitel steht. Im Vorschaumodus ist das besonders hässlich.

Tip

Damit man einen grossen Teil des eigentlichen Bildschirmschoner-Codes erben kann, empfiehlt es sich...

  • den Grafikcode für den Bildschirmschoner so zu schreiben, dass er sich an verschiedene Fenstergrössen anpasst - das muss man eigentlich sowieso, weil der Bildschirmschoner mit verschiedenen Auflösungen zurecht kommen sollte.
  • die eigentliche Graphikroutine in ein Basisformular zu stecken und das Preview- und das eigentliche Schoner-Fenster von diesem Basisformular abzuleiten. Da der Preview-Modus sich nicht gross vom eigentlichen "Schonen" unterscheidet.

Passwort-Geschichte

Diese Sache wurde in Windows recht komisch und mysteriös implementiert, und ich musste ein bisschen herumsuchen, bis ich Beispiele gefunden habe, die mir aufzeigten, wie das funktioniert. Dabei eines gleich vorweg :

Die Passwortabfrage wird von NT und dessen Abkömmlingen selbst erledigt. Das heisst, dass die ganze Geschichte, die in diesem Kapitel vorgestellt wird, nur Windows 95, 98 und vermutlich ME betrifft.

Microsoft hat die interessanterweise die Routinen für die Passwortabfrage in einer mpr.dll (Multiple Provider Router) und in password.cpl verpackt. Beide Dateien sind unter NT und NT-Abkömmlingen (Windows 2000) nicht vorhanden. Darum darf man diese Routinen nicht statisch, sondern erst zur Laufzeit dynamisch einbinden.

Wird der Schoner mit dem Parameter 'A' aufgerufen, so muss man mit der folgenden Routine einen Passwort-Aendern-Dialog anzeigen.

procedure SetPassword;
type
TPwdChangePassword = procedure(RegKeyName : PChar; ParentWindow : HWND;
Reserved1, Reserved2 : Integer); stdcall;
var
MultipleProviderRouter : LongInt;
PwdChangePassword : TPwdChangePassword;
begin
MultipleProviderRouter := LoadLibrary('mpr.dll');


if MultipleProviderRouter = 0 then begin
raise Exception.Create('Error accessing mpr.dll');
end;


try
PwdChangePassword := GetProcAddress(MultipleProviderRouter, 'PwdChangePasswordA');


if @PwdChangePassword = nil then begin
raise Exception.Create('Function PwdChangePasswordA in mpr.dll not found');
end;


PwdChangePassword('SCRSAVE',StrToInt(ParamStr(2)),0,0)
finally
FreeLibrary(MultipleProviderRouter);
end;
end;

Dabei wird am Anfang erst einmal versucht die DLL zu laden. Anschliessend sucht man sich mit GetProcAddress einen Pointer auf die gewünschte Funktion, die man eben bei Erfolg dann auch aufrufen kann. Man übergibt man der Routine neben dem Text 'SCRSAVE' (die Routine kann scheinbar mehrere Passwörter ändern, und wir wollen ihr mitteilen, dass es sich um das Bildschirmschonerkennwort handelt) noch das Handle der Elternfensters, das einem wiederum als Integerzahl im zweiten Parameter nach 'A' mitgegeben wird.

Mit der folgenden Prozedur soll man das Passwort prüfen, wenn der Benutzer den Schoner beenden möchte. Der Mechanismus ist analog zu dem oben dargestellten Verfahren.

function EnterPassword(const Form : TForm) : Boolean;
type
TVerifyScreenSavePwd = function(ParentWindow : HWND) : Boolean; stdcall;
var
PasswordApplet : LongInt;
VerifyScreenSavePwd : TVerifyScreenSavePwd;
begin
ShowCursor(True); // Show the mouse pointer


try
Result := False;


PasswordApplet := LoadLibrary('password.cpl');


if PasswordApplet = 0 then begin
raise Exception.Create('Error accessing password.cpl');
end;


try
VerifyScreenSavePwd := GetProcAddress(PasswordApplet, 'VerifyScreenSavePwd');


if @VerifyScreenSavePwd = nil then begin
raise Exception.Create('Function VerifyScreenSavePwd in password.cpl not found');
end;


Result := VerifyScreenSavePwd(Form.Handle)
finally
FreeLibrary(PasswordApplet)
end
finally
ShowCursor(False);
end;
end;

Der Parameter Form wird benötigt, damit wir der Dialogroutine von Windows das Fensterhandle des Elternfensters übergeben können. Dadurch wird der Passwort-Eingabe-Dialog auch wie gewünscht mit unserem Fenster verknüpft.

Damit wir wissen, ob der Benutzer Passwortschutz überhaupt beauftragt hat, fragen wir mal kurz in der Registry nach...

function PasswordRequired : Boolean;
var
Registry : !TRegistry;
begin
Registry :=!TRegistry.Create;
Registry.RootKey:=HKEY_CURRENT_USER;


try
if Registry.OpenKey('Control Panel\desktop', False) then begin
try
Result := Registry.ReadBool('ScreenSaveUsePassword')
except
Result := False
end
end else begin
Result := False;
end;
finally
Registry.Free
end
end;

Jetzt haben wir alles, was wir für die Passwortgeschichte brauchen. Jetzt schauen wir uns nochmals den Close-Handler unseres Schoner-Fensters an :

procedure TfrmParticleSystems.FormClose(Sender: TObject;var Action: TCloseAction);
var dummy : integer;
begin
if (gVersionInfo.Platform<>wpWinNT) and PasswordRequired then begin
if not EnterPassword(self) then begin
Action:=caNone;
exit;
end;
end;


fRunning:=False;


ShowCursor(true);
SystemParametersInfo(SPI_SCREENSAVERRUNNING,Word(false),@dummy,0);
end;

Der erste Teil der If-Bedingung prüft, ob wir uns unter Windows NT oder einem Abkömmling befinden (siehe "Die Windows-Version ermitteln" im Anhang). Der zweite Teil benutzt die weiter oben vorgestellte Routine PasswordRequired, um herauszufinden, ob wir wirklich nach dem Passwort fragen müssen.

Mit EnterPassword wird die Benutzereingabe geprüft. Falls der Benutzer ein falsches Passwort eingegeben hat, wird mit Action:=caNone verhindert, dass sich das Fenster schliesst und mit exit der Close-Handler gleich verlassen. So schonen wir dann einfach fröhlich weiter Anhang Mehrfachen Programmstart verhindern

Die Idee ist ganz einfach. In Delphi hat jede Applikation ein verstecktes (unsichtbares) Hauptfenster, welches den Titel der Applikation trägt. Die Routine sucht nach dem Fenster mit der Fensterklasse "TApplication" und dem Titel der Applikation. Wichtig ist, dass man nicht auf die Idee kommt, den Titel via Application.Title abzufragen, weil das Application-Objekt zu diesem frühen Zeitpunkt noch nicht initialisiert ist. Natürlich könnte man den Test auch später machen; ich wollte aber möglichst früh einen Abbruch erreichen.

procedure PreventToRunTwice;
var otherHnd : HWND;
begin
otherHnd:=FindWindow('!TApplication','Particle Systems');


if otherHnd<>0 then begin
SetForegroundWindow(otherHnd);
halt;
end;
end;

Anhang

WMSysCommand-Nachricht

Nunja, in den Tiefen der Windows-Dokumentation findet man eine nebenläufige Bemerkung, dass Standard-Message-Handler von Screensavern für die WM_SYSCOMMAND-Message im Falle eines SC_SCREENSAVE oder SC_CLOSE-Kommandos false zurückliefern sollen. Und wenn schon Microsoft das so macht, dann sollten wir das auch tun - warum auch immer.

Deklaration
protected
procedure WMSysCommand(var Msg : TWMSysCommand);message WM_SYSCOMMAND;
Implementierung
procedure TfrmParticleSystems.WMSysCommand(var Msg : TWMSysCommand);
begin
if (Msg.CmdType=SC_SCREENSAVE) or (Msg.CmdType=SC_CLOSE) then begin
Msg.Result:=0;
end else begin
Inherited;
end;
end;
Wenn man auf dem Internet und in der Windows-Dokumentation ein wenig herumsucht, findet man heraus, dass die WM_SYSCOMMAND-Nachricht im Zusammenhang mit SC_SCREENSAVE verwendet werden kann, um den Start des Bildschirmschoners zu verhindern. Windows schickt nämlich diese Nachricht an jede Applikation und startet den Schoner nicht, wenn die Nachricht mit Msg.Result:=-1 quittiert wird.

Windows-Version ermitteln

gVersionInfo ist eine globale Variable vom Typ...

TWindowsPlatform = (wpUnkown,wpWinNT,wpWin32,wpWin32s,wpWinCE);
TWindowsVersionInfo = record
Major : integer;
Minor : integer;
Build : integer;
Platform : TWindowsPlatform;
Description : string;
end;

...und wird im Initialization Teil meiner WindowsTool-Unit mit folgender Routine...

procedure GetWindowsVersion(var aVersionInfo : TWindowsVersionInfo);
var versionInfo : POSVersionInfo;
reqSize : integer;
begin
GetMem(versionInfo,SizeOf(TOSVersionInfo));
versionInfo.dwOSVersionInfoSize:=SizeOf(TOSVersionInfo);


if not GetVersionEx(versionInfo^) then begin
reqSize:=versionInfo.dwOSVersionInfoSize;
FreeMem(versionInfo);

GetMem(versionInfo,reqSize);
versionInfo.dwOSVersionInfoSize:=reqSize;

if not GetVersionEx(versionInfo^) then begin
HandleWindowsError('WindowTool.GetWindowsVersion',GetLastError);
end;
end;


aVersionInfo.Major:=versionInfo.dwMajorVersion;
aVersionInfo.Minor:=versionInfo.dwMinorVersion;
aVersionInfo.Build:=versionInfo.dwBuildNumber;


case versionInfo.dwPlatformId of
VER_PLATFORM_WIN32s : aVersionInfo.Platform:=wpWin32s;
VER_PLATFORM_WIN32_WINDOWS : aVersionInfo.Platform:=wpWin32;
VER_PLATFORM_WIN32_NT : aVersionInfo.Platform:=wpWinNT;
//VER_PLATFORM_WIN32_CE : aVersionInfo.Platform:=winWinCE;
else aVersionInfo.Platform:=wpUnkown;
end;


aVersionInfo.Description:=versionInfo.szCSDVersion;
FreeMem(versionInfo);
end;

...initialisiert. Details zu den API-Routinen findet man in der Windows-API-Dokumentation.

Die Prozedur HandleWindowsError übernimmt die Behandlung von Windows-Fehlern und sieht so aus :

function HandleWindowsError(aErrorInfo : string;aErrorCode : integer):boolean;
var msgBuf : PChar;
begin
result:=false;


if aErrorCode<>ERROR_SUCCESS then begin
result:=true;


FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM or
FORMAT_MESSAGE_ALLOCATE_BUFFER or
FORMAT_MESSAGE_IGNORE_INSERTS,
NIL,aErrorCode,0,@msgBuf,0,NIL);


if not IsConsole then begin
MessageBox(0,PChar(aErrorInfo+#13#10+StrPas(msgBuf)),
'Error',MB_OK or MB_ICONINFORMATION or MB_APPLMODAL);
end else begin
WriteLn(aErrorInfo+#13#10+StrPas(msgBuf));
end;


LocalFree(integer(msgBuf));
end;
end;

Ich verwende diese Funktion, um einerseits nach Windows-Fehlern zu prüfen (wenn true zurückkommt, ist etwas schief gelaufen) und andererseits, um eine Fehlermeldung auszugeben (eine MessageBox für normale Anwendungen und eine Fehlermeldung ins auf die Standardausgabe bei Console-Anwendungen. Die Windows-Fehlercodes sind mühsam, wenn man Sie nachschlagen muss - darum lieber gleich eine Meldung ausgeben.