Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Button control template with resizable circle

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?

like image 368
icelava Avatar asked Feb 16 '10 07:02

icelava


2 Answers

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 http://www.freeimagehosting.net/uploads/84e62c4982.png

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

alt text http://www.freeimagehosting.net/uploads/40755babcd.png

like image 143
Adam Sills Avatar answered Oct 11 '22 12:10

Adam Sills


Just ran into this myself.

I had an ellipse object with stretch=uniform to fill a grid, but not go out of bounds. This works ok, however, the circle would not be centered in case the grid was not perfectly square.

Placing the ellipse in a viewbox solved the issue for me:

<Grid>
    <Viewbox>
        <Ellipse Stretch="Uniform"/>
    </Viewbox>
</Grid>

The only problem here is that the base size of the ellipse is 0 (or 1, not sure), which means that using a stroke will automatically fill the entire ellipse (the stroke will be set on the 0-size-ellipse, and then the ellipse will be resized).

With a stroke:

A workaround for the stroke not working, is to use a MinHeight/MinWidth for the ellipse:

<Grid Background="Yellow">
    <Viewbox>
        <Ellipse Style="{Binding Path=StyleStatus, ConverterParameter='CoilEllipse', Converter={StaticResource styleConverter}}"/>
    </Viewbox>
</Grid>

Using the following style:

<Style x:Key="BaseCoilEllipse" TargetType="Ellipse">
    <Setter Property="MinHeight" Value="10" />
    <Setter Property="MinWidth" Value="10" />
    <!-- Because the ellipse is inside a viewbox, the stroke will be dependant on MinHight/MinWidth. 
         Basically the stroke will now be 4/10th of the ellipse (strokethickness*2 / MinHeight/MinWidth) -->
    <Setter Property="StrokeThickness" Value="2"/>
    <Setter Property="Stroke" Value="Green" />

    <Setter Property="Stretch" Value="Uniform" />
    <Setter Property="Fill" Value="Black"/>
</Style>

(Note that the StrokeThickness is now relative)

Result:

Centered ellipse with stroke

like image 37
Deruijter Avatar answered Oct 11 '22 11:10

Deruijter