互斥的可检查菜单项?

时间:2010-09-06 15:45:05

标签: c# wpf xaml menuitem

给出以下代码:

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

在XAML中,有没有办法创建相互排斥的可检查菜单项?用户在哪里检查item2,项目的1和3将自动取消选中。

我可以通过监视菜单上的点击事件,确定检查了哪个项目以及取消选中其他菜单项来完成后面的代码。我想有一种更简单的方法。

有什么想法吗?

18 个答案:

答案 0 :(得分:45)

这可能不是您正在寻找的,但您可以为MenuItem类编写一个扩展,允许您使用GroupName类的RadioButton属性之类的内容。我略微修改了this类似扩展ToggleButton控件的方便示例,并根据您的情况对其进行了一些修改,并提出了这个问题:

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

然后,在XAML中,你会写:

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

这有点痛苦,但它提供了不要强迫你编写任何额外的程序代码(当然除了扩展类)来实现它的好处。

归功于撰写原始ToggleButton解决方案的Brad Cunningham。

答案 1 :(得分:8)

您也可以使用行为。像这样:

<MenuItem Header="menu">

    <MenuItem x:Name="item1" Header="item1" IsCheckable="true" ></MenuItem>
    <MenuItem x:Name="item2" Header="item2" IsCheckable="true"></MenuItem>
    <MenuItem x:Name="item3" Header="item3" IsCheckable="true" ></MenuItem>

    <i:Interaction.Behaviors>
    <local:MenuItemButtonGroupBehavior></local:MenuItemButtonGroupBehavior>
    </i:Interaction.Behaviors>

</MenuItem>


public class MenuItemButtonGroupBehavior : Behavior<MenuItem>
{
    protected override void OnAttached()
    {
        base.OnAttached();

        GetCheckableSubMenuItems(AssociatedObject)
            .ToList()
            .ForEach(item => item.Click += OnClick);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        GetCheckableSubMenuItems(AssociatedObject)
            .ToList()
            .ForEach(item => item.Click -= OnClick);
    }

    private static IEnumerable<MenuItem> GetCheckableSubMenuItems(ItemsControl menuItem)
    {
        var itemCollection = menuItem.Items;
        return itemCollection.OfType<MenuItem>().Where(menuItemCandidate => menuItemCandidate.IsCheckable);
    }

    private void OnClick(object sender, RoutedEventArgs routedEventArgs)
    {
        var menuItem = (MenuItem)sender;

        if (!menuItem.IsChecked)
        {
            menuItem.IsChecked = true;
            return;
        }

        GetCheckableSubMenuItems(AssociatedObject)
            .Where(item => item != menuItem)
            .ToList()
            .ForEach(item => item.IsChecked = false);
    }
}

答案 2 :(得分:7)

在底部添加这个,因为我还没有声誉......

尽管Patrick的答案很有帮助,但它并不能确保不能取消选中项目。为此,应将Checked处理程序更改为Click处理程序,并更改为以下内容:

static void MenuItemClicked(object sender, RoutedEventArgs e)
{
    var menuItem = e.OriginalSource as MenuItem;
    if (menuItem.IsChecked)
    {
        foreach (var item in ElementToGroupNames)
        {
            if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
            {
                item.Key.IsChecked = false;
            }
        }
    }
    else // it's not possible for the user to deselect an item
    {
        menuItem.IsChecked = true;
    }
}

答案 3 :(得分:7)

由于没有samilar答案,我在这里发布我的解决方案:

public class RadioMenuItem : MenuItem
{
    public string GroupName { get; set; }
    protected override void OnClick()
    {
        var ic = Parent as ItemsControl;
        if (null != ic)
        {
            var rmi = ic.Items.OfType<RadioMenuItem>().FirstOrDefault(i =>
                i.GroupName == GroupName && i.IsChecked);
            if (null != rmi) rmi.IsChecked = false;

            IsChecked = true;
        }
        base.OnClick();
    }
}

在XAML中,只需将其用作通常的MenuItem:

<MenuItem Header="OOO">
    <local:RadioMenuItem Header="111" GroupName="G1"/>
    <local:RadioMenuItem Header="222" GroupName="G1"/>
    <local:RadioMenuItem Header="333" GroupName="G1"/>
    <local:RadioMenuItem Header="444" GroupName="G1"/>
    <local:RadioMenuItem Header="555" GroupName="G1"/>
    <local:RadioMenuItem Header="666" GroupName="G1"/>
    <Separator/>
    <local:RadioMenuItem Header="111" GroupName="G2"/>
    <local:RadioMenuItem Header="222" GroupName="G2"/>
    <local:RadioMenuItem Header="333" GroupName="G2"/>
    <local:RadioMenuItem Header="444" GroupName="G2"/>
    <local:RadioMenuItem Header="555" GroupName="G2"/>
    <local:RadioMenuItem Header="666" GroupName="G2"/>
