C# WPF - 如何结合使用DataTrigger和Trigger?

5

我不知道是否需要结合DataTrigger和Trigger,如果有更好的方法,请告诉我。

我的目标是创建一个带有图标的菜单,当遇到悬停或选择事件时,图标会发生变化。

这里有一个枚举定义了所有菜单类型:

public enum PageTypes:byte
{
    NotSet = 0,
    HomePage = 1,
    ShopPage = 2,
    AboutPage = 3
}

然后我创建了一个MenuItemModel来代表每个菜单项:

public class MenuItemModel : INotifyPropertyChanged
{
    private PageTypes _menuItemType = PageTypes.NotSet;
    public PageTypes MenuItemType { get { return _menuItemType; } set { if (value != _menuItemType) { _menuItemType = value; RaisePropertyChanged(() => MenuItemType); } } }

    private bool _isSelected = false;
    public bool IsSelected { get { return _isSelected; } set { if (value != _isSelected) { _isSelected = value; RaisePropertyChanged(() => IsSelected); } } }
}

好的,那么我开始创建用户界面。

<!-- MenuItem Template -->
<DataTemplate x:Key="MenuTemplate">
    <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
        <Image>
            <Image.Style>
                <Style TargetType="Image">
                    <Setter Property="Source" Value="/Image/Home_normal.png"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
                            <Setter Property="Source" Value="/Image/Shop_normal.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="AboutPage">
                            <Setter Property="Source" Value="/Image/About_normal.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>
    </Button>
</DataTemplate>

直到现在,一切都很简单,但是当我尝试制作鼠标悬停和选定效果时,问题就出现了。
例如,如果鼠标悬停在home_normal.png上,它应该改变为home_hover.png,如果IsSelected属性为TRUE,则忽略悬停触发器,然后使用home_selected.png。但是有3个图像,我如何知道哪个图像应该更改?
<!-- MenuItem Template -->
<DataTemplate x:Key="MenuTemplate">
    <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
        <Image>
            <Image.Style>
                <Style TargetType="Image">
                    <Setter Property="Source" Value="/Image/Home_normal.png"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
                            <Setter Property="Source" Value="/Image/Shop_normal.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="AboutPage">
                            <Setter Property="Source" Value="/Image/About_normal.png"/>
                        </DataTrigger>

                        <!-- MY PLAN -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Source" Value="?_hover.png"/>
                        </Trigger>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter Property="Source" Value="?_selected.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>
    </Button>
</DataTemplate>

如果您在“我的计划”评论中看到问号,那就是我的问题:在价值字段中我应该做什么?


嗨@AnjumSKhan,将ImageSource绑定到代码后端很好,但我想把任何可以由XAML解决的东西放在那里,以简化ViewModel。 - Oh My Dog
@哦-迈-哥,你必须使用选择器。在你的DataTrigger中,你可以获取你的条件值,在那里你可以有条件地应用你的样式(从而硬编码图像源值)。 - AnjumSKhan
@AnjumSKhan 你好,我第一次听说StyleSelector。经过搜索和测试后,我认为这个选择器可以解决我的问题。但我仍然需要你的帮助:StyleSelector能在XAML中工作吗?因为我不想使用Converter等内容……我在谷歌上没有找到纯XAML的示例。 - Oh My Dog
@oh-my-god 如果你想根据条件应用样式,那么你可以使用DataTrigger来实现。 - AnjumSKhan
探索行为,它们非常强大,并且允许您完全使用XAML实现一些有趣的事情。 - AnjumSKhan
显示剩余2条评论
2个回答

9
您可以像这样使用MultiDataTrigger。但是,您应该为所有类型的页面添加相同的三个触发器。请注意,下一个触发器将覆盖上一个,并且条件运作方式类似于逻辑AND。
<p:Style.Triggers xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
        <Setter Property="Source" Value="/Image/Shop_normal.png"/>
    </DataTrigger>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding MenuItemType}" Value="ShopPage" />
            <Condition Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsMouseOver}" Value="true" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Source" Value="/Image/Shop_MouseOver.png" />
    </MultiDataTrigger>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding MenuItemType}" Value="ShopPage" />
            <Condition Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsSelected}" Value="true" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Source" Value="/Image/Shop_IsSelected.png" />
    </MultiDataTrigger>
</p:Style.Triggers>

2
在我看来,您已经收到并接受的答案是一个不错的答案。它完全基于XAML,这似乎是您场景中的主要目标,并且应该很好地工作。话虽如此,仅基于XAML的解决方案相当冗长,并涉及大量冗余代码。这已经在上面的场景中看到了,在那里您有两种按钮类型,每种类型都有三种可能的状态。随着您添加按钮类型和状态,情况只会变得更糟。

如果您愿意做一些代码后台处理,我认为您可以用更少的冗余实现同样的效果。

