I'm experiencing a strange behavior with WM_NCHITTEST
messages.
In summary, what happens is that as soon as I have the mouse over the target (ie: Hooked) control and leave the mouse still (or idle), I receive endlessly hundred's of WM_NCHITTEST
messages per second. This happens whether I subclass the WndProc
of that control with WindowProc()
, or if I override the WndProc
method in a descendant class (I subclass in the code below for simplicity).
As far as I could find from online Win32 API docs and other sources, I doubt that this message fires at this frequency, but I might be wrong. Or maybe there is an obvious explanation that I completely missed, or maybe something changed in the APIs that I am not aware of. In any event, I would really like to know what it is, or what is going on.
I've tested the same code (the example below) on two different systems with the same result, though both systems are in the same Delphi/OS version and configuration. I've tried running the app outside of the IDE (so no debugging hook), in both debug and release configurations (latter with no debug info), target both 32-bit and 64-bit, and I always get the same result.
I am developing with Delphi XE7 Enterprise under Win10 Pro 64-bit, version 20H2 (the latest Windows version I believe).
Here is a very simplistic program to reproduce what I am experiencing: a TForm
with a TPanel
, a TCheckBox
, and a TLabel
. The panel is the control being hooked when the checkbox is checked, and the label is displaying how many WM_NCHITTEST
messages are received by the WndProc()
method:
unit Unit5;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;
type
TForm5 = class(TForm)
CheckBox1: TCheckBox;
Label1: TLabel;
Panel1: TPanel;
procedure FormDestroy(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
private
FHookedCtrl: TControl;
FHookedCtrlWndProc: TWndMethod;
FMessageCount: Integer;
procedure SetHookedCtrl(const Value: TControl);
public
procedure ControlWndProc(var Message: TMessage);
property HookedCtrl: TControl read FHookedCtrl write SetHookedCtrl;
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
{ TForm5 }
procedure TForm5.CheckBox1Click(Sender: TObject);
begin
//checkbox activates or deactivates the hook
if CheckBox1.Checked then
//hook the panel's WndProc by subclassing
HookedCtrl := Panel1
//release the hook on WndProc
else HookedCtrl := nil;
end;
procedure TForm5.ControlWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_NCHITTEST:
begin
//show how many messages received with the label's caption
Inc(FMessageCount);
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
end;
end;
//not really handling the messsage, just counting.
FHookedCtrlWndProc(Message);
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
//make sure to clear the hook if assigned
HookedCtrl := nil;
end;
procedure TForm5.SetHookedCtrl(const Value: TControl);
begin
if (Value <> FHookedCtrl) then
begin
if Assigned(FHookedCtrl) then
begin
//release the hook
FHookedCtrl.WindowProc := FHookedCtrlWndProc;
FHookedCtrlWndProc := nil;
FMessageCount := 0;
end;
FHookedCtrl := Value;
if Assigned(FHookedCtrl) then
begin
//hook the panel (i.e. Value)
FHookedCtrlWndProc := FHookedCtrl.WindowProc;
FHookedCtrl.WindowProc := ControlWndProc;
end;
end;
end;
end.
To reproduce: run the app, check the CheckBox, hover the mouse over the panel and leave it idle (still). In my case, I receive 100's of WM_NCHITTEST
messages per second, and it never stops coming. Should this happen?
Can someone explain what's happening here?
I used Microsoft Spy++ tool to see what happens and when.
It is the following line in the WM_NCHITTEST handler
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
which causes the issue. When you remove it, there is no more all those WM_NCHITTEST
messages. To see the number of messages, use a TTimer
with a 1 second interval and display the message count in the label. You'll see that you get a WM_NCHITTEST
each time the timer fires (You still get a message if you have an empty OnTimer handler) and of course when the mouse is moving.
Here is the code I used:
unit Unit5;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;
type
TForm5 = class(TForm)
Label1: TLabel;
CheckBox1: TCheckBox;
Panel1: TPanel;
Timer1: TTimer;
procedure FormDestroy(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
FHookedCtrl: TControl;
FHookedCtrlWndProc: TWndMethod;
FMessageCount: Integer;
procedure SetHookedCtrl(const Value: TControl);
public
procedure ControlWndProc(var Message: TMessage);
property HookedCtrl: TControl read FHookedCtrl write SetHookedCtrl;
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
{ TForm5 }
procedure TForm5.CheckBox1Click(Sender: TObject);
begin
//checkbox activates or deactivates the hook
if CheckBox1.Checked then
//hook the panel's WndProc by subclassing
HookedCtrl := Panel1
else
//release the hook on WndProc
HookedCtrl := nil;
end;
procedure TForm5.ControlWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_NCHITTEST:
//Count how many messages received
Inc(FMessageCount);
end;
//not really handling the messsage, just counting.
FHookedCtrlWndProc(Message);
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
//make sure to clear the hook if assigned
HookedCtrl := nil;
end;
procedure TForm5.SetHookedCtrl(const Value: TControl);
begin
if (Value <> FHookedCtrl) then begin
if Assigned(FHookedCtrl) then begin
//release the hook
FHookedCtrl.WindowProc := FHookedCtrlWndProc;
FHookedCtrlWndProc := nil;
FMessageCount := 0;
end;
FHookedCtrl := Value;
if Assigned(FHookedCtrl) then begin
//hook the panel (i.e. Value)
FHookedCtrlWndProc := FHookedCtrl.WindowProc;
FHookedCtrl.WindowProc := ControlWndProc;
end;
end;
end;
procedure TForm5.Timer1Timer(Sender: TObject);
begin
// Show how many message received
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
end;
end.