Home > WPF > WPF Chrome

WPF Chrome

February 26, 2009

In my last post I talked about creating a WPF search text box, but I skipped out on creating one of the more subtle visuals for it: the chrome. The main reason I did that was to reduce the already complex project, but now with its own blog entry, I can give it some time in the spotlight! 🙂

You might be wondering why we will be creating a chrome element in the first place, and that really is a good question. If you take a look at the standard WPF controls, like the text box, you’ll notice that when you mouse over the text box the highlight border nicely fades in and when you mouse out the highlight border nicely fades out. Well, this is extremely difficult to do in XAML, and frankly, I’m not even convinced that it can be done. And me being my picky self, I’d like to have a similar effect on my controls.

The next logical question that follows is, why not just use the built-in chrome classes. That would be a wonderful idea except for the little thorn that not all of the colors are customizable in the default implementations. So that leaves us with being forced to use the colors of the OS, which might not actually be a bad thing, or creating our own chrome class. Obviously we’ll be choosing to make our own chrome class.

Planning Out Our Chrome

I thought about creating a fully customizable chrome piece for the control but that is just a lot more work than is necessary here, so I’m going to stick with the KISS (keep-it-simple-stupid) approach. With that in mind, we’ll only be creating a chrome piece that has the following properties:

  • BorderBrush – The brush for when the control is in its normal state.
  • OverlayBorderBrush – The brush to use when the mouse is over it or the control has keyboard focus.
  • Background – The background of the control.
  • RenderMouseOver – A flag that lets us know whether we should render the overlay for the mouse over.
  • RenderFocused – A flag that lets us know whether we should render the overlay for having keyboard focus.

You should note that I’m leaving out the DisabledBrush as an exercise for the reader.

Before we can create our properties, we need to actually create our chrome class.

public class Chrome : Decorator {
    static Chrome() {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(Chrome),
            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender));
    }
}

As you can see, we will be deriving from the Decorator type. Basically a decorator is a class that is used to apply some sort of effect around its child content. For more information on decorators, check out the MSDN documentation.

Alright, let’s get down and dirty with our properties:

public static DependencyProperty RenderMouseOverProperty =
    DependencyProperty.Register(
        "RenderMouseOver",
        typeof(bool),
        typeof(Chrome),
        new FrameworkPropertyMetadata(
            false, new PropertyChangedCallback(OnRenderMouseOverPropertyChanged)));

public static DependencyProperty RenderFocusedProperty =
    DependencyProperty.Register(
        "RenderFocused",
        typeof(bool),
        typeof(Chrome),
        new FrameworkPropertyMetadata(
            false, new PropertyChangedCallback(OnRenderFocusedPropertyChanged)));

