Mutually exclusive checkable menu items?

2020-01-27 15:45发布

Given the following code:

<MenuItem x:Name="MenuItem_Root" Header="Root">
    <MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" />
    <MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2"/>
    <MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3"/>
</MenuItem>

In XAML, is there a way to create checkable menuitem's that are mutually exclusive? Where is the user checks item2, item's 1 and 3 are automatically unchecked.

I can accomplish this in the code behind by monitoring the click events on the menu, determining which item was checked, and unchecking the other menuitems. I'm thinking there is an easier way.

Any ideas?

16条回答
Deceive 欺骗
2楼-- · 2020-01-27 16:05

This may not be what you're looking for, but you could write an extension for the MenuItem class that allows you to use something like the GroupName property of the RadioButton class. I slightly modified this handy example for similarly extending ToggleButton controls and reworked it a little for your situation and came up with this:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace WpfTest
{
     public class MenuItemExtensions : DependencyObject
     {
           public static Dictionary<MenuItem, String> ElementToGroupNames = new Dictionary<MenuItem, String>();

           public static readonly DependencyProperty GroupNameProperty =
               DependencyProperty.RegisterAttached("GroupName",
                                            typeof(String),
                                            typeof(MenuItemExtensions),
                                            new PropertyMetadata(String.Empty, OnGroupNameChanged));

           public static void SetGroupName(MenuItem element, String value)
           {
                element.SetValue(GroupNameProperty, value);
           }

           public static String GetGroupName(MenuItem element)
           {
                return element.GetValue(GroupNameProperty).ToString();
           }

           private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
           {
                //Add an entry to the group name collection
                var menuItem = d as MenuItem;

                if (menuItem != null)
                {
                     String newGroupName = e.NewValue.ToString();
                     String oldGroupName = e.OldValue.ToString();
                     if (String.IsNullOrEmpty(newGroupName))
                     {
                          //Removing the toggle button from grouping
                          RemoveCheckboxFromGrouping(menuItem);
                     }
                     else
                     {
                          //Switching to a new group
                          if (newGroupName != oldGroupName)
                          {
                              if (!String.IsNullOrEmpty(oldGroupName))
                              {
                                   //Remove the old group mapping
                                   RemoveCheckboxFromGrouping(menuItem);
                              }
                              ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
                               menuItem.Checked += MenuItemChecked;
                          }
                     }
                }
           }

           private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
           {
                ElementToGroupNames.Remove(checkBox);
                checkBox.Checked -= MenuItemChecked;
           }


           static void MenuItemChecked(object sender, RoutedEventArgs e)
           {
                var menuItem = e.OriginalSource as MenuItem;
                foreach (var item in ElementToGroupNames)
                {
                     if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
                     {
                          item.Key.IsChecked = false;
                     }
                }
           }
      }
 }

Then, in the XAML, you'd write:

        <MenuItem x:Name="MenuItem_Root" Header="Root">
            <MenuItem x:Name="MenuItem_Item1" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item1" />
            <MenuItem x:Name="MenuItem_Item2" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item2"/>
            <MenuItem x:Name="MenuItem_Item3" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item3"/>
        </MenuItem>

It's a bit of a pain, but it offers the perk of not forcing you to write any additional procedural code (aside from the extension class, of course) to implement it.

Credit goes to Brad Cunningham who authored the original ToggleButton solution.

查看更多
你好瞎i
3楼-- · 2020-01-27 16:05

Simply create a Template for MenuItem which will contain a RadioButton with a GroupName set to some value. You can also change the template for the RadioButtons to look like the MenuItem's default check glyph (which can be easily extracted with Expression Blend).

That's it!

查看更多
啃猪蹄的小仙女
4楼-- · 2020-01-27 16:08

Here's another approach that uses RoutedUICommands, a public enum property, and DataTriggers. This is a pretty verbose solution. I unfortunately don't see any way of making the Style.Triggers smaller, because I don't know how to just say that the Binding Value is the only thing different? (BTW, for MVVMers this is a terrible example. I put everything in the MainWindow class just to keep things simple.)

MainWindow.xaml:

<Window x:Class="MutuallyExclusiveMenuItems.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:view="clr-namespace:MutuallyExclusiveMenuItems"
        Title="MainWindow" Height="350" Width="525">

  <Window.CommandBindings>
    <CommandBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}" 
        CanExecute="CanExecute"
        Executed="MenuItem1Execute" />
    <CommandBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}" 
        CanExecute="CanExecute"
        Executed="MenuItem2Execute" />
    <CommandBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}" 
        CanExecute="CanExecute"
        Executed="MenuItem3Execute" />
  </Window.CommandBindings>

  <Window.InputBindings>
    <KeyBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}" Gesture="Ctrl+1"/>
    <KeyBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}" Gesture="Ctrl+2"/>
    <KeyBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}" Gesture="Ctrl+3"/>
  </Window.InputBindings>

  <DockPanel>
    <DockPanel DockPanel.Dock="Top">
      <Menu>
        <MenuItem Header="_Root">
          <MenuItem Command="{x:Static view:MainWindow.MenuItem1Cmd}" 
                    InputGestureText="Ctrl+1">
            <MenuItem.Style>
              <Style>
                <Style.Triggers>
                  <DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}" 
                               Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem1}">
                    <Setter Property="MenuItem.IsChecked" Value="True"/>
                  </DataTrigger>
                </Style.Triggers>
              </Style>
            </MenuItem.Style>
          </MenuItem>
          <MenuItem Command="{x:Static view:MainWindow.MenuItem2Cmd}"
                    InputGestureText="Ctrl+2">
            <MenuItem.Style>
              <Style>
                <Style.Triggers>
                  <DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}" 
                               Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem2}">
                    <Setter Property="MenuItem.IsChecked" Value="True"/>
                  </DataTrigger>
                </Style.Triggers>
              </Style>
            </MenuItem.Style>
          </MenuItem>
          <MenuItem Command="{x:Static view:MainWindow.MenuItem3Cmd}"
                    InputGestureText="Ctrl+3">
            <MenuItem.Style>
              <Style>
                <Style.Triggers>
                  <DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}" 
                               Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem3}">
                    <Setter Property="MenuItem.IsChecked" Value="True"/>
                  </DataTrigger>
                </Style.Triggers>
              </Style>
            </MenuItem.Style>
          </MenuItem>
        </MenuItem>
      </Menu>
    </DockPanel>
  </DockPanel>
