Ecrire un écran de veille



<a href="http://www.darkskull.net/fichiers/tips/scrsaver.zip">sources du projet</a> (11.1 ko)

Un écran de veille s'écrit comme un simple executable, avec l'extension SCR. (un écran de veille porte un nom de type nomdefichier.scr). Pour Demander à Delphi de changer l'extension du fichier à générer, il suffit d'ajouter la directive:

Directive de changement d'extension de fichier

{$E scr}



Ensuite, ce qu'il faut savoir, c'est qu'un écran de veille possède 5 modes d'éxecution différents, qui sont initialisés en fonction des paramètres fournis par Windows au fichier SCR :

1) Mode Configuration standard
2) Mode Configuration dans les propriétés d'affichage
3) Mode Execution
4) Mode Preview
5) Mode Changer le Mot de Passe

Les paramètres donnés au SCR pour lancer un de ces modes sont :

1) Pas de paramètres
2) /C
3) /S
4) /P HandledeFenêtre
5) /A HandledeFenêtre

Comme vous pouvez le remarquer, dans les modes "Preview" et "Changer le Mot de Passe", un Handle de Fenêtre est fourni en paramètre, de sorte à ce qu'on puisse modifier les infos de ces fenêtres dans le cas du mode Preview par exemple. Il nous faudra donc déclarer une variable pour stocker ce handle, ainsi qu'une procédure pour le récupérer à partir des paramètres, car les paramètres sont des chaines de caractère :

Récupératio du 2° paramètre

var
  ParamHandle: THandle;

procedure GetParamHandle;
begin
  ParamHandle := StrToInt(ParamStr(2));
end;



Il nous faut maintenant vérifier, avant la création des fiches, dans quel mode l'écran de veille est lancé, donc dans les sources du projet :

Détection du mode de l'écran de veille

type
  TScreenSaverMode = (smConfigStd, smConfig, smExecute, smPreview, smPassWord);

var
  ScreenSaverMode: TSCreenSaverMode;

procedure GetScreenSaverMode;
var
  c: char;
begin
  Case ParamCount of
    0: ScreenSaverMode := smConfigStd;
    1: begin
         c := UpCase(ParamStr(1)[2]);
         if (c = 'C') then ScreenSaverMode := smConfigStd
         else ScreenSaverMode := smExecute;
       end;
    2: begin
         c := UpCase(ParamStr(1)[2]);
         if (c = 'P') then ScreenSaverMode := smPreview
         else ScreenSaverMode := smPassWord;
         GetParamHandle;
       end;
  end;
end;



Ensuite commence l'implémentation des 5 modes. Nous allons commencer par le plus simple, le changement de mot de passe, qui consiste en un appel à la DLL des mots de passe pour écran de veille :

Lancement de la boîte de dialogue de mot de passe

procedure ExecSetPwd;
var
  hLib : THandle;
  P : function(a: PChar; ParentHandle: THandle; b, c: Integer): Integer; stdcall;
  SysDir : string;
  SysLength : integer;