public static readonly DependencyProperty BorderBrushProperty =
    DependencyProperty.Register(
        "BorderBrush",
        typeof(Brush),
        typeof(Chrome),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

public static readonly DependencyProperty OverlayBorderBrushProperty =
    DependencyProperty.Register(
        "OverlayBorderBrush",
        typeof(Brush),
        typeof(Chrome),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

public static readonly DependencyProperty BackgroundProperty =
    DependencyProperty.Register(
        "Background",
        typeof(Brush),
        typeof(Chrome),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

public bool RenderMouseOver {
    get { return (bool)GetValue(RenderMouseOverProperty); }
    set { SetValue(RenderMouseOverProperty, value); }
}

public bool RenderFocused {
    get { return (bool)GetValue(RenderFocusedProperty); }
    set { SetValue(RenderFocusedProperty, value); }
}

public Brush BorderBrush {
    get { return (Brush)GetValue(BorderBrushProperty); }
    set { SetValue(BorderBrushProperty, value); }
}

public Brush OverlayBorderBrush {
    get { return (Brush)GetValue(OverlayBorderBrushProperty); }
    set { SetValue(OverlayBorderBrushProperty, value); }
}

 

 

 

public Brush Background {
    get { return (Brush)GetValue(BackgroundProperty); }
    set { SetValue(BackgroundProperty, value); }
}

You should note that both the RenderFocused and RenderMouseOver properties have a callback method that we’ll be making use of to control the look of our chrome. Other than that, there really isn’t anything special here.

When we go about the rendering of our chrome, we are going to need to use a Pen to do that, so let’s go ahead and set those up now.

private Pen _borderPen;
private Pen BorderPen {
    get {
        if (_borderPen == null) {
            _borderPen = new Pen(BorderBrush, 1.0);
            _borderPen.Freeze();
        }
        return _borderPen;
    }
}

private Pen _overlayBorderPen;
private Pen OverlayBorderPen {
    get {
        if (_overlayBorderPen == null && OverlayBorderBrush != null) {
            _overlayBorderPen = new Pen(OverlayBorderBrush.Clone(), 1.0);
            _overlayBorderPen.Brush.Opacity = 0.0;
        }
        return _overlayBorderPen;
    }
}

We are calling .Freeze() on the BorderPen as we want to ensure that it doesn’t change, and we are setting the opacity of the OverlayBorderPen to 0.0 because we want to be able to fade it in from 0.0 to 1.0 as an overlay on top of our normal border. Setting it up this way makes our rendering code simpler.

Now it’s time to write the rendering code:

protected override void OnRender(DrawingContext dc) {
    Rect bounds = new Rect(0.5, 0.5, this.ActualWidth, this.ActualHeight);

    dc.DrawRoundedRectangle(Background, null, bounds, 1.0, 1.0);
    dc.DrawRoundedRectangle(null, BorderPen, bounds, 1.0, 1.0);
    dc.DrawRoundedRectangle(null, OverlayBorderPen, bounds, 1.0, 1.0);
}

Yep, it’s really that simple. Now the one thing that you might find odd is that the bounding rect we are using starts at 0.5 instead of 0.0. The reason we need to offset the rect has to do with how WPF renders. In a nutshell, WPF is resolution independent which means that WPF will render items “in-between” pixels at time. When it does this, WPF will anti-alias the edges to fit across the pixels that it is splitting. Well, sometimes this is fine, but in the case of our border that is supposed to be 1 pixel wide, this will make it look fuzzy. To fix that we need to “nudge” our rect over by half of the width of the pen, or by 0.5. This is indeed a hack that is optimized for this specific case. If you want to learn more about this or check out the “proper” way of doing this, check out Christian Moser’s blog.

Now that we have that all setup, let’s go ahead and look back at our WPF search text box control template and make the adjustments there so we can use this new chrome! You did add this to that project, didn’t you? 😉

You should replace the current control template with this:

<l:Chrome x:Name="Border"
          SnapsToDevicePixels="True"
          RenderMouseOver="{TemplateBinding IsMouseOver}"
          RenderFocused="{TemplateBinding IsKeyboardFocused}"
          Background="{TemplateBinding Background}"
          BorderBrush="{TemplateBinding BorderBrush}"
          OverlayBorderBrush="{StaticResource SearchTextBox_Border_MouseOver}">
  <Grid x:Name="LayoutGrid">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent},
                                        Path=ActualHeight}" />
    </Grid.ColumnDefinitions>
    <ScrollViewer Margin="2" x:Name="PART_ContentHost" Grid.Column="0" />
    <Label x:Name="LabelText"
           Margin="2"
           Grid.Column="0"
           Foreground="{Binding RelativeSource={RelativeSource TemplatedParent},
                                Path=LabelTextColor}"
           Content="{Binding RelativeSource={RelativeSource TemplatedParent},
                             Path=LabelText}"
           Padding="2,0,0,0"
           FontStyle="Italic" />
    <Border x:Name="PART_SearchIconBorder"
            Grid.Column="1"
            Margin="1"
            BorderThickness="1"
            VerticalAlignment="Stretch"
            HorizontalAlignment="Stretch"
            BorderBrush="{StaticResource SearchTextBox_SearchIconBorder}"
            Background="{StaticResource SearchTextBox_SearchIconBackground}">
      <Image x:Name="SearchIcon"
             Stretch="None"
             Width="15"
             Height="15"
             HorizontalAlignment="Center"
             VerticalAlignment="Center"
             Source="pack://application:,,,/UIControls;component/Images/search.png" />
     </Border>
  </Grid>
</l:Chrome>

The real important part is to ensure that the RenderMouseOver and RenderFocused are hooked up and bound to the templated parent’s IsMouseOver and IsKeyboardFocused properties, respectively. This is what allows us to know when the mouse is over the control and when the control has keyboard focus. If you forget to do this, it will be painfully obvious when we start writing the logic for changing the border and you see nothing happening.

There is one last property we need to create before we write our callbacks:

private bool SupportsAnimation {
    get {
        return IsEnabled && SystemParameters.ClientAreaAnimation && RenderCapability.Tier > 0;
    }
}

