DataTrigger referencing an enum DependencyProperty - wpf

Solution: Please look at Ilan answer!
I am currently working on some CustomControls and this is one of them. Depending on the DirectionProperty i want to change the direction of the linearGradientBrush with the DataTrigger. I am not really able to get it working and hope for your help.
It looks like the DataTrigger isn't really able to get the Value or the Direction. Thanks in advance
SanHolo
EDIT: Doing it like that i get an error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='CustomControlLibrary.ColoredProgress', AncestorLevel='1''. BindingExpression:Path=Direction; DataItem=null; target element is 'ColoredProgress' (Name=''); target property is 'NoTarget' (type 'Object')
C#
using System.Windows;
using System.Windows.Controls;
namespace CustomControlLibrary
{
public class ColoredProgress : Control
{
public enum colorDirection { Increase, Decrease }
private static DependencyProperty ProgressProperty =
DependencyProperty.Register("Progress", typeof(double), typeof(ColoredProgress), new PropertyMetadata(0.00));
private static DependencyProperty DirectionProperty =
DependencyProperty.Register("Direction", typeof(colorDirection), typeof(ColoredProgress), new PropertyMetadata(colorDirection.Increase));
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, converter(value)); }
}
public colorDirection Direction
{
get { return (colorDirection)GetValue(DirectionProperty); }
set { SetValue(DirectionProperty, value); }
}
public ColoredProgress()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColoredProgress), new FrameworkPropertyMetadata(typeof(ColoredProgress)));
this.Loaded += ColoredProgress_Loaded;
}
private void ColoredProgress_Loaded(object sender, RoutedEventArgs e)
{
double height = (double)GetValue(ColoredProgress.ActualHeightProperty);
SetValue(ProgressProperty, height - (height * Progress));
}
//takes a double between 0-1 (percent of the ProgressBar) and converts it to the value needed in the design
private double converter(double percentage)
{
double height = (double)GetValue(ColoredProgress.ActualHeightProperty);
return height - (height * percentage);
}
}
}
XAML
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlLibrary">
<Style TargetType="{x:Type local:ColoredProgress}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ColoredProgress}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
RenderTransformOrigin="0.5, 0.5"
DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ColoredProgress}}}">
<Grid x:Name="PART_Bar">
<Grid Background="Transparent" Panel.ZIndex="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle Fill="{TemplateBinding Background}" Height="{Binding Path=Progress, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<Grid Panel.ZIndex="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" x:Name="increase"/>
<RowDefinition Height="0" x:Name="decrease"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0">
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Row="1">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Direction, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ColoredProgress}}}" Value="colorDirection.Decrease">
<Setter TargetName="increase" Property="Height" Value="0"/>
<Setter TargetName="decrease" Property="Height" Value="*"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

please use the regular triggers:
<ControlTemplate TargetType="{x:Type local:ColoredProgress}">
...
<ControlTemplate.Triggers>
<Trigger Property="Direction" Value="Decrease">
<Setter TargetName="increase" Property="Height" Value="0"/>
<Setter TargetName="decrease" Property="Height" Value="*"/>
</Trigger>
<Trigger Property="Direction" Value="Increase">
<Setter TargetName="increase" Property="Height" Value="*"/>
<Setter TargetName="decrease" Property="Height" Value="0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
As I understand the data trigger go to the DataContext to check for the value, since you've defined a Direction as a dependency property of your control you, can get the value directly. More over you can't point the data context because you haven't any property in you data context that can provide you with the value you need. That is why you get the binding expression error.
Let me know if you need more explanation.
Regards.

I haven't run your code, but I suppose your problem is that your value binding in the DataTrigger is not set correctly to the Enum value you expect.
Try this: (Note the new Value binding)
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Direction, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ColoredProgress}}}"
Value="{x:Static local:colorDirection.Decrease}">
<Setter TargetName="increase" Property="Height" Value="0"/>
<Setter TargetName="decrease" Property="Height" Value="*"/>
</DataTrigger>
</ControlTemplate.Triggers>
I suppose it should work, but I didn't check the rest of the code, so feel free to update your progress in here.

Related

Trying to pass validation to my custom tooltip control

