4

I've been beating my head for over a day now, going through tons of resources trying to figure out how to receive the WM_POWERBROADCAST Windows message from within a thread.

Currently, I am using AllocateHWnd(WndMethod) inside of a stand-alone component. When I create an instance of said component in a standard VCL Forms Application, everything works fine, and I receive the WM_POWERBROADCAST message every time, as needed.

However, when I create an instance of the very same component from within a TThread, I'm no longer receiving this particular Windows message. I'm receiving all kinds of other messages, just not this particular one.

In my searching for a solution, I've found many resources related to how a Windows Service requires some extra work in order to receive this message. But I'm not using a service, at least not yet. I've also found a couple people mention that a thread needs to have a message loop in order to receive this message, and I've implemented a thread from another answer here, but again, I never receive this particular message.

Below is the complete component how I'm receiving this message, which again works perfectly if this is in a VCL application's main thread. I'm guessing the main thread needs to receive this message and forward it into the thread.

How do I make this receive the WM_POWERBROADCAST message when inside of a TThread?

unit JD.Power.Monitor;

(*
  JD Power Monitor
  by Jerry Dodge

  Purpose: To monitor the current state of power on the computer, and trigger
  events when different power related changes occur.

  Component: TPowerMonitor
  - Create an instance of TPowerMonitor component
  - Choose desired power settings to get notified of using Settings property
  - Implement event handlers for those events you wish to monitor
  - Component automatically takes care of the rest of the work
*)

interface

uses
  System.Classes, System.SysUtils, System.Generics.Collections,
  Winapi.ActiveX, Winapi.Windows, Winapi.Messages;

