【问题标题】:Applying Styles With Binding通过绑定应用样式
【发布时间】:2021-07-10 00:50:38
【问题描述】:

我正在尝试创建一个名为PieceImage 的自定义控件,其中包含几个预定义的Canvas 元素之一。我在名为 PieceImageDictionary.xaml 的ResourceDictionary 中定义了这些画布。我在PieceImageColor1Color2 中设置了一些依赖属性,我想绑定到画布中路径的FillStroke。我对自定义控件和简单的数据绑定有很好的理解,但是样式让我有点困惑。

所以基本上我有一个带有画布的ResourceDictionary,我将其视为图像,我希望能够有多个PieceImageControl实例,每个实例都可以独立选择其中一个图像(不是全局样式)和我希望能够在PieceImage 控件上使用DependencyProperty 设置颜色。

我尝试将我的画布包装在 ControlTemplates 中,但无论出于何种原因,“Canvas”都不是有效的TargetType

如果我将TargetType 设置为“Control”,我可以应用此模板,但是当我尝试将TemplateBinding 添加到Paths 时,我想设置(Color1) 的属性不能成立。我有点理解这一点,因为Color1 属性在我的PieceImage 控件中,所以TemplateBindingControl 甚至Canvas 都不起作用。

然后我尝试使用 AncestorType 的“PieceImageRelativeBinding,但这也不起作用。没有错误,只是一个空白Canvas。我尝试在Canvas 上调用UpdateLayout()InvalidateVisual() 以防万一,但没有任何变化。

我尝试使用 XamlReader 和 Writer 来创建这些画布,而不是使用模板,这最初在我静态定义颜色时有效,但当我尝试添加 RelativeBinding 时,我收到一个 Xaml Parse 错误,说它无法创建字符串“local:PieceImage”中的类型

我尝试在代码中添加绑定,但也得到了一个空白 Canvas。我承认我对编码绑定了解不多,这是我的实现:

var binding = new Binding(Color1Property.Name);
binding.RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent);
BindingOperations.SetBinding(path, Path.FillProperty, binding);

这是我的基本代码,为了清楚起见,我已经清除了所有失败的尝试,否则会一团糟。

PieceImage.cs

public class PieceImage : Control
{
    private Canvas _canvas;
    static PieceImage()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(PieceImage), new FrameworkPropertyMetadata(typeof(PieceImage)));
    }
    public static readonly DependencyProperty Color1Property = DependencyProperty.Register(nameof(Color1), typeof(Brush), typeof(PieceImage), new PropertyMetadata(Brushes.BlanchedAlmond));
    public static readonly DependencyProperty Color2Property = DependencyProperty.Register(nameof(Color2), typeof(Brush), typeof(PieceImage), new PropertyMetadata(Brushes.DarkGray));
    public static readonly DependencyProperty PieceTypeProperty = DependencyProperty.Register(nameof(PieceType), typeof(Enums.PieceType), typeof(PieceImage), new PropertyMetadata(Enums.PieceType.Pawn));
    public static readonly DependencyProperty SwapColorsProperty = DependencyProperty.Register(nameof(SwapColors), typeof(bool), typeof(PieceImage), new PropertyMetadata(false));

    public Brush Color1
    {
        get { return (Brush)GetValue(Color1Property); }
        set { SetValue(Color1Property, value); }
    }
    public Brush Color2
    {
        get { return (Brush)GetValue(Color2Property); }
        set { SetValue(Color2Property, value); }
    }
    public Enums.PieceType PieceType
    {
        get { return (Enums.PieceType)GetValue(PieceTypeProperty); }
        set { SetValue(PieceTypeProperty, value); }
    }
    public bool SwapColors
    {
        get { return (bool)GetValue(SwapColorsProperty); }
        set { SetValue(SwapColorsProperty, value); }
    }
    public override void OnApplyTemplate()
    {
        _canvas = Template.FindName("PART_Canvas", this) as Canvas;
        //Here is where most of my logic would go when I was loading xaml or coding bindings
        base.OnApplyTemplate();
    }
}

Generic.xaml(部分)

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Wagner.Chess.UI.Controls">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="\pieceimagedictionary.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <Style TargetType="{x:Type local:PieceImage}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PieceImage">
                    <Canvas x:Name="PART_Canvas"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

PieceImageDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Wagner.Chess.UI.Controls">
    <Canvas Height="64" Width="64"  x:Key="BishopImage">
        <Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color1}"
              Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
              StrokeThickness="1.5" StrokeMiterLimit="1" StrokeLineJoin="Round">
                <Path.Data>
                    <GeometryGroup>
                        <PathGeometry Figures="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38
                                      C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99
                                      6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
                        <PathGeometry Figures="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5
                                      22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
                        <PathGeometry Figures="M 25 8 A 2.5 2.5 0 1 1  20,8 A 2.5 2.5 0 1 1  25 8 z"/>
                    </GeometryGroup>
                </Path.Data>
            </Path>
        <Path Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
               StrokeThickness="1.5" StrokeMiterLimit="1" StrokeEndLineCap="Round" StrokeStartLineCap="Round" StrokeLineJoin="Miter">
                <Path.Data>
                    <GeometryGroup>
                        <PathGeometry Figures="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" />
                    </GeometryGroup>
                </Path.Data>
            </Path>
            <Canvas.RenderTransform>
                <ScaleTransform ScaleX="1.42222222222" ScaleY="1.42222222222"/>
            </Canvas.RenderTransform>
        </Canvas>
    <Canvas Height="64" Width="64" x:Key="KnightImage">
        <Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color1}"
              Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
              StrokeThickness="1.5" StrokeMiterLimit="1"  StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
            <Path.Data>
                <GeometryGroup>
                    <PathGeometry Figures="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"/>
                    <PathGeometry Figures="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,
                                  30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 
                                  18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"/>
                </GeometryGroup>
            </Path.Data>
        </Path>
        <Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
              Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,AncestorLevel=2,Mode=FindAncestor},Path=Color2}"
              StrokeThickness="0.5" StrokeMiterLimit="1" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
            <Path.Data>
                <GeometryGroup>
                    <PathGeometry Figures="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"/>
                    <PathGeometry Figures="M 15 15.5 A 0.5 1.5 0 1 1  14,15.5 A 0.5 1.5 0 1 1  15 15.5 z">
                        <PathGeometry.Transform>
                            <MatrixTransform Matrix="0.866,0.5,-0.5,0.866,9.693,-5.173"/>
                        </PathGeometry.Transform>
                    </PathGeometry>
                </GeometryGroup>
            </Path.Data>
        </Path>
        <Canvas.RenderTransform>
            <ScaleTransform ScaleX="1.42222222222" ScaleY="1.42222222222"/>
        </Canvas.RenderTransform>
    </Canvas>
