65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 图像按钮 100% XAML 实现

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.79/5 (6投票s)

2016年9月27日

CPOL

6分钟阅读

viewsIcon

37317

downloadIcon

466

本文旨在创建具有可绑定属性的平面图像按钮。

引言

本文旨在创建满足以下条件的图像按钮:

  1. 按钮是平面的,没有文本
  2. 图像完全在 XAML 中定义
  3. 图像可以通过资源键轻松动态设置(可绑定)
  4. 图像具有可绑定属性,例如 ForegroundBackgroundOpacityFill 颜色等。
  5. 按下按钮或当 IsMouseOver="True" 时,按钮的外观可以轻松设置样式,无需太多额外代码

注释

本示例中使用的图标:https://commons.wikimedia.org/wiki/File:Speaker_Icon.svg(许可:公共领域)

在此解决方案中,使用了 nuget 包 PropertyChanged.Fody

它使用 Visual Studio 2015 Community 和 .NET Framework 4.5.2 开发。也在 Visual Studio 2010 .NET Framework 4.0 上进行了测试。

SVG 文件使用 Inkscape 0.91 编辑。

使用 Microsoft XPS Document Writer 转换 SVG 文件。

想法

将带有矢量图像的 Canvas 设置为 VisualBrushVisual,然后将该画笔设置为 ButtonBackground,并使用附加属性使其全部可绑定。

实现细节和代码使用

1. 按钮是平面的,没有文本

这很容易。只需覆盖默认样式

<Style x:Key="StyleButtonTransparent" TargetType="{x:Type Button}">
  <Setter Property ="FocusVisualStyle" Value="{x:Null}"/>
  <Setter Property ="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Button">
        <Border CornerRadius="5" Background="{TemplateBinding Background}" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

它需要进一步设置样式,尽管这是基本思想。请注意,这里没有 ContentPresenter,因为我们的 Button 的外观将仅使用其 Background 属性设置。

2. 图像完全在 XAML 中定义

首先,在 WPF 中,可以使用 VisualBrush 进行绘制。这样的画笔可以设置为控件的 BackgroundVisualBrushVisual 可以是很多对象。在我们的例子中,最简单的解决方案是使用 Canvas 和一些将在 XAML 中定义的矢量图像。

获得这样的定义比你想象的要容易,尽管它需要一两个变通方法。

如果您已经有一个矢量图像,请将其保存为 SVG。如果您有一个栅格图像,可以使用例如一些在线转换器(例如,Google “convert png to svg”)将其转换为 SVG,或者您可以使用 Inkscape 的 Path/Trace Bitmap 来完成。

在此示例中,我们将使用一个已经是 SVG 格式的图标,尽管我在 Inkscape 中对其进行了一些修改,以标准化大小并添加一些背景(文件已附加)。基本上,我添加了一个方形白色背景,将对象分组,将文档属性中的单位更改为英寸,然后将文档宽度和高度设置为 1,并调整对象组的大小以适应文档。这样,我的图像是 1x1 英寸,这将导致在 Microsoft XPS Document Writer 上打印(Inkscape/文件/打印)后,生成的图像为 96x96 像素(因为我的屏幕 dpi),而不会出现烦人的小数位。

为什么要打印它?事实证明,“常规” SVG 格式与 XAML 中使用的语法略有不同。与其手动转换,我们可以使用 Microsoft XPS Document Writer 打印它。然后,将生成的 XPS 文件重命名为 ZIP,并提取 \Documents\1\Pages\1.fpage 中的文件。之后,将 FPAGE 文件的扩展名更改为 XML(或使用文本编辑器打开它)。在其中,您将获得图像的漂亮、XAML 兼容的定义。您需要做的(几乎)最后一件事是将 FixedPage 标签替换为 Canvas

由于我们的 Canvas 和几何图形将具有绑定属性,它们无论如何都无法冻结,因此设置 PresentationOptions:Freeze="True" 将不起作用。另一方面,我们必须设置 x:Shared="False",以便我们可以在应用程序中多次使用我们的 Canvas

生成的 XAML