type
  TPowerSetting = (psACDCPowerSource, psBatteryPercentage,
    psConsoleDisplayState, psGlobalUserPresence, psIdleBackgroundTask,
    psMonitorPower, psPowerSaving, psPowerSchemePersonality,
    psSessionDisplayStatus, psSessionUserPresence, psSystemAwayMode);
  TPowerSettings = set of TPowerSetting;

  TPowerSource = (poAC, poDC, poHot);

  TPowerDisplayState = (pdOff, pdOn, pdDimmed);

  TPowerUserPresence = (puPresent = 0, puInactive = 2);

  TPowerSavingStatus = (psSaverOff, psSaverOn);

  TPowerAwayMode = (paExiting, paEntering);

  TPowerPersonality = (ppHighPerformance, ppPowerSaver, ppAutomatic);

  TPowerMonitorSettingHandles = array[TPowerSetting] of HPOWERNOTIFY;

  TPowerQueryEndSessionEvent = procedure(Sender: TObject; var EndSession: Boolean) of object;

  TPowerEndSessionEvent = procedure(Sender: TObject) of object;

  TPowerSettingSourceChangeEvent = procedure(Sender: TObject;
    const Src: TPowerSource) of object;

  TPowerSettingBatteryPercentEvent = procedure(Sender: TObject;
    const Perc: Single) of object;

  TPowerSettingDisplayStateEvent = procedure(Sender: TObject;
    const State: TPowerDisplayState) of object;

  TPowerSettingUserPresenceEvent = procedure(Sender: TObject;
    const Presence: TPowerUserPresence) of object;

  TPowerSettingSavingEvent = procedure(Sender: TObject;
    const Status: TPowerSavingStatus) of object;

  TPowerAwayModeEvent = procedure(Sender: TObject;
    const Mode: TPowerAwayMode) of object;

  TPowerPersonalityEvent = procedure(Sender: TObject;
    const Personality: TPowerPersonality) of object;

  TPowerMonitor = class(TComponent)
  private
    FHandle: HWND;
    FSettingHandles: TPowerMonitorSettingHandles;
    FSettings: TPowerSettings;
    FBatteryPresent: Boolean;
    FOnQueryEndSession: TPowerQueryEndSessionEvent;
    FOnEndSession: TPowerEndSessionEvent;
    FOnPowerStatusChange: TNotifyEvent;
    FOnResumeAutomatic: TNotifyEvent;
    FOnResumeSuspend: TNotifyEvent;
    FOnSuspend: TNotifyEvent;
    FOnSourceChange: TPowerSettingSourceChangeEvent;
    FOnBatteryPercent: TPowerSettingBatteryPercentEvent;
    FOnConsoleDisplayState: TPowerSettingDisplayStateEvent;
    FOnGlobalUserPresence: TPowerSettingUserPresenceEvent;
    FOnIdleBackgroundTask: TNotifyEvent;
    FOnMonitorPower: TPowerSettingDisplayStateEvent;
    FOnPowerSavingStatus: TPowerSettingSavingEvent;
    FOnSessionDisplayState: TPowerSettingDisplayStateEvent;
    FOnSessionUserPresence: TPowerSettingUserPresenceEvent;
    FOnAwayMode: TPowerAwayModeEvent;
    FOnPersonality: TPowerPersonalityEvent;
    procedure UnregisterSettings;
    procedure RegisterSettings;
    procedure SetSettings(const Value: TPowerSettings);
  protected
    procedure HandlePowerSetting(const Val: PPowerBroadcastSetting);
    procedure WndMethod(var Msg: TMessage);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Settings: TPowerSettings read FSettings write SetSettings;
    property OnQueryEndSession: TPowerQueryEndSessionEvent
      read FOnQueryEndSession write FOnQueryEndSession;
    property OnEndSession: TPowerEndSessionEvent
      read FOnEndSession write FOnEndSession;
    property OnPowerStatusChange: TNotifyEvent
      read FOnPowerStatusChange write FOnPowerStatusChange;
    property OnResumeAutomatic: TNotifyEvent
      read FOnResumeAutomatic write FOnResumeAutomatic;
    property OnResumeSuspend: TNotifyEvent
      read FOnResumeSuspend write FOnResumeSuspend;
    property OnSuspend: TNotifyEvent
      read FOnSuspend write FOnSuspend;
    property OnSourceChange: TPowerSettingSourceChangeEvent
      read FOnSourceChange write FOnSourceChange;
    property OnBatteryPercent: TPowerSettingBatteryPercentEvent
      read FOnBatteryPercent write FOnBatteryPercent;
    property OnConsoleDisplayState: TPowerSettingDisplayStateEvent
      read FOnConsoleDisplayState write FOnConsoleDisplayState;
    property OnGlobalUserPresence: TPowerSettingUserPresenceEvent
      read FOnGlobalUserPresence write FOnGlobalUserPresence;
    property OnIdleBackgroundTask: TNotifyEvent
      read FOnIdleBackgroundTask write FOnIdleBackgroundTask;
    property OnMonitorPower: TPowerSettingDisplayStateEvent
      read FOnMonitorPower write FOnMonitorPower;
    property OnPowerSavingStatus: TPowerSettingSavingEvent
      read FOnPowerSavingStatus write FOnPowerSavingStatus;
    property OnSessionDisplayState: TPowerSettingDisplayStateEvent
      read FOnSessionDisplayState write FOnSessionDisplayState;
    property OnSessionUserPresence: TPowerSettingUserPresenceEvent
      read FOnSessionUserPresence write FOnSessionUserPresence;
    property OnAwayMode: TPowerAwayModeEvent
      read FOnAwayMode write FOnAwayMode;
    property OnPersonality: TPowerPersonalityEvent
      read FOnPersonality write FOnPersonality;
  end;

implementation

{ TPowerMonitor }

constructor TPowerMonitor.Create(AOwner: TComponent);
begin
  inherited;
  FBatteryPresent:= False;
  FHandle := AllocateHWnd(WndMethod);
end;

destructor TPowerMonitor.Destroy;
begin
  UnregisterSettings;
  DeallocateHWnd(FHandle);
  inherited;
end;

