Thursday, January 12, 2012

WPF Flat Button FOR REAL!


Three different solutions are available to create a "flat button" from a standard button. A flat button is one which only pops up when you point your mouse to it or gives it focus by tabbing with the keyboard. Watch the video, try the one that best suits you, and leave your comments. Note that in all three solutions, events of your created button will work as per normal.

In the video, buttons in the first column contain a Polygon. Those in the second column contain an Image, and those in the third contain simple text.

Solution 1: Property-based WPF Flat Buttons

To get these flat buttons, all you have to do is set both Background and BorderBrush properties of a standard button to Transparent. These buttons also show natively the fade-in and fade-out effects when they gain focus or are pointed to.

<Button Background="Transparent" BorderBrush="Transparent" />

However, these flat buttons fail when Windows is using the Classic theme or a High Contrast theme (as you can see from the video). If your application is targeted to work on all themes, try Solution 2 or 3.

Solution 2: Style-based WPF Flat Buttons (non-animated)

Add the following to your application's resources (usually Window.Resources or Page.Resources):

<Style x:Key="FlatButton" TargetType="Button">
 <Setter Property="IsTabStop" Value="False" />
 <Setter Property="Template">
  <Setter.Value>
   <ControlTemplate TargetType="Button">
    <Grid>

     <Button> <!-- Inherit properties here with TemplateBinding -->
      <Button.Style>
       <Style TargetType="Button">
        <Style.Triggers>
         <MultiTrigger>

          <MultiTrigger.Conditions>
           <Condition Property="IsFocused" Value="False" />
           <Condition Property="IsMouseOver" Value="False" />
          </MultiTrigger.Conditions>

          <Setter Property="Opacity" Value="0" />

         </MultiTrigger>
        </Style.Triggers>
       </Style>
      </Button.Style>
     </Button>

     <Label HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Padding="3" IsHitTestVisible="False">
      <ContentPresenter />
     </Label>

    </Grid>
   </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

With this style in place, create your button as follows:

XAML:
<Button Style="{StaticResource FlatButton}" />

Code-behind:
Button myButton = new Button();
myButton.Style = (Style)this.FindResource("FlatButton");

The inner button in the style definition will not automatically inherit some properties such as Background that you set to the above button. If you need to apply any such property that doesn't inherit automatically, you will have to inherit it by editing the button inside the style definition as follows, with Background as an example:

<Button Background="{TemplateBinding Background}" />
 <!-- Inherit properties here with TemplateBinding -->

However, you must not inherit the Opacity property! If you do that, the button will completely fail at being flat. You may add an Opacity property to the inner button to help yourself visualize the buttons during design-time. But you must remove the Opacity property of inner button before runtime, or your created buttons will be stuck at the added opacity value permanently.

If you want to control the runtime opacity, simply set the Opacity property of your created button (there is no need to inherit it to the inner button). Alternatively, for a global effect, you may also change the value in the MultiTrigger's Setter (or) use solution 3 (edit the DoubleAnimation's by setting the duration to "0:0:0" and setting the opacities to your desired values).

Solution 3: Style-based WPF Animated Flat Buttons

Add the following to your application's resources (usually Window.Resources or Page.Resources):

<Style x:Key="AnimatedFlatButton" TargetType="Button">
 <Setter Property="IsTabStop" Value="False" />
 <Setter Property="Template">
  <Setter.Value>
   <ControlTemplate TargetType="Button">
    <Grid>

     <Button Opacity="0.1"> <!-- Inherit properties here with TemplateBinding -->
      <Button.Style>
       <Style TargetType="Button">
        <Style.Triggers>
         <MultiTrigger>

          <MultiTrigger.Conditions>
           <Condition Property="IsFocused" Value="False" />
           <Condition Property="IsMouseOver" Value="False" />
          </MultiTrigger.Conditions>

          <MultiTrigger.EnterActions>
           <BeginStoryboard>
            <Storyboard>
             <DoubleAnimation To="0.0" Duration="0:0:0.5" Storyboard.TargetProperty="Opacity" />
            </Storyboard>
           </BeginStoryboard>
          </MultiTrigger.EnterActions>

          <MultiTrigger.ExitActions>
           <BeginStoryboard>
            <Storyboard>
             <DoubleAnimation To="1.0" Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" />
            </Storyboard>
           </BeginStoryboard>
          </MultiTrigger.ExitActions>

         </MultiTrigger>
        </Style.Triggers>
       </Style>
      </Button.Style>
     </Button>

     <Label HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Padding="3" IsHitTestVisible="False">
      <ContentPresenter />
     </Label>

    </Grid>
   </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

With this style in place, create your button as follows:

XAML:
<Button Style="{StaticResource AnimatedFlatButton}" />

Code-behind:
Button myButton = new Button();
myButton.Style = (Style)this.FindResource("AnimatedFlatButton");

The inner button in the style definition will not automatically inherit some properties such as Background that you set to the above button. If you need to apply any such property that doesn't inherit automatically, you will have to inherit it by editing the button inside the style definition as follows, with Background as an example:

<Button Opacity="0.1" Background="{TemplateBinding Background}" />
 <!-- Inherit properties here with TemplateBinding -->

You may edit the Opacity property to help yourself visualize when designing your form. But remember to set it back to zero or 0.1 because an opacity too high will give the unwelcome visual effect of all your flat buttons fading into flatness when you launch the application window.

If you want to control the runtime opacity, simply set the Opacity property of your created button (there is no need to inherit it to the inner button). To control the fade speed of the buttons, edit the DoubleAnimation's by setting the duration to your desired values. You may also change the opacity values in the DoubleAnimation's if desired.

What if I don't want the button to remain visible after I click?

Simple! Just replace the <MultiTrigger> opening tag with the following tag and delete the <MultiTrigger.Conditions /> tag including its contents. Remember to change all remaining instances of the term MultiTrigger with Trigger. (Note: this is applicable for solutions 2 and 3 only.)

<Trigger Property="IsMouseOver" Value="False">

Admittedly, a true flat button must be "not flat" only when the mouse is pointing at it. I overlooked this the first time and enabled my WPF flat buttons to be "not flat" when they are focused (after click or by pressing Tab key) even if the mouse is not pointing at it.

With this simple change, that anomaly can be removed and you can have a true flat button. When user navigates controls on the form using Tab key, a dotted rectangle appears around the button label to indicate focus. Hence the button itself can remain flat, only showing up when the user points mouse at it.

Backstory:
After searching everywhere on the Internet for a perfect solution, I could only find half-baked solutions of either inheriting the toolbar button style (which looks very ugly with a simple blue box) or hacking up the button and defining your own style (which makes you lose the standard button look and gradients).

I ended up spending two days trying different ways and coming up with a good solution that actually works. The solutions 2 and 3 were created and perfected by me before I stumbled upon solution 1 here. I'm glad I didn't see solution 1 before. Initially I was surprised that the problem could be very easily solved with solution 1, but upon further testing, I found it unreliable if an application is to be deployed in an environment which may use the Classic theme or its derivatives.

Remember to leave your comments if you found it useful or encountered problems.

No comments:

Post a Comment

Comments are moderated, and are usually posted within 24 hours if approved. You must have a minimum of OpenID to post comments.

LinkWithin

Related Posts with Thumbnails