I am trying to make a custom tooltip control that is used for TextBoxes.
It will look like this:
...except for some pixels that comes from the background components that I have gimped away as good as possible.
The idea comes from:
How to implement Balloon message in a WPF application
The problem is that the code behind of my custom control never gets the validation object (that should be passed to it via the trigger in generic.xaml).
Why not?
generic.xaml:
<Style TargetType="{x:Type TextBox}" x:Name="tb">
<Setter Property="Width" Value="200" />
<Setter Property="Background" Value="{StaticResource InputBackgroundColor}" />
<Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Margin" Value="5,0,0,5" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip">
<Setter.Value>
<Windows:ValidationBalloonPopupWindow
Validation="{Binding Path=Validation, ElementName=tb}" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
As you see, I try to refer to Validation by using ElementName of tb.
Seems like Name is not used in templates. If I change to x:Key instead, all my textboxes becomes like 10 pixels wide. Probably not the right thing to do in other words.
The code behind, ValidationBalloonPopupWindow.xaml.cs:
using System.Windows;
using System.Windows.Controls;
namespace Foo.ToolTips
{
public partial class ValidationBalloonPopupWindow : ToolTip
{
public ValidationBalloonPopupWindow()
{
InitializeComponent();
}
public static DependencyProperty ValidationProperty
= DependencyProperty.Register("Validation", typeof(object), typeof(ValidationBalloonPopupWindow),
new PropertyMetadata(null, OnChangedValidationByBinding));
private static void OnChangedValidationByBinding
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ValidationBalloonPopupWindow)d).OnChangedValidationByBinding(e.NewValue);
}
public void OnChangedValidationByBinding(object newValue)
{
txtMessage.Text = newValue.GetType().Name;
}
private object _validation;
public object Validation
{
get
{
return _validation;
}
set
{
_validation = value;
txtMessage.Text = _validation.GetType().Name;
}
}
}
}
Which has a setter that should run, I have tried to put a lot of breakpoints in this file without success.
The xaml for the control itself, ValidationBalloonPopupWindow.xaml:
<ToolTip x:Class="FRAM.Windows.ValidationBalloonPopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Transparent" BorderBrush="Transparent" HasDropShadow="false"
Placement="Bottom"
Height="Auto" Width="Auto">
<Grid Height="126" Width="453">
<Border Margin="7,13,0,0"
CornerRadius="10,10,10,10" Grid.ColumnSpan="4" HorizontalAlignment="Left" Width="429" Height="82" VerticalAlignment="Top" Grid.RowSpan="2">
<Border.Effect>
<DropShadowEffect Color="#FF474747" />
</Border.Effect>
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF58C2FF" Offset="0" />
<GradientStop Color="#FFFFFFFF" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel Orientation="Vertical">
<Label Content="Title" Height="31" HorizontalAlignment="Left"
Margin="12,8,0,0" Name="lblCaption" FontSize="16" FontWeight="Bold" />
<TextBlock Margin="18,0,0,0" Name="txtMessage" Width="378" HorizontalAlignment="Left">Body</TextBlock>
</StackPanel>
</Border>
<Path Data="M25,25L10.9919,0.64 0.7,25" Fill="#FF58C2FF" HorizontalAlignment="Left"
Margin="32,3,0,0" Stretch="Fill" Width="22" Height="10" VerticalAlignment="Top" />
</Grid>
</ToolTip>
Instead of referring by name, get the Textbox itself by using RelativeSource binding.
Try something like this:
<Setter Property="ToolTip">
<Setter.Value>
<Windows:ValidationBalloonPopupWindow
Validation="{Binding RelativeSource={RelativeSource Self}, Path=Validation}" />
</Setter.Value>
</Setter>

How to display a different value for dropdown list values/selected item in a WPF ComboBox?