具体而言,如果您使用<MultiBinding>,则可以将相关属性绑定到集合中,该集合可用于查找正确的图像源。为了实现这一点,我需要创建几个小容器类型来存储查找数据,当然还需要IMultiValueConverter实现来使用它们:

容器类型:

[ContentProperty("Elements")]
class BitmapImageArray
{
    private readonly List<ButtonImageStates> _elements = new List<ButtonImageStates>();

    public List<ButtonImageStates> Elements
    {
        get { return _elements; }
    }
}

class ButtonImageStates
{
    public string Key { get; set; }
    public BitmapImage[] StateImages { get; set; }
}

转换器:
class OrderedFlagConverter : IMultiValueConverter
{
    public object Convert(object[] values,
        Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        BitmapImageArray imageData = (BitmapImageArray)parameter;
        string type = (string)values[0];

        foreach (ButtonImageStates buttonStates in imageData.Elements)
        {
            if (buttonStates.Key == type)
            {
                int index = 1;

                while (index < values.Length)
                {
                    if ((bool)values[index])
                    {
                        break;
                    }

                    index++;
                }

                return buttonStates.StateImages[index - 1];
            }
        }

        return DependencyProperty.UnsetValue;
    }

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

在你的例子中,使用上面的内容可能看起来像这样:
<DataTemplate x:Key="MenuTemplate">
  <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
    <Image>
      <Image.Source>
        <MultiBinding>
          <MultiBinding.Converter>
            <l:OrderedFlagConverter/>
          </MultiBinding.Converter>
          <MultiBinding.ConverterParameter>
            <l:BitmapImageArray>
              <l:ButtonImageStates Key="ShopPage">
                <l:ButtonImageStates.StateImages>
                  <x:Array Type="{x:Type BitmapImage}">
                    <BitmapImage UriSource="/Image/Shop_selected.png"/>
                    <BitmapImage UriSource="/Image/Shop_hover.png"/>
                    <BitmapImage UriSource="/Image/Shop_normal.png"/>
                  </x:Array>
                </l:ButtonImageStates.StateImages>
              </l:ButtonImageStates>
              <l:ButtonImageStates Key="AboutPage">
                <l:ButtonImageStates.StateImages>
                  <x:Array Type="{x:Type BitmapImage}">
                    <BitmapImage UriSource="/Image/About_selected.png"/>
                    <BitmapImage UriSource="/Image/About_hover.png"/>
                    <BitmapImage UriSource="/Image/About_normal.png"/>
                  </x:Array>
                </l:ButtonImageStates.StateImages>
              </l:ButtonImageStates>
            </l:BitmapImageArray>
          </MultiBinding.ConverterParameter>
          <Binding Path="ButtonType"/>
          <Binding Path="IsMouseOver" RelativeSource="{RelativeSource Self}"/>
          <Binding Path="IsSelected"/>
        </MultiBinding>
      </Image.Source>
    </Image>
  </Button>
</DataTemplate>

该转换器以影响按钮视觉状态的属性绑定作为输入。第一个绑定值仅是按钮的类型;这用于查找正确的按钮状态数组。剩余的绑定值(在此方法中可以任意多个)是要搜索的标志;图像按照标志的相同顺序存储,末尾还有一个额外的“默认”图像(即如果未设置任何标志,则返回默认图像)。
通过这种方式,添加新的按钮类型只需添加一个新的ButtonImageStates对象,指定该按钮类型的正确键,并且添加新的按钮状态只需要向每个按钮类型的列表添加一行:与该按钮类型的该状态的图像对应的BitmapImage引用。
以这种方式进行操作可以大大减少需要添加的代码量,因为需要新的按钮类型和状态:给定的按钮类型只需在XAML中提及一次,同样每个触发属性只被提及一次。仅使用XAML的方法将需要大量重复的模板代码,并且实际的图像文件引用将分散在样式声明中。
这是基本技术的简单演示。由于没有一个好的 MCVE可以开始,我不想浪费时间重新创建不是演示所必需的代码部分:
  • 我只创建了四个状态图像,并且当然只编写了处理两种不同按钮类型的四个可能状态的代码。
  • 我也没有将其放在菜单中;我只使用一个普通的ItemsControl来呈现按钮。
  • 当然,视图模型是一个简化的类;我没有关注属性更改通知,因为它在这里不需要。如果包括那个,例子仍然可以工作。

这是演示中使用的图像(我是一名程序员,不是艺术家……我甚至考虑过不去管图像内容,因为这也不是演示基本技术所必需的,但我认为我可以处理四个基本图像:):

red_normal.png
red_hover.png
green_normal.png
green_hover.png

这些被添加到项目的"Resources"文件夹中,Build Action设置为ResourceXAML:
<Window x:Class="TestSO34193266MultiTriggerBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:TestSO34193266MultiTriggerBinding"
        Title="MainWindow" Height="350" Width="525">

