0

I have a PopupMenu in my Application which pops up when a user right clicks on my App's Notification Area icon.

When I right click on this icon, pop up the menu, and do nothing, my App behaves like resuming its work because it looks like it is waiting until I click on a Menu Item.

I want to remove this behavior. I tried fixing the PopupMenu by adding an Auto-Close procedure when no response comes from the user and when the Mouse Pointer leaves the PopupMenu.

I also tried adding a TTimer that closes my TPopUpMenu after a specified time, but it closes after the time I specified without looking if the Mouse Pointer is inside or outside the PopupMenu.

Two Scenarios I want to Achieve are:

  • I want the TPopUpMenu to close when the user moves the Mouse Pointer out of it for more than two or three seconds.

  • When the user moves the Mouse Pointer inside of it, the TPopupMenu should be closed after five minutes, because ANY USER should respond to a PopupMenu within five minutes.

I tried adding the following code with a TTimer to my App's Event Handler that opens the PopupMenu when the user right-clicks on the Tray Icon, but the PopupMenu always closes after two seconds:

procedure TMainForm_1.SysTrayIconMessageHandler(var Msg: TMessage);
var
   SysTrayTimer: TTimer;
   PT: TPoint;
begin
  case Msg.LParam of      
    WM_.....:;
    WM_RBUTTONDOWN:
    begin
      GetCursorPos(PT);
      SysTrayTimer.Enabled := True;
      SysTrayTimer.Interval := 2500;
      SystemTrayPopUpMenu.PopUp(PT.X, PT.Y);
      SystemTrayPopUpMenu.AutoLineReduction := maAutomatic;
    end;
  end;
end;

procedure TMainForm_1.OnSysTrayTimer(Sender: TObject);
begin
  SysTrayTimer.Enabled := False;
  SendMessage(PopupList.Window, WM_CANCELMODE, 0, 0);
end;

I also read this, but after I added the code, nothing changed.

At least, I must be able to do this: close the PopupMenu after the user opens it by right clicking and moves the Mouse Pointer outside of it.

This is how I added new code to achieve this:

unit MainForm_1;

interface

uses
  Windows, SysUtils, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls, Menus, ImgList;

type
  TMainForm_1 = class(TForm);
    SystemTrayPopUpMenu: TPopUpMenu;
    ExitTheProgram: TMenuItem;
    RestoreFromSystemTray: TMenuItem; 
    ReadTheInstructions: TMenuItem;
    Separator1: TMenuItem;
    TrackSysTrayMenuTimer: TTimer;
    CloseSysTrayMenuTimer: TTimer;
    procedure OnTrackSysTrayMenuTimer(Sender: TObject);
    procedure OnCloseSysTrayMenuTimer(Sender: TObject);  
    procedure SysTrayPopUpMenuPopUp(Sender: TObject);
  private
    MouseInSysTrayPopUpMenu: Boolean;
    IconData: TNotifyIconData; 
    procedure SysTrayIconMsgHandler(var Msg: TMessage); message TRAY_CALLBACK;
    procedure AddSysTrayIcon;
    procedure DisplayBalloonTips;
    procedure ApplySystemTrayIcon;
    procedure DeleteSysTrayIcon;
  public
    IsSystemTrayIconShown: Boolean;
  end;

var
  MainForm_1: TMainForm_1;

implementation

uses
  ShlObj, MMSystem, ShellAPI, SHFolder,.....;

procedure TMainForm_1.SysTrayIconMsgHandler(var Msg: TMessage);
var
  PT: TPoint;