I have a WPF combobox bound to a list of items with long descriptions.
The type bound to the ComboBox has both short and long description as properties. Currently, I am binding to the full description.
comboBox.DisplayMemberPath = "FullDescription";
How to ensure that when the item is selected and displayed as a single item in the combobox, it will be displayed as a value of the ShortDescription property while the dropdown will display FullDescription?
Update 2011-11-14
I recently came upon the same requirement again and I wasn't very happy with the solution I posted below. Here is a nicer way to get the same behavior without re-templating the ComboBoxItem. It uses a DataTemplateSelector
First, specify the regular DataTemplate, the dropdown DataTemplate and the ComboBoxItemTemplateSelector in the resources for the ComboBox. Then reference the ComboBoxItemTemplateSelector as a DynamicResource for ItemTemplateSelector
<ComboBox ...
ItemTemplateSelector="{DynamicResource itemTemplateSelector}">
<ComboBox.Resources>
<DataTemplate x:Key="selectedTemplate">
<TextBlock Text="{Binding Path=ShortDescription}"/>
</DataTemplate>
<DataTemplate x:Key="dropDownTemplate">
<TextBlock Text="{Binding Path=FullDescription}"/>
</DataTemplate>
<local:ComboBoxItemTemplateSelector
x:Key="itemTemplateSelector"
SelectedTemplate="{StaticResource selectedTemplate}"
DropDownTemplate="{StaticResource dropDownTemplate}"/>
</ComboBox.Resources>
</ComboBox>
ComboBoxItemTemplateSelector checks if the container is the child of a ComboBoxItem, if it is, then we are dealing with a dropdown item, otherwise it is the item in the ComboBox.
public class ComboBoxItemTemplateSelector : DataTemplateSelector
{
public DataTemplate DropDownTemplate
{
get;
set;
}
public DataTemplate SelectedTemplate
{
get;
set;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
ComboBoxItem comboBoxItem = VisualTreeHelpers.GetVisualParent<ComboBoxItem>(container);
if (comboBoxItem != null)
{
return DropDownTemplate;
}
return SelectedTemplate;
}
}
GetVisualParent
public static T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
Old solution, requires re-templating of ComboBoxItem
<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" />
<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />
<ControlTemplate x:Key="FullDescriptionTemplate" TargetType="ComboBoxItem">
<Border Name="Border" Padding="2" SnapsToDevicePixels="true">
<StackPanel>
<TextBlock Text="{Binding Path=FullDescription}"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="true">
<Setter TargetName="Border" Property="Background" Value="{StaticResource SelectedBackgroundBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ComboBox Name="c_comboBox" ItemsSource="{Binding}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ShortDescription}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="Template" Value="{StaticResource FullDescriptionTemplate}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
This results in the following behavior
It doesn't seem to work for me now, but this one does:
public class ComboBoxItemTemplateSelector : DataTemplateSelector {
public DataTemplate SelectedTemplate { get; set; }
public DataTemplate DropDownTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
var presenter = (ContentPresenter)container;
return (presenter.TemplatedParent is ComboBox) ? SelectedTemplate : DropDownTemplate;
}
}
I modified this custom rounded WPF ComboBox to display a different value from the item selected as well as change the color for each item.
Custom ComboBox
First you need to create the structure:
//Structure
public class COMBOITEM
{
string _ITEM_NAME;
string _ITEM_SHORT_NAME;
Brush _ITEM_COLOR;
public string ITEM_NAME
{
get { return _ITEM_NAME; }
set { _ITEM_NAME = value; }
}
public string ITEM_SHORT_NAME
{
get { return _ITEM_SHORT_NAME; }
set { _ITEM_SHORT_NAME = value; }
}
public Brush ITEM_COLOR
{
get { return _ITEM_COLOR; }
set { _ITEM_COLOR = value; }
}
}
Initialize the structure, fill it with data and bind to ComboBox:
private void Load_Data()
{
Brush Normal_Blue = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF1F4E79"));
//Load first entry
ObservableCollection<COMBOITEM> _Line_Data = new ObservableCollection<COMBOITEM>();
_Line_Data.Add(new COMBOITEM() { ITEM_NAME = "Line Number 1", ITEM_SHORT_NAME = "LN 1", ITEM_COLOR = Normal_Blue });
//Load Test Data
for (int i = 2; i < 10; i++)
{
_Line_Data.Add(new COMBOITEM()
{
ITEM_NAME = "Line Number " + i.ToString(),
ITEM_SHORT_NAME = "LN " + i.ToString(),
ITEM_COLOR = (i % 2 == 0) ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red) //This just changes color
});
}
//Bind data to combobox
cb_Test.ItemsSource = _Line_Data;
}
Now place the ComboBox in your design. To use it as a normal ComboBox, remove DisplayMemberPath and rename "ColorComboBoxItem" to "CustomComboBoxItem":
<ComboBox x:Name="cb_Test" FontSize="36" Padding="1,0" MinWidth="100" MaxWidth="400" Margin="5,53,10,207" FontFamily="Calibri" Background="#FFBFBFBF" Foreground="#FF1F4E79" BorderBrush="#FF1F4E79" VerticalContentAlignment="Center" TabIndex="5" IsSynchronizedWithCurrentItem="False"
Style="{DynamicResource RoundedComboBox}"
ItemContainerStyle="{DynamicResource ColorComboBoxItem}"
DisplayMemberPath="ITEM_SHORT_NAME" />
Now add the following styles/template to App.xaml Application.Resources:
<!-- Rounded ComboBox Button -->
<Style x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<Border
x:Name="Border"
Grid.ColumnSpan="2"
CornerRadius="8"
Background="{TemplateBinding Background}"
BorderBrush="#FF1F4E79"
BorderThickness="2"
/>
<Path
x:Name="Arrow"
Grid.Column="1"
Fill="{TemplateBinding Foreground}"
Stroke="{TemplateBinding Foreground}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 4 4 L 8 0 Z"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="ComboBoxTextBox" TargetType="TextBox">
<Border x:Name="PART_ContentHost" Focusable="True" />
</ControlTemplate>
<!-- ComboBox Template -->
<Style x:Key="RoundedComboBox" TargetType="{x:Type ComboBox}">
<Setter Property="Foreground" Value="#333" />
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="Background" Value="White" />
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="FontSize" Value="13" />
<Setter Property="MinWidth" Value="150"/>
<Setter Property="MinHeight" Value="35"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton
Cursor="Hand"
Name="ToggleButton"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
Style="{StaticResource ComboBoxToggleButton}"
Grid.Column="2"
Focusable="false"
IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<ContentPresenter
Name="ContentSite"
IsHitTestVisible="False"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
Margin="10,3,30,3"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
<TextBox x:Name="PART_EditableTextBox"
Style="{x:Null}"
Template="{StaticResource ComboBoxTextBox}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="3,3,23,3"
Focusable="True"
Visibility="Hidden"
IsReadOnly="{TemplateBinding IsReadOnly}"/>
<Popup
Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Grid
Name="DropDown"
SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border
CornerRadius="10"
x:Name="DropDownBorder"
Background="#FFBFBFBF"
BorderThickness="2"
BorderBrush="#FF1F4E79"
/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
<Trigger Property="IsEditable" Value="true">
<Setter Property="IsTabStop" Value="false"/>
<Setter TargetName="PART_EditableTextBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="ContentSite" Property="Visibility" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
</Style.Triggers>
</Style>
<!--This style uses the normal items.add function-->
<Style x:Key="CustomComboBoxItem" TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="30" />
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border
Name="Border"
Padding="5"
Margin="2"
BorderThickness="2,0,0,0"
CornerRadius="0"
Background="Transparent"
BorderBrush="Transparent">
<TextBlock TextAlignment="Left">
<ContentPresenter />
</TextBlock>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="true">
<Setter TargetName="Border" Property="BorderBrush" Value="#FF3737CB"/>
<Setter TargetName="Border" Property="Background" Value="#FF6ACDEA"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--This style uses the structure to fill items and set the item color-->
<Style x:Key="ColorComboBoxItem" TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="30" />
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Foreground" Value="{Binding ITEM_COLOR}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border
Name="Border"
Padding="5"
Margin="2"
BorderThickness="2,0,0,0"
CornerRadius="0"
Background="Transparent"
BorderBrush="Transparent">
<TextBlock Text="{Binding ITEM_NAME}" TextAlignment="Left">
</TextBlock>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="true">
<Setter TargetName="Border" Property="BorderBrush" Value="#FF3737CB"/>
<Setter TargetName="Border" Property="Background" Value="#FF6ACDEA"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I hope this helps..
This solution is for WPF + MVVM.
Some of the other solutions work, and some of them do not. The problem with some other solutions are that if they do not work, it's sometimes difficult to debug why it is not working, especially if one is not experienced with WPF.
In my opinion, it's preferable to use strings for the bindings, and convert to an enum in C# which means everything is easier to troubleshoot.
You might need to use ReSharper, it will auto-suggest any missing namespaces.
Create an enum with description attributes:
public enum EnumSelectedView
{
[Description("Drop Down 1")]
DropDown1 = 0,
[Description("Drop Down 2")]
DropDown2 = 1,
}
And a ComboBox:
<ComboBox HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="130"
ItemsSource="{Binding AvailableSelectedViews, Mode=OneWay}"
SelectedItem="{Binding SelectedView, Mode=TwoWay, Converter={StaticResource enumToDescriptionConverter}}"
</ComboBox>
The converter in XAML needs to be pointed at the C# class. If you are using a UserControl or a Window, it would be UserControl.Resources or Window.Resources.
<DataTemplate.Resources>
<converters:EnumToDescriptionConverter x:Key="enumToDescriptionConverter" />
</DataTemplate.Resources>
Add some extension methods and a converter anywhere in your project:
using System;
namespace CMCMarkets.Phantom.CoreUI.Converters
{
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Data;
public class EnumToDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((value is Enum) == false) throw new ArgumentException("Error: value is not an enum.");
return ((Enum)value)?.GetDescriptionAttribute();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((value is string) == false)
{
throw new ArgumentException("Error: Value is not a string");
}
foreach (var item in Enum.GetValues(targetType))
{
var asString = (item as Enum).GetDescriptionAttribute();
if (asString == (string)value)
{
return item;
}
}
throw new ArgumentException("Error: Unable to match string to enum description.");
}
}
public static class EnumExtensions
{
/// <summary>
/// For a single enum entry, return the [Description("")] attribute.
/// </summary>
public static string GetDescriptionAttribute(this Enum enumObj)
{
FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
{
return enumObj.ToString();
}
else
{
DescriptionAttribute attrib = attribArray[0] as DescriptionAttribute;
return attrib?.Description;
}
}
/// <summary>
/// For an enum type, return a list of all possible [Description("")] attributes.
/// </summary>
/*
* Example: List<string> descriptions = EnumExtensions.GetDescriptionAttributeList<MyEnumType>();
*/
public static List<string> GetDescriptionAttributeList<T>()
{
return typeof(T).GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
}
/// <summary>
/// For an enum instance, return a list of all possible [Description("")] attributes.
/// </summary>
/*
* Example:
*
* List<string> descriptions = typeof(CryptoExchangePricingOrGraphView).GetDescriptionAttributeList();
*/
public static List<string> GetDescriptionAttributeList(this Type type)
{
return type.GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
}
/// <summary>
/// For an enum instance, return a list of all possible [Description("")] attributes.
/// </summary>
/*
* Example:
*
* MyEnumType x;
* List<string> descriptions = x.GetDescriptionAttributeList();
*/
public static List<string> GetDescriptionAttributeList(this Enum thisEnum)
{
return thisEnum.GetType().GetEnumValues().Cast<Enum>().Select(x => x.GetDescriptionAttribute()).ToList();
}
}
}
In your ViewModel:
public IReadOnlyList<string> AvailableSelectedViews { get; }
And in the constructor:
this.AvailableSelectedViews = typeof(EnumSelectedView).GetDescriptionAttributeList();
The selected item will be bound to this. It uses the converter to go from the string in the combobox straight to the enum. You could also do the conversion inside the property updater by using the extension methods above.
public EnumSelectedView SelectedView { get; set; }
Another option I have found is to place a textbox over the combobox text area. Size and align it so that it lays perfectly over it then use a sub similar to this:
Private Sub ComboBox*_Change()
Dim T As String
T = Left(ComboBox*.Text, 1)
TextBox*.Value = T
End Sub
(replace the * with the relevant numbers)
the result is that when selected the dropdown will display the list as usual but the textbox lying over it will only show its first character.
Hope this helps.
The accepted solution only works if IsEditable is false.
If IsEditable is true, i.e., if the control is a "real" combo box in the sense of combining a list and a free-input text box, there is a really simple solution:
<ComboBox ...
DisplayMemberPath="PropertyToUseForList"
TextSearch.TextPath="PropertyToUseForTextBox" />
Note that this works even if IsTextSearchEnable is false.