</MenuItem>

非常简单干净。当然,你可以通过一些额外的代码使GroupName成为依赖属性,这些代码与其他代码完全相同。

顺便说一句,如果您不喜欢复选标记,可以将其更改为您喜欢的任何内容:

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    var p = GetTemplateChild("Glyph") as Path;
    if (null == p) return;
    var x = p.Width/2;
    var y = p.Height/2;
    var r = Math.Min(x, y) - 1;
    var e = new EllipseGeometry(new Point(x,y), r, r);
    // this is just a flattened dot, of course you can draw
    // something else, e.g. a star? ;)
    p.Data = e.GetFlattenedPathGeometry();
}

如果您在程序中使用了大量此RadioMenuItem,则下面会显示另一个更高效的版本。文字数据来自之前代码段中的e.GetFlattenedPathGeometry().ToString()

private static readonly Geometry RadioDot = Geometry.Parse("M9,5.5L8.7,7.1 7.8,8.3 6.6,9.2L5,9.5L3.4,9.2 2.2,8.3 1.3,7.1L1,5.5L1.3,3.9 2.2,2.7 3.4,1.8L5,1.5L6.6,1.8 7.8,2.7 8.7,3.9L9,5.5z");
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    var p = GetTemplateChild("Glyph") as Path;
    if (null == p) return;
    p.Data = RadioDot;
}

最后,如果你计划将它包装在你的项目中使用,你应该隐藏IsCheckable属性来自基类,因为MenuItem类的自动检查机制会导致无线电检查国家标志着一种错误的行为。

private new bool IsCheckable { get; }

因此,如果新手试图像这样编译XAML,VS将会出错:

  

//请注意,这是一个错误的用法!

     

<local:RadioMenuItem Header="111" GroupName="G1" IsCheckable="True"/>

     

//请注意,这是一个错误的用法!

答案 4 :(得分:6)

是的,这可以通过使每个MenuItem成为RadioButton来轻松完成。这可以通过编辑MenuItem的模板来完成。

  1. 右键单击Document-Outline左窗格中的MenuItem&gt; EditTemplate&gt; EditCopy。这将在Window.Resources下添加编辑代码。

  2. 现在,你只需做两次非常简单的改动。

    Mutually Exclusive MenuItems一个。使用一些资源添加RadioButton以隐藏其圆圈部分。

    湾为MenuItem Border部分更改BorderThickness = 0。

    这些更改在下面显示为注释,生成的样式的其余部分应按原样使用:

    <Window.Resources>
            <LinearGradientBrush x:Key="MenuItemSelectionFill" EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#34C5EBFF" Offset="0"/>
                <GradientStop Color="#3481D8FF" Offset="1"/>
            </LinearGradientBrush>
            <Geometry x:Key="Checkmark">M 0,5.1 L 1.7,5.2 L 3.4,7.1 L 8,0.4 L 9.2,0 L 3.3,10.8 Z</Geometry>
            <ControlTemplate x:Key="{ComponentResourceKey ResourceId=SubmenuItemTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}" TargetType="{x:Type MenuItem}">
                <Grid SnapsToDevicePixels="true">
                    <Rectangle x:Name="Bg" Fill="{TemplateBinding Background}" RadiusY="2" RadiusX="2" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"/>
                    <Rectangle x:Name="InnerBorder" Margin="1" RadiusY="2" RadiusX="2"/>
       <!-- Add RadioButton around the Grid 
       -->
                    <RadioButton Background="Transparent" GroupName="MENUITEM_GRP" IsHitTestVisible="False" IsChecked="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=MenuItem}}">
                        <RadioButton.Resources>
                            <Style TargetType="Themes:BulletChrome">
                                <Setter Property="Visibility" Value="Collapsed"/>
                            </Style>
                        </RadioButton.Resources>
       <!-- Add RadioButton Top part ends here
        -->
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition MinWidth="24" SharedSizeGroup="MenuItemIconColumnGroup" Width="Auto"/>
                                <ColumnDefinition Width="4"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="37"/>
                                <ColumnDefinition SharedSizeGroup="MenuItemIGTColumnGroup" Width="Auto"/>
                                <ColumnDefinition Width="17"/>
                            </Grid.ColumnDefinitions>
                            <ContentPresenter x:Name="Icon" ContentSource="Icon" Margin="1" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
    
        <!-- Change border thickness to 0 
        -->    
                            <Border x:Name="GlyphPanel" BorderBrush="#CDD3E6" BorderThickness="0" Background="#E6EFF4" CornerRadius="3" Height="22" Margin="1" Visibility="Hidden" Width="22">
                                <Path x:Name="Glyph" Data="{StaticResource Checkmark}" Fill="#0C12A1" FlowDirection="LeftToRight" Height="11" Width="9"/>
                            </Border>
                            <ContentPresenter Grid.Column="2" ContentSource="Header" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            <TextBlock Grid.Column="4" Margin="{TemplateBinding Padding}" Text="{TemplateBinding InputGestureText}"/>
                        </Grid>
                    </RadioButton>
        <!-- RadioButton closed , thats it !
        -->
                </Grid>
              ...
        </Window.Resources>
    
  3. 应用样式,

    <MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
    

