views:

443

answers:

2

I am learning about control templates in WPF and checking out how to replace the button look with custom template styles. I see that to make a circle button, a Ellipse has to be defined with the same height and width.

<Style x:Key="Button2" TargetType="Button">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid>
                    <Ellipse Fill="LightGreen" Width="80" Height="80"/>
                    <ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Control.Margin" Value="10"/>
</Style>

Of course, that only forces all buttons using that style to have a circle with a diameter of 80 pixels, regardless of how the button is resized. I'd like for the circle to take on the shorter of the height/width values, so that it can dynamically scale according to the button sizing.

However i have not read any material that teaches how this can be done in pure XAML template? It seems that some code-behind is required to achieve this effect?

+3  A: 

This is where TemplateBinding comes in (TemplateBinding is used inside control templates and is used to retrieve values from the templated control, in this case the Button).

<Ellipse Fill="LightGreen" 
    Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"/>

Note that this is a shorter form of using:

{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}

The TemplateBinding markup extension is just optimized for only TemplatedParent bindings.

That said, if you wanted it to be a circle only, if your ellipse was the smaller of W/H, then your content will easily flow out of it, which I doubt is what you actually want..? I had thought of using a multi value converter to do that, but you can't bind to the converter parameter, so that's out.

In that case, an attached behavior would work, but it's not pretty.

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:WpfApplication1"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="Window1" Height="300" Width="300">

    <Grid>
        <Button Content="Yo!" Width="50" Height="30">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Grid>
                        <Ellipse Fill="LightGreen" local:ConstrainWidthHeight.ConstrainedWidth="{TemplateBinding ActualWidth}" local:ConstrainWidthHeight.ConstrainedHeight="{TemplateBinding ActualHeight}"/>
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                </ControlTemplate>
            </Button.Template>
        </Button>
    </Grid>
</Window>

...and the attached behavior:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace WpfApplication1 {
    public class ConstrainWidthHeight {
        public static readonly DependencyProperty ConstrainedWidthProperty =
            DependencyProperty.RegisterAttached( "ConstrainedWidth", typeof( double ), typeof( ConstrainWidthHeight ), new PropertyMetadata( double.NaN, OnConstrainValuesChanged ) );
        public static readonly DependencyProperty ConstrainedHeightProperty =
            DependencyProperty.RegisterAttached( "ConstrainedHeight", typeof( double ), typeof( ConstrainWidthHeight ), new UIPropertyMetadata( double.NaN, OnConstrainValuesChanged ) );

        public static double GetConstrainedHeight( FrameworkElement obj ) {
            return (double) obj.GetValue( ConstrainedHeightProperty );
        }

        public static void SetConstrainedHeight( FrameworkElement obj, double value ) {
            obj.SetValue( ConstrainedHeightProperty, value );
        }

        public static double GetConstrainedWidth( FrameworkElement obj ) {
            return (double) obj.GetValue( ConstrainedWidthProperty );
        }

        public static void SetConstrainedWidth( FrameworkElement obj, double value ) {
            obj.SetValue( ConstrainedWidthProperty, value );
        }

        private static void OnConstrainValuesChanged( object sender, DependencyPropertyChangedEventArgs e ) {
            FrameworkElement element = sender as FrameworkElement;
            if( element != null ) {
                double width = GetConstrainedWidth( element );
                double height = GetConstrainedHeight( element );

                if( width != double.NaN && height != double.NaN ) {
                    double value = Math.Min( width, height );

                    element.Width = value;
                    element.Height = value;
                }
            }
        }
    }
}

Okay, now the reason why using an attached behavior is required (AFAICT anyway), is that in order to center the ellipse (in a non-square/non-circle scenario), you need the HorizontalAlignment and VerticalAlignment to be able to take effect. The default value of both is Stretch, and when an explicit Width/Height is set, it behaves like Center.

With Stretch="Uniform" on, your Ellipse will always physically take up the whole space, it's only the drawing of the Ellipse that will be constrained. Using this, your drawn Ellipse figure will always start at the top left. So in this case if your button is Wider than it is tall, the drawn portion of the Ellipse won't get centered along with the text.

This code is a good example of what you are probably not looking for:

<Ellipse Height="{TemplateBinding ActualHeight}" Width="{TemplateBinding ActualWidth}" Fill="LightGreen" Stretch="Uniform" />

...and the button using it (with a non-square width/height):

<Button Content="YO!" Style="{StaticResource Button2}" Width="120" Height="53" VerticalAlignment="Top"></Button>

Looks like this:

Ugly

... compared to this with the attached property option:

alt text

Adam Sills
ok i have tried this method and it seems applying HorizontalAlignment="Center" VerticalAlignment="Center" to the Ellipse also makes the circle shape disappear after resizing to a large size. I wonder what's happening.
icelava
Updated my answer to use ActualWidth and ActualHeight. Binding to W/H is why it's doing what you're seeing.
Adam Sills
After changing to ActualWidth and ActualHeight it appears to render in the centre now. Although i'm still haven't fully understood why so, thanks.
icelava
A: 

Set Stretch=Uniform on Ellipse will automatically works like Min(Height,Width) You dont need an attached property as like Adam suggests.

    <Style x:Key="Button2" TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid>
                        <Ellipse Height="{TemplateBinding Height}" Width="{TemplateBinding Width}" Fill="LightGreen" Stretch="Uniform"/>
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Jobi Joy
This format works but when i add HorizontalAlignment="Center" VerticalAlignment="Center" to the Ellipse element to centralise it with the ContentPresenter, it disappears. Any ideas?
icelava