XAML image source set dynamically based on content

Well it's not really very dynamic, at least it won't change at runtime.
The idea is I have buttons and each one has a unique image ( icon 32x32 ). The buttons all share a style where I mess with the ControlTemplate. So each image also has 2 colors one normal and another when I mouse over.
I noticed that when I declare the source path for the images is that they are almost all the same so I though DRY ( Don't Repeat Yourself ). What if I could use the button Name or some other property as part of the source path ( i.e. the name of the image file ). That would be good programming.
Problem is I'm new to XAML, WPF and perhaps programming all together so I'm not sure how to do that. I guess this would need code behind or a converter of some sort ( guess I'll read about converters a bit more ). Here is a bit of code ( this doesn't work but it gives you the general idea ( hopefully )):
<Style x:Key="ButtonPanelBigButton" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="ButtonBorder"
Height="78"
MaxWidth="70"
MinWidth="50"
BorderThickness="0.5"
BorderBrush="Transparent"
CornerRadius="8" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Here I wan't to put in the Name property of the button because there is a picture there to match -->
<Image x:Name="ButtonIcon" Source="..\Images\Icons\32x32\Blue\{Binding Name}.png"
Margin="4"
Height="32"
Width="32"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<TextBlock Grid.Row="1"
Padding="5,2,5,2"
TextWrapping="Wrap"
Style="{StaticResource MenuText}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ContentPresenter ContentSource="Content" />
</TextBlock>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter TargetName="ButtonIcon" Property="Source" Value="..\Images\Icons\32x32\Green\user.png" /> <!-- Same Here -->
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{StaticResource SecondColorBrush}" />
<Setter TargetName="ButtonBorder" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" Opacity="0.5">
<GradientStop Color="{StaticResource MainColor}" Offset="1" />
<GradientStop Color="{StaticResource SecondColor}" Offset="0.5" />
<GradientStop Color="{StaticResource MainColor}" Offset="0" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Hopefully you get where I'm going with this and someone might be able to help me so my code is nice and DRY ( now I'M repeating myself!!! ).
You're right: The easy way to solve this is to use a converter.
The Source property takes an ImageSource, so you'll need to load the bitmap yourself in your converter.
The converter is used like this:
<Image Source="{Binding Name,
RelativeSource={RelativeSource TemplatedParent},
Converter={x:Static local:ImageSourceLoader.Instance},
ConverterParameter=..\Images\Icons\32x32\Blue\{0}.png}" />
And implemented like this:
public class ImageSourceLoader : IValueConverter
{
public static ImageSourceLoader Instance = new ImageSourceLoader();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var path = string.Format((string)parameter, value.ToString());
return BitmapFrame.Create(new Uri(path, UriKind.RelativeOrAbsolute));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Note that this simple solution can't handle relative Uris because the BaseUri property of the Image is unavailable to the converter. If you want to use relative Uris, you can do this by binding an attached property and using StringFormat:
<Image local:ImageHelper.SourcePath="{Binding Name,
RelativeSource={RelativeSource TemplatedParent},
StringFormat=..\Images\Icons\32x32\Blue\{0}.png}" />
And the attached property's PropertyChangedCallback handles loads the image after combining the BaseUri with the formatted Uri string:
public class ImageHelper : DependencyObject
{
public static string GetSourcePath(DependencyObject obj) { return (string)obj.GetValue(SourcePathProperty); }
public static void SetSourcePath(DependencyObject obj, string value) { obj.SetValue(SourcePathProperty, value); }
public static readonly DependencyProperty SourcePathProperty = DependencyProperty.RegisterAttached("SourcePath", typeof(string), typeof(ImageHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
((Image)obj).Source =
BitmapFrame.Create(new Uri(((IUriContext)obj).BaseUri, (string)e.NewValue));
}
});
}
You can use the Tag Property and set the full path of the image in Tag and then use it.
the following my code to be more clear
*<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#373737" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="12" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="7" Background="{TemplateBinding Background}" FlowDirection="RightToLeft">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image x:Name="imgDistance" Source="{Binding Tag,RelativeSource={RelativeSource TemplatedParent}}"
Width="35" Height="35" HorizontalAlignment="Left" Margin="42,7,0,8" Grid.Column="0"/>*
and then I used it in the button as following
<Button x:Name="btnDistance" Height="50" VerticalAlignment="Top" Margin="0,4,0,0" Curs
or="Hand" Click="btnDistance_Click" Tag="/Images/distance.png">

