Managarm
Managarm

Reputation: 1121

Override default window/system menu

I want to add an "Always-On-Top"-menuentry to the system menu of all windows (the menu which opens when you right click the titlebar or click the icon). I'd prefer C# or C++, but if worst comes to worst I'll also use VB...

I know there are some applications like Dexpot which do this, but I was unable to find useful source code or free applications which do this for all windows and not just their own. I also know that there are other ways to achieve this functionality (AutoHotkeys or small programs which live in the system tray and let you select windows which should stay on top), but I'm looking for a more fluent and intuitive way. Ideally I'd add a small pin button to the titlebar, but my guess is that that's much more involved, so I'll stick with the menu variant for now.

Ideas? Thanks!

Upvotes: 0

Views: 904

Answers (2)

Loathing
Loathing

Reputation: 5291

Use the AddMenuItems() method and test using MS-Paint. One thing I noticed is that after the program is closed, the modified system menu becomes wonky. Possibly this is because the events are not coming from the process' UI thread. A possible work-around is in the ApplicationExit event to call GetMenu(hMainWindowHandle, true), where true means revert the menu.

public static class AlwaysOnTop {

    static AlwaysOnTop() {
        Application.ApplicationExit += delegate {
            try {
                foreach (DictionaryEntry de in htThreads) {
                    Hook h = (Hook) de.Value;
                    RemoveMenu(h.hMenu, h.uniqueId, 0);
                    //DeleteMenu(h.hMenu, h.uniqueId, 0);
                    UnhookWinEvent(h.hWinEventHook);
                }
            } catch {
            }
        };
    }

    private const int EVENT_OBJECT_INVOKED = 0x8013;
    private const int OBJID_SYSMENU = -1;
    private const int WINEVENT_OUTOFCONTEXT = 0;
    private const int MF_STRING = 0x00000000;
    private const int HWND_TOPMOST = -1;
    private const int HWND_NOTOPMOST = -2;
    private const int SWP_NOMOVE = 0x0002;
    private const int SWP_NOSIZE = 0x0001;
    private const uint MF_UNCHECKED = 0x00000000;
    private const uint MF_CHECKED = 0x00000008;

    [DllImport("user32.dll")]
    private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

    [DllImport("user32.dll")]
    private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, String lpNewItem);

    [DllImport("user32.dll", SetLastError = true)]
    static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    [DllImport("user32.dll",SetLastError=true)]
    private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern int UnhookWinEvent(IntPtr hWinEventHook);

    [DllImport("user32.dll", SetLastError=true)]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll")]
    private static extern bool CheckMenuItem(IntPtr hMenu, uint uIDCheckItem, uint uCheck);

    [DllImport("user32.dll")]
    private static extern bool RemoveMenu(IntPtr hMenu, uint uPosition, uint uFlags);

    //[DllImport("user32.dll")]
    //private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);

    private static Hashtable htThreads = new Hashtable();
    private static WinEventProc CallWinEventProc = new WinEventProc(EventCallback);
    private delegate void WinEventProc(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime);
    private static void EventCallback(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime) {
        //callback function, called when message is intercepted
        if (iEvent == EVENT_OBJECT_INVOKED) {
            if (idObject == OBJID_SYSMENU) {
                Hook h = (Hook) htThreads[(uint) dwEventThread];
                if (h != null && h.uniqueId == idChild) {
                    bool b = !h.Checked;
                    if (b)
                        SetWindowPos(h.hMainWindowHandle, (IntPtr) HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
                    else
                        SetWindowPos(h.hMainWindowHandle, (IntPtr) HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);

                    CheckMenuItem(h.hMenu, h.uniqueId, (b ? MF_CHECKED : MF_UNCHECKED));
                    h.Checked = b;
                }
            }
        }
    }

    private class Hook {
        public uint uniqueId = 1001;
        public IntPtr hWinEventHook;
        public IntPtr hMenu;
        public IntPtr hMainWindowHandle;
        public bool Checked;
    }

    public static void AddMenuItems() {
        Process[] arr = Process.GetProcesses();
        foreach (Process p in arr) {
            if (p.MainWindowHandle == IntPtr.Zero)
                continue;

            if (p.ProcessName != "mspaint") // <-- remove or change this line
                continue;

            IntPtr hMenu = GetSystemMenu(p.MainWindowHandle, false);
            if (hMenu == IntPtr.Zero)
                continue;

            bool b = AppendMenu(hMenu, MF_STRING, 1001, "Always On Top");
            uint pid = 0;
            uint tid = GetWindowThreadProcessId(p.MainWindowHandle, out pid);

            Hook h = (Hook) htThreads[tid];
            if (h == null) {
                h = new Hook();
                h.hMenu = hMenu;
                h.hWinEventHook = SetWinEventHook(EVENT_OBJECT_INVOKED, EVENT_OBJECT_INVOKED, IntPtr.Zero, CallWinEventProc, pid, tid, WINEVENT_OUTOFCONTEXT);
                h.hMainWindowHandle = p.MainWindowHandle;
                htThreads[tid] = h;
            }
        }
    }
}

