互斥的可检查菜单项?

发布于 2024-09-18 02:03:05 字数 501 浏览 12 评论 0 原文

给出以下代码:

<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时,item 1和3会自动取消选中。

我可以通过监视菜单上的单击事件、确定选中哪个项目并取消选中其他菜单项,在后面的代码中完成此操作。我想有一个更简单的方法。

有什么想法吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(19

无声情话 2024-09-25 02:03:06

这可能不是您想要的,但您可以为 MenuItem 类编写一个扩展,该扩展允许您使用 GroupName 属性之类的内容RadioButton 类。我稍微修改了 这个 类似扩展的方便示例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>

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

感谢 Brad Cunningham,他是最初的 ToggleButton 解决方案的作者。

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.

旧人九事 2024-09-25 02:03:06

由于我还没有声誉,因此将其添加到底部...

尽管帕特里克的答案很有帮助,但它并不能确保无法取消选中项目。为此,应将 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;
    }
}

Adding this at the bottom since I don't have the reputation yet...

As helpful as Patrick's answer is, it doesn't ensure that items cannot be unchecked. In order to do that, the Checked handler should be changed to a Click handler, and changed to the following:

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;
    }
}
余生共白头 2024-09-25 02:03:06

您还可以使用行为。就像这个:

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

You can also use a Behavior. Like this one:

<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);
    }
}
却一份温柔 2024-09-25 02:03:06

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

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 将给出错误:

//注意这是错误的用法!

//注意这是错误的用法!

Since there is not a samilar answer, I post my solution here:

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

In XAML just use it as an usual 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>

Quite simple and clean. And of course you can make the GroupName a dependency property by some additional codes, that's all the same as others.

BTW, if you do not like the check mark, you can change it to whatever you like:

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

If you used plenty of this RadioMenuItem in your program, there is another more efficient version shown below. The literal data is aquired from e.GetFlattenedPathGeometry().ToString() in previous code snippet.

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

And at last, if you plan to wrap it for use in your project, you should hide IsCheckable property from the base class, since the auto check machenism of MenuItem class will lead the radio check state mark a wrong behavior.

private new bool IsCheckable { get; }

Thus VS will give an error if a newbie try to compile XAML like this:

// note that this is a wrong usage!

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

// note that this is a wrong usage!

北陌 2024-09-25 02:03:06

是的,通过将每个 MenuItem 设为 RadioButton 可以轻松完成此操作。这可以通过编辑菜单项的模板来完成。

  1. 右键单击文档大纲左窗格中的菜单项>编辑模板>编辑复制。这将在 Window.Resources 下添加用于编辑的代码。

  2. 现在,您只需要做两处更改,非常简单。

    互斥的菜单项a。添加带有一些资源的 RadioButton 以隐藏其圆圈部分。

    b.更改 MenuItem 边框部分的 BorderThickness = 0。

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

    ;
            
                
                
            
            <几何x:Key=“复选标记”>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
            
                <网格 SnapsToDevicePixels="true">
                    <矩形 x:Name="Bg" Fill="{TemplateBinding 背景}" RadiusY="2" RadiusX="2" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"/>
                    <矩形x:名称=“InnerBorder”边距=“1”RadiusY=“2”RadiusX=“2”/>
       
                    
                        
                            
    

  3. 应用样式,

Yes, this can be done easily by making every MenuItem a RadioButton. This can be done by Editing Template of MenuItem.

  1. Right-Click the MenuItem in the Document-Outline left pane > EditTemplate > EditCopy. This will add the code for editing under Window.Resources.

  2. Now, you have to do only two-changes which are very simple.

    Mutually Exclusive MenuItemsa. Add the RadioButton with some Resources to hide its circle portion.

    b. Change BorderThickness = 0 for MenuItem Border part.

    These changes are shown below as comments, rest of the generated style should be used as is :

    <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. Apply the Style ,

    <MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
    
悲欢浪云 2024-09-25 02:03:06

这是一个简单的、基于 MVVM 的解决方案,它利用每个 MenuItem 的简单 IValueConverterCommandParameter

无需将任何 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>

祝你好运!

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!

所谓喜欢 2024-09-25 02:03:06

我只是想我会提出我的解决方案,因为没有一个答案满足我的需求。我的完整解决方案在这里...

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>

并且应该添加以下事件 click ,以便在单击 MenuItem 时检查 RadioButton (否则您必须精确单击 RadioButton ):

private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e)
{
    if (sender is MenuItem mi)
    {
        if (mi.Icon is RadioButton rb)
        {
            rb.IsChecked = true;
        }
    }
}

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)
{
    if (sender is MenuItem mi)
    {
        if (mi.Icon is RadioButton rb)
        {
            rb.IsChecked = true;
        }
    }
}
剑心龙吟 2024-09-25 02:03:06

XAML 中没有内置方法可以执行此操作,您需要推出自己的解决方案或获取现有解决方案(如果可用)。

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.

萌︼了一个春 2024-09-25 02:03:06

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

首先声明一个变量:

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


                }

I achieved this using a couple of lines of code:

First declare a variable:

MenuItem LastBrightnessMenuItem =null;

When we are considering a group of menuitems, there is a probability of using a single event handler. In this case we can use this logic:

    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


                }
纵山崖 2024-09-25 02:03:06

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

但它有一个怪癖:如果单击选定的菜单项,它就会无效,如通常的红色矩形所示。我通过为 MenuItem.Click 添加一个处理程序来解决这个问题,该处理程序只需将 IsChecked 设置回 true 即可防止取消选择。

代码...我绑定到枚举类型,因此我使用枚举转换器,如果绑定属性等于提供的参数,则该转换器返回 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;
        }
    }

I find that I get mutually exclusive menu items when binding MenuItem.IsChecked to a variable.

But it has one quirk: If you click the selected menu item, it gets invalid, shown by the usual red rectangle. I solved it by adding a handler for MenuItem.Click that prevents unselecting by just setting IsChecked back to true.

The code... I'm binding to an enum type, so I use an enum converter that returns true if the bound property is equal to the supplied parameter. Here is the 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"/>

And here is the code behind:

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

        if (! menuItem.IsChecked)
        {
            menuItem.IsChecked = true;
        }
    }
情话难免假 2024-09-25 02:03:06

几年后,我看到这篇文章,其中包含我写的关键字...我认为在 wpf 中有一个简单的解决方案...也许是我,但我认为为这么小的东西拥有如此庞大的武器库有点特别作为公认的解决方案。我什至没有谈论 6likes 的解决方案,我不明白在哪里单击才能获得此选项。

所以也许它真的一点也不优雅......但这里有一个简单的解决方案。它的作用很简单......循环父级包含的所有元素,将其设置为 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 有问题,但必须这样做才能只选择一个菜单项似乎令人难以置信。

Several years after i see this post with the keywords i wrote... i thought there was an easy solution, in wpf... Perhaps it's me, but i think it's a bit special to have a such massive arsenal for a so little thing as accepted solution. I don't even talk about the solution with 6likes i didn't understood where to click to have this options.

So perhaps it's really no elegant at all... But here a simple solution. What it do is simple.. a loop to all elements contained by the parent, to put it at false. The most of time people split this part from the others parts, of course it's only correct in this case.

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

that's all and easy, xaml is a classic code with absolutaly nothing particular

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

Of course you could have a need of the click method, it's not a problem, you can make a method that accept an object sender and each of your click method will use this method. It's old, it's ugly but for the while it works.
And i have some problems to imagine so much code line for a so little thing, it's probably me that have a problem with xaml, but it seems incredible to have to do this to obtains to just have only one menuitem selected.

吹梦到西洲 2024-09-25 02:03:06

对@Patrick 答案的一个小补充。

正如 @MK10 提到的,该解决方案允许用户取消选择组中的所有项目。但他建议的改变现在对我不起作用。也许,WPF 模型从那时起就发生了变化,但现在当取消选中某个项目时,不会触发 Checked 事件。

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

我更改了这些过程:

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

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

A small addition to the @Patrick answer.

As @MK10 mentioned, this solution allows user to deselect all items in a group. But the changes he suggested doesn't work for me now. Maybe, the WPF model was changed since that time, but now Checked event doesn't fired when an item is unchecked.

To avoid it, I would suggest to process the Unchecked event for MenuItem.

I changed these procedures:

        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
        }

and added the next handler:

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

Now the checked item remains checked when user clicks it second time.

一百个冬季 2024-09-25 02:03:06

这是另一种方法——无论如何都不容易,但它兼容 MVVM、可绑定且高度可单元测试。如果您可以自由地将转换器添加到项目中,并且不介意每次打开上下文菜单时以新项目列表的形式出现一些垃圾,那么这非常有效。它满足了如何在上下文菜单中提供一组互斥的选中项的原始问题。

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

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

使用一个代表菜单选项的简单类进行说明。示例容器使用具有 String 和 Integer 属性的 Tuple,这使得很容易将紧密耦合的人类可读文本片段与机器友好的值配对。您可以单独使用字符串或字符串和枚举来跟踪值,以便对当前内容做出决策。
Type3VM.cs 是分配给 Type3.Xaml 的 DataContext 的 ViewModel。无论您如何在现有应用程序框架中分配数据上下文,请在此处使用相同的机制。所使用的应用程序框架依赖于 INotifyPropertyChanged 将更改的值传达给 WPF 及其绑定对象。如果您有依赖属性,您可能需要稍微调整代码。

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

该应用程序使用 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>
{ }

Here is yet another way – not easy by any stretch but it is MVVM compatible, bindable and highly unit testable. If you have the freedom to add a Converter to your project and don’t mind a little garbage in the form of a new list of items every time the context menu opens, this works really well. It meets the original question of how to provide a mutually exclusive set of checked items in a context menu.

I think if you want to extract all of this into a user control you could make it into a reusable library component to reuse across your application.
Components used are Type3.Xaml with a simple grid, one text block and the context menu. Right-click anywhere in the grid to make the menu appear.

A value converter named AllValuesEqualToBooleanConverter is used to compare each menu item’s value to the current value of the group and show the checkmark next to the menu item that is currently selected.

A simple class that represent your menu choices is used for illustration. The sample container uses Tuple with String and Integer properties that make is fairly easy to have a tightly coupled human readable snippet of text paired with a machine-friendly value. You can use strings alone or String and an Enum to keep track of the Value for making decisions over what is current.
Type3VM.cs is the ViewModel that is assigned to the DataContext for Type3.Xaml. However you contrive to assign your data context in your existing application framework, use the same mechanism here. The application framework in use relies on INotifyPropertyChanged to communicate changed values to WPF and its binding goo. If you have dependency properties you may need to tweak the code a little bit.

The drawback to this implementation, aside from the converter and its length, is that a garbage list is created every time the context menu is opened. For single user applications this is probably ok but you should be aware of it.

The application uses an implementation of RelayCommand that is readily available from the Haacked website or any other ICommand-compatible helper class available in whatever framework you are using.

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>
{ }
暖心男生 2024-09-25 02:03:06
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.
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.
若言繁花未落 2024-09-25 02:03:06

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

就是这样!

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!

栖竹 2024-09-25 02:03:06

你可以这样做:

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

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

You could do something like this:

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

It has some weird side effect visually (you'll see when you use it), but it works nonetheless

自由范儿 2024-09-25 02:03:06

这是另一种使用 RoutedUICommands(公共枚举属性)和 DataTriggers 的方法。这是一个非常冗长的解决方案。不幸的是,我没有看到任何使 Style.Triggers 变小的方法,因为我不知道如何说 Binding Value 是唯一不同的东西? (顺便说一句,对于 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
  }
}

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
  }
}
蹲在坟头点根烟 2024-09-25 02:03:06

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

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

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>

Here is a custom control that i've created for this purpose.
It handles correctly checking, unchecking, clicks events and group name changes.

If you want you can override the style of the menu item and change the checkmark to a radiomark, but is not necessary:

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

Example:

<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>
沫雨熙 2024-09-25 02:03:06

实现互斥工具条菜单项的最佳且正确的方法是:

private void ChangecheckedItemsOnDropdownItemsClick(object sender, ToolStripItemClickedEventArgs e)
{
 //get the currently checked item
 ToolStripMenuItem itemChecked = (ToolStripMenuItem)sender;

 // loop through the dropdown items for the main ToolStrip item
 foreach (ToolStripMenuItem item in NameofParentToolStripMenuItem.DropDownItems)
 {
 // if the current item is same as checked item, continue
 if (item == itemChecked) continue;
  // else uncheck the item
 else
  item.Checked = false;
 }
}

请将 NameofParentToolStripMenuItem 替换为您想要具有此属性的菜单项。

The best and correct way to implement mutually exclusive toolstrip menu item is;

private void ChangecheckedItemsOnDropdownItemsClick(object sender, ToolStripItemClickedEventArgs e)
{
 //get the currently checked item
 ToolStripMenuItem itemChecked = (ToolStripMenuItem)sender;

 // loop through the dropdown items for the main ToolStrip item
 foreach (ToolStripMenuItem item in NameofParentToolStripMenuItem.DropDownItems)
 {
 // if the current item is same as checked item, continue
 if (item == itemChecked) continue;
  // else uncheck the item
 else
  item.Checked = false;
 }
}

Please replace NameofParentToolStripMenuItem with the menuitem where you want to have this property.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文