Шаблон управления кнопкой с изменяемым размером круга

Я изучаю шаблоны элементов управления в WPF и проверяю, как заменить внешний вид кнопки пользовательскими стилями шаблонов. Я вижу, что для создания круглой кнопки необходимо определить эллипс с одинаковой высотой и шириной.

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

Конечно, это только заставляет все кнопки, использующие этот стиль, иметь круг диаметром 80 пикселей, независимо от того, как изменяется размер кнопки. Я бы хотел, чтобы круг принимал более короткие значения высоты/ширины, чтобы он мог динамически масштабироваться в соответствии с размером кнопки.

Однако я не читал никаких материалов, которые учат, как это можно сделать в чистом шаблоне XAML? Кажется, что для достижения этого эффекта требуется какой-то программный код?


person icelava    schedule 16.02.2010    source источник


Ответы (3)


Именно здесь появляется TemplateBinding (TemplateBinding используется внутри шаблонов элементов управления и используется для извлечения значений из шаблонного элемента управления, в данном случае из кнопки).

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

Обратите внимание, что это более короткая форма использования:

{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}

Расширение разметки TemplateBinding оптимизировано только для привязок TemplatedParent.

Тем не менее, если вы хотите, чтобы это был только круг, если ваш эллипс был меньше W/H, тогда ваш контент будет легко вытекать из него, что я сомневаюсь, что это то, что вы на самом деле хотите..? Я думал об использовании многозначного преобразователя для этого, но вы не можете привязываться к параметру преобразователя, так что это исключено.

В этом случае привязанное поведение будет работать, но это некрасиво.

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

... и приложенное поведение:

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

Хорошо, теперь причина, по которой требуется использование прикрепленного поведения (во всяком случае, AFAICT), заключается в том, что для центрирования эллипса (в сценарии, отличном от квадрата/не круга) вам нужны HorizontalAlignment и VerticalAlignment, чтобы иметь возможность действовать . Значение по умолчанию для обоих — «Растянуть», и когда задана явная ширина/высота, оно ведет себя как центр.

При включенном Stretch="Uniform" ваш эллипс всегда будет физически занимать все пространство, ограничиваться будет только рисунок эллипса. Используя это, ваша нарисованная фигура Эллипса всегда будет начинаться сверху слева. Таким образом, в этом случае, если ваша кнопка шире, чем высота, нарисованная часть эллипса не будет центрирована вместе с текстом.

Этот код является хорошим примером того, что вы, вероятно, не ищете:

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

... и кнопка, использующая его (с неквадратной шириной/высотой):

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

Выглядит так:

http://www.freeimagehosting.net/uploads/84e62c4982.png

... по сравнению с этим с опцией прикрепленного свойства:

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

person Adam Sills    schedule 17.02.2010
comment
хорошо, я попробовал этот метод, и кажется, что применение HorizontalAlignment=Center VerticalAlignment=Center к эллипсу также приводит к исчезновению формы круга после изменения размера до большого размера. Интересно, что происходит. - person icelava; 26.02.2010
comment
Обновлен мой ответ, чтобы использовать ActualWidth и ActualHeight. Привязка к W/H — вот почему он делает то, что вы видите. - person Adam Sills; 26.02.2010
comment
После изменения на ActualWidth и ActualHeight теперь он отображается в центре. Хотя я до сих пор не до конца понял, почему так, спасибо. - person icelava; 03.03.2010

Просто столкнулся с этим сам.

У меня был объект эллипса с stretch=uniform, чтобы заполнить сетку, но не выходить за ее пределы. Это работает нормально, однако круг не будет центрирован, если сетка не будет идеально квадратной.

Размещение эллипса в окне просмотра решило проблему для меня:

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

Единственная проблема здесь заключается в том, что базовый размер эллипса равен 0 (или 1, не уверен), а это означает, что использование stroke автоматически заполнит весь эллипс (обводка будет установлена ​​на эллипс размера 0, а затем размер эллипса будет изменен).

С инсультом:

Обходной путь для неработающего штриха — использовать MinHeight/MinWidth для эллипса:

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

Используя следующий стиль:

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

(Обратите внимание, что StrokeThickness теперь относительна)

Результат:

Эллипс по центру с обводкой

person Deruijter    schedule 17.01.2013

Set Stretch=Uniform на Ellipse будет автоматически работать как Min(Height,Width). Вам не нужно присоединенное свойство, как предлагает Адам.

    <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>
person Jobi Joy    schedule 17.02.2010
comment
Этот формат работает, но когда я добавляю HorizontalAlignment=Center VerticalAlignment=Center к элементу Ellipse, чтобы централизовать его с помощью ContentPresenter, он исчезает. Любые идеи? - person icelava; 18.02.2010