begin
  case Msg.LParam of
    WM_MOUSEMOVE:;
    WM_LBUTTONUP:;
    WM_LBUTTONDBLCLK:;
    WM_RBUTTONUP:;
    WM_RBUTTONDBLCLK:;
    WM_LBUTTONDOWN:;
    NIN_BALLOONSHOW:;
    NIN_BALLOONHIDE:;
    NIN_BALLOONTIMEOUT:;
    NIN_BALLOONUSERCLICK:;
    WM_RBUTTONDOWN:
    begin
      GetCursorPos(PT);
      SetForegroundWindow(Handle);
      SystemTrayPopUpMenu.OnPopup := SysTrayPopUpMenuPopUp;
      SystemTrayPopUpMenu.PopUp(Pt.X, Pt.Y);
      PostMessage(Handle, WM_NULL, 0, 0);
      TrackSysTrayMenuTimer.Enabled := False;
      CloseSysTrayMenuTimer.Enabled := False;
    end;
  end;
end;

procedure TMainForm_1.SysTrayPopUpMenuPopup(Sender: TObject);
begin
  MouseInSysTrayMenu := True;
  TrackSysTrayMenuTimer.Interval := 100;
  TrackSysTrayMenuTimer.OnTimer := OnTrackSysTrayMenuTimer;
  TrackSysTrayMenuTimer.Enabled := True;
  CloseSysTrayMenuTimer.Interval := 300000;
  CloseSysTrayMenuTimer.OnTimer := OnCloseSysTrayMenuTimer;
  CloseSysTrayMenuTimer.Enabled := True;
end;

procedure TMainForm_1.OnTrackSysTrayMenuTimer(Sender: TObject);
var
  hPopupWnd: HWND;
  R: TRect;
  PT: TPoint;
begin
  hPopupWnd := FindWindow('#32768', nil);
  if hPopupWnd = 0 then Exit;
  GetWindowRect(hPopupWnd, R);
  GetCursorPos(Pt);
  if PtInRect(R, Pt) then begin
    if not MouseInSysTrayMenu then begin
      MouseInSysTrayMenu := True;
      CloseSysTrayMenuTimer.Interval := 300000;
    end;
  end else begin
    if MouseInSysTrayMenu then begin
      MouseInSysTrayMenu := False;
      CloseSysTrayMenuTimer.Interval := 2500;
    end;
  end;
end; 

procedure TMainForm_1.OnCloseSysTrayMenuTimer(Sender: TObject);
begin
  CloseSysTrayMenuTimer.Enabled := False;
  SendMessage(PopupList.Window, WM_CANCELMODE, 0, 0);
end;

How two TTimers are used in the App's MainForm:

image

How I assigned TrackSysTrayMenuTimer's property values.....

image

How I assigned CloseSysTrayMenuTimer's property values.....

image

I also got an Exception Message like this.....

enter image description here

It is a message I wrote like this to check what is failing in the Code..... So with that I can identify if FindWindow is failing or not.....

...
hPopupWnd := FindWindow('#32768', nil);
if hPopupWnd = 0 then
begin
TrackSysTrayMenuTimer.Enabled := False;
if ShowErrors = True and TestingMode = True then
Application.MessageBox('The PopUp Menu "SystemTrayPopUpMenu" could not be found.' +
' FindWindow will abort.', '                                      Exception Message', MB_ICONSTOP or MB_OK);
exit;

The Last Error I received is:

enter image description here

Thanks in Advance.

Mike Torrettinni
  • 1,816
  • 2
  • 17
  • 47
GTAVLover
  • 1,407
  • 3
  • 22
  • 41

2 Answers2

2

A standard popup menu is not supposed to auto-close when the user moves the mouse outside of it. The user is meant to click somewhere to dismiss it.

If you really want to auto-close a popup menu when the mouse moves outside of it, you have to manually implement your own tracking to know when the mouse is outside of the menu's current display coordinates.

That being said, there is also a bug in your code that you need to fix. Per MSDN documentation:

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.

This is further discussion by Microsoft Support:

PRB: Menus for Notification Icons Do Not Work Correctly

When you display a context menu for a notification icon (see Shell_NotifyIcon), clicking anywhere besides the menu or the window that created the menu (if it is visible) doesn't cause the menu to disappear. When this behavior is corrected, the second time this menu is displayed, it displays and then immediately disappears.

To correct the first behavior, you need to make the current window the foreground window before calling TrackPopupMenu or TrackPopupMenuEx. If the current window is a child window, set the (top-level) parent window as the foreground window.

