Click here to Skip to main content
Click here to Skip to main content

WPF: Windows 7 like Button Color Hot Tracking

, , 28 Oct 2009
Rate this:
Please Sign up or sign in to vote.
A nice idea that you can apply to your code

Introduction

A few days ago, I read a great blog post from my good friend Rudi Grobler, where he explained how to implement Color Hot Tracking in Windows 7 on a Button. I commented to him that it generates a major shift in implementation and I would like to share this with you all. I take this opportunity to thank Rudi, who in addition to a great developer, is also an amazing guy.

Background

This article is based on the implementation described in this blog post: Make your WPF buttons color hot-track!

And the idea of the converter came from Grant Hinkson in his post: “McGuffin”-Enabling Image Converter.

Using the Code

The technique is actually very simple. We create an AttachedProperty that will be used to apply the background brush with the most common color in the element it contains.

The AttachedProperty is defined like this:

public static readonly DependencyProperty ApplyHotTrackingProperty =
    DependencyProperty.RegisterAttached("ApplyHotTracking",
        typeof(Boolean),
        typeof(Win7ColorHotTrackExtension),
        new UIPropertyMetadata(
            new PropertyChangedCallback(
                (sender, e) =>
                {
                    if ((bool)e.NewValue)
                    {
                        (sender as FrameworkElement).PreviewMouseMove += 
                          new System.Windows.Input.MouseEventHandler(
                          Win7ColorHotTrackExtenssion_PreviewMouseMove);
                        (sender as FrameworkElement).Loaded += 
                          new RoutedEventHandler(Win7ColorHotTrackExtenssion_Loaded);
                    }
                    else
                        (sender as FrameworkElement).PreviewMouseMove -= 
                          Win7ColorHotTrackExtenssion_PreviewMouseMove;
                }))); 

public static Boolean GetApplyHotTracking(DependencyObject sender)
{
    return (Boolean)sender.GetValue(Win7ColorHotTrackExtension.ApplyHotTrackingProperty);
}

public static void SetApplyHotTracking(DependencyObject sender, Boolean value)
{
    sender.SetValue(Win7ColorHotTrackExtension.ApplyHotTrackingProperty, value);
}

As you can see, the PropertyChangedCallback is defined as an Anonymous Method, and inside this method, we have two events being wired: the first one is the PreviewMouseMove to animate as the cursor moves over the Control, and the other is the load event so we can perform the conversions after the visual is rendered. These methods are defined as follows:

static void Win7ColorHotTrackExtenssion_Loaded(object sender,
            RoutedEventArgs e)
{
    // As stated before, it will not work if the element
    // is not a content control so an exception is thrown
    if (!(sender is ContentControl))
        throw new NotSupportedException("This attached property " + 
                  "is just supported by an ContentControl");

    var control = sender as ContentControl;

    // verify if any data is binded to the Tag property,
    // because if it is, we don't want to lose it
    if (control.GetValue(FrameworkElement.TagProperty) == null)
    {
        // Instantiate and Invalidate the VisualBrush
        // needed for the analysis of the content
        VisualBrush b = new VisualBrush();

        b.SetValue(VisualBrush.VisualProperty, control.Content);
        control.InvalidateVisual();

        // if the control has no visual (with a height lesser or equal to zero) 
        // we don't need to perform any action,
        // because the result will be a transparent brush anyway
        if ((control as FrameworkElement).ActualHeight <= 0)
            return;

        // Render the visual of the element
        // to an bitmap with the RenderTargetBitmap class
        RenderTargetBitmap RenderBmp = new RenderTargetBitmap(
            (int)(control.Content as FrameworkElement).Width,
            (int)(control.Content as FrameworkElement).Height,
            96,
            96,
            PixelFormats.Pbgra32);

        RenderBmp.Render(b.Visual);

        // Set the value to the Tag property
        control.SetValue(FrameworkElement.TagProperty, RenderBmp);

        // Instantiate and initialize a Binding element to handle the new tag property
        Binding bindBG = new Binding("Tag");
        bindBG.Source = control;
        // Define the converter that will be used to handle
        // the transformation from an image to average color
        bindBG.Converter = new IconToAvgColorBrushConverter();

        // Set the binding to the Background property
        control.SetBinding(ContentControl.BackgroundProperty, bindBG);

        // if the Background is a LinearGradientBrush
        // we also want our control to use the border
        // colored as Win7 does
        if (control.Background is LinearGradientBrush)
        {
            Binding bindBorder = new Binding("GradientStops[1].Color");
            bindBorder.Source = control.Background;
            control.SetBinding(ContentControl.BorderBrushProperty, bindBorder);
        }
    }
}

