I am writing an application which should draw a circle in place where user clicks a mouse. To achieve that i am hooking the mouse globally using SetWindowHookEx(WH_MOUSE,...)
The hooking, and the procedure that processes mouse action is in DLL. The procedure posts a registered message when it finds that mouse button was clicked using PostMessage(FindWindow('TMyWindow',nil), MyMessage, 0,0);
My application with TMyWindow form processes the messages in WndProc procedure. I check whether the message that came is the same as my registered one and only then draw the circle. After drawing the circle i create a timer, which should free the image after 500ms.
So everything seems to work just fine until i actually click on any part of my application form (for example click on still existing circle that was drawn not long ago). When i do that, form starts receiving my registered messages infinitely ans of course circle drawing procedure gets called every time.
I dont understand why is it doing so. Why is it working fine when i click somewhere off my application form but hangs when i click inside my form?
Let me know if you need more details.
Thanks
EDIT 1:
Main unit. $202 message is WM_LBUTTONUP.
unit main;
interface
uses
HookCommon,
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls, Menus, AppEvnts;
type
TTimer2 = class(TTimer)
private
FShape: TShape;
public
destructor Destroy; override;
property Shape: TShape read FShape write FShape;
end;
type
TShowMouseClick = class(TForm)
timerCountTimer: TTimer;
tray: TTrayIcon;
popMenu: TPopupMenu;
mnuExit: TMenuItem;
mnuActive: TMenuItem;
N1: TMenuItem;
mnuSettings: TMenuItem;
timersStx: TStaticText;
procedure timerCountTimerTimer(Sender: TObject);
procedure mnuExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
timerList: TList;
procedure shape();
procedure freeInactive(var Msg: TMessage); message WM_USER + 1545;
public
shapeColor: Tcolor;
procedure TimerExecute(Sender: TObject);
protected
procedure WndProc(var Message: TMessage); override;
{ Public declarations }
end;
var
ShowMouseClick: TShowMouseClick;
implementation
{$R *.dfm}
uses settings;
{$REGION 'Hide from TaskBar'}
procedure TShowMouseClick.FormActivate(Sender: TObject);
begin
ShowWindow(Application.Handle, SW_HIDE);
end;
procedure TShowMouseClick.FormShow(Sender: TObject);
begin
ShowWindow(Application.Handle, SW_HIDE);
end;
{$ENDREGION}
procedure TShowMouseClick.WndProc(var Message: TMessage);
begin
inherited WndProc(Message);
if (Message.Msg = HookCommon.MouseHookMessage) and
(Message.WParam = $202) then
shape;
end;
procedure TShowMouseClick.FormCreate(Sender: TObject);
begin
BorderStyle := bsNone;
FormStyle := fsStayOnTop;
WindowState := wsMaximized;
mnuActive.Checked := true;
HookCommon.HookMouse;
timerList := TList.Create;
timerList.Clear;
shapeColor := clGreen;
end;
procedure TShowMouseClick.FormDestroy(Sender: TObject);
begin
HookCommon.UnHookMouse;
end;
procedure TShowMouseClick.mnuExitClick(Sender: TObject);
begin
Close;
end;
procedure TShowMouseClick.timerCountTimerTimer(Sender: TObject);
begin
timersStx.Caption := 'Active timers: ' + IntToStr(timerList.Count);
end;
procedure TShowMouseClick.shape;
var
tm: TTimer2;
begin
tm := TTimer2.Create(nil);
tm.Tag := 0 ;
tm.Interval := 1;
tm.OnTimer := TimerExecute;
tm.Shape := nil;
timerList.Add(tm);
timersStx.Caption := 'Active timers: ' + IntToStr(timerList.Count);
tm.Enabled := true;
end;
procedure TShowMouseClick.TimerExecute(Sender: TObject);
var
img: TShape;
snd: TTimer2;
begin
snd := nil;
if Sender is TTimer2 then
snd := TTimer2(Sender);
if snd = nil then Exit;
if snd.Tag = 0 then
begin
snd.Interval := 500;
img := TShape.Create(nil);
img.Parent := ShowMouseClick;
img.Brush.Color := clGreen;
img.Shape := stCircle;
img.Width := 9;
img.Height := 9;
img.Left := Mouse.CursorPos.X-4;
img.Top := Mouse.CursorPos.Y-3;
snd.Tag := 1;
snd.Shape := img;
end else begin
snd.Enabled := false;
PostMessage(ShowMouseClick.Handle,WM_USER + 1545 , 0,0);
Application.ProcessMessages;
end;
end;
procedure TShowMouseClick.freeInactive(var Msg: TMessage);
var
i: integer;
begin
for i := timerList.Count - 1 downto 0 do
if TTimer2(timerList[i]).Enabled = false then
begin
TTimer2(timerList[i]).Free;
timerList.Delete(i);
end;
end;
destructor TTimer2.Destroy;
begin
FreeAndNil(FShape);
inherited;
end;
end.
Common unit.
unit HookCommon;
interface
uses Windows;
var
MouseHookMessage: Cardinal;
procedure HookMouse;
procedure UnHookMouse;
implementation
procedure HookMouse; external 'MouseHook.DLL';
procedure UnHookMouse; external 'MouseHook.DLL';
initialization
MouseHookMessage := RegisterWindowMessage('MouseHookMessage');
end.
DLL code.
library MouseHook;
uses
Forms,
Windows,
Messages,
HookCommon in 'HookCommon.pas';
{$J+}
const
Hook: HHook = 0;
{$J-}
{$R *.res}
function HookProc(nCode: Integer; MsgID: WParam; Data: LParam): LResult; stdcall;
var
notifyTestForm : boolean;
begin
notifyTestForm := false;
if msgID = $202 then
notifyTestForm := true;
if notifyTestForm then
begin
PostMessage(FindWindow('TShowMouseClick', nil), MouseHookMessage, MsgID, 0);
end;
Result := CallNextHookEx(Hook,nCode,MsgID,Data);
end;
procedure HookMouse; stdcall;
begin
if Hook = 0 then Hook:=SetWindowsHookEx(WH_MOUSE,#HookProc,HInstance,0);
end;
procedure UnHookMouse; stdcall;
begin
UnhookWindowsHookEx(Hook);
Hook:=0;
end;
exports
HookMouse, UnHookMouse;
begin
end.
The source of the mouse hook stuff is this
Why is it working fine when i click somewhere off my application form
but hangs when i click inside my form?
You're not posting the message to other windows when you click on them. First you should ask yourself, "what happens if I posted a message in my hook callback to all windows which are posted a WM_LBUTTONUP?".
Replace this line
PostMessage(FindWindow('TShowMouseClick', nil), MouseHookMessage, MsgID, 0);
in your dll code, with this:
PostMessage(PMouseHookStruct(Data).hwnd, MouseHookMessage, MsgID, 0);
It doesn't matter if the other applications would know or not what MouseHookMessage is, they will ignore the message. Launch your application and click the mouse wildly to other windows. Generally nothing will happen. Unless you click in the client area of any Delphi application. You'll instantly freeze it.
The answer to this question lies in both how a VCL message loop runs and how a WH_MOUSE hook works. A quote from MouseProc callback function's documentation.
[..] The system calls this function whenever an application calls the
GetMessage or PeekMessage function and there is a mouse message to be
processed.
Suppose you launch your application and the mouse is hooked, then you hover the mouse on your form and wait till your application calls 'WaitMessage', that it is idle. Now click in the client area to generate mouse messages. What happens is that the OS places messages to your application's main thread's message queue. And what your application does is that to remove and dispatch these messages with PeekMessage. This is where applications differ. The VCL first calls 'PeekMessage' with 'PM_NOREMOVE' passed in 'wRemoveMsg' parameter, while most other applications either removes the message with a call to 'PeekMessage' or do the same by using 'GetMessage'.
Now suppose it is 'WM_LBUTTONUP's turn. Refer to the quote above. As soon as PeekMessage is called, the OS calls the MouseProc callback. The call happens from 'user32.dll', that is, when your hook callback is called the statement following the 'PeekMessage' is not executed yet. Also, remember the VCL loop, the message is still in the queue, it has not been removed. Now, your callback function posts a message to the same message queue and returns. Execution returns to the VCL message loop and VCL again calls 'PeekMessage', this time to remove and dispatch the message, but instead of removing the 'WM_LBUTTONUP', it removes the custom message that you posted. 'WM_LBUTTONUP' remains in the queue. After the custom message is dispatched, since 'WM_LBUTTONUP' is still in the queue, 'PeekMessage' is again called, and again the OS calls the callback so that the callback can post another custom message to be removed instead of the mouse message. This loop effectively freezes the application.
To resolve, either post your message to a different thread that has its own message loop which would in some way synchronize with the main thread, or, I would not especially advice it but, instead of posting the message, send it. As an alternative you can remove the 'WM_LBUTTONUP' message yourself from the queue if one exists:
procedure TShowMouseClick.WndProc(var Message: TMessage);
begin
inherited WndProc(Message);
if (Message.Msg = HookCommon.MouseHookMessage) and
(Message.WParam = $202) then begin
if PeekMessage(Msg, Handle, WM_LBUTTONUP, WM_LBUTTONUP, PM_REMOVE) then
DispatchMessage(Msg); // or eat if you don't need it.
..
end;
The disadvantage to this approach is that, the PeekMessage itself, as mentioned above, will cause another custom message to be posted, so you'll be receiving those in pairs.
Either your Mouse click or your MyMessage messages are not removed from the Message Queue (unlikely) or they are somehow echoed back, or your code loops in a recursion.
I would try to remove any code from your TMyWindow.WndProc and replace it with some innocuous code (like an OutputDebugString to see it called in the message area of the IDE) to see if it is still looping or not.
Something like:
with Message do
case Msg of
WM_MyMessage: OutputDebugString('MyMessage received. Drawing a circle');
else
inherited WndProc(Message);
If it's only writing once per click, then the recursion is in your handling of the message (or in the timer handler) to draw/erase the circle.
If it's looping, then your click generates multiple messages or 1 that is spinning forever...
Update:
After giving a look at your code, I'd change the way you deal with the timers.
- Don't create the timer with an interval of 1 for the purpose of creating the shape. You'll be flooding your app with Timer events.
- As soon as you enter the Execute, disable the timer
- Avoid calling Application.ProcessMessages.
- You may have some reasons, but I find this very convoluted when it seems to me that a simple OnMouse event on your form could achieve this easily.
This happens because FindWindow actually sends messages on its own that also wind up in your hook. Specifically, it sends a WM_GETTEXT to get the window's title.
To avoid that, do the FindWindow up front (outside the hook's callback).
Related
I want to display a form when the cursor enters the icon, and it disappears shortly after the cursor leaves the icon, similar to the Process Hacker software.
(It displays a form above the system tray that displays information about running applications when the cursor enters the icon, with additional options such as attaching the form that will not disappear, opening settings and more, and the form disappears a few seconds after leaving the icon)
I saw this post
Edit:
I do not need to add these events as properties (like the built-in OnMouseMove, and as in TTrayIconEx), I just want to add a handler procedure that will receive the messages sent from the icon when the cursor hovers over it or leaves it. For example, the messages Remy Lebeau mentioned in his reply (NIN_POPUPOPEN, NIN_POPUPCLOSE)
Is this possible, and how?
updating:
In fact, I used this code as experience:
unit MainFormtest;
interface
uses
ShellAPI, Windows, Messages, SysUtils, Classes,
Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls, Vcl.Menus;
type
TTesterForm = class(TForm)
DelayHide: TTimer;
TestPopupMenu: TPopupMenu;
PMClose: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure DelayHideTimer(Sender: TObject);
procedure FormMouseEnter(Sender: TObject);
procedure FormMouseLeave(Sender: TObject);
procedure FormHide(Sender: TObject);
procedure PMCloseClick(Sender: TObject);
procedure TestPopupMenuPopup(Sender: TObject);
protected
procedure CreateParams(var Params: TCreateParams); override;
public
TrayIconForTest: TNotifyIconData;
procedure TrayMouseMessage(var Msg: TMessage); Message WM_SYSTEM_TRAY_MESSAGE;
end;
var
TesterForm: TTesterForm;
implementation
{$R *.dfm}
procedure TTesterForm.FormCreate(Sender: TObject);
begin
with TrayIconForTest do
begin
cbSize := TNotifyIconData.SizeOf;
Wnd := Handle;
uID := $20;
uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP;
uCallBackMessage := WM_SYSTEM_TRAY_MESSAGE;
hIcon := Application.Icon.Handle;
szTip := 'this is test icon';
uVersion := 4;
dwInfoFlags := NIIF_USER;
hBalloonIcon := Application.Icon.Handle;
end;
Shell_NotifyICon(NIM_ADD, #TrayIconForTest);
Shell_NotifyIcon(NIM_SETVERSION, #TrayIconForTest);
end;
procedure TTesterForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle and not WS_EX_APPWINDOW;
Params.WndParent := Application.Handle;
end;
procedure TTesterForm.TestPopupMenuPopup(Sender: TObject);
begin
DelayHide.Enabled := False;
end;
procedure TTesterForm.TrayMouseMessage(var Msg: TMessage);
begin
case LOWORD(Msg.Lparam) of
NIN_POPUPOPEN:
begin
TesterForm.Show;
DelayHide.Enabled := False;
TestPopupMenu.CloseMenu;
end;
NIN_POPUPCLOSE:
DelayHide.Enabled := True;
WM_RBUTTONDOWN, WM_LBUTTONDOWN:
TestPopupMenu.Popup(Mouse.CursorPos.X, Mouse.CursorPos.Y);
end;
end;
procedure TTesterForm.DelayHideTimer(Sender: TObject);
begin
TesterForm.Hide;
end;
procedure TTesterForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Shell_NotifyIcon(NIM_DELETE, #TrayIconForTest);
Application.Terminate;
end;
procedure TTesterForm.FormHide(Sender: TObject);
begin
DelayHide.Enabled := False;
end;
procedure TTesterForm.FormMouseEnter(Sender: TObject);
begin
DelayHide.Enabled := False;
end;
procedure TTesterForm.FormMouseLeave(Sender: TObject);
begin
DelayHide.Enabled := True;
end;
procedure TTesterForm.PMCloseClick(Sender: TObject);
begin
testerForm.Close;
end;
end.
But he suffers from several problems:
When I press the right / left button to open the menu, the message NIN_POPUPCLOSE is not read again until the cursor enters the icon again and exits.
The menu does not close when a mouse button is pressed anywhere (so I added TestPopupMenu.CloseMenu to close the menu when the cursor re-enters).
The form window tends to display in the back when targeting another application window. I tried applying TesterForm.BringToFront and also SetForegroundWindow and it did not help.
Can you direct me to fix the code?
TTrayIcon, and TTrayIconEx provided in the other post you mention, do not support what you want.
However, the underlying Win32 tray icon API, Shell_NotifyIcon(), does. After adding your icon with NIM_ADD, you have to use NIM_SETVERSION setting NOTIFYICONDATA.uVersion to NOTIFYICON_VERSION_4 or higher to enable the API to send NIN_POPUPOPEN and NIN_POPUPCLOSE notification messages to your tray icon's owner window (note: this works only on Vista+):
NIN_POPUPOPEN. Sent when the user hovers the cursor over an icon to indicate that the richer pop-up UI should be used in place of a standard textual tooltip.
NIN_POPUPCLOSE. Sent when a cursor no longer hovers over an icon to indicate that the rich pop-up UI should be closed.
UPDATE:
The menu does not close when a mouse button is pressed anywhere (so I added TestPopupMenu.CloseMenu to close the menu when the cursor re-enters).
This is a well-known issue (multiple questions on StackOverflow about it), and is covered in the Remarks section of the documentation for the Win32 TrackPopupMenu() function (which TPopupMenu.Popup() calls internally):
To display a context menu for a notification icon, the current window must be the foreground window before the application calls TrackPopupMenu or TrackPopupMenuEx. Otherwise, the menu will not disappear when the user clicks outside of the menu or the window that created the menu (if it is visible). If the current window is a child window, you must set the (top-level) parent window as the foreground window.
However, when the current window is the foreground window, the second time this menu is displayed, it appears and then immediately disappears. To correct this, you must force a task switch to the application that called TrackPopupMenu. This is done by posting a benign message to the window or thread, as shown in the following code sample:
SetForegroundWindow(hDlg);
// Display the menu
TrackPopupMenu( hSubMenu,
TPM_RIGHTBUTTON,
pt.x,
pt.y,
0,
hDlg,
NULL);
PostMessage(hDlg, WM_NULL, 0, 0);
In this case, hDlg can be the Handle of your TForm, eg:
WM_RBUTTONDOWN, WM_LBUTTONDOWN: begin
SetForegroundWindow(Handle);
with Mouse.CursorPos do
TestPopupMenu.Popup(X, Y);
PostMessage(Handle, WM_NULL, 0, 0);
end;
I'm trying to free a component when i click it. So, i've written the simplest code i could imagine to achieve this: a procedure that frees it's sender. But on Delphi 7 (Tried on Delphi XE 10 and it worked with no errors) it sometimes throws an Access Violation or Abstract Error randomly. The easiest way to replicate this is to insert like 30 Buttons and assign an onclick procedure with the code below, then click them.
I've tried the two codes below, both on onclick:
procedure FreeMe(Sender: TObject);
begin
TButton(Sender).Free;
end;
or
procedure FreeMe(Sender: TObject);
begin
(Sender as TButton).Free;
end;
You need to delay the freeing until after the button's OnClick event handier has fully exited. It is important that the freeing happens when the object being freed is idle and not in the middle of processing anything.
One way to do that is to use PostMessage(), eg:
var
MyReleaseWnd: HWND;
procedure TMyMainForm.FormCreate(Sender: TObject);
begin
MyReleaseWnd := AllocateHWnd(MyReleaseWndProc);
end;
procedure TMyMainForm.FormDestroy(Sender: TObject);
begin
DeallocateHWnd(MyReleaseWnd);
end;
procedure TMyMainForm.MyReleaseWndProc(var Message: TMessage);
begin
if Message.Msg = CM_RELEASE then
TObject(Msg.LParam).Free
else
Message.Result := DefWindowProc(MyReleaseWnd, Message.Msg, Message.WParam, Message.LParam);
end;
procedure DelayFreeMe(Sender: TObject);
begin
PostMessage(MyReleaseWnd, CM_RELEASE, 0, LPARAM(Sender));
end;
Alternatively, in 10.2 Tokyo and later, you can use TThread.ForceQueue() instead:
procedure DelayFreeMe(Sender: TObject);
begin
TThread.ForceQueue(nil, Sender.Free);
end;
Either way, you can then do this:
procedure TSomeForm.ButtonClick(Sender: TObject);
begin
DelayFreeMe(Sender);
end;
I've copied code from this article:
Controlling the number of application instances
However, the message being sent by SendMessage is not being 'caught' by the main form.
This is the code in the DPR file, where we are registering the message, and then broadcasting it if an instance of the application is already running:
var
Mutex: THandle;
begin
MyMsg := RegisterWindowMessage('Show_Main_Form');
Mutex := CreateMutex(nil, True, 'B8C24BD7-4CFB-457E-841E-1978A8ED0B16');
if (Mutex = 0) or (GetLastError = ERROR_ALREADY_EXISTS) then
begin
SendMessage(HWND_BROADCAST, MyMsg, 0, 0);
end
This is code from the main form:
var
fmMain: TfmMain;
MyMsg: Cardinal;
implementation
uses
uSettings;
{$R *.dfm}
procedure TfmMain.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
if (Msg.Message = MyMsg) then
begin
beep;
Application.Restore;
Application.MainForm.Visible := True;
SetForeGroundWindow(Application.MainForm.Handle);
Handled := True;
end;
end;
procedure TfmMain.FormCreate(Sender: TObject);
begin
Application.OnMessage := AppMessage;
end;
The problem is that the procedure AppMessage does not get called. What is wrong?
OnMessage is used to intercept queued messages. However, this message is sent rather than queued. You need to override the form's window procedure in order to receive it:
Add this to the protected part of your form's type declaration:
procedure WndProc(var Message: TMessage); override;
Implement it like this:
procedure TfmMain.WndProc(var Message: TMessage);
begin
inherited;
if Message.Msg = MyMsg then
begin
Beep;
Application.Restore;
Application.MainForm.Visible := True;
SetForeGroundWindow(Application.MainForm.Handle);
end;
end;
Since this form is presumably the single instance of the application's main form you might replace the body of the message handler with this:
Application.Restore;
Visible := True;
SetForeGroundWindow(Handle);
I would also comment that broadcasting such a message seems a little risky to me. You'll be sending that message to every top-level window in the system. I think that has definite potential to cause problems if you encounter a program that reacts to that message when it should not.
Were it me I would identify the window which you intend to target, and send the message directly to that window. And I would use SendMessageTimeout to be robust to the scenario where the target app is not responding. In that scenario, SendMessage will never return and the sending application will also become hung.
I have a Delphi 2007 project that has run fine on Windos XP, Vista and "7" for years. It was an upgrade from Delphi 5 thus "MainFormOnTaskBar" was "false" by default (I never changed it in DPR). In this scenario, the system-wide hot key worked "system-wide" with following code in main form's OnCreate event handler.
HotKey_xyz := GlobalAddAtom('Hotkey_xyz');
if NOT RegisterHotKey(Self.Handle, HotKey_xyz, MOD_CONTROL, VK_F12) then
ShowMessage('Unable to register Control-F12 as system-wide hot key') ;
(I have GlobalDeleteAtom() and UnregisterHotKey() in Form.OnDestroy as expected.)
Now, I need a Form to show its own button on Taskbar, so I set "Application.MainFormOnTaskBar := True" in DPR. This works as expected. However, this has the side-effect that Control-F12 does NOT work system-wide, it works ONLY IF my application has focus (so, it is NOT "system-wide" anymore.)
I have extensively searched the 'Net have found many articles regarding how/why "MainFormOnTaskBar" affects certain subform/modal form behaviors. However, I have found nothing regarding its effect on a "System-Wide Hot Key" issue that I describe above. I have tested and retested my application with MainFormOnTaskBar set to true and false while all else remains exactly the same. I can positively verify that the above described issue with System-wide hot key relates to MainFormOnTaskBar flag.
I will greatly appreciate any guidance regarding a work-around. I do need BOTH - a system-wide hot key AND a form with its own button on taskbar.
Thank You very much.
TApplication.MainFormOnTaskbar has no effect on system-wide hotkeys at all. I can positively confirm that. I am able to receive WM_HOTKEY messages regardless of what MainFormOnTaskbar is set to, regardless of whether the app is focused or not, etc. So whatever you are seeing is not what you think is happening.
Most likely, the Form's Handle is simply being recreated behind your back after you have called RegisterHotKey(), so you lose the HWND that would receive the WM_HOTKEY messages. Instead of using the OnCreate event, you should override the Form's CreateWindowHandle() and DestroyWindowHandle() methods instead to ensure the hot key is always registered for the Form's current HWND no matter what happens to it (you should always do that whenever you tie any kind of data to the Form's Handle), eg:
type
TForm1 = class(TForm)
private
HotKey_xyz: WORD;
procedure WMHotKey(var Message: TMessage); message WM_HOTKEY;
protected
procedure CreateWindowHandle(const Params: TCreateParams); override;
procedure DestroyWindowHandle; override;
end;
procedure TForm1.CreateWindowHandle(const Params: TCreateParams);
begin
inherited;
HotKey_xyz := GlobalAddAtom('Hotkey_xyz');
if HotKey_xyz <> 0 then
RegisterHotKey(Self.Handle, HotKey_xyz, MOD_CONTROL, VK_F12);
end;
procedure TForm1.DestroyWindowHandle(const Params: TCreateParams);
begin
if HotKey_xyz <> 0 then
begin
UnregisterHotKey(Self.Handle, HotKey_xyz);
GlobalDeleteAtom(HotKey_xyz);
HotKey_xyz := 0;
end;
inherited;
end;
procedure TForm1.WMHotKey(var Message: TMessage);
begin
...
end;
A better option is to use AllocateHWnd() to allocate a separate dedicated HWND just for handling the hot key messages (then you can use the OnCreate and OnDestroy events again), eg:
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
HotKey_xyz: WORD;
HotKeyWnd: HWND;
procedure HotKeyWndProc(var Message: TMessage);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
HotKeyWnd := AllocateHwnd(HotKeyWndProc);
HotKey_xyz := GlobalAddAtom('Hotkey_xyz');
if HotKey_xyz <> 0 then
RegisterHotKey(HotKeyWnd, HotKey_xyz, MOD_CONTROL, VK_F12);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if HotKey_xyz <> 0 then
begin
UnregisterHotKey(HotKeyWnd, HotKey_xyz);
GlobalDeleteAtom(HotKey_xyz);
HotKey_xyz := 0;
end;
if HotKeyWnd <> 0 then
begin
DeallocateHWnd(HotKeyWnd);
HotKeyWnd := 0;
end;
end;
procedure TForm1.HotKeyWndProc(var Message: TMessage);
begin
if Message.Msg = WM_HOTKEY then
begin
...
end else
Message.Result := DefWindowProc(HotKeyWnd, Message.Msg, Message.WParam, Message.LParam);
end;
I'm working on something which will require monitoring of many forms. From outside the form, and without putting any code inside the form, I need to somehow capture events from these forms, most likely in the form of windows messages. But how would you capture windows messages from outside the class it's related to?
My project has an object which wraps each form it is monitoring, and I presume this handling will go in this object. Essentially, when I create a form I want to monitor, I create a corresponding object which in turn gets added to a list of all created forms. Most importantly, when that form is closed, I have to know so I can remove this form's wrapper object from the list.
These events include:
Minimize
Maximize
Restore
Close
Focus in/out
What I DON'T want:
Any code inside any forms or form units for this handling
Inheriting the forms from any custom base form
Using the form's events such as OnClose because they will be used for other purposes
What I DO want:
Handling of windows messages for these events
Any tips on how to get windows messages from outside the class
Which windows messages I need to listen for
Question re-written with same information but different approach
You need to listen for particular windows messages being delivered to the form. The easiest way to do this is to assign the WindowProc property of the form. Remember to keep a hold of the previous value of WindowProc and call it from your replacement.
In your wrapper object declare a field like this:
FOriginalWindowProc: TWndMethod;
Then in the wrapper's constructor do this:
FOriginalWindowProc := Form.WindowProc;
Form.WindowProc := NewWindowProc;
Finally, implement the replacement window procedure:
procedure TFormWrapper.NewWindowProc(var Message: TMessage);
begin
//test for and respond to the messages of interest
FOriginalWindowProc(Message);
end;
Here's a more complete example of the solution that David Provided:
private
{ Private declarations }
SaveProc : TWndMethod;
procedure CommonWindowProc(var Message: TMessage);
...
procedure TForm1.Button1Click(Sender: TObject);
var
f : tForm2;
begin
f := tForm2.Create(nil);
SaveProc := f.WindowProc;
f.WindowProc := CommonWindowProc;
f.Show;
end;
procedure TForm1.CommonWindowProc(var Message: TMessage);
begin
case Message.Msg of
WM_SIZE : Memo1.Lines.Add('Resizing');
WM_CLOSE : Memo1.Lines.Add('Closing');
CM_MOUSEENTER : Memo1.Lines.Add('Mouse enter form');
CM_MOUSELEAVE : Memo1.Lines.Add('Mouse leaving form');
// all other messages will be available as needed
end;
SaveProc(Message); // Call the original handler for the other form
end;
A better solution than trying to work outside of the form would be to make every form descend from a common base form that implements the functionality. The form event handlers are exactly the right place to add this code but you'd write it all in the ancestor form. Any descendant form could still use the form events and as long as they always call inherited somewhere in the event handler the ancestor code would still execute.
Another option is create TApplicationEvents and assign a handler to OnMessage event. Once if it fired, use the FindControl function and Msg.hWnd to check if it is the tform type and do what ever you want without hookin
Using Windows Messages can really attain a fine granularity (Yes, its part of your requirements!) but in some user cases where relying just on the VCL Event Framework suffices, a similar solution can be suggested:
unit Host;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
THostForm = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
FFormResize: TNotifyEvent;
FFormActivate: TNotifyEvent;
FFormDeactivate: TNotifyEvent;
FFormDestroy: TNotifyEvent;
procedure _FormResize(Sender: TObject);
procedure _FormActivate(Sender: TObject);
procedure _FormDeactivate(Sender: TObject);
procedure InternalEventHandlerInit(const AForm:TForm);
public
procedure Log(const Msg:string);
procedure Logln(const Msg:string);
end;
var
HostForm: THostForm;
implementation
{$R *.dfm}
procedure THostForm.Button1Click(Sender: TObject);
var
frm: TForm;
begin
frm := TForm.Create(nil);
frm.Name := 'EmbeddedForm';
frm.Caption := 'Embedded Form';
//
InternalEventHandlerInit(frm);
//
Logln('<'+frm.Caption+'> created.');
//
frm.Show;
end;
procedure THostForm.InternalEventHandlerInit(const AForm: TForm);
begin
FFormResize := AForm.OnResize;
AForm.OnResize := _FormResize;
//
FFormActivate := AForm.OnActivate;
AForm.OnActivate := _FormActivate;
//
FFormDeactivate := AForm.OnDeactivate;
AForm.OnDeactivate := _FormDeactivate;
end;
procedure THostForm.Log(const Msg: string);
begin
Memo1.Lines.Add(Msg);
end;
procedure THostForm.Logln(const Msg: string);
begin
Memo1.Lines.Add(Msg);
Memo1.Lines.Add('');
end;
procedure THostForm._FormActivate(Sender: TObject);
begin
Log('Before OnActivate <'+(Sender as TCustomForm).Caption+'>');
//
if Assigned(FFormActivate) then
FFormActivate(Sender) // <<<
else
Log('No OnActivate Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
//
Logln('After OnActivate <'+(Sender as TCustomForm).Caption+'>');
end;
procedure THostForm._FormDeactivate(Sender: TObject);
begin
Log('Before OnDeactivate <'+(Sender as TCustomForm).Caption+'>');
//
if Assigned(FFormDeactivate) then
FFormDeactivate(Sender)
else
Log('No OnDeActivate Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
//
Logln('After OnDeactivate <'+(Sender as TCustomForm).Caption+'>');
end;
procedure THostForm._FormResize(Sender: TObject);
begin
Log('Before OnResize <'+(Sender as TCustomForm).Caption+'>');
//
if Assigned(FFormResize) then
FFormResize(Sender)
else
Log('No OnResize Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
//
Logln('After OnResize <'+(Sender as TCustomForm).Caption+'>');
end;
end.