65.9K
CodeProject 正在变化。 阅读更多。
Home

在WPF应用程序中添加系统菜单项的MVVM友好方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (26投票s)

2010年4月3日

CPOL

3分钟阅读

viewsIcon

122221

downloadIcon

931

本文展示了如何以MVVM兼容的方式向系统菜单添加菜单项并附加命令处理程序。

图 1: 设置和问候!在菜单中被禁用

图 2: 所有菜单项均已启用

引言

大多数 MFC 应用程序在主窗口的系统菜单中一直有一个关于...菜单项,这主要是因为 App Wizard 默认会生成相应的代码。我想在我正在开发的 WPF 应用程序中实现类似的功能,并且希望以 MVVM 友好的方式来实现。在这篇文章中,我将解释一种巧妙的实现方法,让您可以轻松地添加菜单项并为它们附加命令处理程序,同时保留基本的 MVVM 范式。代码支持命令参数以及 UI 启用/禁用机制。基本思想是让添加系统菜单项并绑定命令变得非常容易,而无需对代码进行重大更改。

使用代码

使用该类只有两个步骤。

  1. 将您的主窗口类从 SystemMenuWindow 派生,而不是从 Window 派生。如果您有自己的 DerivedWindow 类,则需要将该类更改为从 SystemMenuWindow 派生。您还需要修改窗口的 Xaml 以反映此更改。

  2. MenuItems 标签内添加系统菜单命令处理程序。

就这样,您就可以开始使用了!

演示应用程序

演示应用程序有三个按钮。

  • 一个“关于”按钮,它始终启用,并会弹出一个“关于”对话框(在演示中是一个消息框)。
  • 一个“设置”按钮,可以根据主窗口上的复选框启用或禁用。启用后,它会弹出一个设置对话框(同样是一个消息框)。
  • 一个“问候!”按钮,只有当名称文本框至少有一个字符时才启用。点击它会弹出一个问候消息框,其中文本框中的文本将作为命令参数传递。

这三个按钮在主窗口的系统菜单中都有相应的条目。

视图模型类

这是相当简单的视图模型类,它展示了各种命令处理程序。

internal class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private void FirePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private ICommand aboutCommand;

    public ICommand AboutCommand
    {
        get
        {
            return aboutCommand ?? (aboutCommand = new DelegateCommand(
                () =>
                    MessageBox.Show(
                        "Copyright (c) Nish Sivakumar. All rights reserved.",
                        "About...")
                    ));
        }
    }

    private ICommand settingsCommand;

    public ICommand SettingsCommand
    {
        get
        {
            return settingsCommand ?? (settingsCommand = 
              new DelegateCommand(
                () => MessageBox.Show(
                        "Settings dialog placeholder.",
                        "Settings"),
                () => SettingsEnabled
                    ));
        }
    }

    private ICommand greetingCommand;

    public ICommand GreetingCommand
    {
        get
        {
            return greetingCommand ?? 
             (greetingCommand = new DelegateCommand<string>(
                (s) => MessageBox.Show(
                        String.Concat("Hello ", s, ". How are you?"),
                        "Greeting"),
                (s) => !String.IsNullOrEmpty(s)
                    ));
        }
    }

    private bool settingsEnabled;

    public bool SettingsEnabled
    {
        get
        {
            return settingsEnabled;
        }

        set
        {
            if (settingsEnabled != value)
            {
                settingsEnabled = value;
                this.FirePropertyChanged("SettingsEnabled");
            }
        }
    }

    private string enteredName;

    public string EnteredName
    {
        get
        {
            return enteredName;
        }

        set
        {
            if (enteredName != value)
            {
                enteredName = value;
                this.FirePropertyChanged("EnteredName");
            }
        }
    }
}

视图 (Xaml)

这是主窗口的 Xaml 代码。

