locked
Window context menu handle. RRS feed

  • Question

  • Hi. I am trying to obtain a handle to context menu of a windows that dose not belong to my application. the User32 exported function GetMenu dose works with some Windows and fails with others. Is there something im missing? Is it even possible to obtain the handle with GetMenu?
    Wednesday, February 17, 2010 3:59 PM

Answers

  • using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Windows.Forms;
    
    public class Form1 : Form
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    
        ListBox listBox;
        Button button1, button2;
    
        public Form1()
        {
            Text = "System Menu Test";
            ClientSize = new Size(640, 480);
    
            listBox = new ListBox
            {
                Dock = DockStyle.Fill,
                Location = new Point(10, 35),
                Size = new Size(320, 430),
                Font = new Font("Courier New", 9),
                BorderStyle = BorderStyle.None,
            };
            listBox.SelectedIndexChanged += (object sender, EventArgs e) => button2.Enabled = (sender as ListBox).SelectedIndex >= 0;
            Controls.Add(listBox);
    
            Panel panel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
            Controls.Add(panel);
    
            button1 = new Button { Text = "Refresh", AutoSize = true };
            button1.Click += (object sender, EventArgs e) => Populate();
            panel.Controls.Add(button1);
    
            button2 = new Button { Text = "Invoke System Menu", AutoSize = true, Enabled = false };
            button2.Click += (object sender, EventArgs e) => InvokeSystemMenu();
            panel.Controls.Add(button2);
        }
    
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int GetWindowText(HandleRef hWnd, StringBuilder lpString, int nMaxCount);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool IsWindow(HandleRef hWnd);
        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern bool SetWindowPos(HandleRef hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int flags);
    
        private class Item
        {
            public IntPtr Handle;
            public string Text;
    
            public override string ToString()
            {
                return Text;
            }
        }
    
        private void Populate()
        {
            listBox.Items.Clear();
            button2.Enabled = false;
            WindowFinder finder = new WindowFinder();
            IntPtr[] mainWindows = finder.FindMainWindows();
            foreach (IntPtr hwnd in mainWindows)
            {
                StringBuilder text = new StringBuilder(512);
                GetWindowText(new HandleRef(this, hwnd), text, text.Capacity);
                listBox.Items.Add(new Item { Handle = hwnd, Text = String.Format("[{0}] {1}", hwnd.ToString("X8"), text) });
            }
        }
    
        private void InvokeSystemMenu()
        {
            if (listBox.SelectedIndex >= 0)
            {
                Item item = (Item)listBox.Items[listBox.SelectedIndex];
                IntPtr hwnd = item.Handle;
                Point point = PointToScreen(Point.Empty);
                if (IsWindow(new HandleRef(this, hwnd)))
                {
                    // SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE);
                    SetWindowPos(new HandleRef(this, hwnd), IntPtr.Zero, 0, 0, 0, 0, 3);
                    SendMessage(new HandleRef(this, hwnd), 0x0313, IntPtr.Zero, (IntPtr)((point.Y << 16) | point.X));
                }
            }
        }
    }
    
    internal class WindowFinder
    {
        private delegate bool EnumThreadWindowsCallback(IntPtr hWnd, IntPtr lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool EnumWindows(EnumThreadWindowsCallback callback, IntPtr extraData);
        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern IntPtr GetWindow(HandleRef hWnd, int uCmd);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern bool IsWindowVisible(HandleRef hWnd);
    
        private List<IntPtr> handles;
    
        private bool EnumWindowsCallback(IntPtr handle, IntPtr extraParameter)
        {
            if (IsMainWindow(handle))
                handles.Add(handle);
            return true;
        }
    
        public IntPtr[] FindMainWindows()
        {
            handles = new List<IntPtr>();
            EnumThreadWindowsCallback callback = new EnumThreadWindowsCallback(EnumWindowsCallback);
            EnumWindows(callback, IntPtr.Zero);
            GC.KeepAlive(callback);
            return handles.ToArray();
        }
    
        private bool IsMainWindow(IntPtr handle)
        {
            return (!(GetWindow(new HandleRef(this, handle), 4) != IntPtr.Zero) && IsWindowVisible(new HandleRef(this, handle)));
        }
    }
    
    • Marked as answer by NullRefrenceException Friday, February 19, 2010 9:54 AM
    • Edited by Tergiver Friday, February 19, 2010 8:48 PM no need for GetSystemMenu and TrackPopupMenu
    Friday, February 19, 2010 4:16 AM

All replies

  • Not all application frameworks (or even framework-less applications) use a system menu object to display menus. They might use a regular window, in which case you could find it with FindWindow (given that it had some unique quality to find), or a global hook, but this is a fool's errand. The best you can hope for is "most" of them, never all of them. It is possible, if less likely, that an application can pop up something that appears to be a stock window/menu when it's nothing more than drawing code.

    One might think a global hook could be used to watch for WM_CONTEXTMENU, but just because an app receives WM_CONTEXTMENU doesn't mean it opens a menu (also there are a lot of ignorant programmers out there that display context menus in response to WM_RBUTTONDOWN instead of WM_CONTEXTMENU).

    Perhaps you can look at the problem another way. What is your ultimate goal? Maybe there is different way to achieve it.
    Wednesday, February 17, 2010 9:11 PM
  • Basically im writing a replacement shell. What i want is to simulate the taskbar and show the windows original context menu once clicked on window representation item in task bar.
    Thursday, February 18, 2010 8:46 AM
  • Ah hah! See? It's always better to ask for what you really want, rather than what you think you want.

    That's called the System Menu. It is not a context menu. All you need is:
    http://msdn.microsoft.com/en-us/library/ms647985(VS.85).aspx
    Thursday, February 18, 2010 2:10 PM
  • Thank you very much! I will check it out.
    Thursday, February 18, 2010 2:15 PM
  • Ok i managed to obtain the handle of the system menu and never null but the problem is that trackpopupex function fails to show the menu and reports invalid handle. It shows the menu normaly for the windows that created within my own application but not any other process window. Any thoughts?
    Thursday, February 18, 2010 4:56 PM
  • I did some searching on the Net and found a few cases where people were encountering this, but not one single solution.

    How are you obtaining the HWND values you are trying to display system menus for?
    Thursday, February 18, 2010 10:20 PM
  • By enumerating the windows EnumWindows.
    Its strange as other functions like GetMenuItemsCount works for some windows and dont work for others. Still the TrackPopupOnly works for my application windows and not any other process window :(

    Friday, February 19, 2010 12:25 AM
  • I found that there are two different system menus in use by applications on my machine (64-bit Vista):

    1) Old style, no left-side bitmap area divider.
    2) New style with the left-side bitmap area divider.

    Explorer is capable of invoking both. I found the differences by simply right-clicking on various items in my taskbar. In every case I tested, the old-style menus were not displayed by using GetSystemMenu and TrackPopupMenuEx, but all of the new-style ones were fine.

    So now to discover why the difference...

    Below is the test program I used.
    Friday, February 19, 2010 4:16 AM
  • using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Windows.Forms;
    
    public class Form1 : Form
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    
        ListBox listBox;
        Button button1, button2;
    
        public Form1()
        {
            Text = "System Menu Test";
            ClientSize = new Size(640, 480);
    
            listBox = new ListBox
            {
                Dock = DockStyle.Fill,
                Location = new Point(10, 35),
                Size = new Size(320, 430),
                Font = new Font("Courier New", 9),
                BorderStyle = BorderStyle.None,
            };
            listBox.SelectedIndexChanged += (object sender, EventArgs e) => button2.Enabled = (sender as ListBox).SelectedIndex >= 0;
            Controls.Add(listBox);
    
            Panel panel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true };
            Controls.Add(panel);
    
            button1 = new Button { Text = "Refresh", AutoSize = true };
            button1.Click += (object sender, EventArgs e) => Populate();
            panel.Controls.Add(button1);
    
            button2 = new Button { Text = "Invoke System Menu", AutoSize = true, Enabled = false };
            button2.Click += (object sender, EventArgs e) => InvokeSystemMenu();
            panel.Controls.Add(button2);
        }
    
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int GetWindowText(HandleRef hWnd, StringBuilder lpString, int nMaxCount);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool IsWindow(HandleRef hWnd);
        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern bool SetWindowPos(HandleRef hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int flags);
    
        private class Item
        {
            public IntPtr Handle;
            public string Text;
    
            public override string ToString()
            {
                return Text;
            }
        }
    
        private void Populate()
        {
            listBox.Items.Clear();
            button2.Enabled = false;
            WindowFinder finder = new WindowFinder();
            IntPtr[] mainWindows = finder.FindMainWindows();
            foreach (IntPtr hwnd in mainWindows)
            {
                StringBuilder text = new StringBuilder(512);
                GetWindowText(new HandleRef(this, hwnd), text, text.Capacity);
                listBox.Items.Add(new Item { Handle = hwnd, Text = String.Format("[{0}] {1}", hwnd.ToString("X8"), text) });
            }
        }
    
        private void InvokeSystemMenu()
        {
            if (listBox.SelectedIndex >= 0)
            {
                Item item = (Item)listBox.Items[listBox.SelectedIndex];
                IntPtr hwnd = item.Handle;
                Point point = PointToScreen(Point.Empty);
                if (IsWindow(new HandleRef(this, hwnd)))
                {
                    // SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE);
                    SetWindowPos(new HandleRef(this, hwnd), IntPtr.Zero, 0, 0, 0, 0, 3);
                    SendMessage(new HandleRef(this, hwnd), 0x0313, IntPtr.Zero, (IntPtr)((point.Y << 16) | point.X));
                }
            }
        }
    }
    
    internal class WindowFinder
    {
        private delegate bool EnumThreadWindowsCallback(IntPtr hWnd, IntPtr lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool EnumWindows(EnumThreadWindowsCallback callback, IntPtr extraData);
        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern IntPtr GetWindow(HandleRef hWnd, int uCmd);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern bool IsWindowVisible(HandleRef hWnd);
    
        private List<IntPtr> handles;
    
        private bool EnumWindowsCallback(IntPtr handle, IntPtr extraParameter)
        {
            if (IsMainWindow(handle))
                handles.Add(handle);
            return true;
        }
    
        public IntPtr[] FindMainWindows()
        {
            handles = new List<IntPtr>();
            EnumThreadWindowsCallback callback = new EnumThreadWindowsCallback(EnumWindowsCallback);
            EnumWindows(callback, IntPtr.Zero);
            GC.KeepAlive(callback);
            return handles.ToArray();
        }
    
        private bool IsMainWindow(IntPtr handle)
        {
            return (!(GetWindow(new HandleRef(this, handle), 4) != IntPtr.Zero) && IsWindowVisible(new HandleRef(this, handle)));
        }
    }
    
    • Marked as answer by NullRefrenceException Friday, February 19, 2010 9:54 AM
    • Edited by Tergiver Friday, February 19, 2010 8:48 PM no need for GetSystemMenu and TrackPopupMenu
    Friday, February 19, 2010 4:16 AM
  • It is NOT a simple case of 32-bit processes vs. 64-bit processes because I built a 32-bit straight-api app which uses the new-style (system menu works just fine) and Internet Explorer 8 (32-bit) uses the old-style menu (invalid menu handle).

    Curiously Internet Explorer 8 (64-bit) uses the new-style menu and the test program successfully invokes its system menu.

    Friday, February 19, 2010 4:28 AM
  • I found something really strange. Remember I said IE 8 64-bit uses the new-style system menu? I have found that after some period of time, IE 8 64-bit switches from the new-style to the old-style. I have seen it happen now twice while messing around. I have found no pattern, it just switches after a while.
    Friday, February 19, 2010 5:57 AM
  • I changed the code above to send WM_INITMENU so that the menu would have the correct items enabled/disabled, etc. It works.. sorta.

    It always works if the application enables/disables, checks/unchecks the menu item directly, but it does not work on the stock items (like Restore), except for the very first time.

    This lead me to Spy on an application (actually several) when it was minimized to the taskbar to see what messages were getting sent to it by explorer:

    <00001> 001001EE S WM_WINDOWPOSCHANGING lpwp:0056E9BC
    <00002> 001001EE R WM_WINDOWPOSCHANGING
    <00003> 001001EE S WM_WINDOWPOSCHANGED lpwp:0056E9BC
    <00004> 001001EE R WM_WINDOWPOSCHANGED
    <00005> 001001EE S WM_ACTIVATEAPP fActive:True dwThreadID:00000738
    <00006> 001001EE R WM_ACTIVATEAPP
    <00007> 001001EE S WM_NCACTIVATE fActive:True
    <00008> 001001EE R WM_NCACTIVATE
    <00009> 001001EE S WM_ACTIVATE fActive:WA_ACTIVE fMinimized:True hwndPrevious:(null)
    <00010> 001001EE R WM_ACTIVATE
    <00011> 001001EE S WM_GETICON fType:False
    <00012> 001001EE R WM_GETICON hicon:003D028D
    <00013> 001001EE S message:0x0313 [Unknown] wParam:00000000 lParam:04A6007D
    <00014> 001001EE S message:0x00AE [Unknown] wParam:00001001 lParam:00000000
    <00015> 001001EE R message:0x00AE [Unknown] lResult:00000000
    <00016> 001001EE S message:0x00AE [Unknown] wParam:00001001 lParam:00000000
    <00017> 001001EE R message:0x00AE [Unknown] lResult:00000000
    <00018> 001001EE S message:0x00AE [Unknown] wParam:00001001 lParam:00000000
    <00019> 001001EE R message:0x00AE [Unknown] lResult:00000000
    <00020> 001001EE S message:0x00AE [Unknown] wParam:00001001 lParam:00000000
    <00021> 001001EE R message:0x00AE [Unknown] lResult:00000000
    <00022> 001001EE S WM_ENTERMENULOOP fIsTrackPopupMenu:False
    <00023> 001001EE R WM_ENTERMENULOOP
    <00024> 001001EE S WM_SETCURSOR hwnd:001001EE nHittest:HTCAPTION wMouseMsg:0000
    <00025> 001001EE R WM_SETCURSOR fHaltProcessing:False
    <00026> 001001EE S WM_INITMENU hmenuInit:07E80465
    <00027> 001001EE R WM_INITMENU
    <00028> 001001EE S WM_INITMENUPOPUP hmenuPopup:02060841 uPos:0 fSystemMenu:True
    <00029> 001001EE R WM_INITMENUPOPUP
    <00030> 001001EE S message:0x0093 [Unknown] wParam:00000000 lParam:0056E704
    <00031> 001001EE R message:0x0093 [Unknown] lResult:00000000
    <00032> 001001EE S WM_ENTERIDLE fuSource:MSGF_MENU hwnd:000E0BA6
    <00033> 001001EE R WM_ENTERIDLE

    <00034> 001001EE P WM_TIMER wTimerID:20785 tmprc:00000000
    <00035> 001001EE S WM_CAPTURECHANGED hwndNewCapture:00000000
    <00036> 001001EE R WM_CAPTURECHANGED
    <00037> 001001EE S WM_UNINITMENUPOPUP
    <00038> 001001EE R WM_UNINITMENUPOPUP
    <00039> 001001EE S WM_MENUSELECT uItem:0 fuFlags:FFFF (menu was closed) hmenu:00000000
    <00040> 001001EE R WM_MENUSELECT
    <00041> 001001EE S WM_EXITMENULOOP fIsTrackPopupMenu:False
    <00042> 001001EE R WM_EXITMENULOOP
    <00043> 001001EE R message:0x0313 [Unknown] lResult:00000000
    <00044> 001001EE S WM_NCACTIVATE fActive:False
    <00045> 001001EE R WM_NCACTIVATE fDeactivateOK:True
    <00046> 001001EE S WM_ACTIVATE fActive:WA_INACTIVE fMinimized:True hwndPrevious:(null)
    <00047> 001001EE R WM_ACTIVATE
    <00048> 001001EE S WM_ACTIVATEAPP fActive:False dwThreadID:00000F28
    <00049> 001001EE R WM_ACTIVATEAPP

    The first section represents the messages sent when right-clicking on the taskbar button. The second section is the result of activating Spy, thus closing down the menu. The message:0xXXXX [Unknown] messages I believe are DWM messages that Spy is not aware of. What's most interesting here is this line:

    <00022> 001001EE S WM_ENTERMENULOOP fIsTrackPopupMenu:False

    Note the fIsTrackPopupMenu:False. That means that the application entered a system modal loop for tracking a menu (the system menu), but it was not invoked by TrackPopupMenu! Explorer is not using TrackPopupMenu to invoke the system menu in applications.

    I haven't discovered yet how its doing it. Maybe it uses a global hook to inject code into all processes and invokes the system menu from within the process?
    Friday, February 19, 2010 7:01 AM
  • Since there are no injection DLLs in the processes I looked at, you might initially discount the idea of a global hook. But remember Microsoft wrote both the OS and Explorer.exe app. It's entirely possible that the hook procedure lives in a system DLL, likely User32.dll. Assuming that it is a hook.
    Friday, February 19, 2010 7:10 AM
  • Thank you for doing such a good research. I found that the problem was that i wasnt passing my app hWnd to the TrackPopup but rather the hWnd that actually owned the menu. Once i passed my app hWnd menus started poping up :)
    I havent tested on any other os higer than Xp yet so i guess i will have to do some message snooping to figure out whats going on.
    Thank you very much for your help!

    Friday, February 19, 2010 9:54 AM
  • It's not going to work 100%, even on xp, because sending WM_INITMENU is not sufficient to get the menu properly updated.

    It would be nice to discover how Explorer does it and I'll continue pursuing that over the weekend, but if I can't, it can definitely be done with a global hook. Writing a global hook that works on 64-bit as well as 32-bit OSes is not for the weak, but I've done it before and have a template I can share with you.
    Friday, February 19, 2010 2:51 PM
  • Thanks.
    I have no problem writng the hook itself. The problem is that it would require some kind of ipc framework to comunicate with managed code.
    Anyhow if you have any info im all eyes and ears :)

    Friday, February 19, 2010 2:55 PM
  • Actually this requires only the most rudementary IPC of them all: A single PostMessage from your "shell" to the hook. The message would be as simple as:

    PostMessage(hwnd, RegisterWindowMessage("WM_INVOKESELFSYSTEMMENU"), point.x, point.y);

    The hook (which executes inside the target application's process) would then invoke its own system menu at the location provided.

    It's clear from the spied messages that Explorer is causing the window to invoke its own context menu (in process). We know this because WM_ENTERMENULOOP would not be sent to the window if Explorer were invoking the menu. WM_ENTERMENULOOP would be sent to Explorer, not to the target. The only question is whether it's doing so through a hook, or through some undocumented message, or maybe through another, possibly undocumented API function. I think the undocumented message is unlikely because Spy doesn't show one. You might think that if a message were being sent and a hook procedure was intercepting it that Spy would show that, but Spy uses hooks to see messages and if this imagined hook procedure gets the message and intentionally fails to continue the hook chain (entirely plausable), Spy wouldn't see it.
    Friday, February 19, 2010 3:08 PM
  • Yes pretty possible. Ah i wish ms had documentaded this and caused us much less headache :P
    Friday, February 19, 2010 3:11 PM
  • Doh! It is a private message!

    I was looking at the Spy output above and noticed that one [unknown] message in the second block:

    <00043> 001001EE R message:0x0313 [Unknown] lResult:00000000

    I then noticed that it was 'R' which means that this was the message exiting and its return value. So I looked for its beginning:

    <00013> 001001EE S message:0x0313 [Unknown] wParam:00000000 lParam:04A6007D


    This message is WM_POPUPSYSTEMMENU, an undocumented message. The lParam value is the X and Y position (packed into 32 bits).

    I tried it and it works, but not when the window is minimized. That's the reason for other messages in the list:

    WM_WINDOWPOSCHANGING, WM_ACTIVATEAPP, WM_ACTIVATE, etc.

    So what Explorer is doing is resizing the window, activating it, and then sending WM_POPUPSYSTEMMENU.

    Below is an incomplete (but working) version of InvokeSystemMenu (in sample above). It needs to be fixed so it works for both minimized and not, but it's a start:

        private void InvokeSystemMenu()
        {
            if (listBox.SelectedIndex >= 0)
            {
                Item item = (Item)listBox.Items[listBox.SelectedIndex];
                IntPtr hwnd = item.Handle;
                Point point = PointToScreen(Point.Empty);
                SetWindowPos(new HandleRef(this, hwnd), new HandleRef(null, IntPtr.Zero), 0, 0, 0, 0, 3);
                SendMessage(new HandleRef(this, hwnd), 0x0313, IntPtr.Zero, (IntPtr)((point.Y << 16) | point.X));
            }
        }
    
    Friday, February 19, 2010 7:15 PM
  • Actually it works just fine both minimized and not. The only thing you have to do is figure out where to invoke the menu when the window is not minimized so that it appears where it normally would.

    Note, the SetWindowPos call translated to C++ is:
    SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE);

    So all it is doing is activating the window and bringing it to the front.
    Friday, February 19, 2010 7:32 PM
  • I take it back, there is nothing you need to do for minimized vs. non-minimized. Explorer always displays the system menu down on the taskbar. So that code should suffice.
    Friday, February 19, 2010 7:57 PM
  • Edited the test application code above to reflect this new-found knowledge.
    Friday, February 19, 2010 8:49 PM
  • Excellent work!
    Sunday, February 21, 2010 12:30 PM