Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WPF Image Buttons 100% in XAML

3.79/5 (6 votes)
27 Sep 2016CPOL6 min read 36.8K   413  
The goal of this article is to create flat image buttons with bindable properties.

Introduction

The goal of this article is to create image buttons that fulfill the following conditions:

  1. Buttons are flat, with no text
  2. Images are defined entirely in XAML
  3. Images can be set easily and dynamically by resource key (bindable)
  4. Images have bindable properties such as Foreground, Background, Opacity, Fill color, etc.
  5. The look of buttons when pressed or when IsMouseOver="True" can be styled without too much extra code

Notes

Icon used in this example: https://commons.wikimedia.org/wiki/File:Speaker_Icon.svg (license: Public Domain)

In this solution, nuget package PropertyChanged.Fody is used.

It has been developed using Visual Studio 2015 Community, .NET Framework 4.5.2. Also tested on Visual Studio 2010 .NET Framework 4.0.

SVG file edited using Inkscape 0.91.

Microsoft XPS Document Writer used to convert SVG file.

The Idea

Set Canvas with a vector image as Visual of VisualBrush, then set that brush as Background of the Button and make all of it bindable using Attached Properties.

Implementation Details & Using the Code

1. Buttons Are Flat, With No Text

This one is easy. Just override the default style:

XML
<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>

It has to be further styled, although that's the basic idea. Notice that there is no ContentPresenter here, as the look of our Button will be set using only its Background property.

2. Images Are Defined Entirely in XAML

First, in WPF, it is possible to paint using VisualBrush. Such brush can be set as Background of controls. The Visual of the VisualBrush can be a lot of objects. In our case, the simplest solution is to use Canvas with some vector image that will be defined in XAML.

Getting such a definition is easier than you think, although it requires a workaround or two.

If you already have a vector image, save it as SVG. If you have a raster image, it can be converted to SVG using for example some online converter (e.g., Google "convert png to svg") or you can do it using Inkscape's Path/Trace Bitmap.

In this example, we will be using an icon that is already in SVG format, although I modified it a little in Inkscape to standardize the sizes and add some background (file attached). Basically, I added a square white background, grouped the objects, changed the unit in Document Properties to inches, then set document width and height to 1 and resized the group of objects to fit the document. This way, my image is 1x1 inch, which will cause the resulting image to be 96x96 px (because of my screen dpi) after it is printed (Inkscape/File/Print) on Microsoft XPS Document Writer, without annoying fraction digits.

Why print it? It turns out that the "regular" SVG format is a little different than the syntax used in XAML. Instead of manually converting it, we can use Microsoft XPS Document Writer to print it. Then, rename the resulting XPS file to ZIP, and extract the file in \Documents\1\Pages\1.fpage. After that, change the extension of FPAGE file to XML (or open it with a text editor). Inside, you will have a pretty, XAML-compatible definition of your image. The (almost) last thing you'll have to do is replace the FixedPage tag with Canvas.

Since our Canvas and geometries will have bound properties, they won't be able to freeze anyway, so setting PresentationOptions:Freeze="True" won't do a thing. On the other hand, we have to set x:Shared="False" so we can use our Canvas multiple times in our application.

The resulting XAML:

XML
<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. Images Can Be Set Easily and Dynamically by Key & 4. Images Have Bindable Properties Such as Foreground, Background, Opacity, Fill Color, Etc.

This can be done using Attached Properties. Let's define a class VisBg:

C#
public class VisBg: DependencyObject

It will have 5 properties that will be used to set visual properties of the image: ResourceKey, Foreground, Background, Opacity and Fill. It will also have one property that will expose the resulting VisualBrush, called BrushValue. Then, there will be a private property that will hold the data to which our image (Canvas) will be bound: BrushData.

Let's start with the last one.

C#
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 is a class that is used to get the correct resource and set DataContext of Canvas. For better readability of resource keys and to avoid repetition, the Canvas is declared in the ResourceDictionaries with Key starting with "Canvas_", and while searching for the resource, this header is added by default.

The VisualBackgroundData class also holds the reference to the source FrameworkElement for which it was instantiated. It is used to find resource defined for this element. Application.TryFindResource(key) can be used too, without the source reference, but that will force you to always import all of the resources at the application level, in the App.xaml file.

In VisualBackgroundData class, when the Key changes, the application searches for the appropriate Canvas, and if it is found, it is set as the Visual for the VisualBrush that will later be used to manage backgrounds for controls.

C#
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;
  }
}

The rest of the attached properties (ResourceKey, Foreground, Background, Opacity, Fill and BrushValue) have PropertyChangedCallback method defined. In those methods, the private attached property BrushData that holds the data is instantiated if necessary, and the appropriate property in that instance is set.

Now, let's return to the XAML definition of our image. The Brushes used in the Canvas and its Opacity property:

XML
<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. the Look of Buttons When Pressed or When IsMouseOver="true" Can Be Styled Without Too Much Extra Code

The attached properties defined in the manner described above are bindable and can be used in multiple ways.

(Our classes are defined in the library project, so we have to define the namespace xmlns:lib="clr-namespace:VisExtLib;assembly=VisExtLib", and the test project is VisExtTest).

For example, let's define a button which image has:

  • yellow Background
  • black Fill
  • red Foreground

XML
<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" />

(Notice the brackets around a Path to the attached property in Binding.)

Now, let's define a style, where the Foreground and Fill colors of the image (not the button itself!) change when the button is pressed. Since we do not use the actual Foreground and BorderBrush properties of the Button, we can utilize those too (Button.Foreground for pressedCanvasImage.Fill, and Button.BorderBrush for pressedCanvasImage.Foreground). This way, our Yellow-Black-Red button can change to Yellow-Violet-White when pressed:

XML
<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>
XML
<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" />

Now, let's also make it so the image's Background will change to its Fill color when mouse is over the button. When we are doing that, why not add an Opacity animation too that starts when the Button is pressed?

XML
<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>

... and we can modify the Canvas so only the image proper changes Opacity and not its Background:

XML
<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>

Points of Interest

You can set SnapsToDevicePixels and UseLayoutRounding for Canvas to "True", then the image will be crisper, but it will sometimes cause the parts of geometry to move strangely when resized. It is especially visible for smaller sizes, where one path moving 1 pixel away from another in the image makes a noticeable difference. I guess it depends on the image and if you'll want it to be resizable at runtime. It depends on what you can stomach better: blurry or not entirely accurate.

While Borders and Buttons defined and styled in the above manner show in the designer in Visual Studio 2010 .NET Framework 4.0, they do not in Visual Studio 2015 Community .NET Framework 4.5.2. The application runs without problems though and there are no warnings whatsoever. If someone has any idea as to why it is so and is willing to share, I would be grateful.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)