Взаимоисключающие проверяемые пункты меню? - PullRequest
43 голосов
/ 06 сентября 2010

Учитывая следующий код:

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

В XAML есть ли способ создания проверяемых пунктов меню, которые являются взаимоисключающими?Где пользователь проверяет item2, элементы 1 и 3 автоматически не проверяются.

Я могу сделать это в коде, отслеживая события щелчка в меню, определяя, какой элемент был проверен, и снимая отметки с других элементов меню.Я думаю, что есть более простой способ.

Есть идеи?

Ответы [ 16 ]

45 голосов
/ 06 сентября 2010

Возможно, это не то, что вы ищете, но вы можете написать расширение для класса 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, вы '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>

Это немного мучительно, но оно дает возможность не заставлять вас писать какой-либо дополнительный процедурный код (кроме класса расширения, конечно) для его реализации.

Благодарность достается Брэду Каннингему, который является автором оригинального решения ToggleButton.

8 голосов
/ 10 сентября 2012

Вы также можете использовать Поведение.Как этот:

<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);
    }
}
7 голосов
/ 29 февраля 2016

Поскольку самамильного ответа нет, я выкладываю свое решение здесь:

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

Таким образом, VS выдаст ошибку, если новичок попытается скомпилировать XAML следующим образом:

// обратите внимание, что это неправильное использование!

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

// обратите внимание, что это неправильное использование!

7 голосов
/ 05 сентября 2013

Добавление этого внизу, так как у меня еще нет репутации ...

Как бы ни был полезен ответ Патрика, он не гарантирует, что предметы не могут быть проверены. Для этого обработчик 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;
    }
}
6 голосов
/ 29 февраля 2016

Да, это можно легко сделать, сделав каждый MenuItem радиокнопкой.Это можно сделать, отредактировав шаблон элемента MenuItem.

  1. Щелкните правой кнопкой мыши элемент MenuItem на левой панели Outline документа> EditTemplate> EditCopy.Это добавит код для редактирования в Window.Resources.

  2. Теперь вам нужно сделать только два изменения, которые очень просты.

    Mutually Exclusive MenuItems а.Добавьте RadioButton с некоторыми ресурсами, чтобы скрыть часть его круга.

    b.Измените BorderThickness = 0 для границы элемента MenuItem.

    Эти изменения показаны ниже как комментарии, остальная часть сгенерированного стиля должна использоваться следующим образом:

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

    <MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
    
4 голосов
/ 16 июля 2012

Я просто думал, что добавлю свое решение, поскольку ни один из ответов не отвечал моим потребностям. Мое полное решение здесь ...

Элемент меню WPF в виде кнопки RadioB

Однако основная идея заключается в использовании ItemContainerStyle.

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

И следует добавить следующий щелчок по событию, чтобы при нажатии на MenuItem проверялась RadioButton (в противном случае вы должны точно щелкнуть по RadioButton):

private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e)
{
    MenuItem mi = sender as MenuItem;
    if (mi != null)
    {
        RadioButton rb = mi.Icon as RadioButton;
        if (rb != null)
        {
            rb.IsChecked = true;
        }
    }
}
4 голосов
/ 06 сентября 2010

В XAML нет встроенного способа сделать это, вам нужно будет развернуть собственное решение или получить существующее решение, если оно доступно.

2 голосов
/ 15 марта 2012

Я добился этого с помощью пары строк кода:

Сначала объявите переменную:

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


                }
1 голос
/ 06 февраля 2014

Я обнаружил, что получаю взаимоисключающие пункты меню при привязке MenuItem.IsChecked к переменной.

Но у него есть одна странность: если вы щелкнете по выбранному пункту меню, он станет недействительным, показанным обычным красным прямоугольником.Я решил это, добавив обработчик для MenuItem.Click, который предотвращает отмена выбора, просто установив IsChecked обратно в true.

Код ... Я привязываюсь к типу enum, поэтому я использую конвертер enum, который возвращаетЗначение true, если свойство bound равно указанному параметру.Вот 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;
        }
    }
0 голосов
/ 26 июня 2019

Вот простое решение на основе MVVM , которое использует простые IValueConverter и CommandParameter для MenuItem.

Нет необходимости изменять стиль любого MenuItem как элемента управления другого типа. Элементы меню автоматически отменяются, если значение привязки не соответствует параметру CommandParameter.

Привязка к свойству int (MenuSelection) в 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>

Определите ваш конвертер значений. Это проверит связанное значение с параметром команды и наоборот.

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>

Удачи!

...