如何自定义 Windows 窗体的系统菜单?

51

我想在我的应用程序中添加经典的“关于”菜单项。我想将其添加到应用程序的“系统菜单”中(当我们单击左上角的应用程序图标时弹出的那个菜单)。 那么,我该如何在.NET中实现它?

5个回答

103

使用GetSystemMenu函数,Windows很容易获得窗体系统菜单的句柄以进行自定义。难点在于,您需要使用AppendMenuInsertMenuDeleteMenu等函数对返回的菜单进行适当的修改,就像直接使用Win32 API编程一样。

但是,如果您只想添加一个简单的菜单项,那么事情就不是那么困难了。例如,在菜单末尾只需使用AppendMenu函数即可添加一个或两个项目。要进行更高级的操作(如在菜单中间插入项目、在菜单项上显示位图、显示选中的菜单项、设置默认的菜单项等),需要进行更多的工作。但是一旦知道了如何完成这些操作,您就可以尽情发挥了。有关与菜单相关的函数的文档告诉您所有信息。

以下是为窗体系统菜单(也称为窗口菜单)添加分隔符和“关于”项的完整代码:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public class CustomForm : Form
{
    // P/Invoke constants
    private const int WM_SYSCOMMAND = 0x112;
    private const int MF_STRING = 0x0;
    private const int MF_SEPARATOR = 0x800;

    // P/Invoke declarations
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool AppendMenu(IntPtr hMenu, int uFlags, int uIDNewItem, string lpNewItem);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool InsertMenu(IntPtr hMenu, int uPosition, int uFlags, int uIDNewItem, string lpNewItem);


    // ID for the About item on the system menu
    private int SYSMENU_ABOUT_ID = 0x1;

    public CustomForm()
    {
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        // Get a handle to a copy of this form's system (window) menu
        IntPtr hSysMenu = GetSystemMenu(this.Handle, false);

        // Add a separator
        AppendMenu(hSysMenu, MF_SEPARATOR, 0, string.Empty);

        // Add the About menu item
        AppendMenu(hSysMenu, MF_STRING, SYSMENU_ABOUT_ID, "&About…");
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        // Test if the About item was selected from the system menu
        if ((m.Msg == WM_SYSCOMMAND) && ((int)m.WParam == SYSMENU_ABOUT_ID))
        {
            MessageBox.Show("Custom About Dialog");
        }

    }
}

以下是最终产品的外观:

  具有自定义系统菜单的表单


3
没关系,我找到了评论的答案。你只需要添加字符串"&About...\tAlt+A",使用制表符(\t)分隔即可。 - BoltBait
很酷!但是我能在多个项目(lib,dll)中重复使用它吗? - 123iamking
@123iamking 你是在问代码的许可证吗?就像 Stack Overflow 上的所有内容一样,它是 cc by-sa。这意味着你可以自由使用它,并随意修改,只要你提供归属(即包括我的名字)。源代码中的注释就足够了。如果你在问实现方面,当然可以:你可以将自定义表单类打包成 DLL 或 LIB 或其他格式,然后从那里继承所有的表单。 - Cody Gray
@Cody Gray:谢谢,知道这个对我很有帮助。不过我想问的是如何在多个项目中重复使用这个?我的意思是我不想每次创建新项目时都要复制和粘贴这段代码(我只需要设计“关于”对话框一次即可): )。顺便说一句,感谢您的快速回复:)。 - 123iamking
1
@Yoda 现在轮到我为迟到道歉了... :-) 要向菜单项添加图标,请设置 MENUITEMINFOhbmpItem 字段。 在 Vista 和更高版本中,您可以指定一个 PARGB32 位图以获得漂亮的 alpha-blended 图标。 将来,如果您有一个新问题,请提出一个新问题,而不是将其发布为评论。 您会得到更好的答案。 - Cody Gray
显示剩余4条评论

