6.22.2016 How to detect Mouse Events outside your Form

Form design for Systemwide mouse event capture in Free Pascal, Lazarus
In this article we find out what the user is doing with his mouse, even outside of the program. Let's conquer the whole system!


We have events on components, such as OnClick, OnMouseDown, OnScrollDown etc. But unfortunately these events only work when the mouse is inside the form. But sometimes we need to get a view of what the user is doing on other programs or other windows (or in other words "System-wide"). These "System wide" events can be "caught" through a crazy thing called "hooks".

A Hook is a function that you can tell Windows to run whenever something special happens. You can create a hook for keyboard to run it whenever a key is pressed. Then you can do something based on which key is pressed (in whichever window it is pressed). Today we are going to focus on mouse events. We will catch what the mouse is doing system wide. Wherever the mouse goes, we catch what the user is doing with it.

What we'll do

We will catch when the left, middle and right mouse button is pressed, and additionally when the mouse wheel is scrolled up or down. These things are especially useful in cases like, to create such a program which can record a macro and repeat those tasks later, or create mouse gestures on touching the corner of the screen (like in Gnome 3), or maybe creating a form which acts like a menu so when the user clicks outside it closes... the possibilities are endless.


Any DLL need to be made?

Nope.
Some Delphi/Pascal codes found on the internet demand to create a DLL file. DLL files are a mess and don't forget the DLL hell. In this article, the whole code is Lazarus based and resides in the single .exe file with no DLL involved. Cool, right!

Tutorial

Start Lazarus.

Create a new Application Project (Project->New Project->Application->OK).

Draw a TLabel and a TMemo. Empty the Lines property and set the Scrollbars property to ssVertical of the Memo. Customize the form the way you like it.

Form design for Systemwide mouse event capture in Free Pascal, Lazarus


Optionally, you can set the FormStyle to fsSystemStayOnTop. It will keep the form always on top and let you see the events even when you click outside of the form.

Switch to code view (F12). Now add "windows" unit to uses:

uses
  ..., ..., windows;

Under the type clause enter this:

type
  ...
  ...
  
  MouseLLHookStruct = record
    pt          : TPoint;
    mouseData   : cardinal;
    flags       : cardinal;
    time        : cardinal;
    dwExtraInfo : cardinal;
  end;
 
Before the var clause enter:

...
...

function LowLevelMouseHookProc(nCode, wParam, lParam : integer) : integer; stdcall;

var
...
... 

This function will be our hook function. Windows will run it whenever there is a mouse event.

Under the var clause declare a variable:

var
  ...
  ...
  mHook : cardinal;
 
Now we enter code for our hook function. Under the implementation clause enter:

function LowLevelMouseHookProc(nCode, wParam, lParam : integer) : integer; stdcall;
// possible wParam values: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP
var
  info : ^MouseLLHookStruct absolute lParam;
begin
  result := CallNextHookEx(mHook, nCode, wParam, lParam);
  with info^ do begin
    Form1.Label1.Caption := 'X: '+IntToStr(pt.x)+'  Y: '+ IntToStr(pt.y);
    case wParam of
      wm_lbuttondown : Form1.Memo1.Lines.Append(format('pressed left button (%d, %d)'    , [pt.x, pt.y]));
      wm_lbuttonup   : Form1.Memo1.Lines.Append(format('released left button (%d, %d)'   , [pt.x, pt.y]));
      wm_mbuttondown : Form1.Memo1.Lines.Append(format('pressed middle button (%d, %d)'  , [pt.x, pt.y]));
      wm_mbuttonup   : Form1.Memo1.Lines.Append(format('released middle button (%d, %d)' , [pt.x, pt.y]));
      wm_rbuttondown : Form1.Memo1.Lines.Append(format('pressed right button (%d, %d)'   , [pt.x, pt.y]));
      wm_rbuttonup   : Form1.Memo1.Lines.Append(format('released right button (%d, %d)'  , [pt.x, pt.y]));
      wm_mousewheel  : begin
        if smallInt(mouseData shr 16) > 0
        then Form1.Memo1.Lines.Append('scrolled wheel (up)')
        else Form1.Memo1.Lines.Append('scrolled wheel (down)');
      end;
    end;
  end;