procedure TPowerMonitor.SetSettings(const Value: TPowerSettings);
begin
  UnregisterSettings;
  FSettings := Value;
  RegisterSettings;
end;

procedure TPowerMonitor.WndMethod(var Msg: TMessage);
var
  Handled: Boolean;
begin
  Handled := True;
  case Msg.Msg of
    WM_POWERBROADCAST: begin
      //TODO: Why is this never received when inside of a thread?
      case Msg.WParam of
        PBT_APMPOWERSTATUSCHANGE: begin
          //Power status has changed.
          if Assigned(FOnPowerStatusChange) then
            FOnPowerStatusChange(Self);
        end;
        PBT_APMRESUMEAUTOMATIC: begin
          //Operation is resuming automatically from a low-power state.
          //This message is sent every time the system resumes.
          if Assigned(FOnResumeAutomatic) then
            FOnResumeAutomatic(Self);
        end;
        PBT_APMRESUMESUSPEND: begin
          //Operation is resuming from a low-power state. This message
          //is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered
          //by user input, such as pressing a key.
          if Assigned(FOnResumeSuspend) then
            FOnResumeSuspend(Self);
        end;
        PBT_APMSUSPEND: begin
          //System is suspending operation.
          if Assigned(FOnSuspend) then
            FOnSuspend(Self);
        end;
        PBT_POWERSETTINGCHANGE: begin
          //A power setting change event has been received.
          HandlePowerSetting(PPowerBroadcastSetting(Msg.LParam));
        end;
        else begin

        end;
      end;
    end
    else Handled := False;
  end;
  if Handled then
    Msg.Result := 0
  else
    Msg.Result := DefWindowProc(FHandle, Msg.Msg,
      Msg.WParam, Msg.LParam);
end;

procedure TPowerMonitor.HandlePowerSetting(const Val: PPowerBroadcastSetting);
var
  Pers: TPowerPersonality;
  function ValAsDWORD: DWORD;
  begin
    Result:= DWORD(Val.Data[0]);
  end;
  function ValAsGUID: TGUID;
  begin
    Result:= StringToGUID('{00000000-0000-0000-0000-000000000000}'); //Default
    if SizeOf(TGUID) = Val.DataLength then begin
      Move(Val.Data, Result, Val.DataLength);
    end;
  end;
  function IsVal(G: String): Boolean;
  begin
    Result:= Assigned(Val);
    if Result then
      Result:= IsEqualGUID(StringToGUID(G), Val.PowerSetting);
  end;
  function IsValGuid(G: String): Boolean;
  begin
    Result:= Assigned(Val);
    if Result then
      Result:= IsEqualGUID(StringToGUID(G), ValAsGUID);
  end;
