Search code examples
wpfxamlcontroltemplateexpander

How to make Expander's icon move to the edge when expanded/collapsed


The default Expander's icon position is fixed when expanded/collapsed. Now I need to make the edge of Content's position fixed and to move the icon.

I tried:

<Expander HorizontalAlignment="Left" ExpandDirection="Right" FlowDirection="RightToLeft">
   <Grid>
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="*"></ColumnDefinition>
      </Grid.ColumnDefinitions>

      <Grid Grid.Column="0">
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
         </Grid.ColumnDefinitions>
         <TextBlock Grid.Column="0" Text="123"/>
         <TextBlock Grid.Column="1" Text="abc"/>
      </Grid>
   </Grid>
</Expander>

It is close to what I want except that I got the icon direction backwards. Is it the proper way or how to fix it? Thanks.


Solution

  • Changing the flow direction will also affect other parts of the control like the icon that is flipped. Instead create a copy of the default control template using Blend or Visual Studio and adapt it.

    There is a lot of XAML code that is extracted, but you only need to adapt small portions of it. The other styles are only needed, because they are referenced statically by name. Put all this code in a resource dictionary of your choice that is in scope.

    <SolidColorBrush x:Key="Expander.Static.Circle.Stroke" Color="#FF333333"/>
    <SolidColorBrush x:Key="Expander.Static.Circle.Fill" Color="#FFFFFFFF"/>
    <SolidColorBrush x:Key="Expander.Static.Arrow.Stroke" Color="#FF333333"/>
    <SolidColorBrush x:Key="Expander.MouseOver.Circle.Stroke" Color="#FF5593FF"/>
    <SolidColorBrush x:Key="Expander.MouseOver.Circle.Fill" Color="#FFF3F9FF"/>
    <SolidColorBrush x:Key="Expander.MouseOver.Arrow.Stroke" Color="#FF000000"/>
    <SolidColorBrush x:Key="Expander.Pressed.Circle.Stroke" Color="#FF3C77DD"/>
    <SolidColorBrush x:Key="Expander.Pressed.Circle.Fill" Color="#FFD9ECFF"/>
    <SolidColorBrush x:Key="Expander.Pressed.Arrow.Stroke" Color="#FF000000"/>
    <SolidColorBrush x:Key="Expander.Disabled.Circle.Stroke" Color="#FFBCBCBC"/>
    <SolidColorBrush x:Key="Expander.Disabled.Circle.Fill" Color="#FFE6E6E6"/>
    <SolidColorBrush x:Key="Expander.Disabled.Arrow.Stroke" Color="#FF707070"/>
    <Style x:Key="ExpanderHeaderFocusVisual">
       <Setter Property="Control.Template">
          <Setter.Value>
             <ControlTemplate>
                <Border>
                   <Rectangle Margin="0" StrokeDashArray="1 2" SnapsToDevicePixels="true" StrokeThickness="1" Stroke="Black"/>
                </Border>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    <Style x:Key="ExpanderDownHeaderStyle" TargetType="{x:Type ToggleButton}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Padding="{TemplateBinding Padding}">
                   <Grid Background="Transparent" SnapsToDevicePixels="False">
                      <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="19"/>
                         <ColumnDefinition Width="*"/>
                      </Grid.ColumnDefinitions>
                      <Ellipse x:Name="circle" Fill="{StaticResource Expander.Static.Circle.Fill}" HorizontalAlignment="Center" Height="19" Stroke="{StaticResource Expander.Static.Circle.Stroke}" VerticalAlignment="Center" Width="19"/>
                      <Path x:Name="arrow" Data="M 1,1.5 L 4.5,5 L 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="false" StrokeThickness="2" Stroke="{StaticResource Expander.Static.Arrow.Stroke}" VerticalAlignment="Center"/>
                      <ContentPresenter Grid.Column="1" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
                   </Grid>
                </Border>
                <ControlTemplate.Triggers>
                   <Trigger Property="IsChecked" Value="true">
                      <Setter Property="Data" TargetName="arrow" Value="M 1,4.5  L 4.5,1  L 8,4.5"/>
                   </Trigger>
                   <Trigger Property="IsMouseOver" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.MouseOver.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsPressed" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Stroke}"/>
                      <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Pressed.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsEnabled" Value="false">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Disabled.Arrow.Stroke}"/>
                   </Trigger>
                </ControlTemplate.Triggers>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    <Style x:Key="ExpanderUpHeaderStyle" TargetType="{x:Type ToggleButton}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Padding="{TemplateBinding Padding}">
                   <Grid Background="Transparent" SnapsToDevicePixels="False">
                      <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="19"/>
                         <ColumnDefinition Width="*"/>
                      </Grid.ColumnDefinitions>
                      <Grid>
                         <Grid.LayoutTransform>
                            <TransformGroup>
                               <TransformGroup.Children>
                                  <TransformCollection>
                                     <RotateTransform Angle="180"/>
                                  </TransformCollection>
                               </TransformGroup.Children>
                            </TransformGroup>
                         </Grid.LayoutTransform>
                         <Ellipse x:Name="circle" Fill="{StaticResource Expander.Static.Circle.Fill}" HorizontalAlignment="Center" Height="19" Stroke="{StaticResource Expander.Static.Circle.Stroke}" VerticalAlignment="Center" Width="19"/>
                         <Path x:Name="arrow" Data="M 1,1.5 L 4.5,5 L 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="false" StrokeThickness="2" Stroke="{StaticResource Expander.Static.Arrow.Stroke}" VerticalAlignment="Center"/>
                      </Grid>
                      <ContentPresenter Grid.Column="1" HorizontalAlignment="Left" Margin="4,0,0,0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
                   </Grid>
                </Border>
                <ControlTemplate.Triggers>
                   <Trigger Property="IsChecked" Value="true">
                      <Setter Property="Data" TargetName="arrow" Value="M 1,4.5  L 4.5,1  L 8,4.5"/>
                   </Trigger>
                   <Trigger Property="IsMouseOver" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.MouseOver.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsPressed" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Stroke}"/>
                      <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Pressed.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsEnabled" Value="false">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Disabled.Arrow.Stroke}"/>
                   </Trigger>
                </ControlTemplate.Triggers>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    <Style x:Key="ExpanderLeftHeaderStyle" TargetType="{x:Type ToggleButton}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Padding="{TemplateBinding Padding}">
                   <Grid Background="Transparent" SnapsToDevicePixels="False">
                      <Grid.RowDefinitions>
                         <RowDefinition Height="19"/>
                         <RowDefinition Height="*"/>
                      </Grid.RowDefinitions>
                      <Grid>
                         <Grid.LayoutTransform>
                            <TransformGroup>
                               <TransformGroup.Children>
                                  <TransformCollection>
                                     <RotateTransform Angle="90"/>
                                  </TransformCollection>
                               </TransformGroup.Children>
                            </TransformGroup>
                         </Grid.LayoutTransform>
                         <Ellipse x:Name="circle" Fill="{StaticResource Expander.Static.Circle.Fill}" HorizontalAlignment="Center" Height="19" Stroke="{StaticResource Expander.Static.Circle.Stroke}" VerticalAlignment="Center" Width="19"/>
                         <Path x:Name="arrow" Data="M 1,1.5 L 4.5,5 L 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="false" StrokeThickness="2" Stroke="{StaticResource Expander.Static.Arrow.Stroke}" VerticalAlignment="Center"/>
                      </Grid>
                      <ContentPresenter HorizontalAlignment="Center" Margin="0,4,0,0" RecognizesAccessKey="True" Grid.Row="1" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
                   </Grid>
                </Border>
                <ControlTemplate.Triggers>
                   <Trigger Property="IsChecked" Value="true">
                      <Setter Property="Data" TargetName="arrow" Value="M 1,4.5  L 4.5,1  L 8,4.5"/>
                   </Trigger>
                   <Trigger Property="IsMouseOver" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.MouseOver.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsPressed" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Stroke}"/>
                      <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Pressed.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsEnabled" Value="false">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Disabled.Arrow.Stroke}"/>
                   </Trigger>
                </ControlTemplate.Triggers>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    <Style x:Key="ExpanderRightHeaderStyle" TargetType="{x:Type ToggleButton}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Padding="{TemplateBinding Padding}">
                   <Grid Background="Transparent" SnapsToDevicePixels="False">
                      <Grid.RowDefinitions>
                         <RowDefinition Height="19"/>
                         <RowDefinition Height="*"/>
                      </Grid.RowDefinitions>
                      <Grid>
                         <Grid.LayoutTransform>
                            <TransformGroup>
                               <TransformGroup.Children>
                                  <TransformCollection>
                                     <RotateTransform Angle="-90"/>
                                  </TransformCollection>
                               </TransformGroup.Children>
                            </TransformGroup>
                         </Grid.LayoutTransform>
                         <Ellipse x:Name="circle" Fill="{StaticResource Expander.Static.Circle.Fill}" HorizontalAlignment="Center" Height="19" Stroke="{StaticResource Expander.Static.Circle.Stroke}" VerticalAlignment="Center" Width="19"/>
                         <Path x:Name="arrow" Data="M 1,1.5 L 4.5,5 L 8,1.5" HorizontalAlignment="Center" SnapsToDevicePixels="false" StrokeThickness="2" Stroke="{StaticResource Expander.Static.Arrow.Stroke}" VerticalAlignment="Center"/>
                      </Grid>
                      <ContentPresenter HorizontalAlignment="Center" Margin="0,4,0,0" RecognizesAccessKey="True" Grid.Row="1" SnapsToDevicePixels="True" VerticalAlignment="Top"/>
                   </Grid>
                </Border>
                <ControlTemplate.Triggers>
                   <Trigger Property="IsChecked" Value="true">
                      <Setter Property="Data" TargetName="arrow" Value="M 1,4.5  L 4.5,1  L 8,4.5"/>
                   </Trigger>
                   <Trigger Property="IsMouseOver" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.MouseOver.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.MouseOver.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsPressed" Value="true">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Stroke}"/>
                      <Setter Property="StrokeThickness" TargetName="circle" Value="1.5"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Pressed.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Pressed.Arrow.Stroke}"/>
                   </Trigger>
                   <Trigger Property="IsEnabled" Value="false">
                      <Setter Property="Stroke" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Stroke}"/>
                      <Setter Property="Fill" TargetName="circle" Value="{StaticResource Expander.Disabled.Circle.Fill}"/>
                      <Setter Property="Stroke" TargetName="arrow" Value="{StaticResource Expander.Disabled.Arrow.Stroke}"/>
                   </Trigger>
                </ControlTemplate.Triggers>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    
    <!-- This is the style that actually needs to be adapted. -->
    <Style x:Key="ExpanderStyle" TargetType="{x:Type Expander}">
       <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
       <Setter Property="Background" Value="Transparent"/>
       <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
       <Setter Property="VerticalContentAlignment" Value="Stretch"/>
       <Setter Property="BorderBrush" Value="Transparent"/>
       <Setter Property="BorderThickness" Value="1"/>
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type Expander}">
                <Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="3" SnapsToDevicePixels="true">
                   <DockPanel>
                      <ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" DockPanel.Dock="Bottom" FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}" FontWeight="{TemplateBinding FontWeight}" FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}" FontFamily="{TemplateBinding FontFamily}" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" MinHeight="0" MinWidth="0" Margin="1" Padding="{TemplateBinding Padding}" Style="{StaticResource ExpanderDownHeaderStyle}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
                      <ContentPresenter x:Name="ExpandSite" DockPanel.Dock="Top" Focusable="false" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Visibility="Collapsed"/>
                   </DockPanel>
                </Border>
                <ControlTemplate.Triggers>
                   <Trigger Property="IsExpanded" Value="true">
                      <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
                   </Trigger>
                   <Trigger Property="ExpandDirection" Value="Right">
                      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/>
                      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/>
                      <Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderRightHeaderStyle}"/>
                   </Trigger>
                   <Trigger Property="ExpandDirection" Value="Up">
                      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Bottom"/>
                      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Top"/>
                      <Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderUpHeaderStyle}"/>
                   </Trigger>
                   <Trigger Property="ExpandDirection" Value="Left">
                      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/>
                      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/>
                      <Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderLeftHeaderStyle}"/>
                   </Trigger>
                   <Trigger Property="IsEnabled" Value="false">
                      <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                   </Trigger>
                </ControlTemplate.Triggers>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
    </Style>
    

    The only changes are in the last style ExpanderStyle. There is a DockPanel that contains the icon and header, which are displayed in a ToggleButton and the content that is displayed in a ContentPresenter. I just flipped the DockPanel.Dock values in those controls and the triggers.

    You can apply the style like this without setting the flow direction.

    <Expander Style="{StaticResource ExpanderStyle}" HorizontalAlignment="Left" ExpandDirection="Right">