WPF 图像按钮 100% XAML 实现
本文旨在创建具有可绑定属性的平面图像按钮。
引言
本文旨在创建满足以下条件的图像按钮:
- 按钮是平面的,没有文本
- 图像完全在 XAML 中定义
- 图像可以通过资源键轻松动态设置(可绑定)
- 图像具有可绑定属性,例如
Foreground
、Background
、Opacity
、Fill
颜色等。 - 按下按钮或当
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
设置为 VisualBrush
的 Visual
,然后将该画笔设置为 Button
的 Background
,并使用附加属性使其全部可绑定。
实现细节和代码使用
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
进行绘制。这样的画笔可以设置为控件的 Background
。VisualBrush
的 Visual
可以是很多对象。在我们的例子中,最简单的解决方案是使用 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 个属性,用于设置图像的视觉属性:ResourceKey
、Foreground
、Background
、Opacity
和 Fill
。它还将有一个属性,用于公开生成的 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
是一个类,用于获取正确的资源并设置 Canvas
的 DataContext
。为了提高资源键的可读性并避免重复,Canvas
在 ResourceDictionaries
中声明,其 Key
以“Canvas_
”开头,并且在搜索资源时,此标题默认添加。
VisualBackgroundData
类还保存了它所实例化来源 FrameworkElement
的引用。它用于查找为此元素定义的资源。Application.TryFindResource(key)
也可以使用,无需源引用,但这将强制您始终在应用程序级别(在 App.xaml 文件中)导入所有资源。
在 VisualBackgroundData
类中,当 Key
更改时,应用程序会搜索适当的 Canvas
,如果找到,则将其设置为 VisualBrush
的 Visual
,该 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;
}
}
其余的附加属性(ResourceKey
、Foreground
、Background
、Opacity
、Fill
和 BrushValue
)都定义了 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
周围的括号。)
现在,让我们定义一个样式,当按钮被按下时,图像的 Foreground
和 Fill
颜色(不是按钮本身!)会改变。由于我们不使用 Button
的实际 Foreground
和 BorderBrush
属性,我们也可以利用它们(Button.Foreground
用于 pressedCanvasImage.Fill
,Button.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>
关注点
您可以将 Canvas
的 SnapsToDevicePixels
和 UseLayoutRounding
设置为 "True"
,这样图像会更清晰,但有时在调整大小时,几何图形的某些部分会奇怪地移动。对于较小尺寸尤其明显,图像中一条路径移动 1 像素与另一条路径的距离会产生显着差异。我想这取决于图像以及您是否希望它在运行时可调整大小。这取决于您更能忍受什么:模糊还是不完全准确。
尽管以上述方式定义和设置样式的边框和按钮在 Visual Studio 2010 .NET Framework 4.0 的设计器中显示,但在 Visual Studio 2015 Community .NET Framework 4.5.2 中不显示。但是,应用程序运行没有任何问题,也没有任何警告。如果有人知道为什么会这样并愿意分享,我将不胜感激。