image button template?

I want Image button with two state(normal , mouse over). that button must change image with Mouse Over event trigger automatically.
this image button must be a user control. Also i want to set image for each state form code in which form i use that user control.
Solution is using a template with "Value Converter" but i don't know how?
Why must this image button be a user control? If a regular button with a new control template is fine, this should work:
<Button>
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Image Name="HoverImage" Source="hover_image.png" Visibility="Hidden" />
<Image Name="DefaultImage" Source="default_image.png" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="DefaultImage" Property="Visibility" Value="Hidden" />
<Setter TargetName="HoverImage" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
If you need a simple rollover effect, you don't need a template for it.. article below has a solution to it..
http://www.c-sharpcorner.com/Resources/Detail.aspx?ResourceId=706
In this article user uses SolidColorBrush, you can use ImageBrush to set image as background of button.
I found This on Code-project-Article(Cool example)
http://www.codeproject.com/KB/WPF/WPF_xaml_taskbar_window.aspx
First He create Wpf-Custom-control(you can create class inherit from Button like this)
public class ImageButton : Button
{
private string cornerRadius;
public string CornerRadius
{
get { return cornerRadius; }
set { cornerRadius = value; }
}
private string highlightBackground;
public string HighlightBackground
{
get { return highlightBackground; }
set { highlightBackground = value; }
}
private string pressedBackground;
public string PressedBackground
{
get { return pressedBackground; }
set { pressedBackground = value; }
}
}
As second step you must Create template in resource-dictionary(here is code)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Phone.Controls">
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type local:ImageButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="MouseOverButton">
<ThicknessAnimation Storyboard.TargetName="ButtonBackgroundBorder"
Storyboard.TargetProperty="(Control.Margin)"
Duration="0:0:0.05"
FillBehavior="Stop"
From="0,0,0,0" To="2,2,2,2"
AutoReverse="True" />
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="ButtonOuterGrid">
<Border x:Name="ButtonBackgroundBorder"
CornerRadius="{Binding Path=CornerRadius, RelativeSource={RelativeSource TemplatedParent}}"
Background="{Binding Path=HighlightBackground, RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="Black"
BorderThickness="0.8"
Opacity="0">
<Border.BitmapEffect>
<OuterGlowBitmapEffect GlowColor="#FFFFFFFF" GlowSize="2.75" Noise="0.20"/>
</Border.BitmapEffect>
</Border>
<Border x:Name="ButtonEdgesBorder" CornerRadius="{Binding Path=CornerRadius, RelativeSource={RelativeSource TemplatedParent}}"
Opacity="1"
BorderBrush="Transparent"
BorderThickness="0" />
<Border x:Name="ButtonContentBorder"
CornerRadius="{Binding Path=CornerRadius, RelativeSource={RelativeSource TemplatedParent}}"
Opacity="1"
BorderThickness="1">
<ContentPresenter x:Name="ContentText"
Width="Auto" Height="Auto"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.Setters>
<Setter Property="Opacity" TargetName="ButtonBackgroundBorder" Value="1"/>
<Setter Property="TextElement.Foreground" TargetName="ContentText" Value="Black"/>
</Trigger.Setters>
</Trigger>
<EventTrigger RoutedEvent="Grid.MouseEnter"
SourceName="ButtonOuterGrid">
<BeginStoryboard Storyboard="{StaticResource MouseOverButton}"/>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="ImageButton" TargetType="{x:Type Button}">
<Setter Property="Template" Value="{StaticResource ButtonTemplate}" />
</Style>
And this is last Step, in xaml file you must insert this custom-control
<ImageButton x:Name="btnConfigs"
Style="{StaticResource ImageButton}"
Width="25" Height="25"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="0,31.125,16.418,0">
<Image x:Name="ImgConfigs"
Stretch="Fill"/>
</ImageButton >
and in cs file do this
this.ImgConfigs.Source="any imag-source"
also we can change this image-source on btnconfig-click event
With special thanks from Murray-Foxcroft for create that article