</Window>

MainWindow.xaml.cs:

using System.Windows;
using System.Windows.Input;
using System.ComponentModel;

namespace MutuallyExclusiveMenuItems
{
  public partial class MainWindow : Window, INotifyPropertyChanged
  {
    public MainWindow()
    {
      InitializeComponent();
      DataContext = this;
    }

    #region Enum Property
    public enum CurrentItemEnum { EnumItem1, EnumItem2, EnumItem3 };

    private CurrentItemEnum _currentMenuItem;
    public CurrentItemEnum CurrentMenuItem
    {
      get { return _currentMenuItem; }
      set
      {
        _currentMenuItem = value;
        OnPropertyChanged("CurrentMenuItem");
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion Enum Property

    #region Commands
    public static RoutedUICommand MenuItem1Cmd = 
      new RoutedUICommand("Item_1", "Item1cmd", typeof(MainWindow));
    public void MenuItem1Execute(object sender, ExecutedRoutedEventArgs e)
    {
      CurrentMenuItem = CurrentItemEnum.EnumItem1;
    }
    public static RoutedUICommand MenuItem2Cmd = 
      new RoutedUICommand("Item_2", "Item2cmd", typeof(MainWindow));
    public void MenuItem2Execute(object sender, ExecutedRoutedEventArgs e)
    {
      CurrentMenuItem = CurrentItemEnum.EnumItem2;
    }
    public static RoutedUICommand MenuItem3Cmd = 
      new RoutedUICommand("Item_3", "Item3cmd", typeof(MainWindow));
    public void MenuItem3Execute(object sender, ExecutedRoutedEventArgs e)
    {
      CurrentMenuItem = CurrentItemEnum.EnumItem3;
    }
    public void CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = true;
    }
    #endregion Commands
  }
}
查看更多
We Are One
5楼-- · 2020-01-27 16:09

Here's a simple, MVVM-based solution that leverages a simple IValueConverter and CommandParameter per MenuItem.

No need to re-style any MenuItem as a different type of control. MenuItems will automatically be deselected when the bound value doesn't match the CommandParameter.

Bind to an int property (MenuSelection) on the DataContext (ViewModel).

<MenuItem x:Name="MenuItem_Root" Header="Root">
    <MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" IsChecked="{Binding MenuSelection, ConverterParameter=1, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
    <MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2" IsChecked="{Binding MenuSelection, ConverterParameter=2, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
    <MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3" IsChecked="{Binding MenuSelection, ConverterParameter=3, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
</MenuItem>

Define your value converter. This will check the bound value against the command parameter and vice versa.

public class MatchingIntToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var paramVal = parameter as string;
        var objVal = ((int)value).ToString();

        return paramVal == objVal;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool)
        {
            var i = System.Convert.ToInt32((parameter ?? "0") as string);

            return ((bool)value)
                ? System.Convert.ChangeType(i, targetType)
                : 0;
        }

        return 0; // Returning a zero provides a case where none of the menuitems appear checked
    }
}

Add your resource

<Window.Resources>
    <ResourceDictionary>
        <local:MatchingIntToBooleanConverter x:Key="MatchingIntToBooleanConverter"/>
    </ResourceDictionary>
</Window.Resources>

Good luck!

查看更多
你好瞎i
6楼-- · 2020-01-27 16:12

There is not a built-in way to do this in XAML, you will need to roll your own solution or get an existing solution if available.

查看更多
▲ chillily
7楼-- · 2020-01-27 16:15

I just thought I would throw in my solution, since none of the answers met my needs. My full solution is here...

WPF MenuItem as a RadioButton

However, the basic idea is to use ItemContainerStyle.

<MenuItem.ItemContainerStyle>
    <Style TargetType="MenuItem">
        <Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
        <EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
    </Style>
</MenuItem.ItemContainerStyle>

And the following event click should be added so that the RadioButton is checked when the MenuItem is clicked (otherwise you have to click exactly on the RadioButton):

private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e)
{
    MenuItem mi = sender as MenuItem;
    if (mi != null)
    {
        RadioButton rb = mi.Icon as RadioButton;
        if (rb != null)
        {
            rb.IsChecked = true;
        }
    }
}
查看更多
登录 后发表回答