static void Win7ColorHotTrackExtenssion_PreviewMouseMove(object sender, 
    System.Windows.Input.MouseEventArgs e)
{
    // As already said the sender must be a Content Control
    if (!(sender is ContentControl))
        return;

    ContentControl element = sender as ContentControl;

    // if the Brush is not a linearGradientBrush
    // we don't need to do anything so just returns
    if (!(element.GetValue(ContentControl.BackgroundProperty) 
          is LinearGradientBrush))
        return;

    // Get the brush
    LinearGradientBrush b = element.GetValue(ContentControl.BackgroundProperty) 
                            as LinearGradientBrush;

    // Get the ActualWidth of the sender
    Double refZeroX = (double)element.GetValue(ContentControl.ActualWidthProperty);

    // Get the new point for the StartPoint and EndPoint of the Gradient
    System.Windows.Point p = 
      new System.Windows.Point(e.GetPosition(element).X / refZeroX, 1);

    // Set the new values
    b.StartPoint = new System.Windows.Point(1 - p.X, 0);
    b.EndPoint = p;
}

So now, all we have to do is make use of the AttachedProperty in our XAML as follows:

<Window 
    x:Class="ColorHotTrackButton.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ColorHotTrackButton"
    Title="Color Hot-Track Buttons" 
    SizeToContent="WidthAndHeight">
    <Window.Resources>
        <BooleanToVisibilityConverter 
            x:Key="BooleanToVisibilityConverter"
            />
        <LinearGradientBrush 
            x:Key="GlossGradient" 
            EndPoint="0.0149999996647239,0.0160000007599592" 
            StartPoint="0.486000001430511,0.723999977111816"
            >
            <GradientStop 
                Color="#0CFFFFFF"
                />
            <GradientStop 
                Color="#4CFFFFFF" 
                Offset="1"
                />
        </LinearGradientBrush>
        <local:IconToAvgColorBrushConverter 
            x:Key="iconToAvgColorBrushConverter"
            />
        <Style 
            x:Key="ColorHotTrackButton" 
            TargetType="{x:Type Button}" 
            BasedOn="{x:Null}"
            >
            <Setter 
                Property="Background" 
                Value="#00FFFFFF"
                />
            <Setter 
                Property="BorderBrush" 
                Value="#00FFFFFF"
                />
            <Setter 
                Property="Template"
                >
                <Setter.Value>
                    <ControlTemplate 
                        TargetType="{x:Type Button}"
                        >
                        <ControlTemplate.Resources>
                            <Storyboard 
                                x:Key="GotFocus"
                                >
                                <DoubleAnimationUsingKeyFrames 
                                    BeginTime="00:00:00" 
                                    Storyboard.TargetName="rectangle" 
                                    Storyboard.TargetProperty="(UIElement.Opacity)"
                                    >
                                    <SplineDoubleKeyFrame 
                                        KeyTime="00:00:00" 
                                        Value="0"
                                        />
                                    <SplineDoubleKeyFrame 
                                        KeyTime="00:00:00.3000000" 
                                        Value="1"
                                        />
                                </DoubleAnimationUsingKeyFrames>
                            </Storyboard>
                        </ControlTemplate.Resources>
                        <Grid 
                            x:Name="Grid" 
                            ClipToBounds="True"
                            >
                            <Border 
                                x:Name="Border" 
                                Background="{TemplateBinding Background}" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                Padding="{TemplateBinding Padding}" 
                                CornerRadius="3,3,3,3" 
                                Opacity="0.0" 
                                ClipToBounds="True"
                                />
                            <Rectangle 
                                x:Name="rectangle" 
                                RadiusX="3" 
                                RadiusY="3" 
                                Fill="#33FFFFFF" 
                                Opacity="0"
                                />
                            <ContentPresenter 
                                HorizontalAlignment="{TemplateBinding 
                                                     HorizontalContentAlignment}" 
                                Margin="{TemplateBinding Padding}"
                                VerticalAlignment="{TemplateBinding 
                                                   VerticalContentAlignment}"
                                RecognizesAccessKey="True"
                                />
                            <Path
                                x:Name="gloss"
                                Fill="{StaticResource GlossGradient}" 
                                Stretch="Fill"
                                Margin="2,2,2,29.204"
                                ClipToBounds="True" 
                                Data="M2.9999995,0 L151,0 C152.65686,1.0728836E-06 154, 
                                      1.3431468 154,3.0000018 L154,21.0382 151.53519, 
                                      21.193919 C90.378815,
                                      25.365844 36.495198,48.231778 1.1935941, 
                                      97.114381 L0,98.795694 0,3.0000018 C4.7683716E-07, 
                                      1.3431468 1.3431462,1.0728836E-06 2.9999995,0 z"
                                />
                        </Grid>
                    
                        <ControlTemplate.Triggers>
                            <Trigger 
                                Property="IsFocused"
                                Value="True"
                                >
                                <Trigger.ExitActions>
                                    <RemoveStoryboard 
                                        BeginStoryboardName="GotFocus_BeginStoryboard"
                                        />
                                </Trigger.ExitActions>
                                <Trigger.EnterActions>
                                    <BeginStoryboard 
                                        x:Name="GotFocus_BeginStoryboard" 
                                        Storyboard="{StaticResource GotFocus}"
                                        />
                                </Trigger.EnterActions>
                            </Trigger>
                            <Trigger 
                                Property="IsKeyboardFocused" 
                                Value="true"
                                >
                                <Setter
                                    Property="BorderBrush"
                                    Value="{DynamicResource DefaultedBorderBrush}"
                                    TargetName="Border"
                                    />
                            </Trigger>
                            <Trigger 
                                Property="IsMouseOver"
                                Value="true"
                                >
                                <Setter 
                                    Property="Opacity" 
                                    Value="1.0" 
                                    TargetName="Border"
                                    />
                            </Trigger>
                            <Trigger 
                                Property="IsEnabled" 
                                Value="true"
                                />
                            <Trigger 
                                Property="IsEnabled"
                                Value="false"
                                >
                                <Setter 
                                    Property="Background" 
                                    Value="{DynamicResource DisabledBackgroundBrush}" 
                                    TargetName="Border"
                                    />
                                <Setter 
                                    Property="BorderBrush" 
                                    Value="{DynamicResource DisabledBorderBrush}"
                                    TargetName="Border"
                                    />
                                <Setter 
                                    Property="Foreground"
                                    Value="{DynamicResource DisabledForegroundBrush}"
                                    />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>    
    </Window.Resources>
    <Window.Background>
        <LinearGradientBrush
            EndPoint="0.5,1" 
            StartPoint="0.5,0"
            >
            <GradientStop 
                Color="Black" 
                Offset="0.974"
                />
            <GradientStop 
                Color="#FF656565"
                />
        </LinearGradientBrush>
    </Window.Background>
    
    <DockPanel>       
        <Border 
            DockPanel.Dock="Bottom"
            >
            <StackPanel 
                Orientation="Horizontal" 
                MaxHeight="140"
                >
                <Grid>
                    <Button 
                        Margin="2.5,0,2.5,0" 
                        Style="{DynamicResource ColorHotTrackButton}" 
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/
                                    Assets/Burning Box V2 .ico" 
                            Width="128" 
                            Height="128"
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
                <Grid>
                    <Button 
                        Margin="2.5,0,2.5,0"
                        Style="{DynamicResource ColorHotTrackButton}" 
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/
                                    Assets/carte graphique.ico" 
                            Width="128"
                            Height="128" 
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
                <Grid>
                    <Button 
                        Margin="2.5,0,2.5,0"
                        Style="{DynamicResource ColorHotTrackButton}" 
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/Assets/
                                    connection réseaux Bagg's.ico" 
                            Width="128"
                            Height="128" 
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
                <Grid>
                    <Button 
                        Margin="2.5,0,2.5,0"
                        Style="{DynamicResource ColorHotTrackButton}"
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/
                                    Assets/favoris'Box.ico" 
                            Width="128" 
                            Height="128" 
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
                <Grid>
                    <Button 
                        Margin="2.5,0,2.5,0"
                        Style="{DynamicResource ColorHotTrackButton}" 
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/
                                    Assets/lecteur box.ico"
                            Width="128" 
                            Height="128"
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
                <Grid>
                    <Button
                        Margin="2.5,0,2.5,0" 
                        Style="{DynamicResource ColorHotTrackButton}" 
                        local:Win7ColorHotTrackExtension.ApplyHotTracking="True"
                        >
                        <Image 
                            Source="/ColorHotTrackButton;component/
                                    Assets/private Box.ico" 
                            Width="128"
                            Height="128" 
                            Margin="12.5,5,12.5,5"
                            />
                    </Button >
                </Grid>
            </StackPanel>
        </Border>
    </DockPanel>        
</Window>

History

  • 22nd October, 2009: Initial post
  • 27th October, 2009: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Authors

rudigrobler

South Africa South Africa
No Biography provided

Raul Mainardi Neto
Architect
Brazil Brazil
Senior .NET Architect from Brazil.
Follow on   Twitter

Comments and Discussions

 
GeneralVery cool... Pinmemberrudigrobler3-Nov-09 20:59 
GeneralRe: Very cool... PinmemberRaul Mainardi Neto4-Nov-09 3:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 28 Oct 2009
Article Copyright 2009 by rudigrobler, Raul Mainardi Neto
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid