Рейтинг@Mail.ru

Как в XAML создать checkable-элементы меню, которые поддерживают только один нажатый элемент (как RadioButton)

автор:
Be the first to comment! Программирование
Print Friendly, PDF & Email

Допустим, мы хотим в XAML создать меню, содержащее список элементов, в котором одновременно может быть выбран только один (поведение наподобие RadioButton).

В первую очередь, создадим вспомогательный класс, который наследуется от DependencyObject и содержит базовую логику.

Namespace Helpers

    ''' <summary>
    ''' Позволяет создать checkable-элементы меню,
    ''' которые поддерживают только один нажатый элемент меню одновременно (как RadioButton).
    ''' </summary>
    Public Class MenuItemExtensions
        Inherits DependencyObject

        Public Shared ElementToGroupNames As New Dictionary(Of MenuItem, String)()
		
	''' <summary>
        ''' Хранит состояние выделенных пунктов меню.
        ''' </summary>
        Private Shared ReadOnly ElementToGroupValues As New Dictionary(Of MenuItem, Boolean)()

        Public Shared ReadOnly GroupNameProperty As DependencyProperty = DependencyProperty.RegisterAttached("GroupName", GetType(String), GetType(MenuItemExtensions), New PropertyMetadata(String.Empty, AddressOf OnGroupNameChanged))

        Public Shared Sub SetGroupName(element As MenuItem, value As String)
            element.SetValue(GroupNameProperty, value)
        End Sub

        Public Shared Function GetGroupName(element As MenuItem) As String
            Return element.GetValue(GroupNameProperty).ToString()
        End Function

        Private Shared Sub OnGroupNameChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
            Dim menuItem As MenuItem = TryCast(d, MenuItem) 
            If (menuItem IsNot Nothing) Then

                Dim newGroupName As String = e.NewValue.ToString()

                If (String.IsNullOrEmpty(newGroupName)) Then
                    RemoveCheckboxFromGrouping(menuItem) 'удаляем из группы чекбокс                
                Else
                    'Переключаемся на новую группу:
                    Dim oldGroupName As String = e.OldValue.ToString()
                    If (newGroupName <> oldGroupName) Then
                        If (Not String.IsNullOrEmpty(oldGroupName)) Then 'удаляем привязку старой группы
                            RemoveCheckboxFromGrouping(menuItem)
                        End If
                        ElementToGroupNames.Add(menuItem, e.NewValue.ToString())
                        AddHandler menuItem.Checked, AddressOf MenuItemChecked
                        AddHandler menuItem.Unchecked, AddressOf MenuItemUnchecked
                    End If
                End If
            End If
        End Sub

        Private Shared Sub RemoveCheckboxFromGrouping(itm As MenuItem)
            ElementToGroupNames.Remove(itm)
            ElementToGroupValues.Remove(itm)
            RemoveHandler itm.Checked, AddressOf MenuItemChecked
            RemoveHandler itm.Unchecked, AddressOf MenuItemUnchecked
        End Sub

        Private Shared Sub MenuItemChecked(sender As Object, e As RoutedEventArgs)
            Dim menuItem As MenuItem = CType(e.OriginalSource, MenuItem)
            For Each item In ElementToGroupNames
                If (item.Key IsNot menuItem) AndAlso (item.Value = GetGroupName(menuItem)) Then
                    item.Key.IsChecked = False
                End If
                ElementToGroupValues(item.Key) = item.Key.IsChecked 'запоминаем выделенный пункт меню
            Next
        End Sub
		
        ''' <summary>
        ''' Проверяет, что есть выделенный пункт меню. Если нет, то восстанавливает выделение.
        ''' </summary>
        ''' <remarks>
        ''' Если нажать на выделенный пункт меню, то по умолчанию выделение снимается. 
        ''' Чтобы этого избежать, проверяем, что есть один выделенный пункт, и если его нет, то восстанавливаем выделение.
        ''' </remarks>
        Private Shared Sub MenuItemUnchecked(sender As Object, e As RoutedEventArgs)
            Dim mi As MenuItem = CType(sender, MenuItem)
            Dim hasCheckedItem As Boolean = False
            Dim sameGroupItems = ElementToGroupNames.Where(Function(x) GetGroupName(x.Key) = GetGroupName(mi))
           'Проверяем, есть ли выделенные элементы:
            For Each item As KeyValuePair(Of MenuItem, String) In sameGroupItems
                If item.Key.IsChecked Then
                    hasCheckedItem = True
                Exit For
            End If
        Next
        'Если нет выделенных элементов, то восстанавливаем выделение:
        If (Not hasCheckedItem) Then
            Dim selectedGroupItems = ElementToGroupValues.Where(Function(x) GetGroupName(x.Key) = GetGroupName(mi))
            For Each item In selectedGroupItems
                If item.Value Then
                    item.Key.IsChecked = True
                    Exit For
                End If
            Next
        End If
        End Sub

    End Class

End Namespace

Конечно же нам понадобится ссылка на пространство имён Helpers в XAML файле:

xmlns:help="clr-namespace:Helpers"

Рассмотрим использование класса MenuItemExtensions на таком примере. Пусть у нас имеется несколько значений толщин линий, которыми мы хотим рисовать некий график. Эти толщины будут представлены в виде списка дочерних элементов меню. Естественно, в один момент времени выбрать можно только одну толщину.

В XAML файле в разделе ресурсов создадим массив толщин:

<x:Array Type="{x:Type sys:Int32}" x:Key="lineWidthItems">
    <sys:Int32>1</sys:Int32>
    <sys:Int32>2<sys:Int32>
    <sys:Int32>3</sys:Int32>
    <sys:Int32>4</sys:Int32>
    <sys:Int32>5</sys:Int32>
</x:Array>

Здесь "sys" – это псевдоним пространства имён System, который мы должны импортировать и указать в атрибутах класса XAML файла:

xmlns:sys="clr-namespace:System;assembly=mscorlib"

Теперь в XAML разметке добавим меню, элементами которого будут толщины линий:

<Menu>
    <MenuItem Header="Толщина линий" ItemsSource="{StaticResource lineWidthItems} Style="{StaticResource mnuLineWidthStyle}" />
</Menu>

Указанный стиль mnuLineWidthStyle необходимо разместить в секции ресурсов XAML файла. Опишем стиль так:

<Style TargetType="MenuItem" x:Key="mnuLineWidthStyle">
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
	    <Style TargetType="MenuItem">
	        <Setter Property="Header" Value="{Binding .}" />
                <Setter Property="help:MenuItemExtensions.GroupName" Value="linewidths" />
                <Setter Property="IsCheckable" Value="True" />
                <Setter Property="IsChecked">
		    <Setter.Value>
		        <MultiBinding Converter="{StaticResource myConv}">
			    <Binding Path="PlotLineWidth" />
                            <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Header" />
			</MultiBinding>
                   </Setter.Value>
		</Setter>
		<EventSetter Event="Checked" Handler="SetLineWidth" />
            </Style>
        </Setter.Value>
    </Setter>
</Style>

Добавим в застраничный код обработчик выбора элемента:

Private Sub SetLineWidth(sender As Object, e As RoutedEventArgs)
    Dim linewidth As String = CType(sender, MenuItem).Header.ToString()
    PlotLineWidth = Integer.Parse(linewidth)
    'PlotLineWidth – это свойство, значение которого содержит ту самую толщину линий
    'Или реализуете здесь какую-то свою логику.
End Sub

Также нужно создать конвертер "myConv", который сравнивает два полученных значения (значение текстового свойства Header элемента меню и свойства PlotLineWidth) и возвращает результат сравнения в вышеупомянутом MultiBinding:

Namespace Converters

    Public Class MyConverter
        Implements IMultiValueConverter

        Public Function Convert(values() As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert
            Dim cm1 As String = values(0).ToString()
            Dim cm2 As String = values(1).ToString()
            Return (cm1 = cm2)
        End Function

        Public Function ConvertBack(value As Object, targetTypes() As Type, parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack
            Throw New NotImplementedException()
        End Function

    End Class

End Namespace

И объявить этот конвертер в ресурсах XAML файла:

<conv:MyConverter x:Key="myConv" />

Само собой, что пространство имён Conveters также необходимо импортировать

xmlns:conv="clr-namespace:Converters"

Теперь всё будет работать, как мы и хотели.

Меню с единственно выделяемым элементом в XAML
Меню с единственно выделяемым элементом в XAML
Last modified onВторник, 02 Сентябрь 2025 17:13 Read 819 times
Ключевые слова: :

Поблагодарить автора:

Поделиться

Print Friendly, PDF & Email