begin
  SetLength(SysDir, MAX_PATH);
  SysLength := GetSystemDirectory(PChar(SysDir), MAX_PATH);
  SetLength(SysDir, SysLength);
  if (Length(SysDir) > 0) and (SysDir[Length(SysDir)] <> '\') then SysDir := SysDir + '\';
  hLib := LoadLibrary(PChar(SysDir + 'MPR.DLL'));
  if hLib <> 0 then
    begin
      P := GetProcAddress (hLib, 'PwdChangePasswordA');
      if assigned (P) then P('SCRSAVE', ParamHandle, 0, 0);
      FreeLibrary (hLib);
    end;
end;




Ensuite, nous écrirons la fiche de configuration. Il nous suffit de mettre une fiche avec un TProgessBar qui nous permettra de régler la vitesse d'execution de l'écran de veille. Nous définirons 5 niveaux de vitesse, et nous sauvegarderons les informations dans la base de registre...


Configuration de l'écran de veille

procedure WriteSpeed(Value: integer);
var
  R: TRegistry;
begin
  R := TRegistry.Create;
  try
    R.OpenKey('\Software\Dark Skull\Screen Saver\', true);
    R.WriteInteger('Speed', Value);
  finally
    R.Free;
  end;
end;

function GetSpeed: integer;
var
  R: TRegistry;
begin
  R := TRegistry.Create;
  try
    R.OpenKey('\Software\Dark Skull\Screen Saver\', true);
    if R.ValueExists('Speed') then Result := R.ReadInteger('Speed')
    else Result := 3;
  finally
    R.Free;
  end;
end;




Ensuite, nous écrivons un Thread qui pourra être executé aussi bien en plein-écran que dans la fenêtre de preview. C'est lui qui s'occupera du dessin. Dans mon exemple, le thread affiche d'abord le bureau en fond d'écran, puis il dessine des rectangles aléatoirement sur l'écran avec des couleurs aléatoires. Pour cela, nous devons lui fournir en paramètres le canvas où il devra dessiner, ainsi que le rectangle auquel il doit se limiter (pour le mode preview par exemple).

Le Thread

unit UnitThread;

interface

uses
  Windows, Classes, Graphics, Forms, UnitConfig;

type
  TDrawThread = class (TThread)
  protected
    Canvas     : TCanvas;
    Left       : integer;
    Top        : integer;
    Width      : integer;
    Height     : integer;
    Background : TBitmap;
    SleepTime  : integer;
    procedure Draw;
  public
    constructor Create(ACanvas : TCanvas; DrawRect : TRect);
    procedure Execute; override;
   end;

implementation

constructor TDrawThread.Create(ACanvas: TCanvas; DrawRect: TRect);
var
  BackgroundCanvas : TCanvas;
  DC : hDC;
begin
  {définition des variables locales}
  Canvas := ACanvas;
  Left   := DrawRect.Left;
  Top    := DrawRect.Top;
  Width  := DrawRect.Right  - DrawRect.Left;
  Height := DrawRect.Bottom - DrawRect.Left;
  SleepTime := GetSpeed;

  {obtenir le bitmap de fond}
  Background := TBitmap.Create;
  Background.Width := Width;
  Background.Height := Height;
  DC := GetDC (0);
  BackgroundCanvas := TCanvas.Create;
  BackgroundCanvas.Handle := DC;
  {diminuer le fond d'écran pour qu'il tienne dans le Canvas (Preview...)}
  Background.Canvas.CopyRect(Rect(0, 0, Width, Height), BackgroundCanvas,
                             Rect(0, 0, Screen.Width, Screen.Height));
  BackgroundCanvas.Free;
  ReleaseDC(0, DC);

  {Initialiser le thread pour qu'il commence tout de suite}
  inherited Create(false);
  FreeOnTerminate := True;
end;

procedure TDrawThread.Execute;
var
  R: TRect;
begin
  try
    R := Rect(Left, Top, Left + Width, Top + Height);
    Canvas.CopyRect (R, Background.Canvas, R);
    while not Terminated do
      begin
        Synchronize(Draw);
        if SleepTime > 0 then Sleep(SleepTime);
      end;
  finally
    Background.Free;
  end;
end;

procedure TDrawThread.Draw;
begin
  Canvas.Brush.Color := rgb(random(255), random(255), random(255));
  Canvas.Rectangle(Left + Random(Width), Top + Random(Height),
                   Left + Random(Width), Top + Random(Height));
end;

end.



Maintenant que nous avons écrit notre thread, nous pouvons nous occuper de la prévisualisation. Comme la prévisualisation ne s'effectue pas dans une fenêtre du projet, mais dans une fenêtre d'un dialogue Windows, nous devons surcharger sa méthode WndProc pour pouvoir y dessiner :

La création de la fenêtre enfant

function MyWndProc(Wnd: HWnd; Msg: integer; wParam: Word;
                   lParam: integer): integer; far; stdcall;
begin
  {si on reçoit un message WM_CLOSE ou WM_DESTROY, on quitte}
  if (Msg = WM_DESTROY) or (Msg = WM_CLOSE) then PostMessage(Wnd, WM_QUIT, 0, 0);
  Result := DefWindowProc(Wnd, Msg, wParam, lParam);
end;

procedure ExecPreview;
var
  PreviewCanvas : TCanvas;
  WndClass : TWndClass;
  DC       : hDC;
  MyWnd    : hWnd;
  Msg      : TMsg;
  MyThread : TDrawThread;
  PrevRect : TRect;
begin
  {créé une nouvelle classe de fenêtre}
  with WndClass do
    begin
      style := CS_PARENTDC;
      lpfnWndProc := @MyWndProc;
      cbClsExtra := 0;
      cbWndExtra := 0;
      hIcon := 0;
      hCursor := 0;
      hbrBackground := 0;
      lpszMenuName := nil;
      lpszClassName := 'DarkSkullScreenSaverPreview';
    end;
  WndClass.hInstance := hInstance;
  Windows.RegisterClass(WndClass);
  {Obtenir quelques infos}
  GetWindowRect(ParamHandle, PrevRect);
  PrevRect.Right := PrevRect.Right - PrevRect.Left;
  PrevRect.Bottom := PrevRect.Bottom - PrevRect.Top;
  PrevRect.Left := 0;
  PrevRect.Top := 0;
  {Et maintenant créér une fenêtre enfant de la preview}
  MyWnd := CreateWindow('DarkSkullScreenSaverPreview', 'Dark Skull Saver',
                        WS_CHILD or WS_DISABLED or WS_VISIBLE, 0, 0,
                        PrevRect.Right, PrevRect.Bottom,
                        ParamHandle, 0, hInstance, nil);
  {obtenir un DC sur la fiche créée}
  DC := GetDC(MyWnd);
  PreviewCanvas := TCanvas.Create;
  PreviewCanvas.Handle := DC;
  MyThread := TDrawThread.Create(PreviewCanvas, PrevRect);
  {Et maintenant la boucle principale}
  while GetMessage(Msg, 0, 0, 0) do begin
    TranslateMessage(Msg);
    DispatchMessage(Msg);
  end;
  MyThread.Terminate;
  PreviewCanvas.Free;
end;



Et maintenant, le plus long à écrire: l'execution normale de l'économiseur d'écran. Il faut tout d'abord faire en sorte que la fiche principale soit toujours au premier plan. Pour cela, il suffit de mettre sa propriété FormStyle sur fsStayOnTop. Ensuite, il faut que la fiche prenne tout l'écran. Pour cela, on met sa propriété Border sur bsNone, et mettre le code suivant dans le OnCreate :

Taille de la fiche

Left := 0;
top := 0;
Width := Screen.Width;
Height := Screen.Height;



Ensuite, il faut empêcher les CTRL-ALT-SUPPR et ALT-TAB, et masquer le curseur. Pour cela, il faut ajouter les lignes suivantes :

OnCreate

 
SystemParametersInfo(97, Word(True), @Dummy, 0); {plus de alt-tab ou ctrl-alt-suppr}
ShowCursor(false);



OnDestroy

SystemParametersInfo(97, Word(False), @Dummy, 0);
ShowCursor(true);



Ensuite, il faut empécher Windows de redessiner la fiche, en gérant l'évennement WM_ERASEBKGND :

WM_ERASEBKGND

procedure TFormMain.WMEraseBkGnd;
begin
  Msg.Result := 0;
end;



Comme vous le savez, un écran de veille se ferme automatiquement dès qu'on appuie sur une touche ou qu'on bouge la souris. Mais si un mot de passe a été configuré, il faut le gérer avant de sortir de l'écran de veille :

Vérification du mot de passe

 
procedure TFormMain.CloseIfOk;
var
  hLib : THandle;
  P : function (Parent : THandle) : Boolean; stdcall;
  SysDir : String;
  NewLength : Integer;
  Registry : TRegistry;
begin
  {On vérifie si on doit regarder le mot de passe}
  Registry := TRegistry.Create;
  Registry.RootKey := HKEY_CURRENT_USER;
  if Registry.OpenKey('Control Panel\desktop', false) then
    begin
      if Registry.ReadInteger('ScreenSaveUsePassword') <> 0 then
        begin
          ShowCursor (True);
          SetLength(SysDir, MAX_PATH);
          NewLength := GetSystemDirectory(PChar(SysDir), MAX_PATH);
          SetLength (SysDir, NewLength);
          if (length (SysDir) > 0) and (SysDir [length (SysDir)] <> '\')
          then SysDir := SysDir + '\';
          hLib := LoadLibrary (PChar (SysDir + 'PASSWORD.CPL'));
          if hLib = 0 then Close {si on ne trouve pas la DLL, on quitte}
          else
            begin
              P := GetProcAddress(hLib, 'VerifyScreenSavePwd');
              if P (Handle) then Close;
              FreeLibrary (hLib);
            end;
          IgnoredEvents := 0;
          ShowCursor(False);
        end
      else Close;
    end
  else Close;
end;




A présent que nous avons défini notre procédure de fermeture, nous pouvons déjà réagir aux évennement Clavier. Pour cela, nous allons réagir à l'évennement OnKeyDown :

OnKeyDown

procedure TFormMain.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  CloseIfOk;
end;



Il nous faut maintenant réagir aux évennements déclenchés par la souris. Malheureusement, le moindre déplacement d'un pixel de la souris déclenche un évennement. Il nous faut donc définir une marge de réaction aux évennement souris, en déclarant 2 variables: une qui contient le nombre d'évennements à ignorer, et une autre qui compte le nombre d'évennements déjà ignorés. Quand le compte dépasse le nombre à ignorer, alors on ferme l'écran de veille. On remet le compteur à zero toutes les secondes grâce à un Timer :

La Souris

var
  TickstoIgnore: integer;
  IgnoredEvents: integer;



Dans le Create

  TickstoIgnore := 5;
  IgnoredEvents := 0;



Dans le Timer

  IgnoredEvents := 0;




Dans les évennements Souris

  IgnoredEvents := IgnoredEvents + 1;
  if IgnoredEvents > TickstoIgnore then CloseIfOk;



Ensuite, il faut empêcher l'écran de veille de s'activer une 2° fois. Pour cela, nous gérons l'évennement WM_SYSCOMMAND :

WM_SYSCOMMAND

procedure TFormMain.WMSysCommand;
begin
  {N'activer l'écran de veille qu'une seule fois}
  if Msg.cmdType = SC_SCREENSAVE then Msg.Result := 1 
  else inherited;
end;




Enfin, à présent que tous les évennements ont été définis, il ne nous reste plus qu'à créér le Thread dans le FormShow, s'il n'a pas déjà été créé :

Création du Thread dans le OnShow

procedure TFormMain.FormShow(Sender: TObject);
begin
  if not Assigned(MyThread) then
    MyThread := TDrawThread.Create(Canvas, Rect(0, 0, Width, Height));
end;




Puis, pour finir, il n'y a plus qu'à lancer la bonne procédure en fonction du mode d'execution dans la source de projet :

Source du projet

begin
  case ScreenSaverMode of
    smConfig, smConfigStd: begin
                             Application.Initialize;
                             Application.CreateForm(TFormConfig, FormConfig);
                             Application.Run;
                           end;
    smExecute:             begin
                             Application.Initialize;
                             Application.CreateForm(TFormMain, FormMain);
                             Application.Run;
                           end;
    smPassWord:            ExecSetPwd;
    smPreview:             ExecPreview;
  end;
end.



Et voilà. Je vous conseille de télécharger les <a href="http://www.darkskull.net/fichiers/tips/scrsaver.zip">sources du projet</a> (11.1 ko) pour que vous compreniez bien tous les mécanismes.


3 requête(s) SQL executée(s) en 0.012 Secs - Temps total de génération de la page : 0.016 Secs