<nsmvvm:SystemMenuWindow x:Class="SystemMenuWindowDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:nsmvvm="clr-namespace:NS.MVVM"
        Title="SystemMenu Window Demo Application" 
        Height="210" Width="330" ResizeMode="NoResize" 
        WindowStartupLocation="CenterScreen">

    <nsmvvm:SystemMenuWindow.MenuItems>
        <nsmvvm:SystemMenuItem Command="{Binding AboutCommand}" 
          Header="About" Id="100" />
        <nsmvvm:SystemMenuItem Command="{Binding SettingsCommand}" 
          Header="Settings" Id="101" />
        <nsmvvm:SystemMenuItem Command="{Binding GreetingCommand}" 
          CommandParameter="{Binding EnteredName}" 
          Header="Greeting!" Id="102" />
    </nsmvvm:SystemMenuWindow.MenuItems>
    
    <Grid>
        <StackPanel  Height="50" HorizontalAlignment="Right"  
                     Name="stackPanelButtons" VerticalAlignment="Bottom" 
                     Width="270" Orientation="Horizontal">
            <Button Content="About" Command="{Binding AboutCommand}" 
              Height="23" Name="buttonAbout" Width="75" Margin="5,0, 5, 0" />
            <Button Content="Settings" Command="{Binding SettingsCommand}" 
              Height="23" Name="buttonSettings" Width="75" Margin="5, 0, 5, 0" />
            <Button Content="Greeting!" Command="{Binding GreetingCommand}" 
              CommandParameter="{Binding EnteredName}" 
              Height="23" Name="buttonGreeting" Width="75" Margin="5, 0, 5, 0" />
        </StackPanel>
        <CheckBox Content="Enable Settings" Height="16" 
          HorizontalAlignment="Left" Margin="45,24,0,0" 
          Name="checkBoxSettingsEnabled" VerticalAlignment="Top" 
          IsChecked="{Binding SettingsEnabled}" />
        <Label Content="Your name:" Height="28" HorizontalAlignment="Left" 
          Margin="47,62,0,0" Name="labelName" VerticalAlignment="Top" />
        <TextBox Height="23" 
          Text="{Binding EnteredName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" 
          Margin="144,64,0,0" Name="textBoxName" 
          VerticalAlignment="Top" Width="152" />
    </Grid>
</nsmvvm:SystemMenuWindow>

按钮和系统菜单条目使用了相同的命令绑定。 

实现细节

系统菜单项由 SystemMenuItem 类表示,该类从 Freezable 派生以实现数据上下文继承。它具有可绑定的属性 CommandCommandParameterId(用于菜单项)和 Header(用于菜单文本)。虽然它类似于 WPF 的 MenuItem 对象,但这完全是不同的类。系统菜单也与 WPF 菜单截然不同,因为它是一个基于原生 Windows HWND(或者更准确地说,是 HMENU)的菜单。

public class SystemMenuItem : Freezable
{
    public static readonly DependencyProperty CommandProperty = 
      DependencyProperty.Register(
        "Command", typeof(ICommand), typeof(SystemMenuItem), 
        new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

    public static readonly DependencyProperty CommandParameterProperty = 
      DependencyProperty.Register(
        "CommandParameter", typeof(object), typeof(SystemMenuItem));

    public static readonly DependencyProperty HeaderProperty = 
      DependencyProperty.Register(
        "Header", typeof(string), typeof(SystemMenuItem));

    public static readonly DependencyProperty IdProperty = 
      DependencyProperty.Register(
        "Id", typeof(int), typeof(SystemMenuItem));

    public ICommand Command
    {
        get
        {
            return (ICommand)this.GetValue(CommandProperty);
        }

        set
        {
            this.SetValue(CommandProperty, value);
        }
    }

    public object CommandParameter
    {
        get
        {
            return GetValue(CommandParameterProperty);
        }

        set
        {
            SetValue(CommandParameterProperty, value);
        }
    }

    public string Header
    {
        get
        {
            return (string)GetValue(HeaderProperty);
        }

        set
        {
            SetValue(HeaderProperty, value);
        }
    }

    public int Id
    {
        get
        {
            return (int)GetValue(IdProperty);
        }

        set
        {
            SetValue(IdProperty, value);
        }
    }

    protected override Freezable CreateInstanceCore()
    {
        return new SystemMenuItem();
    }

    private static void OnCommandChanged(
      DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        SystemMenuItem systemMenuItem = d as SystemMenuItem;

        if (systemMenuItem != null)
        {
            if (e.NewValue != null)
            {
                systemMenuItem.Command = e.NewValue as ICommand;
            }
        }
    }
}

SystemMenuWindow 类是一个 Window 派生类,实现了原生系统菜单处理。它公开了一个 FreezableCollection<SystemMenuItem> 属性 MenuItems,用于指定需要添加的自定义条目。我使用 FreezableCollection<> 的原因是为了数据上下文继承。最初我花费了一些时间编写自己的 IList(是的,非泛型版本是 Xaml 解析器默认查找的)派生的 Freezable 集合类,然后才找到了这个类。

类的实现相当直接。我处理 Loaded 事件,并使用 InsertMenu API 函数将菜单项插入到系统菜单中。使用 HwndSource 添加了一个 WndProc 钩子,并妥善处理了 WM_SYSCOMMANDWM_INITMENUPOPUP。代码如下。

public class SystemMenuWindow : Window
{
    private const uint WM_SYSCOMMAND = 0x112;

    private const uint WM_INITMENUPOPUP = 0x0117;

    private const uint MF_SEPARATOR = 0x800;

    private const uint MF_BYCOMMAND = 0x0;

    private const uint MF_BYPOSITION = 0x400;

    private const uint MF_STRING = 0x0;

    private const uint MF_ENABLED = 0x0;

    private const uint MF_DISABLED = 0x2;
    
    [DllImport("user32.dll")]
    private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool InsertMenu(IntPtr hmenu, int position, 
      uint flags, uint item_id, 
      [MarshalAs(UnmanagedType.LPTStr)]string item_text);

    [DllImport("user32.dll")]
    private static extern bool EnableMenuItem(IntPtr hMenu, 
      uint uIDEnableItem, uint uEnable);

    public static readonly DependencyProperty MenuItemsProperty =
      DependencyProperty.Register(
        "MenuItems", typeof(FreezableCollection<SystemMenuItem>), 
        typeof(SystemMenuWindow), 
        new PropertyMetadata(new PropertyChangedCallback(OnMenuItemsChanged)));

    private IntPtr systemMenu;

    public FreezableCollection<SystemMenuItem> MenuItems
    {
        get
        {
            return (FreezableCollection<SystemMenuItem>)
              this.GetValue(MenuItemsProperty);
        }

        set
        {
            this.SetValue(MenuItemsProperty, value);
        }
    }

    /// <summary>
    /// Initializes a new instance of the SystemMenuWindow class.
    /// </summary>
    public SystemMenuWindow()
    {
        this.Loaded += this.SystemMenuWindow_Loaded;

        this.MenuItems = new FreezableCollection<SystemMenuItem>();
    }

    private static void OnMenuItemsChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        SystemMenuWindow obj = d as SystemMenuWindow;

        if (obj != null)
        {
            if (e.NewValue != null)
            {
                obj.MenuItems = e.NewValue 
                    as FreezableCollection<SystemMenuItem>;
            }
        }
    }

    private void SystemMenuWindow_Loaded(object sender, RoutedEventArgs e)
    {
        WindowInteropHelper interopHelper = new WindowInteropHelper(this);
        this.systemMenu = GetSystemMenu(interopHelper.Handle, false);

        if (this.MenuItems.Count > 0)
        {
            InsertMenu(this.systemMenu, -1, 
                MF_BYPOSITION | MF_SEPARATOR, 0, String.Empty);
        }

        foreach (SystemMenuItem item in this.MenuItems)
        {
            InsertMenu(this.systemMenu, (int)item.Id, 
                MF_BYCOMMAND | MF_STRING, (uint)item.Id, item.Header);
        }

        HwndSource hwndSource = HwndSource.FromHwnd(interopHelper.Handle);
        hwndSource.AddHook(this.WndProc);
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, 
        IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch ((uint)msg)
        {
            case WM_SYSCOMMAND:
                var menuItem = this.MenuItems.Where(
                    mi => mi.Id == wParam.ToInt32()).FirstOrDefault();
                if (menuItem != null)
                {
                    menuItem.Command.Execute(menuItem.CommandParameter);
                    handled = true;
                }

                break;

            case WM_INITMENUPOPUP:
                if (this.systemMenu == wParam)
                {
                    foreach (SystemMenuItem item in this.MenuItems)
                    {
                        EnableMenuItem(this.systemMenu, (uint)item.Id, 
                            item.Command.CanExecute(
                              item.CommandParameter) ? 
                                MF_ENABLED : MF_DISABLED);
                    }
                    handled = true;
                }

                break;
        }

        return IntPtr.Zero;
    }        
}

WM_SYSCOMMAND 处理程序用于 Command.Execute,而 WM_INITPOPUP 处理程序用于 Command.CanExecute。这与 MFC 的命令/UI 处理机制非常相似。有些东西还是不变的 :-)

结论

一如既往,各种反馈、批评和建议都受到欢迎并十分感谢。谢谢。

历史

  • 2010 年 4 月 4 日 - 文章首次发布。
  • 2010 年 4 月 6 日 - 从源代码和文章正文中删除了多余的 Cast<>()
  • 2010 年 4 月 9 日 - 移除了我不必要地添加的用于实时文本绑定的附加行为,并将其替换为 UpdateSourceTrigger=PropertyChanged。感谢 Richard Deeming
© . All rights reserved.