WPF: 4 варианта кнопки с иконкой и текстом

Я думаю что каждый (или почти каждый), из тех, кто приходит в WPF из WinForms, поначалу испытывает растерянность по поводу функционала стандартных контролов.
Казалось бы – вот он — знакомый контрол.
Он очень похож на старого знакомого из WinForms. Даже сигнатура обычных методов либо полностью совпадает, либо претерпела незначительную трансформацию (ну, например, свойство Enabled получило приставку Is). Настроек у контролов много, от параметров визуализации рябит в глазах.
Но при чуть более тесном знакомстве и попытке натянуть привычные способы построения интерфейса на XAML и приходит та самая растерянность.

Как же так? Ну неужели у кнопки нет свойства Image? Вы ведь шутите, правда?

Все дело в том, что у WPF (точнее у XAML) совершенно иная идеология организации интерфейса. Базовые контролы представляют лишь базовый (простите за тавтологию) функционал. Простота базовых контролов компенсируется мощными механизмами шаблонов и стилей.
Существуют и сторонние библиотеки компонентов, но они, чаще всего, либо бесполезны, либо безнадежно устарели, либо сильно платные.

Не так давно я в очередной раз столкнулся с необходимостью решения этой очень простой (казалось бы) задачи. Я истерзал весь гугл запросами типа “XAML button with image” “WPF button image text” и т.д.

Среди десятков просмотренных результатов нашлись очевидные как очевидные (и при этом неудобные) пути решения, так и более изощренные.

Небольшое отступление номер 1
После первых же экспериментов стало очевидно, что XAML и иконки в виде png – вещи несовместимые. Не буду долго растекаться почему так – литературы на эту тему хватает, скажу только что в итоге получается и некрасиво, и неудобно, и нефункционально. Картинки размытые, наложенные эффекты и анимация выглядят удручающе и т.д…
Но не стоит огорчаться – в сети десятки и сотни ресурсов с векторными картинками.
Лучшее из того, что я нашел – SyncFusion Metro Studio 2 (не реклама). Это бесплатный продукт, в котором есть 1700 векторных иконок и средства вывода этих иконок в XAML. Результат получается в виде сложного элемента, из которого достаточно скопировать лишь Path, который описывает саму геометрию иконки.
С этим элементом я поступаю так – в проект добавляю ResourceDictionary с именем Icons.xaml и кладу в него все нужные мне иконки:

<ResourceDictionary
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<!-- Resource dictionary entries should be defined here. -->
	
    <Path x:Key="IconTriangle" x:Shared="False" Stretch="Uniform" Data="M50.25,133.5 L49.75,158.25 76,147.25 z" Fill="Black" Stroke="Black"/>
</ResourceDictionary>

Но давайте вернемся к способам реализации кнопки с иконкой.

Первый и самый очевидный способ – описать нужный Content кнопки прямо в коде формы

        <Button 
            HorizontalAlignment="Center" 
            VerticalAlignment="Center">
            <StackPanel Orientation="Horizontal">
                <ContentControl Width="16" Height="16" Margin="4" Content="{StaticResource IconTriangle}"/>
                <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">Button</TextBlock>
            </StackPanel>
        </Button>


Ну, казалось бы, всё на месте. Есть кнопка, у нее есть иконка и текст. Тех, кто поверит в то, что всё так просто и попытается организовать таким подходом красивый и стильный интерфейс, очень скоро ждёт разочарование. И вот почему: простая настройка стиля приводит к распуханию XAML за счет дублирования описания параметров элементов прямо в коде формы. 10 кнопок – 10 идентичных описаний. Простое изменение типа «а давайте-ка покрасим текст на кнопках в зеленый цвет» превращается в утомительный копипаст и еще большее распухание формы.

Второй очевидный способ — наследование от Button

А давайте напишем «свою кнопку с блекджеком и сами знаете с чем еще»?
Наследуемся от Button и добавляем DependencyProperty для ContentControl, через который из XAML формы можно будет задать содержимое для иконки. Не буду останавливаться на подробностях реализации (внизу будет ссылка на источники, там можно будет почитать), но опишу минусы – содержимое кнопки придется задавать из конструктора наследника, на C#. Отсюда получаем массу очевидных и неочевидных проблем, не говоря уже о том, что это не очень хорошо пахнет.