The second problem is caused by a problem with TrackPopupMenu. It is necessary to force a task switch to the application that called TrackPopupMenu at some time in the near future. This can be accomplished by posting a benign message to the window or thread.

Try something more like this:

var
  SysTrayMenuTicks: DWORD;
  MouseInSysTrayMenu: Boolean;

// drop a TTimer on the TForm at design-time, set its Interval
// property to 100, its Enabled property to false, and assign
// on OnTimer event handler...

procedure TMainForm_1.SysTrayIconMessageHandler(var Msg: TMessage);
var
  Pt: TPoint;
begin
  case Msg.LParam of
    ...
    WM_RBUTTONDOWN:
    begin
      // FYI, the `WM_RBUTTONDOWN` notification provides you with
      // screen coordinates where the popup menu should be displayed,
      // you don't need to use `GetCursorPos()` to figure it out...
      GetCursorPos(Pt);

      SetForegroundWindow(Handle); // <-- bug fix!
      SystemTrayPopUpMenu.PopUp(Pt.X, Pt.Y);
      PostMessage(Handle, WM_NULL, 0, 0); // <-- bug fix!

      SysTrayTimer.Enabled := False;
    end;
    ...
  end;
end;

procedure TMainForm_1.SystemTrayPopUpMenuPopup(Sender: TObject);
begin
  MouseInSysTrayMenu := True;
  SysTrayMenuTicks := GetTickCount;
  SysTrayTimer.Enabled := True;
end;

procedure TMainForm_1.SysTrayTimerTimer(Sender: TObject);
var
  hPopupWnd: HWND;
  R: TRect;
  Pt: TPoint;
begin
  // get the HWND of the current active popup menu...
  hPopupWnd := FindWindow('#32768', nil);
  if hPopupWnd = 0 then Exit;

  // get the popup menu's current position and dimensions...
  GetWindowRect(hPopupWnd, R);

  // get the mouse's current position...
  GetCursorPos(Pt);

  if PtInRect(R, Pt) then
  begin
    // mouse is over the menu...

    if not MouseInSysTrayMenu then
    begin
      // just entered, reset timeout...
      MouseInSysTrayMenu := True;
      SysTrayMenuTicks := GetTickCount;
      Exit;
    end;

    // has the mouse been over the menu for < 5 minutes?
    if (GetTickCount - SysTrayMenuTicks) < 300000 then
      Exit; // yes...

  end else
  begin
    // mouse is not over the menu...

    if MouseInSysTrayMenu then
    begin
      // just left, reset timeout...
      MouseInSysTrayMenu := False;
      SysTrayMenuTicks := GetTickCount;
      Exit;
    end;

    // has the mouse been outside the menu for < 2.5 seconds?
    if (GetTickCount - SysTrayMenuTicks) < 2500 then
      Exit; // yes...

  end;

  // timeout! Close the popup menu...
  SendMessage(PopupList.Window, WM_CANCELMODE, 0, 0);
end;

Alternatively:

var
  MouseInSysTrayMenu: Boolean;

// drop two TTimers on the TForm at design-time, set their Enabled
// properties to false, and assign OnTimer event handlers...

procedure TMainForm_1.SysTrayIconMessageHandler(var Msg: TMessage);
var
  Pt: TPoint;
begin
  case Msg.LParam of
    ...
    WM_RBUTTONDOWN:
    begin
      // FYI, the `WM_RBUTTONDOWN` notification provides you with
      // screen coordinates where the popup menu should be displayed,
      // you don't need to use `GetCursorPos()` to figure it out...
      GetCursorPos(Pt);

      SetForegroundWindow(Handle); // <-- bug fix!
      SystemTrayPopUpMenu.PopUp(Pt.X, Pt.Y);
      PostMessage(Handle, WM_NULL, 0, 0); // <-- bug fix!

      TrackSysTrayMenuTimer.Enabled := False;
      CloseSysTrayMenuTimer.Enabled := False;
    end;
    ...
  end;