<Canvas x:Key="Canvas_Speaker" x:Shared="False" Width="96" Height="96" >
    <Path Data="F1 M 0,0 L 96,0 96,96 0,96 z"  Fill="#ffffffff" />
    <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z"  
    Fill="#ff111111" />
    <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z"  
    Stroke="#ff111111" StrokeThickness="6.4" StrokeLineJoin="Round" />
    <Path Data="F1 M 61.6,62.72 C 64,58.72 65.44,54.08 65.44,49.12 65.44,44 64,39.2 61.44,35.2"  
    Stroke="#ff111111" StrokeThickness="6.4" StrokeLineJoin="Miter" 
    StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" />
    <Path Data="F1 M 70.4,26.24 C 75.2,32.64 77.92,40.48 77.92,
                49.12 77.92,57.44 75.2,65.28 70.56,71.68"  
    Stroke="#ff111111" StrokeThickness="6.4" StrokeLineJoin="Miter" 
    StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" />
    <Path Data="F1 M 78.88,80 C 85.6,71.52 89.76,60.8 89.76,49.12 89.76,
                37.28 85.6,26.4 78.72,17.92"  
    Stroke="#ff111111" StrokeThickness="6.4" StrokeLineJoin="Miter" 
    StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" 
    Clip="M 65.28,4.48 L 96,4.48 96,93.6 65.28,93.6 z"  />
</Canvas>

3. 图像可以通过键轻松动态设置 & 4. 图像具有可绑定属性,例如 Foreground、Background、Opacity、Fill Color 等。

这可以使用 Attached Properties 来完成。让我们定义一个类 VisBg

public class VisBg: DependencyObject

它将有 5 个属性,用于设置图像的视觉属性:ResourceKeyForegroundBackgroundOpacityFill。它还将有一个属性,用于公开生成的 VisualBrush,称为 BrushValue。然后,将有一个 private 属性,用于保存我们的图像(Canvas)将绑定到的数据:BrushData

让我们从最后一个开始。

private static readonly DependencyProperty BrushDataProperty = 
  DependencyProperty.RegisterAttached(
      "BrushData", typeof(VisualBackgroundData), typeof(VisBg),
      new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable | 
      FrameworkPropertyMetadataOptions.Inherits));

  private static VisualBackgroundData GetBrushData(DependencyObject d)
  {
    return (VisualBackgroundData)d.GetValue(BrushDataProperty);
  }

  private static void SetBrushData(DependencyObject d, VisualBackgroundData value)
  {
    d.SetValue(BrushDataProperty, value);
  }

VisualBackgroundData 是一个类,用于获取正确的资源并设置 CanvasDataContext。为了提高资源键的可读性并避免重复,CanvasResourceDictionaries 中声明,其 Key 以“Canvas_”开头,并且在搜索资源时,此标题默认添加。

VisualBackgroundData 类还保存了它所实例化来源 FrameworkElement 的引用。它用于查找为此元素定义的资源。Application.TryFindResource(key) 也可以使用,无需源引用,但这将强制您始终在应用程序级别(在 App.xaml 文件中)导入所有资源。

VisualBackgroundData 类中,当 Key 更改时,应用程序会搜索适当的 Canvas,如果找到,则将其设置为 VisualBrushVisual,该 VisualBrush 将在以后用于管理控件的背景。

private void OnKeyChanged()
{
  if (string.IsNullOrEmpty(this.Key))
  {
    this.Value = Brushes.Transparent;
    return;
  }
  string key = this.Key;

  object res = this.GetResource(key);
  if (res == null || !(res is Canvas))
  {
    key = cHeader + key;
    res = this.GetResource(key);
    if (res == null || !(res is Canvas))
    {
      this.Value = Brushes.Transparent;
      return;
    }
  }

  if (!(res is Canvas))
  {
    this.Value = Brushes.Transparent;
    return;
  }
  Canvas c = (Canvas)res;
  c.DataContext = this;
  c.SnapsToDevicePixels = true;
  c.UseLayoutRounding = true;

  if (this.Value == null || !(this.Value is VisualBrush))
  {
    VisualBrush b = new VisualBrush(c);
    b.TileMode = TileMode.None;
    b.Stretch = Stretch.Fill;

    this.Value = b;
  }
  else
  {
    ((VisualBrush)this.Value).Visual = c;
  }
}

其余的附加属性(ResourceKeyForegroundBackgroundOpacityFillBrushValue)都定义了 PropertyChangedCallback 方法。在这些方法中,如果需要,将实例化保存数据的 private 附加属性 BrushData,并设置该实例中的相应属性。

现在,让我们回到图像的 XAML 定义。Canvas 中使用的 Brushes 及其 Opacity 属性

