Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF
Article

WPF Color Picker VS2010 Style

Rate me:
Please Sign up or sign in to vote.
4.83/5 (28 votes)
21 Jul 2011CPOL5 min read 73.6K   4.5K   35   17
Implementing a WPF Color Picker similar to the Visual Studio 2010 color picker.

Sample Image

Introduction

As a relatively newcomer to WPF, I decided to create a (yet another) Color Picker control, styled after the Visual Studio 2010 Color Picker. It seemed like a good exercise and also practically useful for many applications. In this article, I describe briefly some of the obstacles I stumbled on and a few lessons I learned during the process. The full source code is provided in the link above. The Color Picker control is composed of two parts: the right side is a standard RGB control (with Alpha value) and the left side is an HSV (Hue, Saturation, Value) control. Modification of the RGB control affects the HSV control and vice versa.

Using the Code

You can use the ColorPicker control directly, or the ColorComboBox that opens the ColorPicker in a popup window. In both cases, just embed the control in your window and use the SelectedColor property to get/set the color.

Also, some of the inner components, such as ColorSlider, are implemented as custom controls and can be used directly.

TemplateBinding Catch

The ColorSlider custom control is derived from the Slider control. Its background is a linear gradient brush between two Dependency Properties: LeftColor and RightColor. The natural way to set the background brush is as follows:

XML
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="{TemplateBinding LeftColor}/>
    <GradientStop Color="{TemplateBinding RightColor}/>
</LinearGradientBrush>

However, this doesn't work. After scratching my head for a while, I recalled that TemplateBinding is a lightweight version of binding with limitation. Replacing the above code with:

XML
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="{Binding RelativeSource={RelativeSource 
                          TemplatedParent}, Path=LeftColor}" Offset="0"/>
    <GradientStop Color="{Binding RelativeSource={RelativeSource 
                          TemplatedParent}, Path=RightColor}" Offset="1"/>
</LinearGradientBrush>

did the trick. The moral? When TemplateBinding doesn't work - replace it with regular binding.

Property Binding Puzzle

The SpectrumSlider custom control displays the rainbow colors and allows to select a color by moving the thumb. As it is derived from the Slider control, the position of the thumb is linked to the Value property. So I added a new dependency property SelectedColor and bound it in the template to the Value property using an appropriate converter. Here is the relevant code part:

XML
<Setter Property="SelectedColor">
    <Setter.Value>
      <Binding 
        Path="Value" 
        RelativeSource="{RelativeSource Self}"
        Converter="{StaticResource ValueToSelectedColorConverter}" />
    </Setter.Value>
</Setter>

This took care of the binding between the two properties. All that was left was to set the SelectedColor to some initial value and everything should work fine.

Except that it didn't.

The initial color set to the slider did not affect the Value, and moving the slider thumb did change the Value but did not change the SelectedColor. It seemed that the binding did not work. Well, perhaps binding within the control template is problematic, so I tried to bind in the class constructor instead:

C#
public SpectrumSlider()
{
    ...
    Binding binding = new Binding();
    binding.Path = new PropertyPath("Value");
    binding.RelativeSource = new System.Windows.Data.RelativeSource(RelativeSourceMode.Self);
    binding.Mode = BindingMode.TwoWay;
    binding.Converter = new ValueToSelectedColorConverter();
  
    BindingOperations.SetBinding(this, SelectedColorProperty, binding);
    ...
}

That did not work either.

After some more confusion, the puzzle was solved: setting the SelectedColor to a specific color in the test program removed the binding. I was not aware that binding is managed like a value, and when another value is set to the bound property, the binding is removed. The solution in this case is to link the properties manually in the code-behind (and take care not to create an endless loop).

TextBox Binding On Enter

The ColorPicker contains TextBox controls in order to allow quick setting of the RGB and Alpha values. The TextBox value is bound to the slider Value property, so setting one affects the other. Surprisingly, when the user fills a value in the TextBox and presses Enter, the value of the slider is not updated. Only after the focus leaves the TextBox, by pressing [tab] for example, is the value updated. This is due to the fact that the binding takes effect when the TextBox loses focus, but not when an Enter is pressed. This is a well known problem of the standard TextBox binding behavior, and there are a lot of discussions in the related forums.

A standard way to resolve it is to set the UpdateSourceTrigger property to PropertyChanged:

XML
<TextBox 
    Text="{Binding Path=Value, ElementName=PART_Slider, Mode=TwoWay, 
           UpdateSourceTrigger=PropertyChanged}"/>

This causes an undesirable effect: each keystroke is reflected immediately in the thumb position. When the user types "123", the thumb jumps to position 1, then to position 12, and finally to position 123.

An interesting suggestion was to use an Attached Property. The general scheme is as follows: the Attached Property hooks to the TextBox KeyDown event. When an Enter is pressed, it retrieves the relevant binding and updates the source:

C#
public static class BindingHelper
{
    public static readonly DependencyProperty BindOnEnterProperty =
      DependencyProperty.RegisterAttached("BindOnEnter", 
      typeof(DependencyProperty), typeof(BindingHelper),
      new UIPropertyMetadata(null, new PropertyChangedCallback(OnBindOnEnterChanged)));