  <Window.Resources>
    <l:OrderedFlagConverter x:Key="orderedFlagConverter1"/>

    <BitmapImage x:Key="bitmapRedNormal"
                 UriSource="pack://application:,,,/Resources/red_normal.png"/>
    <BitmapImage x:Key="bitmapRedHover"
                 UriSource="pack://application:,,,/Resources/red_hover.png"/>
    <BitmapImage x:Key="bitmapGreenNormal"
                 UriSource="pack://application:,,,/Resources/green_normal.png"/>
    <BitmapImage x:Key="bitmapGreenHover"
                 UriSource="pack://application:,,,/Resources/green_hover.png"/>

    <l:ViewModel x:Key="redViewModel" ButtonType="Red"/>
    <l:ViewModel x:Key="greenViewModel" ButtonType="Green"/>

    <x:Array x:Key="items" Type="{x:Type l:ViewModel}">
      <StaticResource ResourceKey="redViewModel"/>
      <StaticResource ResourceKey="greenViewModel"/>
    </x:Array>

    <x:Array x:Key="redButtonStates" Type="{x:Type BitmapImage}">
      <StaticResource ResourceKey="bitmapRedHover"/>
      <StaticResource ResourceKey="bitmapRedNormal"/>
    </x:Array>

    <x:Array x:Key="greenButtonStates" Type="{x:Type BitmapImage}">
      <StaticResource ResourceKey="bitmapGreenHover"/>
      <StaticResource ResourceKey="bitmapGreenNormal"/>
    </x:Array>

    <l:BitmapImageArray x:Key="allButtonStates">
      <l:ButtonImageStates Key="Red" StateImages="{StaticResource redButtonStates}"/>
      <l:ButtonImageStates Key="Green" StateImages="{StaticResource greenButtonStates}"/>
    </l:BitmapImageArray>

    <ItemsPanelTemplate x:Key="panelTemplate">
      <StackPanel IsItemsHost="True" Orientation="Horizontal"/>
    </ItemsPanelTemplate>

    <DataTemplate x:Key="template" DataType="l:ViewModel">
      <Button>
        <Image Stretch="None">
          <Image.Source>
            <MultiBinding Converter="{StaticResource orderedFlagConverter1}"
                          ConverterParameter="{StaticResource allButtonStates}">
              <Binding Path="ButtonType"/>
              <Binding Path="IsMouseOver" RelativeSource="{RelativeSource Self}"/>
            </MultiBinding>
          </Image.Source>
        </Image>
      </Button>
    </DataTemplate>

    <!-- explicit namespace only for the benefit of Stack Overflow formatting -->
    <p:Style TargetType="ItemsControl"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
      <Setter Property="ItemsSource" Value="{StaticResource items}"/>
      <Setter Property="ItemsPanel" Value="{StaticResource panelTemplate}"/>
    </p:Style>
  </Window.Resources>

  <StackPanel>
    <ItemsControl ItemTemplate="{StaticResource template}"/>
  </StackPanel>
</Window>

C#:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

class ViewModel
{
    public string ButtonType { get; set; }
}

class OrderedFlagConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        BitmapImageArray imageData = (BitmapImageArray)parameter;
        string type = (string)values[0];

        foreach (ButtonImageStates buttonStates in imageData.Elements)
        {
            if (buttonStates.Key == type)
            {
                int index = 1;

                while (index < values.Length)
                {
                    if ((bool)values[index])
                    {
                        break;
                    }

                    index++;
                }

                return buttonStates.StateImages[index - 1];
            }
        }

        return DependencyProperty.UnsetValue;
    }

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

[ContentProperty("Elements")]
class BitmapImageArray
{
    private readonly List<ButtonImageStates> _elements = new List<ButtonImageStates>();

    public List<ButtonImageStates> Elements
    {
        get { return _elements; }
    }
}

class ButtonImageStates
{
    public string Key { get; set; }
    public BitmapImage[] StateImages { get; set; }
}

一个小问题:不知为何,在XAML编辑器中,<Window>元素声明会出现以下错误消息:

集合属性“TestSO34193266MultiTriggerBinding.ButtonImageStates”。“StateImages”为空。

显然,我未能跳过XAML编辑器要求我就ButtonImageStates的声明和/或实现所需达成的协议,但我不知道是什么。代码可以正确编译和运行,所以我没有费心去解决那个部分。也许有更好的方法来表示按钮状态图像的映射,但这种方式有效,并且除了虚假的错误之外,对我来说还好。


抱歉今天才看到你的回答...我已经加班很久了....无论如何,我会尝试你的解决方案,如果更好的话我会采纳。但是我不能接受你的答案,因为我已经接受了bar222的。非常感谢。@Peter - Oh My Dog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接