How to create trapezoid tabs in WPF tab control

How to create trapezoid tabs in WPF tab control? I'd like to create non rectangular tabs that look like tabs in Google Chrome or like tabs in code editor of VS 2008.
Can it be done with WPF styles or it must be drawn in code?
Is there any example of code available on internet?
Edit:
There is lots of examples that show how to round corners or change colors of tabs, but I could not find any that changes geometry of tab like this two examples:
VS 2008 Code Editor Tabs
Google Chrome tabs
Tabs in this two examples are not rectangles, but trapezes.
I tried to find some control templates or solutions for this problem on internet, but I didn’t find any “acceptable” solution for me. So I wrote it in my way and here is an example of my first (and last=)) attempt to do it:
<Window x:Class="TabControlTemplate.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:TabControlTemplate"
Title="Window1" Width="600" Height="400">
<Window.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#FF3164a5" Offset="1"/>
<GradientStop Color="#FF8AAED4" Offset="0"/>
</LinearGradientBrush>
</Window.Background>
<Window.Resources>
<src:ContentToPathConverter x:Key="content2PathConverter"/>
<src:ContentToMarginConverter x:Key="content2MarginConverter"/>
<SolidColorBrush x:Key="BorderBrush" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="HoverBrush" Color="#FFFF4500"/>
<LinearGradientBrush x:Key="TabControlBackgroundBrush" EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Color="#FFa9cde7" Offset="0"/>
<GradientStop Color="#FFe7f4fc" Offset="0.3"/>
<GradientStop Color="#FFf2fafd" Offset="0.85"/>
<GradientStop Color="#FFe4f6fa" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="TabItemPathBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FF3164a5" Offset="0"/>
<GradientStop Color="#FFe4f6fa" Offset="1"/>
</LinearGradientBrush>
<!-- TabControl style -->
<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="1" BorderThickness="2,0,2,2" Panel.ZIndex="2" CornerRadius="0,0,2,2"
BorderBrush="{StaticResource BorderBrush}"
Background="{StaticResource TabControlBackgroundBrush}">
<ContentPresenter ContentSource="SelectedContent"/>
</Border>
<StackPanel Orientation="Horizontal" Grid.Row="0" Panel.ZIndex="1" IsItemsHost="true"/>
<Rectangle Grid.Row="0" Height="2" VerticalAlignment="Bottom"
Fill="{StaticResource BorderBrush}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- TabItem style -->
<Style x:Key="{x:Type TabItem}" TargetType="{x:Type TabItem}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Grid x:Name="grd">
<Path x:Name="TabPath" StrokeThickness="2"
Margin="{Binding ElementName=TabItemContent, Converter={StaticResource content2MarginConverter}}"
Stroke="{StaticResource BorderBrush}"
Fill="{StaticResource TabItemPathBrush}">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="False" StartPoint="1,0"
Segments="{Binding ElementName=TabItemContent, Converter={StaticResource content2PathConverter}}">
</PathFigure>
</PathGeometry>
</Path.Data>
<Path.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Path.LayoutTransform>
</Path>
<Rectangle x:Name="TabItemTopBorder" Height="2" Visibility="Visible"
VerticalAlignment="Bottom" Fill="{StaticResource BorderBrush}"
Margin="{Binding ElementName=TabItemContent, Converter={StaticResource content2MarginConverter}}" />
<ContentPresenter x:Name="TabItemContent" ContentSource="Header"
Margin="10,2,10,2" VerticalAlignment="Center"
TextElement.Foreground="#FF000000"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True" SourceName="grd">
<Setter Property="Stroke" Value="{StaticResource HoverBrush}" TargetName="TabPath"/>
</Trigger>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter Property="Fill" TargetName="TabPath">
<Setter.Value>
<SolidColorBrush Color="#FFe4f6fa"/>
</Setter.Value>
</Setter>
<Setter Property="BitmapEffect">
<Setter.Value>
<DropShadowBitmapEffect Direction="302" Opacity="0.4"
ShadowDepth="2" Softness="0.5"/>
</Setter.Value>
</Setter>
<Setter Property="Panel.ZIndex" Value="2"/>
<Setter Property="Visibility" Value="Hidden" TargetName="TabItemTopBorder"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="20">
<TabControl Grid.Row="0" Grid.Column="1" Margin="5" TabStripPlacement="Top"
Style="{StaticResource TabControlStyle}" FontSize="16">
<TabItem Header="MainTab">
<Border Margin="10">
<TextBlock Text="The quick brown fox jumps over the lazy dog."/>
</Border>
</TabItem>
<TabItem Header="VeryVeryLongTab" />
<TabItem Header="Tab" />
</TabControl>
</Grid>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace TabControlTemplate
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
}
}
public class ContentToMarginConverter: IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new Thickness(0, 0, -((ContentPresenter)value).ActualHeight, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
public class ContentToPathConverter: IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var ps = new PathSegmentCollection(4);
ContentPresenter cp = (ContentPresenter)value;
double h = cp.ActualHeight > 10 ? 1.4 * cp.ActualHeight : 10;
double w = cp.ActualWidth > 10 ? 1.25 * cp.ActualWidth : 10;
ps.Add(new LineSegment(new Point(1, 0.7 * h), true));
ps.Add(new BezierSegment(new Point(1, 0.9 * h), new Point(0.1 * h, h), new Point(0.3 * h, h), true));
ps.Add(new LineSegment(new Point(w, h), true));
ps.Add(new BezierSegment(new Point(w + 0.6 * h, h), new Point(w + h, 0), new Point(w + h * 1.3, 0), true));
return ps;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
These two converters I wrote to adjust tab size to its content. Actually, I making Path object depending on content size. If you don’t need tabs with various widths, you can use some modified copy of this:
<Style x:Key="tabPath" TargetType="{x:Type Path}">
<Setter Property="Stroke" Value="Black"/>
<Setter Property="Data">
<Setter.Value>
<PathGeometry Figures="M 0,0 L 0,14 C 0,18 2,20 6,20 L 60,20 C 70,20 80,0 84,0"/>
</Setter.Value>
</Setter>
</Style>
screen:
sample project(vs2010)
Note: This is only an appendix to rooks' great answer.
While rooks' solution was working perfectly at runtime for me I had some trouble when opening the MainWindow in the VS2010 WPF designer surface: The designer threw exceptions and didn't display the window. Also the whole ControlTemplate for TabItem in TabControl.xaml had a blue squiggle line and a tooltip was telling me that a NullReferenceException occurred. I had the same behaviour when moving the relevant code into my application. The problems were on two different machines so I believe it wasn't related to any problems of my installation.
In case that someone experiences the same issues I've found a fix so that the example works now at runtime and in the designer as well:
First: Replace in the TabControl-XAML code ...
<Path x:Name="TabPath" StrokeThickness="2"
Margin="{Binding ElementName=TabItemContent,
Converter={StaticResource content2MarginConverter}}"
Stroke="{StaticResource BorderBrush}"
Fill="{StaticResource TabItemPathBrush}">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="False" StartPoint="1,0"
Segments="{Binding ElementName=TabItemContent,
Converter={StaticResource content2PathConverter}}">
</PathFigure>
</PathGeometry>
</Path.Data>
<Path.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Path.LayoutTransform>
</Path>
... by ...
<Path x:Name="TabPath" StrokeThickness="2"
Margin="{Binding ElementName=TabItemContent,
Converter={StaticResource content2MarginConverter}}"
Stroke="{StaticResource BorderBrush}"
Fill="{StaticResource TabItemPathBrush}"
Data="{Binding ElementName=TabItemContent,
Converter={StaticResource content2PathConverter}}">
<Path.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Path.LayoutTransform>
</Path>
Second: Replace at the end of the Convert method of the ContentToPathConverter class ...
return ps;
... by ...
PathFigure figure = new PathFigure(new Point(1, 0), ps, false);
PathGeometry geometry = new PathGeometry();
geometry.Figures.Add(figure);
return geometry;
I have no explanation why this runs stable in the designer but not rooks' original code.
I just finished a Google Chrome-like Tab Control for WPF. You can find the project at https://github.com/realistschuckle/wpfchrometabs and blog posts describing it at
Google Chrome-Like WPF Tab Control
ChromeTabControl and Visual Children in WPF
WPF Chrome Tabs Functioning
Hope that helps you get a better understanding of building a custom tab control from scratch.
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type TabControl}">
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style>
<Setter Property="Control.Height" Value="20"></Setter>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Margin="0 0 -10 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10">
</ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="10"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Path Data="M10 0 L 0 20 L 10 20 " Fill="{TemplateBinding Background}" Stroke="Black"></Path>
<Rectangle Fill="{TemplateBinding Background}" Grid.Column="1"></Rectangle>
<Rectangle VerticalAlignment="Top" Height="1" Fill="Black" Grid.Column="1"></Rectangle>
<Rectangle VerticalAlignment="Bottom" Height="1" Fill="Black" Grid.Column="1"></Rectangle>
<ContentPresenter Grid.Column="1" ContentSource="Header" />
<Path Data="M0 20 L 10 20 L0 0" Fill="{TemplateBinding Background}" Grid.Column="2" Stroke="Black"></Path>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.Setters>
<Setter Property="Background" Value="Beige"></Setter>
<Setter Property="Panel.ZIndex" Value="1"></Setter>
</Trigger.Setters>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Trigger.Setters>
<Setter Property="Background" Value="LightGray"></Setter>
</Trigger.Setters>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<TabControl>
<TabItem Header="One" ></TabItem>
<TabItem Header="Two" ></TabItem>
<TabItem Header="Three" ></TabItem>
</TabControl>
</Grid>
I know this is old but I'd like to propose:
XAML:
<Window.Resources>
<ControlTemplate x:Key="trapezoidTab" TargetType="TabItem">
<Grid>
<Polygon Name="Polygon_Part" Points="{Binding TabPolygonPoints}" />
<ContentPresenter Name="TabContent_Part" Margin="{TemplateBinding Margin}" Panel.ZIndex="100" ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="False">
<Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
<Setter TargetName="Polygon_Part" Property="Fill" Value="DimGray" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Polygon_Part" Property="Fill" Value="Goldenrod" />
<Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Panel.ZIndex" Value="90"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Panel.ZIndex" Value="100"/>
<Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
<Setter TargetName="Polygon_Part" Property="Fill" Value="LightSlateGray "/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<!-- Test the tabs-->
<TabControl Name="FruitTab">
<TabItem Header="Apple" Template="{StaticResource trapezoidTab}" />
<TabItem Margin="-8,0,0,0" Header="Grapefruit" Template="{StaticResource trapezoidTab}" />
<TabItem Margin="-16,0,0,0" Header="Pear" Template="{StaticResource trapezoidTab}"/>
</TabControl>
ViewModel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Media;
namespace TrapezoidTab
{
public class TabHeaderViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _tabHeaderText;
private List<Point> _polygonPoints;
private PointCollection _pointCollection;
public TabHeaderViewModel(string tabHeaderText)
{
_tabHeaderText = tabHeaderText;
TabPolygonPoints = GenPolygon();
}
public PointCollection TabPolygonPoints
{
get { return _pointCollection; }
set
{
_pointCollection = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("TabPolygonPoints"));
}
}
public string TabHeaderText
{
get { return _tabHeaderText; }
set
{
_tabHeaderText = value;
TabPolygonPoints = GenPolygon();
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("TabHeaderText"));
}
}
private PointCollection GenPolygon()
{
var w = new FormattedText(_tabHeaderText, CultureInfo.GetCultureInfo("en-us"), FlowDirection.LeftToRight, new Typeface("Tahoma"), 12, Brushes.Black);
var width = w.Width + 30;
_polygonPoints = new List<Point>(4);
_pointCollection = new PointCollection(4);
_polygonPoints.Add(new Point(2, 21));
_polygonPoints.Add(new Point(10, 2));
_polygonPoints.Add(new Point(width, 2));
_polygonPoints.Add(new Point(width + 8, 21));
foreach (var point in _polygonPoints)
_pointCollection.Add(point);
return _pointCollection;
}
}
}
Main:
namespace TrapezoidTab
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
foreach (var obj in FruitTab.Items)
{
var tab = obj as TabItem;
if (tab == null) continue;
tab.DataContext = new TabHeaderViewModel(tab.Header.ToString());
}
}
}
}
yep, you can do that--basically all you have to do is make a custom control-template. Check out http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out
(Dead link. WaybackMachine redirects here)
for a tutorial. Just googling "wpf" "tabcontrol" "shape" turns up pages of results.
I have not tried this myself, but you should be able to replace the tag(s) in the template with tags to get the shape that you want.
To slope both left and right tab edges, here is a modification of Slauma's enhancement to rook's accepted answer. This is a replacement of Convert method of the ContentToPathConverter class:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var ps = new PathSegmentCollection(4);
ContentPresenter cp = (ContentPresenter)value;
double h = cp.ActualHeight > 10 ? 1.4 * cp.ActualHeight : 10;
double w = cp.ActualWidth > 10 ? 1.25 * cp.ActualWidth : 10;
// Smaller unit, so don't need fractional multipliers.
double u = 0.1 * h;
// HACK: Start before "normal" start of tab.
double x0 = -4 * u;
// end of transition
double x9 = w + 8 * u;
// transition width
double tw = 8 * u;
// top "radius" (actually, gradualness of curve. Larger value is more rounded.)
double rt = 5 * u;
// bottom "radius" (actually, gradualness of curve. Larger value is more rounded.)
double rb = 3 * u;
// "(x0, 0)" is start point - defined in PathFigure.
// Cubic: From previous endpoint, 2 control points + new endpoint.
ps.Add(new BezierSegment(new Point(x0 + rb, 0), new Point(x0 + tw - rt, h), new Point(x0 + tw, h), true));
ps.Add(new LineSegment(new Point(x9 - tw, h), true));
ps.Add(new BezierSegment(new Point(x9 - tw + rt, h), new Point(x9 - rb, 0), new Point(x9, 0), true));
// "(x0, 0)" is start point.
PathFigure figure = new PathFigure(new Point(x0, 0), ps, false);
PathGeometry geometry = new PathGeometry();
geometry.Figures.Add(figure);
return geometry;
}
Also in TabControl's ControlTemplate, add left (and optionally right) margins to the container of the tab items (the only change is added Margin="20,0,20,0"):
<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
...
<Setter Property="Template">
...
<StackPanel Grid.Row="0" Panel.ZIndex="1" Orientation="Horizontal" IsItemsHost="true" Margin="20,0,20,0"/>
Issue: There is a slight visual "glitch" at bottom of left edge of tab, when it is not the selected tab. I think it is related to "going backwards" to start before beginning of tab area. Or else related to drawing of line line at bottom of tab (which doesn't know we start before the left edge of the tabs "normal" rectangle).

Resources