end;

procedure TMainForm_1.SystemTrayPopUpMenuPopup(Sender: TObject);
begin
  MouseInSysTrayMenu := True;

  TrackSysTrayMenuTimer.Interval := 100;
  TrackSysTrayMenuTimer.Enabled := True;

  CloseSysTrayMenuTimer.Interval := 300000; // 5 minutes
  CloseSysTrayMenuTimer.Enabled := True;
end;

procedure TMainForm_1.TrackSysTrayMenuTimerTimer(Sender: TObject);
var
  hPopupWnd: HWND;
  R: TRect;
  Pt: TPoint;
begin
  // get the HWND of the current active popup menu...
  hPopupWnd := FindWindow('#32768', nil);
  if hPopupWnd = 0 then Exit;

  // get the popup menu's current position and dimensions...
  GetWindowRect(hPopupWnd, R);

  // get the mouse's current position...
  GetCursorPos(Pt);

  if PtInRect(R, Pt) then
  begin
    // mouse is over the menu...
    if not MouseInSysTrayMenu then
    begin
      // just entered, reset timeout...
      MouseInSysTrayMenu := True;
      CloseSysTrayMenuTimer.Interval := 300000; // 5 minutes
    end;
  end else
  begin
    // mouse is not over the menu...
    if MouseInSysTrayMenu then
    begin
      // just left, reset timeout...
      MouseInSysTrayMenu := False;
      CloseSysTrayMenuTimer.Interval := 2500; // 2.5 seconds
    end;
  end;
end;