This property helps us determine if the user’s machine is actually capable of running animations well. A user wouldn’t be very happy to see things slow down in the UI just so we can render some nice fade-ins and fade-outs.

Alright, now for the moment of truth.

Creating the RenderMouseOver Callback

We’ll start with the mouse over as that is the easier of the two to test.

The code is below, but the basic idea of the code is this:

  • If the mouse is enters the bounds of the control, start the animation to fade-in the overlay highlighting
  • If the mouse is leaves the bounds of the control, start the animation to fade-out the overlay highlighting

public static void OnRenderMouseOverPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
    Chrome chrome = o as Chrome;

    if (chrome == null || !chrome.IsEnabled) return;
    if (chrome.RenderFocused) return;
    if (chrome.OverlayBorderPen == null) return; 

    bool newValue = (bool)e.NewValue;
    bool oldValue = (bool)e.OldValue; 

    if (newValue == oldValue) return;

    if (newValue != oldValue)
        chrome.InvalidateVisual(); 

 

 

 

    if (newValue) {
        if (chrome.SupportsAnimation) {
            Duration duration = new Duration(TimeSpan.FromSeconds(0.3));
            DoubleAnimation animation = new DoubleAnimation(1.0, duration);
            chrome.OverlayBorderPen.Brush.BeginAnimation(Brush.OpacityProperty, animation);
        }
    }
    else {
        if (chrome.SupportsAnimation) {
            Duration duration = new Duration(TimeSpan.FromSeconds(0.2));
            DoubleAnimation animation = new DoubleAnimation();
            animation.Duration = duration;
            chrome.OverlayBorderPen.Brush.BeginAnimation(Brush.OpacityProperty, animation);
        }
    }
}

Creating the RenderFocused Callback

Now for the RenderFocused callback.

The idea of this code is essentially the same as the mouse over logic.

  • If the control gets keyboard focus, start the animation to fade-in the overlay highlighting
  • If the control loses keyboard focus, start the animation to fade-out the overlay highlighting

public static void OnRenderFocusedPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
    Chrome chrome = o as Chrome;
    if (chrome == null) return; 

    bool newValue = (bool)e.NewValue;
    bool oldValue = (bool)e.OldValue; 

    if (newValue == oldValue) return; 

    if (newValue != oldValue)
        chrome.InvalidateVisual(); 

    if (newValue) {
        if (chrome.SupportsAnimation) {
            Duration duration = new Duration(TimeSpan.FromSeconds(0.3));
            DoubleAnimation animation = new DoubleAnimation(1.0, duration);
            chrome.OverlayBorderPen.Brush.BeginAnimation(Brush.OpacityProperty, animation);
        }
    }
    else {
        if (chrome.RenderMouseOver) return;

        if (chrome.SupportsAnimation) {
            Duration duration = new Duration(TimeSpan.FromSeconds(0.2));
            DoubleAnimation animation = new DoubleAnimation();
            animation.Duration = duration;
            chrome.OverlayBorderPen.Brush.BeginAnimation(Brush.OpacityProperty, animation);
        }
    }
}

 

 

 

There we have it! You should now have animating chrome. Remember, you’ll still need to support the disabled state of the code, but that should be fairly trivial now.

Advertisements
Categories: WPF
  1. March 1, 2009 at 12:59 am

    Just passing by.Btw, you website have great content!

    _________________________________
    Making Money $150 An Hour

  2. July 11, 2009 at 2:54 am

    Excellent! This is just what I needed. I tried to emulate the mouseover and focused highlight with triggers in XAML but it never behaved as nicely as the standard chome. Now it does. Thanks!

  3. February 14, 2010 at 4:04 pm

    Mate i Thik Your Site Is messed Up cuz the page is loading nd keeps on loading then show some error am using chrome is the problem with ma browser or your site ??

  4. February 14, 2010 at 9:56 pm

    No problems here… using Chrome as well.

  5. DeCaf
    April 29, 2011 at 10:19 am

    I love your articles covering this search text box. But I have one question: Why do we need to provide our own decorator that looks just like the standard chrome? Couldn’t we simply use the Microsoft.Windows.Themes.ListBoxChrome decorator instead?

    Although I really do appreciate the tutorial in how to create decorators or “Chrome” like this, there are certainly times when this is useful, but I’m wondering if in this case it wouldn’t work just as well to use the already existing decorator used by the standard text box from which we are deriving SearchTextBox?

  1. No trackbacks yet.
Comments are closed.
%d bloggers like this: