互斥的可检查菜单项?

TER*_*ytE 44 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>
Run Code Online (Sandbox Code Playgroud)

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

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

有任何想法吗?

Pat*_*ick 46

这可能不是您正在寻找的,但您可以为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;
                     }
                }
           }
      }
 }
Run Code Online (Sandbox Code Playgroud)

然后,在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>
Run Code Online (Sandbox Code Playgroud)

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

感谢撰写原始ToggleButton解决方案的Brad Cunningham.


use*_*735 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);
    }
}
Run Code Online (Sandbox Code Playgroud)


MK1*_*K10 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;
    }
}
Run Code Online (Sandbox Code Playgroud)


qaq*_*111 7

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在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>
Run Code Online (Sandbox Code Playgroud)

非常简单干净。当然,您可以GroupName通过一些其他代码将dependency属性设置为其他属性。

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

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();
}
Run Code Online (Sandbox Code Playgroud)

如果您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;
}
Run Code Online (Sandbox Code Playgroud)

最后,如果您打算包装它以便在项目中使用,则应该IsCheckable对基类隐藏属性,因为类的自动检查机制MenuItem会导致无线电检查状态标记错误的行为。

private new bool IsCheckable { get; }
Run Code Online (Sandbox Code Playgroud)

因此,如果新手尝试编译XAML,则VS将给出错误消息:

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

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

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


Anj*_*han 6

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

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

  2. 现在,您只需进行两项非常简单的更改.

    互斥的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>
    
    Run Code Online (Sandbox Code Playgroud)
  3. 应用风格,

    <MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
    
    Run Code Online (Sandbox Code Playgroud)


Rhy*_*ous 5

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

WPF MenuItem作为单选按钮

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

<MenuItem.ItemContainerStyle>
    <Style TargetType="MenuItem">
        <Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
        <EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
    </Style>
</MenuItem.ItemContainerStyle>
Run Code Online (Sandbox Code Playgroud)

并且应该添加以下事件单击,以便在单击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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Cha*_*man 5

这是一个简单的、基于 MVVM 的解决方案,它利用一个简单的IValueConverter和每个 MenuItem 的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>
Run Code Online (Sandbox Code Playgroud)

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

添加您的资源

<Window.Resources>
    <ResourceDictionary>
        <local:MatchingIntToBooleanConverter x:Key="MatchingIntToBooleanConverter"/>
    </ResourceDictionary>
</Window.Resources>
Run Code Online (Sandbox Code Playgroud)

祝你好运!