    ...

    private static void OnBindOnEnterChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
      UIElement element = d as UIElement;
      element.KeyDown += new KeyEventHandler(OnKeyDown);
    }

    private static void OnKeyDown(object sender, KeyEventArgs e)
    {
      if (e.Key == Key.Enter)
      {
        UIElement element = e.Source as UIElement;
        DependencyProperty property = 
          (DependencyProperty)element.GetValue(BindOnEnterProperty);

        BindingExpression binding = 
          BindingOperations.GetBindingExpression(element, property);
        if (binding != null)
          binding.UpdateSource();
      }
    }
}

The XAML code looks like:

XML
<TextBox 
    local:BindingHelper.BindOnEnter="TextBox.Text"
    Text="{Binding Path=Value, ElementName=PART_Slider, Mode=TwoWay}"/>

This solution has the benefit that the Attached Property can be used for controls other than just TextBox. However, I chose a much simpler option: create a class derived from TextBox and override OnKeyDown:

C#
public class BindOnEnterTextBox : TextBox
{
    protected override void OnKeyDown(KeyEventArgs e)
    {
      base.OnKeyDown(e);

      if (e.Key == Key.Enter)
      {
        BindingExpression binding = 
           BindingOperations.GetBindingExpression(this, TextProperty);
        if (binding != null)
          binding.UpdateSource();
      }
    }
}

Popup Catch 22

After implementing the ColorPicker, I wanted to wrap it in a ColorComboBox that opens the ColorPicker in a popup window and behaves in a way similar to a regular ComboBox. When the down arrow is pressed, the popup window opens. When the down arrow is pressed again or mouse is clicked outside the popup, it closes. An obvious solution is to create a class derived from ComboBox and provide the appropriate template. This seems like too heavy a weapon for the task and creating it from scratch seems easy enough: just create a ToggleButton, embed the ColorPicker in a popup window, and bind the ToggleButton IsChecked property to the popup IsOpen property. Here we encounter the popup catch:

If the StaysOpen property of the popup is set to true, the popup is opened when the ToggleButton is clicked once and is closed when the button is clicked again. But it is not closed when any other point is clicked, not even when the top window is dragged or another window is activated. If the StaysOpen property is set to false, the behavior changes: when the popup is open and a point outside it is clicked, it is closed due to focus lost, but if the point is within the ToggleButton button, it is reopened as a result of the button click. One way out of this catch is to disable the ToggleButton when the popup is opened and re-enable it after the popup is closed. That way, when clicking on it, the popup is closed, but the ToggleButton does not fire the click event since it is disabled.

License

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


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy 5 Pin
Shemeer NS19-Aug-14 5:53
professionalShemeer NS19-Aug-14 5:53 
QuestionHow to bind SelectedColor? Pin
Member 82406519-Jan-14 2:25
Member 82406519-Jan-14 2:25 
AnswerRe: How to bind SelectedColor? Pin
Member 82406519-Jan-14 6:41
Member 82406519-Jan-14 6:41 
QuestionProblem with binding to an ObservableCollection Pin
co/oce10-Oct-12 3:28
co/oce10-Oct-12 3:28 
QuestionReally good Pin
CalifBreton9-Feb-12 17:56
CalifBreton9-Feb-12 17:56 
BugProblem with ColorComboBox Pin
Filip D'haene26-Dec-11 4:52
Filip D'haene26-Dec-11 4:52 
QuestionProblem with ColorComboBox Pin
Filip D'haene17-Dec-11 11:22
Filip D'haene17-Dec-11 11:22 
AnswerRe: Problem with ColorComboBox Pin
Ury Jamshy18-Dec-11 3:24
Ury Jamshy18-Dec-11 3:24 
GeneralRe: Problem with ColorComboBox Pin
Filip D'haene19-Dec-11 4:35
Filip D'haene19-Dec-11 4:35 
QuestionSelectedColorEvent for ColorComboBox Pin
Thomasian198518-Nov-11 17:39
Thomasian198518-Nov-11 17:39 
AnswerRe: SelectedColorEvent for ColorComboBox Pin
Ury Jamshy20-Nov-11 0:03
Ury Jamshy20-Nov-11 0:03 
GeneralRe: SelectedColorEvent for ColorComboBox Pin
Thomasian198520-Nov-11 14:35
Thomasian198520-Nov-11 14:35 
GeneralRe: SelectedColorEvent for ColorComboBox Pin
Ury Jamshy18-Dec-11 2:20
Ury Jamshy18-Dec-11 2:20 
Questionvery nice Pin
PradeepDhawan27-Jul-11 5:55
PradeepDhawan27-Jul-11 5:55 
Questionnice Pin
BillW3322-Jul-11 3:29
professionalBillW3322-Jul-11 3:29 
QuestionMy vote of 5 Pin
Filip D'haene22-Jul-11 1:22
Filip D'haene22-Jul-11 1:22 
QuestionVery nice job Pin
Sacha Barber21-Jul-11 21:48
Sacha Barber21-Jul-11 21:48 

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

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