begin
  if IsVal('{5d3e9a59-e9D5-4b00-a6bd-ff34ff516548}') then begin
    //GUID_ACDC_POWER_SOURCE
    if Assigned(FOnSourceChange) then
      FOnSourceChange(Self, TPowerSource(ValAsDWORD));
  end else
  if IsVal('{a7ad8041-b45a-4cae-87a3-eecbb468a9e1}') then begin
    //GUID_BATTERY_PERCENTAGE_REMAINING
    //We assume that if we get this message, that there is a battery connected.
    //Otherwise if this never occurs, then a battery is not present.
    //TODO: How to handle if battery is detached and no longer present?
    FBatteryPresent:= True;
    if Assigned(FOnBatteryPercent) then
      FOnBatteryPercent(Self, ValAsDWORD);
  end else
  if IsVal('{6fe69556-704a-47a0-8f24-c28d936fda47}') then begin
    //GUID_CONSOLE_DISPLAY_STATE
    if Assigned(FOnConsoleDisplayState) then
      FOnConsoleDisplayState(Self, TPowerDisplayState(ValAsDWORD));
  end else
  if IsVal('{786E8A1D-B427-4344-9207-09E70BDCBEA9}') then begin
    //GUID_GLOBAL_USER_PRESENCE
    if Assigned(FOnGlobalUserPresence) then
      FOnGlobalUserPresence(Self, TPowerUserPresence(ValAsDWORD));
  end else
  if IsVal('{515c31d8-f734-163d-a0fd-11a08c91e8f1}') then begin
    //GUID_IDLE_BACKGROUND_TASK
    if Assigned(FOnIdleBackgroundTask) then
      FOnIdleBackgroundTask(Self);
  end else
  if IsVal('{02731015-4510-4526-99e6-e5a17ebd1aea}') then begin
    //GUID_MONITOR_POWER_ON
    if Assigned(FOnMonitorPower) then
      FOnMonitorPower(Self, TPowerDisplayState(ValAsDWORD));
  end else
  if IsVal('{E00958C0-C213-4ACE-AC77-FECCED2EEEA5}') then begin
    //GUID_POWER_SAVING_STATUS
    if Assigned(FOnPowerSavingStatus) then
      FOnPowerSavingStatus(Self, TPowerSavingStatus(ValAsDWORD));
  end else
  if IsVal('{245d8541-3943-4422-b025-13A784F679B7}') then begin
    //GUID_POWERSCHEME_PERSONALITY
    if IsValGuid('{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c}') then begin
      Pers:= TPowerPersonality.ppHighPerformance;
    end else
    if IsValGuid('{a1841308-3541-4fab-bc81-f71556f20b4a}') then begin
      Pers:= TPowerPersonality.ppPowerSaver;
    end else
    if IsValGuid('{381b4222-f694-41f0-9685-ff5bb260df2e}') then begin
      Pers:= TPowerPersonality.ppAutomatic;
    end else begin
      //TODO: Handle unrecognized GUID
      Pers:= TPowerPersonality.ppAutomatic;
    end;
    if Assigned(FOnPersonality) then
      FOnPersonality(Self, Pers);
  end else
  if IsVal('{2B84C20E-AD23-4ddf-93DB-05FFBD7EFCA5}') then begin
    //GUID_SESSION_DISPLAY_STATUS
    if Assigned(FOnSessionDisplayState) then
      FOnSessionDisplayState(Self, TPowerDisplayState(ValAsDWORD));
  end else
  if IsVal('{3C0F4548-C03F-4c4d-B9F2-237EDE686376}') then begin
    //GUID_SESSION_USER_PRESENCE
    if Assigned(FOnSessionUserPresence) then
      FOnSessionUserPresence(Self, TPowerUserPresence(ValAsDWORD));
  end else
  if IsVal('{98a7f580-01f7-48aa-9c0f-44352c29e5C0}') then begin
    //GUID_SYSTEM_AWAYMODE
    if Assigned(FOnAwayMode) then
      FOnAwayMode(Self, TPowerAwayMode(ValAsDWORD));
  end else begin
    //TODO: Handle Unrecognized GUID
  end;
end;

function PowerSettingGUID(const Setting: TPowerSetting): TGUID;
begin
  case Setting of
    psACDCPowerSource: Result:= StringToGUID('{5d3e9a59-e9D5-4b00-a6bd-ff34ff516548}');
    psBatteryPercentage: Result:= StringToGUID('{a7ad8041-b45a-4cae-87a3-eecbb468a9e1}');
    psConsoleDisplayState: Result:= StringToGUID('{6fe69556-704a-47a0-8f24-c28d936fda47}');
    psGlobalUserPresence: Result:= StringToGUID('{786E8A1D-B427-4344-9207-09E70BDCBEA9}');
    psIdleBackgroundTask: Result:= StringToGUID('{515c31d8-f734-163d-a0fd-11a08c91e8f1}');
    psMonitorPower: Result:= StringToGUID('{02731015-4510-4526-99e6-e5a17ebd1aea}');
    psPowerSaving: Result:= StringToGUID('{E00958C0-C213-4ACE-AC77-FECCED2EEEA5}');
    psPowerSchemePersonality: Result:= StringToGUID('{245d8541-3943-4422-b025-13A784F679B7}');
    psSessionDisplayStatus: Result:= StringToGUID('{2B84C20E-AD23-4ddf-93DB-05FFBD7EFCA5}');
    psSessionUserPresence: Result:= StringToGUID('{3C0F4548-C03F-4c4d-B9F2-237EDE686376}');
    psSystemAwayMode: Result:= StringToGUID('{98a7f580-01f7-48aa-9c0f-44352c29e5C0}');
  end;