15
我将 Cody Gray 的解决方案推进了一步,并将其制作成可重复使用的类。它是我的应用程序日志提交工具的一部分,应该将其关于信息隐藏在系统菜单中。 https://github.com/ygoe/FieldLog/blob/master/LogSubmit/Unclassified/UI/SystemMenu.cs 它可以像这样轻松地使用:
class MainForm : Form
{
    private SystemMenu systemMenu;

    public MainForm()
    {
        InitializeComponent();

        // Create instance and connect it with the Form
        systemMenu = new SystemMenu(this);

        // Define commands and handler methods
        // (Deferred until HandleCreated if it's too early)
        // IDs are counted internally, separator is optional
        systemMenu.AddCommand("&About…", OnSysMenuAbout, true);
    }

    protected override void WndProc(ref Message msg)
    {
        base.WndProc(ref msg);

        // Let it know all messages so it can handle WM_SYSCOMMAND
        // (This method is inlined)
        systemMenu.HandleMessage(ref msg);
    }

    // Handle menu command click
    private void OnSysMenuAbout()
    {
        MessageBox.Show("My about message");
    }
}

我想尝试你的技巧,但是我找不到SystemMenu的参考。它在哪里? - John
@John,这是在链接的GitHub存储库中提供所述功能并处理P/Invoke的类。 - Yoda
@John 噢,我错过了那个评论了...我也更新了链接,使其更具未来性。 - ygoe
我必须通知您,您的代码(GitHub存储库)的修改版本已被用作示例(而不是答案本身的一部分)在此问题中。您的代码已经被修改,添加了一个允许在系统菜单中插入位图的操作。但是,如果您不同意,我将删除它。请告诉我您的想法。 - Jimi
@Jimi 没问题!我已经在本地参考副本中记录了您帖子的链接,以便将来需要图标支持时知道该去哪里查找。 :-) - ygoe

9

我知道这个答案有些陈旧,但我真的很喜欢LonelyPixel的答案。不过,它需要一些修改才能在WPF中正确运行。下面是我写的一个WPF版本,所以你不必自己动手 :)

/// <summary>
/// Extends the system menu of a window with additional commands.
/// Adapted from:
/// https://github.com/dg9ngf/FieldLog/blob/master/LogSubmit/Unclassified/UI/SystemMenu.cs
/// </summary>
public class SystemMenuExtension
{
    #region Native methods

    private const int WM_SYSCOMMAND = 0x112;
    private const int MF_STRING = 0x0;
    private const int MF_SEPARATOR = 0x800;

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool AppendMenu(IntPtr hMenu, int uFlags, int uIDNewItem, string lpNewItem);

    #endregion Native methods

    #region Private data
    private Window window;
    private IntPtr hSysMenu;
    private int lastId = 0;
    private List<Action> actions = new List<Action>();
    private List<CommandInfo> pendingCommands;

    #endregion Private data

    #region Constructors

    /// <summary>
    /// Initialises a new instance of the <see cref="SystemMenu"/> class for the specified
    /// <see cref="Form"/>.
    /// </summary>
    /// <param name="window">The window for which the system menu is expanded.</param>
    public SystemMenuExtension(Window window)
    {
        this.window = window;
        if(this.window.IsLoaded)
        {
            WindowLoaded(null, null);
        }
        else
        {
            this.window.Loaded += WindowLoaded;
        }
    }

    #endregion Constructors

    #region Public methods

    /// <summary>
    /// Adds a command to the system menu.
    /// </summary>
    /// <param name="text">The displayed command text.</param>
    /// <param name="action">The action that is executed when the user clicks on the command.</param>
    /// <param name="separatorBeforeCommand">Indicates whether a separator is inserted before the command.</param>
    public void AddCommand(string text, Action action, bool separatorBeforeCommand)
    {
        int id = ++this.lastId;
        if (!this.window.IsLoaded)
        {
            // The window is not yet created, queue the command for later addition
            if (this.pendingCommands == null)
            {
                this.pendingCommands = new List<CommandInfo>();
            }
            this.pendingCommands.Add(new CommandInfo
            {
                Id = id,
                Text = text,
                Action = action,
                Separator = separatorBeforeCommand
            });
        }
        else
        {
            // The form is created, add the command now
            if (separatorBeforeCommand)
            {
                AppendMenu(this.hSysMenu, MF_SEPARATOR, 0, "");
            }
            AppendMenu(this.hSysMenu, MF_STRING, id, text);
        }
        this.actions.Add(action);
    }