end;

Switch to Form view (F12), then double click on the form, and enter:

procedure TForm1.FormCreate(Sender: TObject);
const
  WH_MOUSE_LL = 14;
begin

  mHook := SetWindowsHookEx(WH_MOUSE_LL, @LowLevelMouseHookProc, hInstance, 0);
  
end;

Switch to form view again (F12), then select the form, go to Object Inspector -> Events, then click OnDestroy, then click the [...] button beside it. Now enter:

procedure TForm1.FormDestroy(Sender: TObject);
begin

  UnhookWindowsHookEx(mHook);
  
end;

Now run the project (F9 or Run -> Run).

Systemwide mouse event capture in Free Pascal, Lazarus

Now, when you use this code on your project you will need to customize the LowLevelMouseHookProc() function to suit your needs. Try things yourself. The sky is the limit here!

...And for the mouse position, it is better to use the Mouse.CursorPos.x or .y because the solution in this article sometimes begets negative values, which might not be what you would like. The code is kept just to show how it is done in the hooks style.

Download Sample Code ZIP

You can download the above example tutorial project's source code from here.
Or here.
Size: 561KB
The package contains compiled executable EXE file.

Ref:
https://www.experts-exchange.com/questions/21838717/Delphi-detect-a-left-right-mouse-click-anywhere-on-the-screen.html - thanks to ZhaawZ, his answer helped greatly

6 comments:

Unknown said...

But how to do it in Linux or crossplatform?

Adnan Shameem said...

@tsr84
Hello.
Well, that's a nice question. But unfortunately I don't have a straight forward answer to that.
For linux, there might be something in the x.org api to handle this: https://www.x.org/wiki/guide/
For Mac there could be something in the APIs: https://developer.apple.com/documentation/

You'll have to search yourself and find a solution for each platform. If its important to you, you can contact platform specific developers to get a better idea, and then port the solution to Lazarus.

Another easier cross platform way is to use something else that is cross platform to achieve what you want. For example, when clicked outside form, the form loses focus. So, you can use OnDeactivate to detect if clicked outside. This is not precise, but a good alternative when you are not looking for perfection.

Hope that helps.
Regards.

terefere in da london said...

Tip for anyone who will run on this tutorial
Very usefull tutorial, although I had to modify it otherwise it wouldn't start. I'm using Lazarus IDE v1.8.4 with FPC 3.0.4 on Windows10 64bit
change:
function LowLevelMouseHookProc(nCode, wParam, lParam : integer) : integer; stdcall;
to:
function LowLevelMouseHookProc(nCode: integer; wParam: WPARAM; lParam : LPARAM): LRESULT; stdcall;

Cheers
Raf

al_kinnon said...

Hi, great article.

I'm getting an issue in my FormCreate procedure for this line:

mHook := SetWindowsHookEx(WH_MOUSE_LL, @LowLevelMouseHookProc, hInstance, 0);

The error is:

inpview_unit1.pas(111,64) Error: Incompatible type for arg no. 2: Got "< address of function(LongInt;LongInt;LongInt):LongInt;StdCall >", expected ""

It seems to want to include the parameters for LowLevelMouseHookProc.

I'm using Lazarus 1.8.4 (dated 2019-09-18) FPC 3.0.4
Windows 10 x64

Can Anyone help?

al_kinnon said...

Worked it out. I commented out the line before the var clause and it started to work.

john61 said...

Great article - THANKS!

It saved me a lot of time searching for WinAPI calls.
I've built a form in Delphi 10.4 using this code with practically no adjustments.
Tested on 4k display - mouse coordinates seem to be reported exactly as expected in full screen coordinate system (inverted Y-axis). Even moves/clicks on dynamically exposed taskbar era reported.

 
Copyright 2013 LazPlanet
Carbon 12 Blogger template by Blogger Bits. Supported by Bloggermint