<Canvas x:Key="Canvas_Speaker" x:Shared="False" 
Width="96" Height="96" Opacity="{Binding Opacity}">
  <Path Data="F1 M 0,0 L 96,0 96,96 0,96 z" Fill="{Binding Background}" />
  <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z" 
  Fill="{Binding FillBrush}"/>
  <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Round" />
  <Path Data="F1 M 61.6,62.72 C 64,58.72 65.44,54.08 65.44,49.12 65.44,44 64,39.2 61.44,35.2"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Miter" 
  StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" />
  <Path Data="F1 M 70.4,26.24 C 75.2,32.64 77.92,40.48 77.92,
              49.12 77.92,57.44 75.2,65.28 70.56,71.68"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Miter" 
  StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" />
  <Path Data="F1 M 78.88,80 C 85.6,71.52 89.76,60.8 89.76,49.12 89.76,
              37.28 85.6,26.4 78.72,17.92"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Miter" 
  StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4"
  Clip="M 65.28,4.48 L 96,4.48 96,93.6 65.28,93.6 z" />
</Canvas>

5. 按下按钮或当 IsMouseOver="true" 时,按钮的外观可以轻松设置样式,无需太多额外代码

以上述方式定义的附加属性是可绑定的,并且可以多种方式使用。

(我们的类在库项目中定义,因此我们必须定义命名空间 xmlns:lib="clr-namespace:VisExtLib;assembly=VisExtLib",测试项目是 VisExtTest)。

例如,让我们定义一个图像具有以下属性的按钮

  • 黄色 Background
  • 黑色 Fill
  • 红色 Foreground

<Button Background="{Binding Path=(lib:VisBg.BrushValue), 
Mode=OneWay, RelativeSource={RelativeSource Self}}"
  Style="{StaticResource StyleButtonTransparent}"
  Margin="10"
  Width="{Binding Path=ActualHeight, 
  RelativeSource={RelativeSource Self}}" VerticalAlignment="Stretch"
  lib:VisBg.ResourceKey="Speaker"
  lib:VisBg.Background="Yellow"
  lib:VisBg.Foreground="Red"
  lib:VisBg.Fill="Black" />

(注意 Binding 中附加属性 Path 周围的括号。)

现在,让我们定义一个样式,当按钮被按下时,图像的 ForegroundFill 颜色(不是按钮本身!)会改变。由于我们不使用 Button 的实际 ForegroundBorderBrush 属性,我们也可以利用它们(Button.Foreground 用于 pressedCanvasImage.FillButton.BorderBrush 用于 pressedCanvasImage.Foreground)。这样,我们黄黑红的按钮在按下时可以变为黄紫白

<Style x:Key="StyleButtonTransparentPressed" TargetType="{x:Type Button}">
  <Setter Property ="FocusVisualStyle" Value="{x:Null}"/>
  <Setter Property ="Template">
  <Setter.Value>
    <ControlTemplate TargetType="Button">
      <Border CornerRadius="5"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              Background="{Binding Path=(local:VisBg.BrushValue), 
              Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}">
      <Border.Style>
      <Style TargetType="Border">
          <Setter Property="local:VisBg.Foreground" 
          Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
          Path=(local:VisBg.Foreground)}"/>
          <Setter Property="local:VisBg.Fill" 
          Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
          Path=(local:VisBg.Fill)}"/>
          <Setter Property="local:VisBg.Opacity" 
          Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
          Path=(local:VisBg.Opacity)}"/>
          <Setter Property="local:VisBg.Background" 
          Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
          Path=(local:VisBg.Background)}"/>
         <Style.Triggers>
          <DataTrigger Binding="{Binding Path=IsPressed, 
          RelativeSource={RelativeSource TemplatedParent}}" Value="True">
            <Setter Property="local:VisBg.Foreground" 
            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
            Path=BorderBrush}" />
            <Setter Property="local:VisBg.Fill" 
            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, 
            Path=Foreground}" />
          </DataTrigger>
         </Style.Triggers>
       </Style>
       </Border.Style>
     </Border>
    </ControlTemplate>
  </Setter.Value>
  </Setter>
</Style>
<Button Background="{Binding Path=(lib:VisBg.BrushValue), 
Mode=OneWay, RelativeSource={RelativeSource Self}}"
  Style="{StaticResource StyleButtonTransparentPressed}"
  Margin="10"
  Width="{Binding Path=ActualHeight, 
  RelativeSource={RelativeSource Self}}" VerticalAlignment="Stretch"
  Foreground="White"
  BorderBrush="Violet"
  BorderThickness="0"
  lib:VisBg.ResourceKey="Speaker"
  lib:VisBg.Background="Yellow"
  lib:VisBg.Foreground="Red"
  lib:VisBg.Fill="Black" />

