WPF - skinning the ComboBox moving the DropDown Menu around

Print Content | More

A couple of days ago a friend of mine asked for some help in skinning a WPF ComboBox control, he had a special need in which the dropdown menu items list of the control had to be aligned to the right side of the ComboBox and had to expand in the left direction (instead of the usual visual appearance, which has the opposite behavior: it is anchored to the left side of the control and expands to the right).

I asked him to build a very simple test project containing the control and its current skin and pass it to me, this is a picture of what he obtained at that time.

SkinningComboboxDropDown1

I have to admit I’m not a very good designer nor a graphic expert so I got the basic control template extracting it using Blend and I looked at it, here is a snippet of the original XAML from the template:

<ControlTemplate TargetType="{x:Type ComboBox}">
	<Grid x:Name="MainGrid" SnapsToDevicePixels="true">
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="*"/>
			<ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
		</Grid.ColumnDefinitions>
		<Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom">
			<Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}">
				<Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
					<ScrollViewer CanContentScroll="true">
						<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
					</ScrollViewer>
				</Border>
			</Microsoft_Windows_Themes:SystemDropShadowChrome>
		</Popup>
		<ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton2}"/>
		<ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
	</Grid>
	<ControlTemplate.Triggers>
		<Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true">
			<Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
			<Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
		</Trigger>
		<Trigger Property="HasItems" Value="false">
			<Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
		</Trigger>
		<Trigger Property="IsEnabled" Value="false">
			<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
			<Setter Property="Background" Value="#FFF4F4F4"/>
		</Trigger>
		<Trigger Property="IsGrouping" Value="true">
			<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
		</Trigger>
	</ControlTemplate.Triggers>
</ControlTemplate>

It comes out that the main part of the WPF ComboBox template is formed by 3 elements:

  • a content presenter for the control itself.
  • a chrome for the dropdown button.
  • and a popup representing the dropdown menu.

To achieve what he needed, that is align the popup to the right side of the content presenter and let it expand to the left, I thought to act on the HorizontalOffset property of the popup control. Knowing the width of both the dropdown control and the main control presenter with some basic math we can obtain the new horizontal offset at which place the dropdown. Thanks God, HorizontalOffset is a dependency property, so it does support binding (and multi binding too, which is what I actually needed). So I wrote a quick MultiValueConverter:

public class PopupHOffsetValueConverter : IMultiValueConverter
{
	#region IMultiValueConverter Members

	public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
	{
		try
		{
			double popupWidth = (double) values[0];
			double controlWidth = (double) values[1];
			return -(popupWidth - controlWidth);
		}
		catch
		{
			return 0;
		}
	}

	public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}

	#endregion
}

And modified the style to use it like this:

...
<Grid x:Name="MainGrid" SnapsToDevicePixels="true">
	<Grid.Resources>
		<WpfApplication1:PopupHOffsetValueConverter x:Key="vc" />
	</Grid.Resources>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="*"/>
		<ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
	</Grid.ColumnDefinitions>
	<Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom">
		<Popup.HorizontalOffset>
			<MultiBinding Converter="{StaticResource vc}">
				<Binding ElementName="Shdw" Path="ActualWidth" />
				<Binding ElementName="MainGrid" Path="ActualWidth" />
			</MultiBinding>
		</Popup.HorizontalOffset>
		<Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}">
			<Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
				<ScrollViewer CanContentScroll="true">
					<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
				</ScrollViewer>
			</Border>
		</Microsoft_Windows_Themes:SystemDropShadowChrome>
	</Popup>
	<ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/>
	<ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
...

Interesting bits are:

  • Lines 3-5 - MultiBinding ValueConverter declaration.
  • Lines 11-16 - the binding in action.

And this is the actual result on his partially skinned control:

SkinningComboboxDropDown2

Pretty easy once I figured out how to do it, WPF is indeed extremely powerful and flexible when it comes to skin controls.



WPF, Skinning, ComboBox

0 comments

Related Post

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)


  1. #1 da http://www.alvinashcraft.com/2011/02/28/dew-drop-february-28-2011/

    Dew Drop &ndash; February 28, 2011 | Alvin Ashcraft&#039;s Morning Dew