Третий очевидный способ — создадим UserControl.

Создадим UserControl, на который покладем одну лишь кнопку. В UserControl создадим DependencyProperty, через которое будем задавать иконку для ContentControl, который лежит в кнопке. Этот способ по праву заслуживает медаль за максимальную корявость. Он наследует почти все недостатки предыдущих способов, и добавляет множество собственных. В коде формы мы получаем UserControl, но теряем кнопку. Теряем вместе со всеми свойствами и событиями. Автор идеи предлагает вытащить все, что было потеряно, через те самые DependencyProperty, в общем вы поняли. Становится непонятно за что же мы боролись.

Четвёртый способ — AttachedProperty

Этот способ я отношу к неочевидным и изощренным. В оригинале статьи автор предлагает задавать картинку через AttachedProperty. Забегая вперед скажу, что именно этот способ я и выбрал для использования в своем продукте и именно его я опишу максимально подробно. Он не лишен некоторых недостатков на этапе разработки (опишу ниже), но всё же мне он понравился больше остальных. В оригинале автор использовал иконку в виде картинки png, я же модифицировал способ для использования векторной иконки и добавил плюшек.

Итак, совсем немного теории. Что же такое это самое AttachedProperty

Каждый XAML разработчик сталкивался с Attached свойствами когда, например, задавал контролу свойство Grid.Column.
Если в трёх словах – то это своей идее немного похоже на Extension из Linq. Можно зарегистрировать свойство, значение которого можно задать любому DependencyObject. Выглядит это примерно так (пример из MSDN):

public class AquariumObject
{
    public static readonly DependencyProperty IsBubbleSourceProperty = DependencyProperty.RegisterAttached(
		"IsBubbleSource",
		typeof(Boolean),
		typeof(AquariumObject),
		new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
    );
    public static void SetIsBubbleSource(UIElement element, Boolean value)
    {
    	element.SetValue(IsBubbleSourceProperty, value);
    }
    public static Boolean GetIsBubbleSource(UIElement element)
    {
	return (Boolean)element.GetValue(IsBubbleSourceProperty);
    } 
}


В этом коде регистрируется свойство IsBubleSource. В результате любому DependencyObject, например тому же Button, можно задать его значение:

<Button AquariumObject.IsBubbleSource="True">Button</Button>

Общий смысл этого кода – при задании свойства IsBubbleSource для кнопки мы автоматически попадаем в метод SetIsBubbleSource, который устанавливает значение. При получении значения, соответственно, попадаем в метод GetIsBubbleSource. Это все происходит автоматически, достаточно лишь написать методы с именами Set и Get, остальное – дело платформы.

Несмотря на то, что написано не так уж и мало кода, самому Button от такой операции ни жарко ни холодно – он просто становится хранилищем обособленного значения, которое можно задавать и спрашивать. Конечно, можно реализовать в методах SetIsBubbleSource и GetIsBubbleSource хитрую логику, которая будет приводить element к Button, доставать из него содержимое, и производить с содержимым различные операции, но это опять плохо пахнет, делать так не нужно.

Приступаем к практической части

Небольшое отступление 2
В оригинале автор использует имя класса EyeCandy и namespace проекта, но это слишком длинно и я надеюсь, что мне простят сокращение – namespace Ext и имя класса E.

В проект WPF добавляем следующий класс:

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

namespace Ext
{
	public class E
	{
		public static readonly DependencyProperty IconProperty;

		static E()
		{
			var metadata = new FrameworkPropertyMetadata(null);
			IconProperty = DependencyProperty.RegisterAttached("Icon",
	typeof(FrameworkElement),
	typeof(E), metadata);

		}

		public static FrameworkElement GetIcon(DependencyObject obj)
		{
			return (FrameworkElement)obj.GetValue(IconProperty);
		}

		public static void SetIcon(DependencyObject obj, FrameworkElement value)
		{
			obj.SetValue(IconProperty, value);
		}
       }
}

Что же здесь происходит? Мы зарегистрировали Attached свойство Icon типа FrameworkElement со значением по умолчанию равным null.

Теперь создадим шаблон для нашей кнопки. Я не буду останавливаться на объяснения «что такое шаблоны и как они работают» – если вдруг кому-то это неизвестно – информации в сети очень много.
Итак, добавляем в наш проект ResourceDictionary с именем Styles.xaml (если вдруг в проекте еще нет ресурса стилей). В этом ResourceDictionary добавим следующий код:

<ResourceDictionary 
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
	xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero">
	<Style x:Key="ButtonFocusVisual">
		<Setter Property="Control.Template">
			<Setter.Value>
				<ControlTemplate>
					<Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
				</ControlTemplate>
			</Setter.Value>
		</Setter>
	</Style>
	<!-- Resource dictionary entries should be defined here. -->
	<LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0">
		<GradientStop Color="#F3F3F3" Offset="0"/>
		<GradientStop Color="#EBEBEB" Offset="0.5"/>
		<GradientStop Color="#DDDDDD" Offset="0.5"/>
		<GradientStop Color="#CDCDCD" Offset="1"/>
	</LinearGradientBrush>
	<SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/>
	<Style TargetType="{x:Type Button}">
		<Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/>
		<Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
		<Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/>
		<Setter Property="BorderThickness" Value="1"/>
		<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
		<Setter Property="HorizontalContentAlignment" Value="Center"/>
		<Setter Property="VerticalContentAlignment" Value="Center"/>
		<Setter Property="Padding" Value="1"/>
		<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">
						<VisualStateManager.VisualStateGroups>
							<VisualStateGroup x:Name="CommonStates">
								<VisualState x:Name="Normal"/>
								<VisualState x:Name="MouseOver"/>
								<VisualState x:Name="Pressed"/>
								<VisualState x:Name="Disabled">
									<Storyboard>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="IconContent">
											<EasingDoubleKeyFrame KeyTime="0" Value="0.5"/>
										</DoubleAnimationUsingKeyFrames>
									</Storyboard>
								</VisualState>
							</VisualStateGroup>
						</VisualStateManager.VisualStateGroups>
						<StackPanel>
							<ContentControl 
								Content="{StaticResource IconTriangle}"
								Width="16"
								Height="16"
								x:Name="IconContent" Margin="4" RenderTransformOrigin="0.5,0.5" IsEnabled="{Binding IsEnabled, ElementName=Chrome}">
								<ContentControl.Effect>
									<DropShadowEffect Opacity="0" BlurRadius="2"/>
								</ContentControl.Effect>
								<ContentControl.RenderTransform>
									<TransformGroup>
										<ScaleTransform/>
										<SkewTransform/>
										<RotateTransform/>
										<TranslateTransform/>
									</TransformGroup>
								</ContentControl.RenderTransform>
							</ContentControl>
							<TextBlock x:Name="textBlock" Margin="4" TextWrapping="Wrap" Text="{TemplateBinding Content}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
						</StackPanel>
					</Microsoft_Windows_Themes:ButtonChrome>
					<ControlTemplate.Triggers>
						<Trigger Property="IsKeyboardFocused" Value="true">
							<Setter Property="RenderDefaulted" TargetName="Chrome" Value="true"/>
						</Trigger>
						<Trigger Property="ToggleButton.IsChecked" Value="true">
							<Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
						</Trigger>
						<Trigger Property="IsEnabled" Value="false">
							<Setter Property="Foreground" Value="#ADADAD"/>
						</Trigger>
					</ControlTemplate.Triggers>
				</ControlTemplate>
			</Setter.Value>
		</Setter>
	</Style>
</ResourceDictionary>

Эта запись ResourceDictionary описывает шаблон любой кнопки нашего проекта. В этом шаблоне задается обычное оформление для кнопки WPF, но переопределяется ее содержимое. В качестве содержимого выступает StackPanel, в котором лежат ContentControl и TextBlock, т.е. точно так же, как и в самом первом примере. Кроме этого в шаблоне я задал следующее поведение для иконки – если для кнопки задано IsEnabled == False, то иконка получает прозрачность 50%, и становится похожей на неактивную.

Добавим на нашу форму 4 простые кнопки. Назначим каждой кнопке свой текст, например вот так: Content=«Кнопка 1».
Запускаем приложение.


Идентичные иконки на каждой кнопке приложения – это не то, чего мы добиваемся, и именно здесь мы пускаем в дело наше секретное оружие – класс Ext.E и механизм AttachedProperty.

Итак, идем в наш ресурсный файл Styles.xaml и добавляем в него новый namespace:

xmlns:Ext="clr-namespace:Ext"

После этого опускаемся ниже и в шаблоне кнопки находим строчку, в которой создается ContentControl и задается его содержимое:
<ContentControl 
	Content="{StaticResource IconTriangle}" 
        .../>

Заменяем вторую строчку:
<ContentControl 
	Content="{Binding (Ext:E.Icon), RelativeSource={RelativeSource TemplatedParent}}"
        .../>

Эта строчка заставляет ContentControl обратиться к свойству Ext.E.Icon у кнопки и получить из него своё содержимое. После этого остается добавить код, устанавливающий значение свойства Ext.E.Icon в саму кнопку. Делается это в коде формы, на которой создается кнопка.

<Button 
	Ext:E.Icon="{StaticResource IconTriangle}"
	Content="Кнопка 1" />

Примитивный вариант кнопки с иконкой готов. Меняя значение IconTriangle на имена других ресурсов, можно задавать различные иконки на кнопках. При этом, в отличии от первых трех способов, мы сохраняем у кнопки все ее врожденные способности к стилизации (за исключением возможности менять структуру Content, само собой). Содержимое кнопки задается не из C#, и все свойства с событиями остались на своем месте.

Пойдем немного дальше

Если мы попытаемся использовать эту кнопку в реальном проекте, то столкнемся вот с чем:
  • Размер иконки не настраивается.
  • Ориентация кнопки (вертикальная или горизонтальная) не настраивается.


Если точнее, то все настраивается, но только в шаблоне, т.е. для всех кнопок сразу, но клонирование шаблонов – это грабли и сплошное неудобство. Боролись мы не за это.
Расширим класс Ext.E. Добавим туда еще два AttachedProperty
  • IconSize типа double
  • Orientation типа Orientation

Исходный код одним архивом будет в конце статьи, поэтому я не буду дублировать аналогичные методы класса Ext.E в статье.

Опишу лишь изменения, которые нужно сделать в шаблоне Button.
Размеры ContentControl связываем со значением IconSize:

<ContentControl 
	Content="{Binding (Ext:E.Icon), RelativeSource={RelativeSource TemplatedParent}}"
	Width="{Binding (Ext:E.IconSize), RelativeSource={RelativeSource TemplatedParent}}"
	Height="{Binding (Ext:E.IconSize), RelativeSource={RelativeSource TemplatedParent}}"
        .../>

Ориентацию StackPanel связываем со значением Orientation

<StackPanel Orientation="{Binding (Ext:E.Orientation), RelativeSource={RelativeSource TemplatedParent}}">

В результате кнопка получила дополнительные параметры, и мы можем написать вот так:

<Button 
	Ext:E.Icon="{StaticResource IconTriangle}"
	Ext:E.IconSize="32"
	Ext:E.Orientation="Horizontal"
Content="Кнопка 1"/>

В результате нехитрых манипуляций можно получить вот такой зоопарк (первая кнопкаIsEnabled=«False»):
"
Ну и напоследок упомяну об ограничениях

Все они касаются процесса и средств разработки:
— XAML дизайнер VisualStudio 2010 реагирует на подобное описание кнопки как-то так:

— Blend 4 и VisualStudio 2012 ведут себя лучше, но тоже с особенностями:
  • После изменения класса Ext.E лучше перезагрузить среду разработки. Без этого изменения чаще всего не определяются и дизайнеры ругаются на то, что добавленные или измененные свойства не существуют.
  • По неустановленному мною алгоритму значения, заданные кнопке с помощью AttachedProperty, то видны дизайнеру, то нет. Чаще не видны, и форма выглядит как-то так:




Но касается это только Designer, в режиме выполнения приложения всё работает как нужно.

Возможно (и даже скорее всего) я описал велосипед, но тот факт что за два дня поисков я не нашел более приемлемой бесплатной реализации говорит о том, что на этом фронте не всё ладно.
Кроме того я получил возможность немного разобраться с механизмом расширения стандартных контролов нестандартным способом и применений этому механизму можно найти массу.

Благодарю за внимание.

UPD: Статья обновлена в связи с тем, что было найдено решение проблемы с использованием одинаковой иконки на разных кнопках.
UPD2: Благодарю onikiychuka за дельное предложение.

Исходный код

Полезные ссылки:

WPF Control Development — 3 Ways to build an ImageButton
blogs.msdn.com/b/knom/archive/2007/10/31/wpf-control-development-3-ways-to-build-an-imagebutton.aspx
Using Attached Properties to Create a WPF Image Button
www.hardcodet.net/2009/01/create-wpf-image-button-through-attached-properties
Пользовательские свойства зависимостей
msdn.microsoft.com/ru-ru/library/ms753358.aspx
SyncFusion Metro Studio 2
www.syncfusion.com/downloads/metrostudio

Комментарии 30

    +2
    У кнопки есть свойство Content. В нем размещаете StackPanel, в ней Image и Text. Все. Зачем такие сложности?
      0
      А теперь попробуйте в шаблоне этой кнопки сделать так, чтобы иконка увеличивала свой размер при наведении фокуса.
        +3
        И что будет? Совершенно не вижу проблемы.

        PS: >>После первых же экспериментов стало очевидно, что XAML и иконки в виде png – вещи несовместимые. Не буду долго растекаться почему так – литературы на эту тему хватает

        Какой-то бред, если и возникают проблемы — смотрите RenderOptions.BitmapScalingMode\UseLayoutRounding
          0
          Можно попросить пример, в котором:
          1. 2 кнопки на форме будут созданы первым способом
          2. Будет использована иконка png
          3. В шаблоне кнопки (в одном месте, без дубляжа на каждой форме) будет задано увеличение иконки (именно иконки, не текста) при наведении фокуса
          4. Иконки будут выглядеть без искажений, независимо от накладываемых эффектов

          Вдруг я действительно несу бред и это можно сделать намного проще.
            +6
            Не поленился:
            Вот разметка: paste.org.ru/?r7bbx4
            Вот скриншот: (я направил курсор на нижнюю кнопку и иконка увеличилась используя анимацию)
            image
              0
              Да, извиняюсь что не уточнил, но думал что это само собой разумеется — интересуют разные иконки на кнопках.
                0
                Ну тогда да, я бы отнаследовался от Button добавив dp Icon. Ну и привязал бы Source playImage на это свойство через RelativeSource FindAncestor (если так уж не хочется переопределять весь Template).
                  0
                  В целом можно не наследоваться и привязаться к свойству Tag. Должно сработать.

                  Автору — статья ниочем. Жду «6 вариантов CheckedListBox и все говно», и аналогичную с RadioListBox.
                    0
                    Надеюсь вы прочли статью до конца?
                    Расскажите как через Tag указать размер иконки и ориентацию StackPanel.
                    Это кроме того, что Tag — вообще мусорное свойство, и им пользуются все кому не лень. Завтра в проект придет студент, который покладёт в Tag какой-нибудь контекст, и вся кнопка на этом закончится.
                      0
                      Я смотрю вы меня раскусили. Разумеется не читал, ведь после такого:
                      >>опишу минусы – содержимое кнопки придется задавать из конструктора наследника, на C#
                      я бросил читать, ибо это бред быдлокодера

                      Для начала стоит четко определить что необходимо от кнопки (слышали про ТЗ?). А то я смотрю вам не угодить. В комментарии выше:

                      >>1. 2 кнопки на форме будут созданы первым способом…
                      Nagg предложил очень простой вариант, удовлетворяющий по всем пунктам.

                      Но тут вылез сами знаете кто:
                      >>Да, извиняюсь что не уточнил, но думал что это само собой разумеется — интересуют разные иконки на кнопках.

                      Ок, и с этим разобрались. Но вот незадача, в тред опять врывается он:
                      >>Надеюсь вы прочли статью до конца? Расскажите как через Tag указать размер иконки и ориентацию StackPanel.

                      Так и хочется написать через жопу. Но ладно, вернемся к делу.

                      >>Это кроме того, что Tag — вообще мусорное свойство, и им пользуются все кому не лень. Завтра в проект придет студент, который покладёт в Tag какой-нибудь контекст, и вся кнопка на этом закончится.
                      Пишите контрол, в таком случае. Как вариант не пускать криворуких студентов.
                        +3
                        Откуда столько ненависти и ярлыков?
                        Nagg предложил вариант, в котором на каждой кнопке одна и та же иконка. Сколько интерфейсов такого рода вы встречали?
                        Я предпочитаю не плодить лишних сущностей и максимум интерфейса программировать через XAML. Наследники от кнопок, наследники от чекбоксов, наследники от всего — и весь проект у вас в наследниках. В случае использования AttachedProperty достаточно создать всего один класс Ext с тремя методами и по 1 шаблону на каждый тип контрола.
                        Не хочется начинать срач о быдлокоде (мне кажется я до сих пор был корректен), но вешать базовый интерфейсный функционал на Tag — это зло.
                    0
                    Мне не кажется, что это намного более простой вариант. Просто еще один хитрый способ.
                    Вместо одно Extension и одного шаблона дерибаним код на наследника от кнопки, шаблон содержимого и шаблон самой кнопки. Три сущности вместо двух.
                  0
                  Для тех кто не смотрел код этого решения.
                  Автор поспешил и по сути повторил промежуточный шаг из статьи. С помощью этого кода можно получить только вот такую картинку (из статьи):

                  А указать разные иконки для кнопок таким способом невозможно, так как иконка задана прямо в шаблоне.
              0
              Для этого следует использовать Triggers.
                0
                Зачем воротить лишний код, если поведение состояний — это одна из базовых функций шаблона.
                  0
                  Все зависит от того, что вам нужно.

                  Мы для анимаций со сменой иконок, плавными переходами цветов используем EventTriggers.
              0
              Видимо для того, чтобы один раз сделал и используешь стили везде где потребуется и не тратить время на кучу костылей. Опять же когда наберется библиотека стилей что-либо новое создать становится очень легко и быстро.
              0
              но тот факт что за два дня поисков я не нашел более приемлемой бесплатной реализации говорит о том, что на этом фронте не всё ладно.

              Контрол по первой вашей ссылке чем не понравился?
                0
                Ну с чего начать?
                Кнопка перестает быть единым контролом. Например Нельзя в едином шаблоне задать анимацию кнопки при наведении фокуса. Реализовать анимацию можно, но только уже через код самой формы, а это дубляж кода. При необходимости чуть-чуть поменять стиль всех кнопок в приложении получаем копипаст и мороку. А этот подход достаточно реализовать один раз, и весь функционал кнопки остается на месте.
                0
                Для быстрого решения проблемы лучше первый вариант.
                Для всего остального, доведенные до ума второй вариант. Лично у меня это выглядит примерно так:

                <control:Button Height="75" Grid.Row="1" Margin="0" Icon="/icons/lens.png" IconMargin="4"/>

                Не каких проблем с использованием или реализацией не испытываю. Настраивать могу все что душе угодно.
                Лично для меня это более удобный и привычный вариант.
                  0
                  Слишком много ни о чем. Было бы лучше, если автор написал какую проблему решает и способы ее решения. Кнопка с картинкой это не проблема.
                    0
                    Ну проблема в общем-то проста и стандартна, не думал что стоит ее описывать:
                    — написать приложение, в котором присутствуют кнопки с иконками;
                    — стили для контролов приложения должны задаваться через шаблоны, не более одного шаблона для контрола каждого типа;
                    — в зависимости от функции кнопки (тулбар, диалоговая кнопка и т.д.) текст может располагаться как под иконкой, так и сбоку, а сами иконки могут быть разных размеров;
                    — минимизировать количество кода, описывающего конкретную кнопку, чтобы не раздувать XAML формы.
                    +3
                    Идеологически верное решение будет выглядеть так:

                    Наследуем наш контрол (IconButton) от ContentControl, добавляем свойства Icon/IconTemplate по образу и подобию Content/ContentTemplate, добавляем другие полезные свойства от кнопки (реализуем интерфейс ICommandSource), в стиле по умолчанию (Themes\Generic.xaml) используем обычную Button, внутрь которого помещаем два ContentPresenter (один для Icon, второй для Content), привязываем кнопочные свойства из ICommandSource. Всё!

                    Используем так (иконки оформляем как DataTemplate во избежание многих проблем):

                    <IconButton IconTemplate="{StaticResource IconTriangle}" Content="Привет!" Command="{Binding MyFancyCommand}" />
                    
                      0
                      Достойный вариант номер 5.
                        +1
                        внутрь которого помещаем два ContentPresenter (один для Icon, второй для Content)

                        А можно примерчик привести такого стиля по умолчанию?

                        Просто я считал, что ContentPresenter используется для указания в шаблоне значения свойства контрола Content. А для чего нужен второй ContentPresenter? Чем это лучше использования Image для описания иконки в шаблоне?

                        Буду весьма благодарен за разъяснение.
                          0
                          Такой подход намного гибче и универсальнее использования Image. Я потом могу использовать в качестве иконки и Image, и Path и даже TextBlock со шрифтом Webdings. ContentPresenter нужен для встраивания в визуальное дерево контрола пользовательского контента, по умолчанию он просто привязывается к Content/ContentTemplate.

                          Шаблон контрола будет выглядеть примерно так:

                          <ControlTemplate TargetType="IconButton">
                           <Button Command="{TemplateBinding Command}"
                               CommandParameter="{TemplateBinding CommandParameter}"
                               Margin="0"
                               Padding="0"
                               HorizontalAlignment="Stretch">
                            <Grid>
                             <Grid.ColumnDefinitions>
                              <ColumnDefinition Width="Auto" />
                              <ColumnDefinition Width="*" />
                             </Grid.ColumnDefinitions>
                          
                             <ContentPresenter Grid.Column="0"
                                      Content="{TemplateBinding Icon}"
                                      ContentTemplate="{TemplateBinding IconTemplate}"
                                      HorizontalAlignment="Left"
                                      VerticalAlignment="Center"
                                      Margin="10,0,0,0" />
                          
                             <ContentPresenter Grid.Column="1"
                                      Content="{TemplateBinding Content}"
                                      ContentTemplate="{TemplateBinding ContentTemplate}"
                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                      Margin="{TemplateBinding Padding}" />
                          
                            </Grid>
                           </Button>
                          </ControlTemplate>
                          
                        0
                        Content="{Binding (Ext:E.Icon), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"

                        Зачем так делать? А если у меня кнопка в кнопке лежит что мне делать? правильный вариант в ControlTemplate выглядит вот так

                        Content="{TemplateBinding (Ext:E.Icon)}"

                        ну или вот так, если хочется чтобы автоматическая конвертация работала

                        Content="{Binding (Ext:E.Icon), RelativeSource={RelativeSource TemplatedParent}}"
                          0
                          Спасибо за дельное замечание. Кнопка в кнопке — это, конечно же, та еще экзотика но по существу так чище.
                          0
                          С PNG нету никаких проблем, просто нужно использовать иконки с запасом размера (желательно кратным отображаемому) под увеличение, а размеры самого элемента, который показывает изображение, уже надо задавать отдельно. При использовании UseLayoutRounding/SnapsToDevicePixels четкость хорошая.
                          Векторные иконки — это очень плохая идея. WPF по умолчанию не кеширует результаты растеризации векторной геометрии, и пользователи с отсутствием аппаратной поддержки механизмов растеризации будут иметь серьёзные проблемы с производительностью при большом одновременном количестве отображаемых векторных иконок в кадре. Каждый экземпляр такой иконки тесселируется и растеризуется по отдельности. Не каждый кадр, но тогда, когда область иконки попадает в «грязный регион» при изменении содержимого окна. Т.е. при любом изменении цвета подложки кнопки. Так-же это плохо виляет на энергопотребление, что критично для мобильных устройств, тесселяция в WPF полностью программная. И если для тесселяции простеньких иконок еще надо произвести не так много вычислений, то сложные иконки с толстой геометрией и большим количеством деталей могут приводить к заметным всплескам процессорной активности в процессе работы пользователя с приложением.
                          Как вариант можно использовать кеширование композиции — свойство UIElement.CacheMode/класс BitmapCache, но тогда становится непонятно, зачем вообще нужно векторное представление иконки, если выводится предварительно растеризованное изображение, т.е. заменимое обычным изображением, уже растеризованным до этапа компиляции приложения.
                            0
                            Я в последнее время использую очень простые иконки, и та же Metro Studio от SyncFusion — тому хороший пример. Понятно, что не стоит использовать полноцветные векторные копии обычных иконок, смысла большого не имеет. Ну а что насчет интерполяции — если с искажениями при изменении размеров иконки с нею еще можно мириться, то при эффектах покачивания или перспективы искажения уже критичны.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое