Archie
Archie

Reputation: 2579

Custom button template in WPF

I want to create a simple button template with an image and text inside it. But I want to keep the System button's look and feel.

How do I create it, step by step?

P.S.: I have already tried it with CustomControl in WPF and BasedOn property.

Upvotes: 17

Views: 79120

Answers (6)

Hemendr
Hemendr

Reputation: 1048

I am providing the code for two ways of doing this while keeping the System button's look and feel.

  • Method-1: Adding Image control through style using DataTemplate and Button.Image AttachedProperty to provide ImageSource as explained by jeffora
  • Method-2A: Adding Image control through Button.AddImage AttachedProperty
  • Method-2B: Adding Image control through Button.AddImage AttachedProperty, also giving flexibility to modify ContentTemplate through style

Here is the output:

enter image description here

Code of mainWindow.xaml

<Window x:Class="ButtonWithImageAndText.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ButtonWithImageAndText"
        Title="MainWindow" Height="250" Width="400">
    <Window.Resources>
        <BitmapImage x:Key="MyImage" UriSource="/panic-button.png"/>
        <Style x:Key="ImageButton1" TargetType="Button">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                            <Image Source="{Binding Path=(local:ButtonProperties.Image), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"></Image>
                            <ContentPresenter Content="{Binding Path=Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"></ContentPresenter>
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="ImageButton2" TargetType="Button">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                            <CheckBox/>
                            <ContentPresenter Content="{Binding Path=Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"></ContentPresenter>
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <StackPanel>
            <!--Method-1: Adding Image control through style using DataTemplate and Button.Image AttachedProperty to provide ImageSource. -->
            <Button HorizontalContentAlignment="Center"  local:ButtonProperties.Image="{StaticResource MyImage}" Content="Test1" Height="20" Style="{StaticResource ImageButton1}"/>
            <!--Method-2A: Adding Image control through Button.AddImage AttachedProperty  -->
            <Button HorizontalContentAlignment="Center"   local:ButtonExtensions.AddImage="{StaticResource MyImage}" Content="Test2" Height="20"/>
            <!--Method-2B: Adding Image control through Button.AddImage AttachedProperty, also giving flexibility to modify ContentTemplate through style  -->
            <Button HorizontalContentAlignment="Center"   local:ButtonExtensions.AddImage="{StaticResource MyImage}" Content="Test3" Height="20" Style="{StaticResource ImageButton2}"/>
        </StackPanel>
    </Grid>
</Window>

Code for ButtonExtensions.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ButtonWithImageAndText
{
    public static class ButtonExtensions
    {
        public static ImageSource GetAddImage(DependencyObject obj)
        {
            return (ImageSource)obj.GetValue(ImageProperty);
        }

        public static void SetAddImage(DependencyObject obj, ImageSource value)
        {
            obj.SetValue(ImageProperty, value);
        }

        //Image is added dynamically to control template using OnImageChanged event handler which is raised whenever the AddImage property is assigned some value.
        public static readonly DependencyProperty ImageProperty =
            DependencyProperty.RegisterAttached("AddImage", typeof(ImageSource), typeof(ButtonExtensions), new UIPropertyMetadata((ImageSource)null, OnAddImageChanged));

        
        private static void OnAddImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is Button btn)
            {
                btn.Loaded += (sender, args) =>
                {
                    // ControlTemplate of Button has Border > ContentPresenter. We will change it to Border > StackPanel(Image+ContentPresenter)
                    ContentPresenter contentPresenter = FindVisualChild<ContentPresenter>(btn);
                    var currentParent = VisualTreeHelper.GetParent(contentPresenter) as Border;

                    if (currentParent != null)
                    {
                        StackPanel newStackPanel = new StackPanel()
                        {
                            Orientation = Orientation.Horizontal,
                            HorizontalAlignment = btn.HorizontalContentAlignment
                        };
                        UIElement existingChild = currentParent.Child;
                        currentParent.Child= newStackPanel;
                        newStackPanel.Children.Add(new Image() { Source = e.NewValue as ImageSource });
                        // Add the ContentPresenter to the new StackPanel
                        newStackPanel.Children.Add(existingChild);
                    }
                };
            }
        }


        // Helper method to find a child of a specific type in the visual tree
        private static T FindVisualChild<T>(DependencyObject visual)
            where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(visual, i);

                if (child != null && child is T)
                {
                    return (T)child;
                }

                T childOfChild = FindVisualChild<T>(child);

                if (childOfChild != null)
                {
                    return childOfChild;
                }
            }

            return null;
        }
    }
}

Code for ButtonProperties.cs

using System.Windows;
using System.Windows.Media;

namespace ButtonWithImageAndText;

public class ButtonProperties
{
    public static ImageSource GetImage(DependencyObject obj)
    {
        return (ImageSource)obj.GetValue(ImageProperty);
    }

    public static void SetImage(DependencyObject obj, ImageSource value)
    {
        obj.SetValue(ImageProperty, value);
    }
    //It is assumed that the Image is added to control template using style.
    public static readonly DependencyProperty ImageProperty =
        DependencyProperty.RegisterAttached("Image", typeof(ImageSource), typeof(ButtonProperties), new UIPropertyMetadata((ImageSource)null));
}

Upvotes: 0

Declan Taylor
Declan Taylor

Reputation: 405

Another answer - improving on u/dogracer's and u/Dave NP:


<Button Content = "{Binding object}" >
    <Button.Style >
        <Style TargetType="Button">
            <Setter Property = "ContentTemplate" >
                <Setter.Value >
                    <DataTemplate >
                        <StackPanel >
                            <Image  Source="{Binding Path=Content.ImageUrl, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}" >
                                <TextBlock Text = "{Binding Path=Content.Text,RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}" />
                        </ StackPanel >
                    </ DataTemplate >
                </ Setter.Value >
            </ Setter >
        </ Style >
    </ Button.Style >
