9

How to detect when the user locks/unlocks the screen in Windows 7?

I found this question which has an answer for C#, but I'd like to use it in Delphi 2009. I'd guess there is some windows message (like these) which could do the work. This is the code I tried, but it didn't work:

const
  NOTIFY_FOR_ALL_SESSIONS = 1;
  {$EXTERNALSYM NOTIFY_FOR_ALL_SESSIONS}
  NOTIFY_FOR_THIS_SESSION = 0;
  {$EXTERNALSYM NOTIFY_FOR_THIS_SESSION}

type

TfrmAlisson = class(TForm)
  lbl2: TLabel;
  procedure FormCreate(Sender: TObject);
  procedure FormDestroy(Sender: TObject);
public
  FLockedCount: Integer;
  procedure WndProc(var Message: TMessage); override;
  function WTSRegisterSessionNotification(hWnd: HWND; dwFlags: DWORD): bool; stdcall;
  function WTSUnRegisterSessionNotification(hWND: HWND): bool; stdcall;
end;

implementation

uses
  // my impl uses here

procedure TfrmAlisson.FormCreate(Sender: TObject);
begin
  if (WTSRegisterSessionNotification(Handle, NOTIFY_FOR_THIS_SESSION)) then
    ShowMessage('Nice')
  else
  begin
    lastError := GetLastError;
    ShowMessage(SysErrorMessage(lastError));
  end;
end;

procedure TfrmAlisson.FormDestroy(Sender: TObject);
begin
  WTSUnRegisterSessionNotification(Handle);
end;

procedure TfrmAlisson.WndProc(var Message: TMessage);
begin
  case Message.Msg of
    WM_WTSSESSION_CHANGE:
      begin
        if Message.wParam = WTS_SESSION_LOCK then
        begin
          Inc(FLockedCount);
        end;
        if Message.wParam = WTS_SESSION_UNLOCK then
        begin
          lbl2.Caption := 'Session was locked ' +
          IntToStr(FLockedCount) + ' times.';
        end;
      end;
  end;
  inherited;
end;

function TfrmAlisson.WTSRegisterSessionNotification(hWnd: HWND; dwFlags: DWORD): bool;
  external 'wtsapi32.dll' Name 'WTSRegisterSessionNotification';

function TfrmAlisson.WTSUnRegisterSessionNotification(hWND: HWND): bool;
  external 'wtsapi32.dll' Name 'WTSUnRegisterSessionNotification';

When FormCreate executes, WTSRegisterSessionNotification returns false and the last OS error returns Invalid Parameter.

Alisson Reinaldo Silva
  • 10,009
  • 5
  • 65
  • 83
  • 1
    call [WTSRegisterSessionNotification](https://msdn.microsoft.com/en-us/library/aa383841(VS.85).aspx) and you got [WM_WTSSESSION_CHANGE](https://stackoverflow.com/questions/5198210/windows-message-for-user-locking-screen?rq=1) messages. when `wParam` is `WTS_SESSION_LOCK` or `WTS_SESSION_UNLOCK` your case – RbMm Aug 31 '17 at 19:02
  • @RbMm thanks, I'm gonna read and tell you if I manage to achieve it. – Alisson Reinaldo Silva Aug 31 '17 at 19:04
  • @RbMm: that should be posted as an answer. – Remy Lebeau Aug 31 '17 at 19:12
  • @RemyLebeau - really this duplicate - for example https://stackoverflow.com/a/5198312/6401656 – RbMm Aug 31 '17 at 19:13
  • @RbMm: then you should have voted to close this question as a duplicate – Remy Lebeau Aug 31 '17 at 19:13
  • @RemyLebeau I edited the question. I tried implementing it, but I'm not familiar with winapi, so I don't know if I did something wrong. When I call `WTSRegisterSessionNotification`, it returns **false** and if I get last OS error, it returns **invalid parameter** (in portuguese, in my case). – Alisson Reinaldo Silva Aug 31 '17 at 19:48
  • You must not pass a VCL window handle. They can be recreated. Use AllocateHWnd. Or the application window. – David Heffernan Aug 31 '17 at 20:02
  • @DavidHeffernan: there is nothing wrong with using a VCL window handle, as long as you account for recreation, in this case by overriding the Form's `CreateWnd()` method. – Remy Lebeau Aug 31 '17 at 20:39
  • 1
    Which this code doesn't do, does it. And there's a window during recreation where you aren't listening. So yes, it is wrong to use a VCL window. – David Heffernan Aug 31 '17 at 20:54
  • @DavidHeffernan thanks for your advice, it's important pointing everything that could fail. My code is now allocating a dedicated window, as the code suggested by Remy. – Alisson Reinaldo Silva Sep 01 '17 at 11:29

1 Answers1

12

Your code does not work because you did not implement it correctly.

You are not declaring WTSRegisterSessionNotification() and WTSUnRegisterSessionNotification() correctly.

Also, you are not accounting for the possibility of the VCL ever recreating the Form's window dynamically during the Form object's lifetime. So, even if WTSRegisterSessionNotification() were successful, you can lose your registration and not realize it.

Try this instead:

interface

uses
  ...;

type
  TfrmAlisson = class(TForm)
    lbl2: TLabel;
  protected
    procedure CreateWnd; override;
    procedure DestroyWindowHandle; override;
    procedure WndProc(var Message: TMessage); override;
  public
    LockedCount: Integer;
  end;

implementation

const
  NOTIFY_FOR_THIS_SESSION = $0;
  NOTIFY_FOR_ALL_SESSIONS = $1;

function WTSRegisterSessionNotification(hWnd: HWND; dwFlags: DWORD): Boolean; stdcall; external 'wtsapi32.dll' name 'WTSRegisterSessionNotification';
function WTSUnRegisterSessionNotification(hWnd: HWND): Boolean; stdcall; external 'wtsapi32.dll' name 'WTSUnRegisterSessionNotification';

procedure TfrmAlisson.CreateWnd;
begin
  inherited;
  if not WTSRegisterSessionNotification(Handle, NOTIFY_FOR_THIS_SESSION) then
    RaiseLastOSError;
end;

procedure TfrmAlisson.DestroyWindowHandle;
begin
  WTSUnRegisterSessionNotification(Handle);
  inherited;
end;

procedure TfrmAlisson.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_WTSSESSION_CHANGE then
  begin
    case Message.wParam of
      WTS_SESSION_LOCK: begin
        Inc(LockedCount);
      end;
      WTS_SESSION_UNLOCK: begin
        lbl2.Caption := Format('Session was locked %d times.', [LockedCount]);
      end;
    end;
  end;
  inherited;
end;

end.

That being said, consider writing the code to not rely on the VCL's window recreation behavior. You can allocate a dedicated window for monitoring the session changes:

interface

uses
  ...;

type
  TfrmAlisson = class(TForm)
    lbl2: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    SessionWnd: HWND;
    procedure SessionWndProc(var Message: TMessage);
  public
    LockedCount: Integer;
  end;

implementation

const
  NOTIFY_FOR_THIS_SESSION = $0;
  NOTIFY_FOR_ALL_SESSIONS = $1;

function WTSRegisterSessionNotification(hWnd: HWND; dwFlags: DWORD): Boolean; stdcall; external 'wtsapi32.dll' name 'WTSRegisterSessionNotification';
function WTSUnRegisterSessionNotification(hWnd: HWND): Boolean; stdcall; external 'wtsapi32.dll' name 'WTSUnRegisterSessionNotification';

procedure TfrmAlisson.FormCreate(Sender: TObject);
begin
  SessionWnd := AllocateHWnd(SessionWndProc);
  if not WTSRegisterSessionNotification(SessionWnd, NOTIFY_FOR_THIS_SESSION) then
    RaiseLastOSError;
end;

procedure TfrmAlisson.FormDestroy(Sender: TObject);
begin
  if SessionWnd <> 0 then
  begin
    WTSUnRegisterSessionNotification(SessionWnd);
    DeallocateHWnd(SessionWnd);
  end;
end;

procedure TfrmAlisson.SessionWndProc(var Message: TMessage);
begin
  if Message.Msg = WM_WTSSESSION_CHANGE then
  begin
    case Message.wParam of
      WTS_SESSION_LOCK: begin
        Inc(LockedCount);
      end;
      WTS_SESSION_UNLOCK: begin
        lbl2.Caption := Format('Session was locked %d times.', [LockedCount]);
      end;
    end;
  end;

  Message.Result := DefWindowProc(SessionWnd, Message.Msg, Message.WParam, Message.LParam);
end;

end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I'm receiving `undeclared identifier: WTSRegisterSessionNotification`. I added `Windows` to interface uses. I forgot to mention I'm compiling with Windows 10, even though the application will be ran by users with Windows 7. – Alisson Reinaldo Silva Aug 31 '17 at 20:41
  • I redeclared it, following [this sample](https://github.com/magicmonty/delphi-code-coverage/blob/master/3rdParty/JWAPI/jwapi2.2a/Win32API/JwaWtsApi32.pas), and it worked with your sample. The only thing I noted is `DestroyWnd` is not called when I'm debugging and close the application (maybe this is called after the process is detached from debugger). – Alisson Reinaldo Silva Aug 31 '17 at 20:53
  • 1
    @Alisson: my bad, the WTS functions were added to the `Windows` unit in XE2. As for `DestroyWnd()`, it is only called when the window is destroyed while the Form remains alive. The VCL bypasses `DestroyWnd` during the Form's destruction. I have updated my answer to account for these. – Remy Lebeau Aug 31 '17 at 21:01
  • It worked nicely, thank you so much. I changed my code to allocate a dedicated window like suggested. – Alisson Reinaldo Silva Sep 01 '17 at 11:20
  • 1
    FWIW, in `CreateWnd` and `DestroyWindowHandle`, it is idiomatic to use `WindowHandle` rather than `Handle`. – David Heffernan Sep 01 '17 at 12:01
  • @DavidHeffernan: they are the same thing in those contexts. They both return `FHandle`. The only difference is `Handle` allocates `FHandle` if zero, `WindowHande` doesn't. In those contexts, `FHandle` is never zero. And I rarely ever see anyone use `WindowHandle` in those contexts. Either one works. – Remy Lebeau Sep 01 '17 at 16:37