    #endregion Public methods

    #region Private methods

    private void WindowLoaded(object sender, RoutedEventArgs e)
    {
        var interop = new WindowInteropHelper(this.window);
        HwndSource source = PresentationSource.FromVisual(this.window) as HwndSource;
        source.AddHook(WndProc);

        this.hSysMenu = GetSystemMenu(interop.EnsureHandle(), false);

        // Add all queued commands now
        if (this.pendingCommands != null)
        {
            foreach (CommandInfo command in this.pendingCommands)
            {
                if (command.Separator)
                {
                    AppendMenu(this.hSysMenu, MF_SEPARATOR, 0, "");
                }
                AppendMenu(this.hSysMenu, MF_STRING, command.Id, command.Text);
            }
            this.pendingCommands = null;
        }
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_SYSCOMMAND)
        {
            if ((long)wParam > 0 && (long)wParam <= lastId)
            {
                this.actions[(int)wParam - 1]();
            }
        }

        return IntPtr.Zero;
    }

    #endregion Private methods

    #region Classes

    private class CommandInfo
    {
        public int Id { get; set; }
        public string Text { get; set; }
        public Action Action { get; set; }
        public bool Separator { get; set; }
    }

    #endregion Classes

6
价值增加对于您需要的pinvoke数量来说相对较小。但是这是可能的。使用GetSystemMenu()来检索系统菜单句柄。然后使用InsertMenuItem添加条目。您必须在OnHandleCreated()的override中执行此操作,以便在窗口重新创建时重新创建菜单。
覆盖WndProc()以识别当用户单击它时生成的WM_SYSCOMMAND消息。访问pinvoke.net获取所需的pinvoke声明。

2

接受答案的VB.NET版本:

Imports System.Windows.Forms
Imports System.Runtime.InteropServices

Public Class CustomForm
    Inherits Form
    ' P/Invoke constants
    Private Const WM_SYSCOMMAND As Integer = &H112
    Private Const MF_STRING As Integer = &H0
    Private Const MF_SEPARATOR As Integer = &H800

    ' P/Invoke declarations
    <DllImport("user32.dll", CharSet := CharSet.Auto, SetLastError := True)> _
    Private Shared Function GetSystemMenu(hWnd As IntPtr, bRevert As Boolean) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet := CharSet.Auto, SetLastError := True)> _
    Private Shared Function AppendMenu(hMenu As IntPtr, uFlags As Integer, uIDNewItem As Integer, lpNewItem As String) As Boolean
    End Function

    <DllImport("user32.dll", CharSet := CharSet.Auto, SetLastError := True)> _
    Private Shared Function InsertMenu(hMenu As IntPtr, uPosition As Integer, uFlags As Integer, uIDNewItem As Integer, lpNewItem As String) As Boolean
    End Function


    ' ID for the About item on the system menu
    Private SYSMENU_ABOUT_ID As Integer = &H1

    Public Sub New()
    End Sub

    Protected Overrides Sub OnHandleCreated(e As EventArgs)
        MyBase.OnHandleCreated(e)

        ' Get a handle to a copy of this form's system (window) menu
        Dim hSysMenu As IntPtr = GetSystemMenu(Me.Handle, False)

        ' Add a separator
        AppendMenu(hSysMenu, MF_SEPARATOR, 0, String.Empty)

        ' Add the About menu item
        AppendMenu(hSysMenu, MF_STRING, SYSMENU_ABOUT_ID, "&About…")
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
        MyBase.WndProc(m)

        ' Test if the About item was selected from the system menu
        If (m.Msg = WM_SYSCOMMAND) AndAlso (CInt(m.WParam) = SYSMENU_ABOUT_ID) Then
            MessageBox.Show("Custom About Dialog")
        End If

    End Sub
End Class

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接