</ Button >
  1. Content is the bound to 'object' with 'ImageUrl' and 'Text' Properties.
  2. This works in a usercontrol outside the main assembly.
  3. Style is that of a regular button

Upvotes: 0

Dave NP
Dave NP

Reputation: 1

Here is my Solution!

<Button Content="Browse" Margin="10" Name="btBrowse">
            <Button.Template>
                <ControlTemplate>
                    <StackPanel Orientation="Vertical" Height="50" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Center">
                        <Image Source="MyIcons\browse.png" Height="30" />
                        <TextBlock Text="{Binding ElementName=btBrowse, Path=Content}" VerticalAlignment="Center" HorizontalAlignment="Center" />
                    </StackPanel>
                </ControlTemplate>
            </Button.Template>
        </Button>

Result is below:

screenshot

Upvotes: 0

dogracer
dogracer

Reputation: 31

If you don't want to write any code-behind, there is another way of doing this (inspired by jeffora's answer). You can use the control's Content field to put the URI to the image you want to see in your button:

<Button Content="https://www.google.com/images/srpr/logo3w.png" Height="100" Width="200" Style="{DynamicResource ButtonStyle1}"/>

And then you can edit the default Button Style to look like this:

<Style>
...
<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="{x:Type Button}">
            <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" RenderDefaulted="{TemplateBinding IsDefaulted}" SnapsToDevicePixels="true">
                <Image x:Name="theImage" Source="{Binding Path=Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}" Margin="4,0,0,0">
                    <Image.ToolTip>
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Image.ToolTip>
                </Image>
            </Microsoft_Windows_Themes:ButtonChrome>
            <ControlTemplate.Triggers>
                ...
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Setter.Value>
</Setter>
</Style>

The magic is in the 'Source=(Binding ...}' part. It has worked well for me to have the ToolTip there for debugging missing/changed images -- but can easily be removed as well.

Upvotes: 3

jeffora
jeffora

Reputation: 4159

You can do this easily with a style and attached property:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ap="clr-namespace:MyProject.Namespace.Path.To.ButtonProperties">
    ...
    <Style x:Key="ImageButton" TargetType="Button">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding Path=(ap:ButtonProperties.Image), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"></Image>
                        <ContentPresenter Content="{Binding Path=Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"></ContentPresenter>
                    </StackPanel>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    ...
</ResourceDictionary>

and

public class ButtonProperties
{
    public static ImageSource GetImage(DependencyObject obj)
    {
        return (ImageSource)obj.GetValue(ImageProperty);
    }

    public static void SetImage(DependencyObject obj, ImageSource value)
    {
        obj.SetValue(ImageProperty, value);
    }

    public static readonly DependencyProperty ImageProperty =
        DependencyProperty.RegisterAttached("Image", typeof(ImageSource), typeof(ButtonProperties), new UIPropertyMetadata((ImageSource)null));
}

Then in markup:

<Button Style="{StaticResource ImageButton}" ap:ButtonProperties.Image="{StaticResource MyImage}" Content="Test">
</Button>

This example looks pretty hideous, but you can easily change the StackPanel to a Grid or something similar to constrain the image proportion. Using the ContentPresenter allows you to preserve the behaviour of a button allowing you to put any UIElement inside, and retaining Command support etc.

Upvotes: 40

Archie
Archie

Reputation: 2579

I have finally created a Button with image + text inside it:

Below is the Full Code:

Step 1 : Create a new User Control called:ImageButtonUC

<UserControl Name="ImageButton" x:Class="WpfApp.ImageButtonUC"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Button VerticalAlignment="Top" Width="100" Height="25" Click="button_Click"> 
            <Button.Content>
                <StackPanel Orientation="Horizontal">
                    <Image Width="16" Height="16" Margin="5,0,5,0" Source="{Binding ElementName=ImageButton, Path=Image}"/>
                    <TextBlock Text="{Binding ElementName=ImageButton, Path=Text}"/>
                </StackPanel>
            </Button.Content>
        </Button>
    </Grid>
</UserControl>

Step 2: Edit ImageButtonUC.xaml.cs

public partial class ImageButtonUC : UserControl
    {
        public event RoutedEventHandler Click;

        public ImageButtonUC()
        {
            InitializeComponent();

        }

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }


        public static readonly DependencyProperty TextProperty =
          DependencyProperty.Register("Text", typeof(string), typeof(ImageButtonUC), new UIPropertyMetadata(""));

        public ImageSource Image
        {
            get { return (ImageSource)GetValue(ImageProperty); }
            set { SetValue(ImageProperty, value); }
        }

        public static readonly DependencyProperty ImageProperty =
           DependencyProperty.Register("Image", typeof(ImageSource), typeof(ImageButtonUC), new UIPropertyMetadata(null));


        private void button_Click(object sender, RoutedEventArgs e)
        {

            if (null != Click)

                Click(sender, e);

        }

    }

Step 3: In your xaml you can use it this way: Add the namespace as

xmlns:Local="clr-namespace:WpfApp"

And use it as:

<Local:ImageButtonUC x:Name="buttonImg" Width="100" Margin="10,0,10,0" Image="/WpfApp;component/Resources/Img.bmp" Text="Browse..." Click="buttonImg_Click"/>

Note: My Image is loacted in the Resources folder here

Reference:

http://blogs.msdn.com/knom/archive/2007/10/31/wpf-control-development-3-ways-to-build-an-imagebutton.aspx

Upvotes: 8

Related Questions