【问题标题】:Override default window/system menu覆盖默认窗口/系统菜单
【发布时间】:2015-08-19 21:18:31
【问题描述】:

我想在所有窗口的系统菜单中添加一个“Always-On-Top”菜单项(右键单击标题栏或单击图标时打开的菜单)。我更喜欢 C# 或 C++,但如果最坏的情况发生,我也会使用 VB...

我知道有一些像 Dexpot 这样的应用程序可以做到这一点,但我找不到有用的源代码或免费应用程序可以为所有窗口而不是它们自己的窗口执行此操作。 我也知道还有其他方法可以实现此功能(AutoHotkeys 或位于系统托盘中的小程序,让您选择应该留在顶部的窗口),但我正在寻找一种更流畅和直观的方式。理想情况下,我会在标题栏上添加一个小的 pin 按钮,但我猜这涉及的内容更多,所以我现在将坚持使用菜单变体。

想法?谢谢!

【问题讨论】:

  • 可以使用DllImport("user32.dll") SetMenuItemInfo函数。然而,如果你想要一个真正定制的菜单,你最好继承ToolStripDropDown 并禁用默认的窗口菜单。然后在单击标题栏区域时显示ToolStripDropDown 而不是默认菜单。
  • “我更喜欢 C# 或 C++,但如果最坏的情况发生,我也会使用 VB...” - VB 可以做 c+​​+ 做不到的事情?
  • @IInspectable 那么你有更多的条目:)

标签: windows winforms winapi


【解决方案1】:

使用AddMenuItems() 方法并使用 MS-Paint 进行测试。我注意到的一件事是程序关闭后,修改后的系统菜单变得不稳定。这可能是因为事件不是来自进程的 UI 线程。一种可能的解决方法是在 ApplicationExit 事件中调用 GetMenu(hMainWindowHandle, true),其中 true 表示恢复菜单。

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;
            }
        }
    }
}

【讨论】:

  • 我还没有测试过这个,但它看起来像是我可以工作的开始,所以我会接受它:) 谢谢!
  • 谢谢。您也可以考虑将SetWindowsHookExWH_CALLWNDPROC 常量一起使用。有几个线程说它需要 C++,即support.microsoft.com/en-us/kb/318804(底部的全局挂钩)。我想 DLL 注入的菜单项功能会持续存在,这与上面的 C# 代码不同,其中菜单项的功能取决于正在运行的应用程序(因此在 Application.Exit 上将其删除)
【解决方案2】:

这是一个使用ToolStripDropDown 代替默认窗口菜单的示例。通过设置背景颜色、字体并在需要时添加一些图标,可以使其看起来更像默认窗口菜单。

隐藏默认窗口菜单非常困难。也许有更好的方法。隐藏菜单的失败尝试保留在下面的代码中。右键单击标题栏时仍显示默认菜单。

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"));
    }
}

【讨论】:

  • 我认为您错过了问题中的 "all windows" 警告;即如何将其添加到系统中的所有窗口
  • 那部分很明显。创建一个通用 Form 类,并让所有其他表单子类化该 Form。或者创建一个扩展方法,使用IMessageFilter 接口预过滤WM_SYSCOMMAND 消息。
  • 不,我认为您仍然没有抓住重点。不是所有的窗体都属于你的程序,所有的窗口都属于所有的程序。例如。如果 Microsoft Word 正在运行,其系统菜单中将包含“始终位于最前面”菜单项。
  • 我将发布另一个答案,因为这需要不同的方法。
猜你喜欢
  • 2015-12-15
  • 1970-01-01
  • 2018-11-09
  • 1970-01-01
  • 1970-01-01
  • 2022-01-12
  • 2020-08-10
  • 2014-11-24
  • 2011-08-03
相关资源
最近更新 更多