4
votes

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?

1
A simpler case could be a blank form with an overriden WndProc. Anyway, I guess what you observe is to be expected. There should be various triggers. One I could tell is that, every time the application goes idle it attempts to find a control which the mouse is on - where WindowFromPoint is called - which is known to cause WM_NCHITTEST ( link ). Although that's not 100s per second, my empty form test application easily goes idle 10s per second. Few similar mechanisms could account for 100s of messages.Sertac Akyuz
@SertacAkyuz, thanks. You’re right, a blank form with overridden WndProc would do the same. I just wanted to put out a simple example with an off/on switch (the checkbox) for testing. Not relevant for the question but I suppose the number of messages received within a given time frame is proportional to the computer capacities. My question arose from within a more complex project which rely on this message (for now). But It makes sense what you said: find the control the mouse is on. What doesn’t make sense though is that Windows keeps “telling” even if nothing happens: The mouse is idle.Guy R
With D7 on Win7x64 I received almost 150000 messages in 15 seconds running 92 processes with at least 26 windows. CPU is 4 cores.AmigoJack
@AmigoJack, 150,000 of any messages I suppose, right? Not 150k of WM_NCHITTEST?Guy R
@GuyR strictly as per your example 10k NcHitTest msgs per second and I'm not happy about it. I'll repeat that test once my system is restarted to see if the window or process count makes a difference (i.e. not having 27240 threads and 321200 handles).AmigoJack

1 Answers

3
votes

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.