Upvotes: 1

Loathing
Loathing

Reputation: 5291

Here is an example of using a ToolStripDropDown instead of the default window menu. It can be made to look more like the default window menu by setting the background color, font and adding some icons if needed.

It was surprisingly more difficult to hide the default window menu. Maybe there is a better way. The failed attempts at hiding the menu are left in the code below. The default menu is still displayed when right clicking on the caption bar.

public class FormCustomMenu : Form {

    WindowMenu WindowMenu = new WindowMenu();

    public FormCustomMenu() {
        //this.ShowIcon = false;
    }

    private const int WM_INITMENU = 0x116;
    private const int WM_INITMENUPOPUP = 0x117;
    private const int WM_SYSCOMMAND = 0x112;
    protected override void WndProc(ref Message m) {
        if (m.Msg == WM_SYSCOMMAND) { //WM_INITMENU || m.Msg == WM_INITMENUPOPUP) {}
            Point pt = Cursor.Position;
            int h = SystemInformation.CaptionHeight;
            Rectangle r = new Rectangle(this.Location, new Size(h, h));
            if (!r.Contains(pt) || Cursor.Current != Cursors.Default)
                base.WndProc(ref m);
            else {
                Rectangle r2 = RectangleToScreen(this.ClientRectangle);
                WindowMenu.Show(r2.Location);
            }
        }
        else {
            base.WndProc(ref m);
        }
    }

/*
    Failed attempts at hiding the default window menu.

    protected override void OnHandleCreated(EventArgs e) {
        base.OnHandleCreated(e);
        //IntPtr hMenu = GetSystemMenu(Handle, false);
        //SendMessage(hMenu, WM_SETREDRAW, (IntPtr) 0, (IntPtr) 0);
        //int count = GetMenuItemCount(hMenu);
        //for (int i = count - 1; i >= 3; i--)
        //  RemoveMenu(hMenu, (uint) i, MF_BYPOSITION);
    }

    [DllImport("user32.dll")]
    public static extern int GetMenuItemCount(IntPtr hMenu);
    [DllImport("user32.dll")]
    private static extern bool RemoveMenu(IntPtr hMenu, uint uPosition, uint uFlags);

    //[DllImport("user32.dll")]
    //public static extern IntPtr DestroyMenu(IntPtr hMenu);
    [DllImport("user32.dll")]
    public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
    [DllImport("user32.dll")]
    private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);

    private const int MF_BYPOSITION = 0x00000400; 
    private const int WM_SETREDRAW = 11;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, IntPtr wParam, IntPtr lParam);


    // this also removes the min/max/close buttons:
    //private const int WS_SYSMENU = 0x80000;
    //protected override CreateParams CreateParams {
    //  get {
    //      var p = base.CreateParams;
    //      p.Style = p.Style & ~WS_SYSMENU;
    //      return p;
    //  }
    //}
    */
}

public class WindowMenu : ToolStripDropDown {
    public WindowMenu() {
        Items.Add(new ToolStripMenuItem("Custom1"));
    }
}

Upvotes: 0

Related Questions