答案 5 :(得分:4)

在XAML中没有内置方法可以执行此操作,您需要自行推出解决方案或获取现有解决方案。

答案 6 :(得分:4)

我只是想我会投入我的解决方案,因为没有一个答案符合我的需要。我的完整解决方案就在这里......

WPF MenuItem as a RadioButton

但是,基本思想是使用ItemContainerStyle。

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

应添加以下事件点击,以便在单击MenuItem时检查RadioButton(否则您必须完全单击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;
        }
    }
}

答案 7 :(得分:2)

我使用几行代码实现了这一点:

首先声明一个变量:

MenuItem LastBrightnessMenuItem =null;

当我们考虑一组菜单项时,有可能使用单个事件处理程序。在这种情况下,我们可以使用这个逻辑:

    private void BrightnessMenuClick(object sender, RoutedEventArgs e)
                {

                    if (LastBrightnessMenuItem != null)
                    {
                        LastBrightnessMenuItem.IsChecked = false;
                    }

                    MenuItem m = sender as MenuItem;
                    LastBrightnessMenuItem = m;

                    //Handle the rest of the logic here


                }

答案 8 :(得分:1)

我发现在将MenuItem.IsChecked绑定到变量时,我获得了互斥的菜单项。

但它有一个怪癖:如果单击所选的菜单项,它将变为无效,由通常的红色矩形显示。我通过为MenuItem.Click添加一个处理程序解决了这个问题,它通过将IsChecked设置为true来阻止取消选择。

代码...我绑定到枚举类型,所以我使用枚举转换器,如果bound属性等于提供的参数,则返回true。这是XAML:

    <MenuItem Header="Black"
              IsCheckable="True"
              IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Black}"
              Click="MenuItem_OnClickDisallowUnselect"/>
    <MenuItem Header="Red"
              IsCheckable="True"
              IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Red}"
              Click="MenuItem_OnClickDisallowUnselect"/>

这是背后的代码:

    private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e)
    {
        var menuItem = e.OriginalSource as MenuItem;
        if (menuItem == null) return;

        if (! menuItem.IsChecked)
        {
            menuItem.IsChecked = true;
        }
    }

答案 9 :(得分:0)

这是我为此目的创建的自定义控件。 它可以正确处理选中、取消选中、点击事件和组名更改。

如果您愿意,您可以覆盖菜单项的样式并将复选标记更改为单选标记,但不是必需的:

public class RadioMenuItem : MenuItem
{
    private bool abortCheckChange = false;

    [DefaultValue("")]
    public string GroupName
    {
        get => (string)GetValue(GroupNameProperty);
        set => SetValue(GroupNameProperty, value);
    }

    public static readonly DependencyProperty GroupNameProperty =
        DependencyProperty.Register(nameof(GroupName), typeof(string), typeof(RadioMenuItem),
            new PropertyMetadata("", (d, e) => ((RadioMenuItem)d).OnGroupNameChanged((string)e.OldValue, (string)e.NewValue)));


    static RadioMenuItem()
    {
        IsCheckedProperty.OverrideMetadata(typeof(RadioMenuItem),
            new FrameworkPropertyMetadata(null, (d, o) => ((RadioMenuItem)d).abortCheckChange ? d.GetValue(IsCheckedProperty) : o));
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new RadioMenuItem();
    }

    protected override void OnClick()
    {
        //This will handle correctly the click, but prevents the unchecking.
        //So the menu item acts that is correctly clicked (e.g. the menu disappears
        //but the user can only check, not uncheck the item.
        if (IsCheckable && IsChecked) abortCheckChange = true;
        base.OnClick();
        abortCheckChange = false;
    }

    protected override void OnChecked(RoutedEventArgs e)
    {
        base.OnChecked(e);
        //If the menu item is checked, other items of the same group will be unchecked.
        if (IsChecked) UncheckOtherGroupItems();
    }

    protected virtual void OnGroupNameChanged(string oldGroupName, string newGroupName)
    {
        //If the menu item enters on another group and is checked, other items will be unchecked.
        if (IsChecked) UncheckOtherGroupItems();
    }

    private void UncheckOtherGroupItems()
    {
        if (IsCheckable)
        {
            IEnumerable<RadioMenuItem> radioItems = Parent is ItemsControl parent ? parent.Items.OfType<RadioMenuItem>()
                .Where((item) => item.IsCheckable && (item.DataContext == parent.DataContext || item.DataContext != DataContext)) : null;

            if (radioItems != null)
            {
                foreach (RadioMenuItem item in radioItems)
                {
                    if (item != this && item.GroupName == GroupName)
                    {
                        //This will uncheck all other items on the same group.
                        item.IsChecked = false;
                    }
                }
            }
        }
    }
}

示例:

<Grid Background="Red" HorizontalAlignment="Left" Height="125" Margin="139,120,0,0" VerticalAlignment="Top" Width="120">
    <Grid.ContextMenu>
        <ContextMenu>
            <MenuItem IsCheckable="True" Header="Normal check 1"/>
            <MenuItem IsCheckable="True" Header="Normal check 2"/>
            <Separator/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 1" GroupName="Group1"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 2" GroupName="Group1"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 3" GroupName="Group1"/>
            <Separator/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 4" GroupName="Group2"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 5" GroupName="Group2"/>
        </ContextMenu>
    </Grid.ContextMenu>
</Grid>

答案 10 :(得分:0)

You can hook both check and uncheck event for the MenuItem and inside the event you can call a common method like below:

        private void MenuItem_Unchecked(object sender, RoutedEventArgs e)
        {
            this.UpdateCheckeditem(sender as MenuItem);
        }

        private void MenuItem_Checked(object sender, RoutedEventArgs e)
        {
            this.UpdateCheckeditem(sender as MenuItem);
        }

        private void UpdateCheckedstatus(MenuItem item)
        {
             MenuItem itemChecked = (MenuItem)sender;
            MenuItem itemParent = (MenuItem)itemChecked.Parent;

            foreach (MenuItem item in itemParent.Items)
            {

                if (item != itemChecked && item.IsChecked)
                {
                    item.IsChecked = false;
                    break;
                }
            }
        }

I think this will give you the expected behavior.

答案 11 :(得分:0)

这是一个简单的基于 MVVM的解决方案,它对每个MenuItem都使用了一个简单的 IValueConverter CommandParameter

无需将任何MenuItem重新设置为其他类型的控件。当绑定值与CommandParameter不匹配时,将自动取消选择MenuItems。

绑定到DataContext(ViewModel)上的int属性(MenuSelection)。

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

定义您的价值转换器。这将对照命令参数检查绑定值,反之亦然。

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

添加您的资源

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

祝你好运!

答案 12 :(得分:0)

@Patrick答案中的一小部分。

如@ MK10所述,此解决方案允许用户取消选择组中的所有项目。但是他建议的更改现在对我不起作用。也许自那以后就更改了WPF模型,但是现在取消选中某个项目时不会触发Checked事件。

为避免这种情况,我建议为Unchecked处理MenuItem事件。

我更改了以下步骤:

        private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is MenuItem menuItem))
                return;

            var newGroupName = e.NewValue.ToString();
            var oldGroupName = e.OldValue.ToString();
            if (string.IsNullOrEmpty(newGroupName))
            {
                RemoveCheckboxFromGrouping(menuItem);
            }
            else
            {
                if (newGroupName != oldGroupName)
                {
                    if (!string.IsNullOrEmpty(oldGroupName))
                    {
                        RemoveCheckboxFromGrouping(menuItem);
                    }
                    ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
                    menuItem.Checked += MenuItemChecked;
                    menuItem.Unchecked += MenuItemUnchecked; // <-- ADDED
                }
            }
        }

        private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
        {
            ElementToGroupNames.Remove(checkBox);
            checkBox.Checked -= MenuItemChecked;
            checkBox.Unchecked -= MenuItemUnchecked;   // <-- ADDED
        }

并添加了下一个处理程序:

    private static void MenuItemUnchecked(object sender, RoutedEventArgs e)
    {
        if (!(e.OriginalSource is MenuItem menuItem))
            return;

        var isAnyItemChecked = ElementToGroupNames.Any(item => item.Value == GetGroupName(menuItem) && item.Key.IsChecked);
        if (!isAnyItemChecked)
            menuItem.IsChecked = true;
    }

现在,当用户第二次单击时,选中的项目仍保持选中状态。

答案 13 :(得分:0)

几年后,我看到这篇文章写有我写的关键词……我认为在wpf中有一个简单的解决方案……也许是我,但是我认为拥有如此庞大的武器库对于这么少的东西作为公认的解决方案。我什至不谈论6like的解决方案,但我不知道该在哪里单击以获取此选项。

因此,这根本不是一件高雅的事情……但这是一个简单的解决方案。它的作用很简单..循环到父级包含的所有元素,将其设置为false。人们最经常将这部分与其他部分分开,当然,在这种情况下,这是正确的。

private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{
    MenuItem itemChecked = (MenuItem)sender;
    MenuItem itemParent = (MenuItem)itemChecked.Parent;

    foreach (MenuItem item in itemParent.Items)
    {
        if (item == itemChecked)continue;

        item.IsChecked = false;
    }
}

那很简单,xaml是经典代码,绝对没有什么特别的

<MenuItem Header="test">
    <MenuItem Header="1"  Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
    <MenuItem Header="2"  Click="MenuItem_Click_1" IsCheckable="True"  StaysOpenOnClick="True"/>
</MenuItem>

当然,您可能需要click方法,这不是问题,您可以创建一个接受对象发送者的方法,并且每个click方法都将使用此方法。它很旧,很丑陋,但是有一段时间了。 而且我有一些问题想不出这么多事情来处理这么多代码行,可能是我遇到了xaml的问题,但是要想仅选择一个菜单项就必须这样做,这似乎令人难以置信。

答案 14 :(得分:0)

这是另一种方式 - 任何延伸都不容易,但它兼容MVVM,可绑定且高度可单元测试。如果您可以自由地将Converter添加到项目中,并且每次上下文菜单打开时都不介意以新项目列表形式存在一些垃圾,这非常有效。它符合如何在上下文菜单中提供互斥的一组检查项目的原始问题。

我认为如果您想将所有这些内容提取到用户控件中,您可以将其转换为可重用的库组件,以便在整个应用程序中重用。 使用的组件是Type3.Xaml,具有简单的网格,一个文本块和上下文菜单。右键单击网格中的任意位置以显示菜单。

名为AllValuesEqualToBooleanConverter的值转换器用于将每个菜单项的值与组的当前值进行比较,并显示当前所选菜单项旁边的复选标记。

用于表示菜单选项的简单类用于说明。样本容器使用带有字符串和整数属性的元组,这样可以很容易地将一个紧密耦合的人类可读文本片段与机器友好的值配对。您可以单独使用字符串,也可以使用String和Enum来跟踪值,以便对当前的内容做出决策。 Type3VM.cs是分配给Type3.Xaml的DataContext的ViewModel。但是,您设法在现有应用程序框架中分配数据上下文,请在此处使用相同的机制。使用的应用程序框架依赖于INotifyPropertyChanged将更改的值传递给WPF及其绑定goo。如果你有依赖属性,你可能需要稍微调整一下代码。

除了转换器及其长度之外,此实现的缺点是每次打开上下文菜单时都会创建一个垃圾列表。对于单用户应用程序,这可能没问题,但你应该知道它。

该应用程序使用RelayCommand的实现,该实现可以从Haacked网站或您正在使用的任何框架中提供的任何其他ICommand兼容的帮助程序类中获得。

public class Type3VM : INotifyPropertyChanged
    {
        private List<MenuData> menuData = new List<MenuData>(new[] 
        {
            new MenuData("Zero", 0),
            new MenuData("One", 1),
            new MenuData("Two", 2),
            new MenuData("Three", 3),
        });

        public IEnumerable<MenuData> MenuData { get { return menuData.ToList(); } }

        private int selected;
        public int Selected
        {
            get { return selected; }
            set { selected = value; OnPropertyChanged(); }
        }

        private ICommand contextMenuClickedCommand;
        public ICommand ContextMenuClickedCommand { get { return contextMenuClickedCommand; } }

        private void ContextMenuClickedAction(object clicked)
        {
            var data = clicked as MenuData;
            Selected = data.Item2;
            OnPropertyChanged("MenuData");
        }

        public Type3VM()
        {
            contextMenuClickedCommand = new RelayCommand(ContextMenuClickedAction);
        }

        private void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class MenuData : Tuple<String, int>
    {
        public MenuData(String DisplayValue, int value) : base(DisplayValue, value) { }
    }

<UserControl x:Class="SampleApp.Views.Type3"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:Views="clr-namespace:SampleApp.Views"
             xmlns:Converters="clr-namespace:SampleApp.Converters"
             xmlns:ViewModels="clr-namespace:SampleApp.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{d:DesignInstance ViewModels:Type3VM}"
             >
    <UserControl.Resources>
        <Converters:AllValuesEqualToBooleanConverter x:Key="IsCheckedVisibilityConverter" EqualValue="True" NotEqualValue="False" />
    </UserControl.Resources>
    <Grid>
        <Grid.ContextMenu>
            <ContextMenu ItemsSource="{Binding MenuData, Mode=OneWay}">
                <ContextMenu.ItemContainerStyle>
                    <Style TargetType="MenuItem" >
                        <Setter Property="Header" Value="{Binding Item1}" />
                        <Setter Property="IsCheckable" Value="True" />
                        <Setter Property="IsChecked">
                            <Setter.Value>
                                <MultiBinding Converter="{StaticResource IsCheckedVisibilityConverter}" Mode="OneWay">
                                    <Binding Path="DataContext.Selected" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}"  />
                                    <Binding Path="Item2" />
                                </MultiBinding>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Command" Value="{Binding Path=DataContext.ContextMenuClickedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}}" />
                        <Setter Property="CommandParameter" Value="{Binding .}" />
                    </Style>
                </ContextMenu.ItemContainerStyle>
            </ContextMenu>
        </Grid.ContextMenu>
        <Grid.RowDefinitions><RowDefinition Height="*" /></Grid.RowDefinitions>
        <Grid.ColumnDefinitions><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" FontSize="30" Text="Right Click For Menu" />
    </Grid>
</UserControl>

public class AreAllValuesEqualConverter<T> : IMultiValueConverter
{
    public T EqualValue { get; set; }
    public T NotEqualValue { get; set; }

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        T returnValue;

        if (values.Length < 2)
        {
            returnValue = EqualValue;
        }

        // Need to use .Equals() instead of == so that string comparison works, but must check for null first.
        else if (values[0] == null)
        {
            returnValue = (values.All(v => v == null)) ? EqualValue : NotEqualValue;
        }
        else
        {
            returnValue = (values.All(v => values[0].Equals(v))) ? EqualValue : NotEqualValue;
        }

        return returnValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ValueConversion(typeof(object), typeof(Boolean))]
public class AllValuesEqualToBooleanConverter : AreAllValuesEqualConverter<Boolean>
{ }

答案 15 :(得分:0)

这是另一种使用RoutedUICommands,公共枚举属性和DataTriggers的方法。这是一个非常详细的解决方案。遗憾的是我没有看到任何使Style.Triggers变小的方法,因为我不知道怎么只说绑定值是唯一不同的东西? (顺便说一句,对于MVVMers来说,这是一个可怕的例子。我把所有内容放在MainWindow类中只是为了简单起见。)

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

答案 16 :(得分:0)

你可以这样做:

        <Menu>
            <MenuItem Header="File">
                <ListBox BorderThickness="0" Background="Transparent">
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel />
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                    <ListBox.ItemContainerStyle>
                        <Style TargetType="{x:Type ListBoxItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <MenuItem IsCheckable="True" IsChecked="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Header="{Binding Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </ListBox.ItemContainerStyle>
                    <ListBox.Items>
                        <ListBoxItem Content="Test" />
                        <ListBoxItem Content="Test2" />
                    </ListBox.Items>
                </ListBox>
            </MenuItem>
        </Menu>

它在视觉上有一些奇怪的副作用(当你使用它时你会看到),但它仍然有效

答案 17 :(得分:0)

只需为MenuItem创建一个模板,该模板将包含一个GroupName设置为某个值的RadioButton。 您还可以将RadioButtons的模板更改为MenuItem的默认检查字形(可以使用Expression Blend轻松提取)。

就是这样!