现在,我们还要让鼠标悬停在按钮上时,图像的 Background 颜色变为其 Fill 颜色。既然如此,为什么不也添加一个在 Button 被按下时开始的 Opacity 动画呢?

<Button Background="{Binding Path=(lib:VisBg.BrushValue), 
Mode=OneWay, RelativeSource={RelativeSource Self}}"
  Width="{Binding Path=ActualHeight, 
  RelativeSource={RelativeSource Self}}" VerticalAlignment="Stretch"
  Foreground="White"
  BorderBrush="Violet"
  BorderThickness="0"
  Margin="10"
  lib:VisBg.ResourceKey="Speaker"
  lib:VisBg.Foreground="Red"
  lib:VisBg.Fill="Black" >
  <Button.Style>
  <Style TargetType="Button" 
  BasedOn="{StaticResource StyleButtonTransparentPressed}" >
  <Setter Property="lib:VisBg.Background" Value="Yellow"/>
  <Style.Triggers>
  <Trigger Property="IsMouseOver" 
  Value="True">
  <Setter Property="lib:VisBg.Background" 
  Value="{Binding Path=(lib:VisBg.Fill), RelativeSource={RelativeSource Self}}"/>
  </Trigger>
  </Style.Triggers>
  </Style>
  </Button.Style>
  <Button.Triggers>
  <EventTrigger RoutedEvent="Button.Click">
  <BeginStoryboard>
  <Storyboard>
  <DoubleAnimation Storyboard.TargetProperty="(lib:VisBg.Opacity)"
  From="1"
  To="0.3"
  Duration="0:0:2"
  AutoReverse="True"
  RepeatBehavior="2x"/>
  </Storyboard>
  </BeginStoryboard>
  </EventTrigger>
  </Button.Triggers>
  </Button>

... 我们可以修改 Canvas,使只有图像本身改变 Opacity,而不是其 Background

<Canvas x:Key="Canvas_Speaker" x:Shared="False" 
Width="96" Height="96">
  <Path Data="F1 M 0,0 L 96,0 96,96 0,96 z" Fill="{Binding Background}" />
  <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z" 
  Fill="{Binding FillBrush}" Opacity="{Binding Opacity}"/>
  <Path Data="F1 M 7.68,60.96 L 28,60.96 50.4,80.32 50.4,17.6 28.32,36.48 7.68,36.48 z" 
  Opacity="{Binding Opacity}"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Round" />
  <Path Data="F1 M 61.6,62.72 C 64,58.72 65.44,54.08 65.44,49.12 65.44,44 64,39.2 61.44,35.2"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" StrokeLineJoin="Miter"

  Opacity="{Binding Opacity}"
  StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeMiterLimit="4" />
  <Path Data="F1 M 70.4,26.24 C 75.2,32.64 77.92,40.48 77.92,49.12 77.92,
              57.44 75.2,65.28 70.56,71.68"
  Opacity="{Binding Opacity}"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" 
  StrokeLineJoin="Miter" StrokeStartLineCap="Round" 
  StrokeEndLineCap="Round" StrokeMiterLimit="4" />
  <Path Data="F1 M 78.88,80 C 85.6,71.52 89.76,60.8 89.76,49.12 89.76,
              37.28 85.6,26.4 78.72,17.92"
  Opacity="{Binding Opacity}"
  Stroke="{Binding Foreground}" StrokeThickness="6.4" 
  StrokeLineJoin="Miter" StrokeStartLineCap="Round" 
  StrokeEndLineCap="Round" StrokeMiterLimit="4"
  Clip="M 65.28,4.48 L 96,4.48 96,93.6 65.28,93.6 z" />
</Canvas>

关注点

您可以将 CanvasSnapsToDevicePixelsUseLayoutRounding 设置为 "True",这样图像会更清晰,但有时在调整大小时,几何图形的某些部分会奇怪地移动。对于较小尺寸尤其明显,图像中一条路径移动 1 像素与另一条路径的距离会产生显着差异。我想这取决于图像以及您是否希望它在运行时可调整大小。这取决于您更能忍受什么:模糊还是不完全准确。

尽管以上述方式定义和设置样式的边框和按钮在 Visual Studio 2010 .NET Framework 4.0 的设计器中显示,但在 Visual Studio 2015 Community .NET Framework 4.5.2 中不显示。但是,应用程序运行没有任何问题,也没有任何警告。如果有人知道为什么会这样并愿意分享,我将不胜感激。

© . All rights reserved.