end;

procedure TPowerMonitor.RegisterSettings;
var
  V: TPowerSetting;
begin
  for V := Low(TPowerSetting) to High(TPowerSetting) do begin
    if V in FSettings then begin
      FSettingHandles[V]:= RegisterPowerSettingNotification(FHandle,
        PowerSettingGUID(V), 0);
    end;
  end;
end;

procedure TPowerMonitor.UnregisterSettings;
var
  V: TPowerSetting;
begin
  for V := Low(TPowerSetting) to High(TPowerSetting) do begin
    if V in FSettings then begin
      UnregisterPowerSettingNotification(FSettingHandles[V]);
    end;
  end;
end;

end.

Further, based on the other answer as mentioned, here's how I'm attempting to capture this message using only a thread, although I'm sure this is not actually what I need since WM_POWERBROADCAST is not a posted message:

unit JD.ThreadTest;

interface

uses
  System.Classes, Winapi.Messages, Winapi.Windows;

type
  TDataThread = class(TThread)
  private
    FTitle: String;
    FWnd: HWND;
    FWndClass: WNDCLASS;
    procedure HandlePower(AMsg: TMsg);
  protected
    procedure Execute; override;
    procedure DoTerminate; override;
  public
    constructor Create(const Title: String); reintroduce;
  end;

implementation

constructor TDataThread.Create(const Title: String);
begin
  inherited Create(True);
  FTitle := Title;
  with FWndClass do begin
    Style := 0;
    lpfnWndProc := @DefWindowProc;
    cbClsExtra := 0;
    cbWndExtra := 0;
    hInstance := HInstance;
    hIcon := 0;
    hCursor := LoadCursor(0, IDC_ARROW);
    hbrBackground := COLOR_WINDOW;
    lpszMenuName := nil;
    lpszClassName := PChar(Self.ClassName);
  end;
end;

procedure TDataThread.Execute;
var
  Msg: TMsg;
begin
  if Winapi.Windows.RegisterClass(FWndClass) = 0 then Exit;
  FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_DLGFRAME,
    0, 0, 698, 517, 0, 0, HInstance, nil);
  if FWnd = 0 then Exit;
  while GetMessage(Msg, FWnd, 0, 0) = True do begin
    if Terminated then Break;
    case Msg.message of
      WM_POWERBROADCAST: begin
        HandlePower(Msg); //Never receives this message
      end;
      else begin
        TranslateMessage(msg);
        DispatchMessage(msg)
      end;
    end;
  end;
end;

procedure TDataThread.HandlePower(AMsg: TMsg);
begin

end;

procedure TDataThread.DoTerminate;
begin
  if FWnd <> 0 then DestroyWindow(FWnd);
  Winapi.Windows.UnregisterClass(PChar(Self.ClassName), FWndClass.hInstance);
  inherited;
end;

end.

PS: The end goal is to make this component re-usable, and to be using it inside of a thread, which will be spawned inside of a service.


EDIT

Just to show some perspective, here's a screenshot of my results, when I put my computer into sleep mode. The form on the left is my 100% working UI application, without any worker thread. It receives many messages through the WM_POWERBROADCAST message. The one on the right is where I attempt to capture this message inside of a thread - updated with the code below in Remy's answer.

enter image description here

Obviously the "Power Setting" specific messages are not received, because I haven't called RegisterPowerSettingNotification - but there are other cases when I should still receive this message regardless, such as PBT_APMSUSPEND.

