Overriding button background in WPF on Aero

Michael Goldshteyn picture Michael Goldshteyn · Dec 13, 2010 · Viewed 9.5k times · Source

So, the desire is simple, change a button's background to LightGreen, Green when the mouse cursor hovers over it and DarkGreen when it is pressed. The following XAML is my first attempt:

<Window x:Class="ButtonMouseOver.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Button Background="LightGreen">
      Hello
      <Button.Style>
        <Style TargetType="Button">
          <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
              <Setter Property = "Background" Value="Green"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="true">
              <Setter Property = "Foreground" Value="DarkGreen"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </Button.Style>
    </Button>
  </Grid>
</Window>

But, alas this doesn't work. We're only 1/3 of the way towards the goal. Yes, the button becomes light green, but as soon as you hover over it or press it you get standard Aero chrome for the respective button states. Not wanting to give up, I attempt the following debauchery:

...
    <Button xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
            Background="LightGreen">
      Hello
      <Button.Template>
        <ControlTemplate TargetType="{x:Type Button}">
          <Microsoft_Windows_Themes:ButtonChrome SnapsToDevicePixels="true" 
            x:Name="Chrome" Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding BorderBrush}" RenderDefaulted="{TemplateBinding IsDefaulted}" 
            RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}"
            >
            <ContentPresenter Margin="{TemplateBinding Padding}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          RecognizesAccessKey="True"
                                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
          </Microsoft_Windows_Themes:ButtonChrome>
        </ControlTemplate>
      </Button.Template>
      <Button.Style>
        <Style TargetType="Button">
          <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
              <Setter Property = "Background" Value="Green"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="true">
              <Setter Property = "Foreground" Value="DarkGreen"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </Button.Style>
    </Button>
...

Now we've got the whole friggin' control template thrown in there lifted from Aero.NormalColor.xaml. Alas, this changes nothing. I then go about removing two attributes (RenderPressed and RenderMouseOver) from the Microsoft_Windows_Themes:ButtonChrome element. Still, there is no change. I then remove the setting of the Button's Background attribute, leaving just the namespace declaration for the PresentationFramework.Aero assembly:

...
<Button xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero">
  Hello
...

Finally, we have progress. We've gone from 1/3 of the requirements to 2/3 of the way, with IsMouseOver and IsPressed working, but no light green background during the button's normal state (of not being moused over or pressed), since I've removed the Background property from the button, to get the other two states to apply and be visible. Now, after this maniacal XAML fails to get us the LightGreen background for the normal button state, I modify the style to throw the background color in there as well:

  <Button xmlns...
    <ControlTemplate...
    ...
    </ControlTemplate>
    <Button.Style> 
      <Style TargetType="Button">
        <!-- ##### Normal state button background added ##### -->
        <Setter Property="Background" Value="LightGreen" />
        <!-- ################################################ -->
        <Style.Triggers>
          <Trigger Property="IsMouseOver" Value="true">
            <Setter Property = "Background" Value="Green"/>
          </Trigger>
          <Trigger Property="IsPressed" Value="true">
            <Setter Property = "Background" Value="DarkGreen"/>
          </Trigger>
        </Style.Triggers>
      </Style>
    </Button.Style> 
  </Button>

Finally, it works as intended.

Am I crazy, or are these (and more) the hoops you have to go through with an Aero theme to just change a button's background color in some of its various states (normal, hovered over, pressed)? Maybe, life wouldn't be so bad if I didn't have to include the whole dang ButtonTemplate in there just to remove the two attributes (RenderMouseOver and RenderPressed) from it.

Thank you for any help - I am at wit's end.

Update: But wait there's more to this. Instead of removing the RenderMouseOver and RenderPressed attributes from the control template, simply changing them from:

<Microsoft_Windows_Themes:ButtonChrome SnapsToDevicePixels="true" ...
  RenderMouseOver="{TemplateBinding IsMouseOver}"  
  RenderPressed="{TemplateBinding IsPressed}" ...

to:

<Microsoft_Windows_Themes:ButtonChrome SnapsToDevicePixels="true" ...
  RenderMouseOver="{Binding IsMouseOver}"  
  RenderPressed="{Binding IsPressed}" ...

..., also fixes the problem (of ignoring the button's style triggers). I think the root of the issue is that the template bindings are applied at compile time (for performance reasons), while regular bindings are applied at run time. Because of this, the bindings from the style triggers from the individual buttons are not used if attributes use template bindings (i.e., the IsMouseOver and IsPressed from the original template are used instead).

Having said all of the above, here is the canonical example for changing a button's background in Aero while keeping its original control template:

<Window x:Class="ButtonMouseOver.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Button>
      Hello
      <Button.Template>
        <ControlTemplate TargetType="{x:Type Button}">
          <Microsoft_Windows_Themes:ButtonChrome SnapsToDevicePixels="true" 
            x:Name="Chrome" Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding BorderBrush}" RenderDefaulted="{TemplateBinding IsDefaulted}" 
            RenderMouseOver="{Binding IsMouseOver}" RenderPressed="{Binding IsPressed}"
            >
            <ContentPresenter Margin="{TemplateBinding Padding}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          RecognizesAccessKey="True"
                                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
          </Microsoft_Windows_Themes:ButtonChrome>
        </ControlTemplate>
      </Button.Template>
      <Button.Style>
        <Style TargetType="Button">
          <Setter Property="Background" Value="LightGreen"/>
          <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
              <Setter Property = "Background" Value="Green"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="true">
              <Setter Property = "Foreground" Value="DarkGreen"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </Button.Style>
    </Button>
  </Grid>
</Window>

This is of course assuming that only one button is so styled, otherwise the template and style can be moved out into a resources section somewhere. Also, there is no trigger for the default state of a button, but it is easily added to the above example (don't forget to change the RenderDefaulted attribute in the control template to use Binding instead of TemplateBinding).

If anyone has any suggestions on how to override the TemplateBinding with Binding for only the two attributes in the control template that need to change, without repeating the entire chrome definition from the aero xaml wholesale, I am all ears.

Answer

Thomas Levesque picture Thomas Levesque · Dec 13, 2010

The default template for Button uses a ButtonChrome, which performs its own drawing. If you want to get completely rid of the default appearance, you need to define your own template, without the ButtonChrome. I know it sounds tedious, but you only have to do it once: you can put your custom style in a resource dictionary, and just refer to it with StaticResource. Note that you can also apply the style to all buttons in your app by using {x:Type Button} as the style's key (x:Key attribute)