Over a million developers have joined DZone.

WPF - skinning the ComboBox moving the DropDown Menu around

· Mobile Zone

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:

01	...
02 <Grid x:Name="MainGrid" SnapsToDevicePixels="true">
03 <Grid.Resources>
04 <WpfApplication1:PopupHOffsetValueConverter x:Key="vc" />
05 </Grid.Resources>
06 <Grid.ColumnDefinitions>
07 <ColumnDefinition Width="*"/>
08 <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
09 </Grid.ColumnDefinitions>
10 <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">
11 <Popup.HorizontalOffset>
12 <MultiBinding Converter="{StaticResource vc}">
13 <Binding ElementName="Shdw" Path="ActualWidth" />
14 <Binding ElementName="MainGrid" Path="ActualWidth" />
15 </MultiBinding>
16 </Popup.HorizontalOffset>
17 <Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}">
18 <Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
19 <ScrollViewer CanContentScroll="true">
20 <ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
21 </ScrollViewer>
22 </Border>
23 </Microsoft_Windows_Themes:SystemDropShadowChrome>
24 </Popup>
25 <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/>
26 <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}"/>
27 </Grid>
28 ...

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.

Topics:

Published at DZone with permission of Alessandro Giorgetti, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}