unit JD.ThreadTest;

interface

uses
  System.Classes, System.SysUtils, Winapi.Messages, Winapi.Windows;

type

  TMessageEvent = procedure(Sender: TObject; Message: TMessage) of object;

  TDataThread = class(TThread)
  private
    FTitle: String;
    FWnd: HWND;
    FWndClass: WNDCLASS;
    FOnMessage: TMessageEvent;
    FMsg: TMessage;
    procedure HandleMessage(var Message: TMessage);
  protected
    procedure Execute; override;
    procedure DoTerminate; override;
    procedure DoOnMessage;
  public
    constructor Create(const Title: String); reintroduce;
    property OnMessage: TMessageEvent read FOnMessage write FOnMessage;
  end;

implementation

function DataThreadWndProc(Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  Thread: TDataThread;
  Message: TMessage;
begin
  if Msg = WM_NCCREATE then
  begin
    Thread := TDataThread(PCREATESTRUCT(lParam)^.lpCreateParams);
    SetWindowLongPtr(Wnd, GWLP_USERDATA, LONG_PTR(Thread));
  end else
    Thread := TDataThread(GetWindowLongPtr(Wnd, GWLP_USERDATA));

  if Thread <> nil then
  begin
    Message.Msg := Msg;
    Message.WParam := wParam;
    Message.LParam := lParam;
    Message.Result := 0;
    Thread.HandleMessage(Message);
    Result := Message.Result;
  end else
    Result := DefWindowProc(Wnd, Msg, wParam, lParam);
end;

constructor TDataThread.Create(const Title: String);
begin
  inherited Create(True);
  FTitle := Title;
  with FWndClass do
  begin
    Style := 0;
    lpfnWndProc := @DataThreadWndProc;
    cbClsExtra := 0;
    cbWndExtra := 0;
    hInstance := HInstance;
    hIcon := 0;
    hCursor := LoadCursor(0, IDC_ARROW);
    hbrBackground := COLOR_WINDOW;
    lpszMenuName := nil;
    lpszClassName := 'TDataThread';
  end;
end;

procedure TDataThread.Execute;
var
  Msg: TMsg;
begin
  if Winapi.Windows.RegisterClass(FWndClass) = 0 then Exit;
  FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_DLGFRAME, 0, 0, 698, 517, 0, 0, HInstance, Self);
  if FWnd = 0 then Exit;
  while GetMessage(Msg, 0, 0, 0) do
  begin
    if Terminated then Exit;
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
end;

procedure TDataThread.DoOnMessage;
begin
  if Assigned(FOnMessage) then
    FOnMessage(Self, FMsg);
end;

procedure TDataThread.DoTerminate;
begin
  if FWnd <> 0 then DestroyWindow(FWnd);
  Winapi.Windows.UnregisterClass(FWndClass.lpszClassName, HInstance);
  inherited;
end;

procedure TDataThread.HandleMessage(var Message: TMessage);
begin
  FMsg:= Message;
  Synchronize(DoOnMessage);
  case Message.Msg of
    WM_POWERBROADCAST:
    begin

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

end.
Community
  • 1
  • 1
Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • Just an idea: are you using the right handle when calling RegisterPowerSettingNotification? ie. the **window handle** returned by AllocateHwnd, not the thread handle? – Ondrej Kelle Mar 11 '17 at 19:39
  • @OndrejKelle I'm using the same handle which was allocated just for this component. The component has no knowledge whether it's inside of a worker thread. – Jerry Dodge Mar 11 '17 at 19:44
  • We can't see the relevant code because you didn't make a MCVE. Instead you showed acres of irrelevant code. You should know better by now. Where's the code where you dispatch messages? The code in your thread which ensures messages are dispatched. Read this: [mcve] – David Heffernan Mar 11 '17 at 19:45
  • Neglecting to check for errors isn't a good idea either. – David Heffernan Mar 11 '17 at 19:46
  • @David Not sure what more you want - the second code block *is* an MCVE. I just missed pasting it the first time around. – Jerry Dodge Mar 11 '17 at 19:55
  • The code that doesn't use the component, doesn't register to receive power messages, makes a complete mess of window registration? That code. No. The question is a mess. You need to delete it and get your facts together. You didn't write that component to then create a thread which ignores it. Did you? – David Heffernan Mar 11 '17 at 19:57
  • @David You're not understanding me then. The thread is not supposed to be using the component. It's a **minimal** test just to attempt to capture the message inside of a thread. The component is the *working* code. The thread is the *not working* code. In either case, as I explain, when I create the component inside of *any* thread (surely you know how to call `TPowerMonitor.Create;`) that message is not received. And that Windows message is used for much more than only the Power Settings, for example `PBT_APMSUSPEND`. – Jerry Dodge Mar 11 '17 at 19:59
  • So why are you showing the component then. This is a quite terrible mess. The thread doesn't register to receive power broadcasts. You know better than this. Delete the question. Get your facts straight. Ask with a clean question. – David Heffernan Mar 11 '17 at 20:00

1 Answers1

3

WM_POWERBROADCAST is not a posted message, so your message loop will never see it. You need a window procedure to receive that message. Your thread code is using DefWindowProc() directly as the window procedure. Change the call to RegisterClass() to register a custom procedure instead, that then calls DefWindowProc() for unhandled messages. GetMessage() will dispatch any sent message directly to the window procedure, and DispatchMessage() will dispatch any posted messages to the same window procedure.

unit JD.ThreadTest;

interface

uses
  System.Classes, Winapi.Messages, Winapi.Windows;

type
  TDataThread = class(TThread)
  private
    FTitle: String;
    FWnd: HWND;
    FWndClass: WNDCLASS;
    procedure HandleMessage(var Message: TMessage);
  protected
    procedure Execute; override;
    procedure DoTerminate; override;
  public
   constructor Create(const Title: String); reintroduce;
  end;

implementation

function DataThreadWndProc(Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  Thread: TDataThread;
  Message: TMessage;
begin
  if Msg = WM_NCCREATE then
  begin
    Thread := TDataThread(PCREATESTRUCT(lParam)^.lpCreateParams);
    SetWindowLongPtr(Wnd, GWLP_USERDATA, LONG_PTR(Thread));
  end else
    Thread := TDataThread(GetWindowLongPtr(Wnd, GWLP_USERDATA));

  if Thread <> nil then
  begin
    Message.Msg := Msg;
    Message.WParam := wParam;
    Message.LParam := lParam;
    Message.Result := 0;
    Thread.HandleMessage(Message);
    Result := Message.Result;
  end else
    Result := DefWindowProc(Wnd, Msg, wParam, lParam);
end;

constructor TDataThread.Create(const Title: String);
begin
  inherited Create(True);
  FTitle := Title;
  with FWndClass do
  begin
    Style := 0;
    lpfnWndProc := @DataThreadWndProc;
    cbClsExtra := 0;
    cbWndExtra := 0;
    hInstance := HInstance;
    hIcon := 0;
    hCursor := LoadCursor(0, IDC_ARROW);
    hbrBackground := COLOR_WINDOW;
    lpszMenuName := nil;
    lpszClassName := 'TDataThread';
  end;
end;

procedure TDataThread.Execute;
var
  Msg: TMsg;
begin
  if Winapi.Windows.RegisterClass(FWndClass) = 0 then Exit;
  FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, HInstance, Self);
  if FWnd = 0 then Exit;
  while GetMessage(Msg, 0, 0, 0) do
  begin
    if Terminated then Exit;
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
end;

procedure TDataThread.DoTerminate;
begin
  if FWnd <> 0 then DestroyWindow(FWnd);
  Winapi.Windows.UnregisterClass(FWndClass.lpszClassName, HInstance);
  inherited;
end;

procedure TDataThread.HandleMessage(var Message: TMessage);
begin
  case Message.Msg of
    WM_POWERBROADCAST:
    begin
      // ...
    end;
  else
    Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam);
  end;
end;

end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Worth also mentioning the lifetime issue with PChar(Self.ClassName) – David Heffernan Mar 11 '17 at 21:02
  • Thank you - this is resulting in `Access violation at 0x005c6115: read of address 0x00000044` on the line `Message.Result := DefWindowProc...` Upon closer inspection, `Message.Msg` is `0` - and this is the first message it receives upon startup. – Jerry Dodge Mar 11 '17 at 21:41
  • It would be far easier to use AllocateHWnd in the thread rather than trying to replicate it. The problem with Remy's code here is that the window proc is called from CreateWindow. For WM_NCCREATE IIRC. So the user data hasn't been set yet. Just use AllocateHWnd and run the bog standard message loop. You know the one. GetMessage, TranslateMessage, DispatchMesage. That's it. Nothing more. – David Heffernan Mar 11 '17 at 21:56
  • @JerryDodge sorry, my bad. I forgot the `stdcall` calling convention. I have fixed it. This is why I hate that [type-checked pointets](http://docwiki.embarcadero.com/RADStudio/en/Type-checked_pointers_(Delphi)) is OFF by default. – Remy Lebeau Mar 11 '17 at 23:07
  • @DavidHeffernan `AllocateHWnd()` is not thread-safe, it uses resources that are not protected from concurrent access across thread boundaries. It should never be used outside of the main UI thread. Along those lines, if you did use `AllocateHWnd()`, you could use it in the main UI thread to catch `WM_POWERBROADCAST` and forward it to any worker thread that needs it. – Remy Lebeau Mar 11 '17 at 23:18
  • Thanks for that - besides having to remove `static;` from the implementation part for it to compile, the message loop appears to be working fine, but still does not receive `WM_POWERBROADCAST` messages. For the sake of testing, I also used `FSetting:= RegisterPowerSettingNotification(FWnd, StringToGUID('{5d3e9a59-e9D5-4b00-a6bd-ff34ff516548}'), 0);` (AC/DC Power Source) just after creating the handle, and before the message loop, but still nothing. Elsewhere, when it does work, that particular setting is triggered from the start, returning AC Power. – Jerry Dodge Mar 11 '17 at 23:55
  • Would it be easier if I just caught this message in the main thread, and then redirected it into this worker thread? According to the docs, this message is sent to all *top level* windows. – Jerry Dodge Mar 11 '17 at 23:57
  • @JerryDodge this code is creating a top-level window. Threading does not affect that. The `static` is required to remove the `Self` parameter from `WndProc` so it is compatible for use as a window procedure. But I have now removed `WndProc` from the class so `static` is not a factor anymore – Remy Lebeau Mar 12 '17 at 00:34
  • Message pump is working great, but still not receiving `WM_POWERBROADCAST` :-/ Running my working UI app aside this test threaded app, when I put my computer into sleep mode, the UI app receives numerous different messages, but the test thread still receives nothing. It only receives two messages, of ID `129` and `130` and nothing further after that. – Jerry Dodge Mar 12 '17 at 00:44
  • See my screenshot I added to my Q. – Jerry Dodge Mar 12 '17 at 00:55
  • Would it be worth mentioning that during my search for a solution, I came across an entire forum dedicated to developers who were pulling their hair out with the same problems with monitoring Windows power state? It wasn't related to Delphi, but Windows Messages in general. – Jerry Dodge Mar 12 '17 at 01:01
  • Ah yes, I forgot that detail. In my code I use an AllocateHWnd that is thread safe. – David Heffernan Mar 12 '17 at 04:33
  • @Jerry If you want the threads to receive the message directly, you need them to dispatch messages. Generally worker threads don't want to do that. So yes, your design is probably not going to work out. – David Heffernan Mar 12 '17 at 04:35