</ResourceDictionary>

我希望有人可以提供一些提示或指出正确的方向,因为 MSDocs 有针对 StyleSelectorsDataTemplateSelectors 的指南,这两者都更多地用于数据项样式,而不是使用样式设置另一个变量控件。 谢谢,

【问题讨论】:

    标签: c# wpf xaml data-binding wpf-controls


    【解决方案1】:

    如果我正确理解您的问题,您希望在您的 PieceImage 控件中显示基于 PieceType 的一块 Canvas。然后,您不必将Canvas 嵌套在另一个Canvas 中。您可以将PieceImage 模板中的Canvas 替换为ContentPresenter (PART_ContentPresenter)。

    <Style TargetType="{x:Type local:PieceImage}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="local:PieceImage">
                <ContentPresenter x:Name="PART_ContentPresenter"/>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    

    在您的PieceImage 控件中,您可以使用FindResource 方法搜索相应的PieceType 资源(Canvas)并将其分配给ContentPresenter。这里我假设我们可以使用模式&lt;Enum Constant Name&gt;Image 映射PieceType。如果它更复杂,您可以使用字典或自定义转换器。

    public class PieceImage : Control
    {
       private ContentPresenter _contentPresenter;
    
       // ...your constructor, dependency property definitions, etc..
    
       public override void OnApplyTemplate()
       {
          base.OnApplyTemplate();
    
          _contentPresenter = Template.FindName("PART_ContentPresenter", this) as ContentPresenter;
    
          if (_contentPresenter != null)
             _contentPresenter.Content = FindPieceImage();
       }
    
       private Canvas FindPieceImage()
       {
          var pieceTypeName = PieceType.ToString();
          var pieceTypeCanvasKey = $"{pieceTypeName}Image";
          return FindResource(pieceTypeCanvasKey) as Canvas;
       }
    }
    

    由于控件模板中原来的Canvas已经没有了,请把你的作品图片中的AncestorLevel=2属性全部去掉,否则找不到绑定源。此外,将x:Shared="False" 添加到您的Canvases。这很重要,因为您的棋子图像现在可能会被多次使用(我猜您正在构建棋盘),但如果不将 x:Shared 设置为 false相同的实例将被重复使用。

    当设置为 false 时,修改 WPF 资源检索行为,以便对属性资源的请求为每个请求创建一个新实例,而不是为所有请求共享同一个实例。

    这是有问题的,因为每个控件只能有一个父元素。这意味着当您多次分配相同的Canvas 实例时,只有最后一个元素会将其作为子元素。您可以想象一个棋盘,其中只显示最后分配的棋子,而棋盘的其余部分是空的。这是对您的代码的修改的摘录。

    <Canvas Height="64" Width="64"  x:Key="BishopImage"  x:Shared="False">
        <Path Fill="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,Mode=FindAncestor},Path=Color1}"
              Stroke="{Binding RelativeSource={RelativeSource AncestorType=local:PieceImage,Mode=FindAncestor},Path=Color2}"
              StrokeThickness="1.5" StrokeMiterLimit="1" StrokeLineJoin="Round">
                <Path.Data>
                    <GeometryGroup>
                        <PathGeometry Figures="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38
                                      C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99
                                      6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
                        <PathGeometry Figures="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5
                                      22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
                        <PathGeometry Figures="M 25 8 A 2.5 2.5 0 1 1  20,8 A 2.5 2.5 0 1 1  25 8 z"/>
                    </GeometryGroup>
                </Path.Data>
            </Path>
            <!-- ...other markup code. -->
    

    绑定现在可以工作并解析为PieceImage,无需任何额外的代码或标记。

    【讨论】:

    • 谢谢!这很好用。唯一的小问题是 x:Shared 仅在我将画布从 PieceImageDictionary.xaml 中取出并将它们直接放入 App.xaml 时才有效,但这不是一个大问题。我也不知道 AncestorLevel 引起了问题,我认为这是放弃前的最大限制。我很感激!
    • @MichaelWagner 相反,AncestorLevel 允许您跳过匹配元素。假设您的可视化树中有多个PieceImage 控件,那么2 中的AncestorLevel跳过 最近的PieceImage 并绑定到找到的下一个。如果没有找到匹配的元素,搜索将自动继续到“最大限制”,即可视化树的根,而不管AncestorLevel
    猜你喜欢
    • 1970-01-01
    • 2014-08-14
    • 2013-08-22
    • 1970-01-01
    • 2013-09-25
    • 2011-07-03
    • 1970-01-01
    • 1970-01-01
    • 2010-10-15
    相关资源
    最近更新 更多