Search code examples
delphiwinapivcldelphi-xe7

Is WM_NCHITTEST supposed to be perpetually generated by Win10, at a frequency of 100's per second, even if mouse is idle?


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?


Solution

  • 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.