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






4.96/5 (26投票s)
本文展示了如何以MVVM兼容的方式向系统菜单添加菜单项并附加命令处理程序。
图 1: 设置和问候!在菜单中被禁用
图 2: 所有菜单项均已启用
引言
大多数 MFC 应用程序在主窗口的系统菜单中一直有一个关于...菜单项,这主要是因为 App Wizard 默认会生成相应的代码。我想在我正在开发的 WPF 应用程序中实现类似的功能,并且希望以 MVVM 友好的方式来实现。在这篇文章中,我将解释一种巧妙的实现方法,让您可以轻松地添加菜单项并为它们附加命令处理程序,同时保留基本的 MVVM 范式。代码支持命令参数以及 UI 启用/禁用机制。基本思想是让添加系统菜单项并绑定命令变得非常容易,而无需对代码进行重大更改。
使用代码
使用该类只有两个步骤。
- 将您的主窗口类从
SystemMenuWindow
派生,而不是从Window
派生。如果您有自己的DerivedWindow
类,则需要将该类更改为从SystemMenuWindow
派生。您还需要修改窗口的 Xaml 以反映此更改。 - 在
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
派生以实现数据上下文继承。它具有可绑定的属性 Command
、CommandParameter
、Id
(用于菜单项)和 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_SYSCOMMAND
和 WM_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。