procedure TMainForm_1.CloseSysTrayMenuTimerTimer(Sender: TObject);
begin
  // timeout! Close the popup menu...
  CloseSysTrayMenuTimer.Enabled := False;
  SendMessage(PopupList.Window, WM_CANCELMODE, 0, 0);
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank You Very Much, you also helped to find an Issue in the Application......Now it not behaves like resuming......However I like to know what is that `< 300000` and for what behavior it belongs to? For the Auto Close of PopUp Menu when user keeps it open for more than 5 minutes? I asked like that because when I inside Popup Menu's Control and when keeps it open for more than 2.5 secs, it closes........What can be a good condition to add to this Code to let `TTimer` know that I only need to do this when user out of its control? **Otherwise** it must close within 5 Minutes, not 2.5 Seconds.... – GTAVLover Aug 04 '16 at 00:15
  • Yes, the 300000 is for the 5 minute handling when the mouse is over the popup menu. I have updated the code to make this clearer. – Remy Lebeau Aug 04 '16 at 00:56
  • I tried adding both of your updated examples to my current Project File, in two tries, but I can't believe why PopUp Menu never closes even my Mouse is inside it or outside it........But, when checking your examples' lines, those seem to work.........But actually why this PopUp Menu isn't closing according to `TTimer`s Timings? :-( **NOTE: I set both Timer's Interval to `0` in `Obj.Insp.` and assigned Intervals in OnPopUp Handler.......Is this the wrong thing???** – GTAVLover Aug 04 '16 at 02:02
  • I tested both examples, they work fine for me in both scenarios. You are probably not incorporating them into your project correctly. Did you verify that the timers are actually running, and fire the `OnTimer` events at the expected intervals? Please update your question with your latest code. – Remy Lebeau Aug 04 '16 at 02:18
  • Okay .......after testing for a long time, what I detected is that my PopUp Menu Window is not finding by `FindWindow()` Function........I opened Notepad using correct expected Intervals using two TTimers......Both Timers are working very well........Why my PopUp Menu Window is not finding? How can I find this Window using another way without this unreliable Function? As Window is not finding, how can `TrackSysTrayTimer` reset? :) That's why PopUp Menu not closes when mouse is outside it....... **But, `CloseSysTrayTimer` is working well as it don't want to find a Window.....** – GTAVLover Aug 04 '16 at 05:09
  • As I told you, I tested the code and it works fine for me. `FindWindow()` works fine. A popup menu has a window class name of `#32768`, [this is documented](https://msdn.microsoft.com/en-us/library/windows/desktop/ms633574.aspx#system) and reliable. If you can't get it to work, try using [`SetWinEventHook()`](https://msdn.microsoft.com/en-us/library/windows/desktop/dd373640.aspx) instead to receive `EVENT_SYSTEM_MENUPOPUPSTART` and `EVENT_SYSTEM_MENUPOPUPEND` notifications. The reported HWND should be the popup menu's window. – Remy Lebeau Aug 04 '16 at 05:49
  • Okay...........I turned on my Application's Exception Messages and I posted a Screen Shot of the Message appeared when I right clicked on Icon..........See my question. – GTAVLover Aug 04 '16 at 05:52
  • That exception is not a system error message or a VCL error message. It has to be something you wrote in code you have not shown here. It is not relevant to the issue at hand. – Remy Lebeau Aug 04 '16 at 06:34
  • Yes it is......it is not a System Error message and I wrote it into the `if not FindWindow` Function.........Now I shown it..........I told that I enabled Exception messages and when enabled them this one is also enabled because I set to `(Win32Exception = Yes)` now to test what's happening...........Now, I like to know why this `FindWindow` not working? A Problem in Window Handle? I also set app form as foreground window when this pops up. – GTAVLover Aug 04 '16 at 06:52
  • The use of an exception is irrelevant. The issue is `FindWindow()` failing. Check `GetLastError()` to find out why. Maybe you are calling it before the menu is visible onscreen, or after it is closed. I don't know. Debug your code and figure it out. It works for me, but not for you, so it has to be environmental on your system, so you will likely not get an answer here. – Remy Lebeau Aug 04 '16 at 07:03
  • No.......you can see that my PopUp Menu was already appeared when Message displayed.........I will find last Error and notify you in a moment. – GTAVLover Aug 04 '16 at 07:05
  • Okay I received `A Call to an OS Function Failed.` Error when I Got the last Error...........I think this is because `FindWindow` failed or call to it failed. – GTAVLover Aug 04 '16 at 07:19
  • I did not ask you to raise an exception. I asked you to report the error code from `GetLastError()`. But whatever. The error message you quoted is what `RaiseLastOSError()` reports when `GetLastError()` returns 0. Meaning `FindWindow()` is not actually failing, it simply doesn't find an active menu window. I can't answer why, since I can't reproduce what you are seeing. You will have to find another solution to your problem. Or else stop this exercise altogether and use the PopupMenu the way it is *meant* to be used. Auto-Closing a menu on a timeout is **not** standard UI behavior. – Remy Lebeau Aug 04 '16 at 07:35
  • StackOverflow is not a discussion forum. This line of comments has gone on far enough. The question has been answered as asked. You are having a different problem now, so please post a new question for it. – Remy Lebeau Aug 04 '16 at 07:39
  • I will ask a new question later........if you think what is causing the problem for `FindWindow` not to find PopUp Menu, [continue this discussion in chat.....](http://chat.stackoverflow.com/rooms/120080/discussion-between-gtavlover-and-remy-lebeau). – GTAVLover Aug 04 '16 at 08:05
0

Try like this:

.....
hPopupWnd := FindWindow('#32768', SystemTrayPopUpMenu);
if hPopupWnd = 0 then Exit;

.....
GetWindowRect(SystemTrayPopUpMenu.Handle, R);
Blueeyes789
  • 543
  • 6
  • 18
  • I tried it, but same thing happened. When I try Right Clicking the Notification Area Icon, the Handler `OnTrackSysTrayTimer` exits without executing because `FindWindow` can't find a PopUp Menu or it returns `NULL`. :( – GTAVLover Aug 05 '16 at 04:04
  • 1
    The given answer for this question is working very well!!!!!!! I discovered that the VCL Skin I used caused the PopUp Menu not to find by `FindWindow ` and close...........After I unloaded the Skin all worked fine......... Thank You Again.............. :) :) – GTAVLover Aug 17 '16 at 02:48