Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WinForms ColorEditor Displayed Modeless

0.00/5 (No votes)
20 Jun 2006 1  
A usercontrol that can show the ColorEditor permanently on a form.

Sample Image

Introduction

The WinForms ColorEditor class used by the .NET PropertyGrid was designed as a drop-down window. Although attributed for internal framework usage, controls using it have been published. Two excellent VB.NET examples with a lot of background explanation were submitted by palomraz:

Probably because of the popup nature, ColorEditor is deemed a 'cool' component. This article describes an 'uncool' user control, which can display the WinForms ColorEditor permanently on a main form. A ColorChanged event signals a change in user selection, and the selected color and the custom colors can be (pre-)set at runtime. This is achieved with the unsupported practice of modifying internal properties by Reflection (the 'black magic' side of Reflection).

Background

To show ColorEditor, we must implement two interfaces, where IServiceProvider.GetService() just returns our IWindowsFormsEditorService implementation.

// namespace System


public interface IServiceProvider
{
    object GetService(Type serviceType);
}

// namespace System.Windows.Forms.Design


public interface IWindowsFormsEditorService
{
    void CloseDropDown();
    void DropDownControl(Control control);
    DialogResult ShowDialog(Form dialog);
}

ColorEditor's layout, as far it concerns us, is shown here:

// namespace System.Drawing.Design


public class ColorEditor : UITypeEditor
{
    public ColorEditor();
    // Methods

    public override object EditValue(ITypeDescriptorContext context, 
                    IServiceProvider provider, object value);
    // Nested Types

    private class ColorUI : Control {}
    private class ColorPalette : Control {}
    private class CustomColorDialog : ColorDialog {}      
}

When invoking ColorEditor.EditValue(null, provider, initialColor), it does the following:

  • Neglects the ITypeDescriptorContext argument.
  • Queries the passed in IServiceProvider instance for an IWindowsFormEditorService implementation.
  • Creates an instance of a private ColorUI class, which implements the actual user interface and interacts with the user. ColorUI.Start() initializes with the passed in color value, and stores the IWindowsFormEditorService reference in a private field, edSvc.
  • Calls the IWindowsFormEditorService.DropDownControl method, passing it the ColorUI instance. This method embeds ColorUI inside a form, and shows the form at an appropriate screen location. The method must block, while dispatching all messages using the MsgWaitForMultipleObjects API function. Simply put, it waits until the user finishes editing.
  • When the user selects a new color, the ColorEditor calls IWindowsFormEditorService.CloseDropDown(), which closes the drop-down UI and causes IWindowsFormEditorService.DropDownControl() to return.
  • ColorUI.End() nulls the IWindowsFormEditorService reference.
  • EditValue() returns the selected color value.
  • The ColorUI instance remains valid for further EditValue() calls.

Scratched your head twice? Reading Implementing IWindowsFormsEditorService Interface (last section) did help my understanding.

ColorUI's layout, as far it concerns us:

private class ColorUI : Control
{
    public ColorUI(ColorEditor editor);
    // Methods

    public void Start(IWindowsFormsEditorService edSvc, object value);
    public void End();
    // Properties

    public object Value { get; }
    // Fields

    private IWindowsFormsEditorService edSvc;
    // Nested Types

    private class ColorEditorTabControl : TabControl {}
    private class ColorEditorListBox : ListBox {}
}

Once we receive the ColorUI instance in our IWindowsFormEditorService.DropDownControl method, we show it by adding it to our UserControl.Controls collection. Although the user interface is made up of private classes, we can access their respective base types:

Control colorUI;
TabControl tab = (TabControl)colorUI.Controls[0];
Control palette = tab.TabPages[0].Controls[0];
ListBox lbCommon = (ListBox)tab.TabPages[1].Controls[0];
ListBox lbSystem = (ListBox)tab.TabPages[2].Controls[0];

I used here the WinForms internal naming. The palette control is on the first tab page (US-English: "Custom"), and the common listbox shows the web colors. Note, that we can add here our own tabpages too.

The hack

As stated above, the drop-down operation requires that the IWindowsFormEditorService.DropDownControl method must not return until the user finishes editing. We can omit this feature, our DropDownControl method, and thereby ColorEditor.EditValue() will return immediately. In other words, we use EditValue() to launch the editor and set an (initial) color, but we can not use its return value (our initial color).

To achieve our goals, we must overcome four problems:

  • Prevent ColorUI from closing down, after the user selects a color:

    This one is easy, in our IWindowsFormEditorService.CloseDropDown method, we simply don't remove it from our UserControl.Controls collection.

  • Retrieve the selected color value:

    Instead, we take the invoking of CloseDropDown() as an indication, that a selection change occurred. In the case of a selected web or system color, we cast the Listbox.SelectedItem property to a color value:

    ListBox lb = (ListBox)tab.SelectedTab.Controls[0];
    Color value = (Color)lb.SelectedItem;

    For a selected palette color, we must rely on Reflection ('white magic'). ColorUI exposes a public property Value (object), but remember ColorUI is a private class, and so we only can access its Control base type:

    Type t = colorUI.GetType();
    PropertyInfo pInfo = t.GetProperty("Value");
    Color value = (Color)pInfo.GetValue(colorUI, null);
  • Close ColorUI on request (i.e., on disposal):

    Pressing the Return key on any tab page invokes CloseDropDown. We simulate it by sending a WM_KEYDOWN message to the control on the active tab page. In this case, we remove ColorUI from our UserControl.Controls collection. Remember, that any added custom tab page must be removed before shutting down, otherwise this could fail.

  • Prevent a NullReferenceException in System.Drawing.Design.dll:

    As mentioned, ColorUI keeps a reference to our IWindowsFormEditorService in a private field. As a well-behaving component, it nulls this reference, after IWindowsFormEditorService.DropDownControl() returns. We let DropDownControl() return immediately, thus we launch ColorUI with an invalid reference. Subsequent user selection, instead of calling CloseDropDown(), will result in a NullReferenceException.

    So 'black magic' comes into play, by restoring the reference in the private field edSvc:

    Type t = colorUI.GetType();
    FieldInfo fInfo = t.GetField("edSvc", 
              BindingFlags.NonPublic | BindingFlags.Instance);
    fInfo.SetValue(colorUI, service);

    Nice though that we need to do this only once after calling EditValue(), either when launching the editor, or when setting a new color with the editor already running. Once restored, ColorUI calls CloseDropDown() on subsequent user input, and will not invalidate the reference again.

ocColorEditor

To make it a fully functional user control, there was a lot more coding necessary that I won't cover here. To help in understanding the main operation, here is a skeleton of the UserControl as a mixture of interface declaration and pseudo code:

// namespace OC.Windows.Forms


public class ocColorEditor : UserControl
{
    public event EventHandler ColorChanged

    public ocColorEditor() : base()

    private ColorEditorService service;
    protected ColorEditor editor;
    protected Control colorUI;

    public Color Color { get; set; }
    public Color[] CustomColors { get; set; }
    
    public void ShowEditor()
    {
        service = new ColorEditorService();
        editor = new ColorEditor();
        editor.EditValue(service, _Color);
        // restore EditorService reference

    }
    
    public void CloseEditor()
    {
        service.CloseDropDownInternal();
        // send return key

    }
        
    private void service_ColorUIAvailable(object sender, 
                         EditorServiceEventArgs e)
    {
        if (e.ColorUI != null)
        {
            // ColorUI ready to show or new Color set

            if (colorUI == null)
            {
                // show ColorUI

                colorUI = e.ColorUI;
                Controls.Add(colorUI);
                // set CustomColors

            }
        }
        else
        {
            // ColorUI ready to close

            colorUI = null;
            service = null;
        }
    }
    
    private void service_ColorChanged(object sender, EventArgs e)
    {
        // get selected color value

        // test if custom colors were modified

        // deselect former selected color

        ColorChanged(this, EventArgs.Empty);
    }
    
    private class ColorEditorService : IServiceProvider, 
                                       IWindowsFormsEditorService
    {
        public event EventHandler<EditorServiceEventArgs> 
                     ColorUIAvailable
        public event EventHandler ColorChanged
 
        private bool closeEditor;
       
        public void CloseDropDownInternal()
        {
            closeEditor = true;
        }
        
        // IServiceProvider Members

        public object GetService(Type serviceType)
        {
            return this;
        }
        
        // IWindowsFormsEditorService Members

        public void DropDownControl(Control control)
        {
            ColorUIAvailable(this, 
                  new EditorServiceEventArgs(control));
        }

        public void CloseDropDown()
        {
            if (!closeEditor)
                // user selected color

                ColorChanged(this, EventArgs.Empty);
            else
                // close editor

                ColorUIAvailable(this, 
                     new EditorServiceEventArgs(null));
        }

        public DialogResult ShowDialog(Form dialog)
        {
            throw new Exception("Not implemented");
        }
    }

    private class EditorServiceEventArgs : EventArgs    
}

Instancing ColorUI is a CPU-intensive task, so this is done by calling ShowEditor(), rather than in the lazy constructor. Implementing IWindowsFormsEditorService and IServiceProvider in a private class won't clutter our public interface.

UserControl size

ColorUI uses a fixed size of 202 x 202 pixels for its palette window. The overall ColorUI height (220 default) varies, as the tab headers are adjusted to accommodate the used font. ocColorEditor enlarges this by 2 x 2 pixels for optimum appearance, and ensures a constant client size as needed regardless of the chosen border style. By setting the FixedSize property to false, you can override this behavior and specify a lager size to fit it in your control layout. Every time ColorEditor.EditValue() is invoked, ColorUI adjusts its size. To keep our size settings, a NativeWindow class prevents unwanted resizing.

Tab key operation

The Tab key wraps and confines the selection to the editor's tabpages. This is proper behaviour for a popup component, but is annoying now with other controls present on the form. To allow tabbing out (AllowTabOut property), if the first or last tab page is selected, we must find and select the next control on the form. Control.SelectNextControl() was conceived for this task, but with any arguments I passed, it returned me only the editor's tab pages. So, I resorted to construct a list of selectable sibling controls with their respective tab order positions myself. If you dynamically load controls, toggling the AllowTabOut property will refresh the internal list.

Custom colors

I usually preload custom colors with all non-default application colors, so I took some lengths to ensure that this is possible with the editor already running. This, again, requires 'black magic' to manipulate a private field. Given the way I learned to right-click on a custom color (I wondered what a customized ColorDialog was doing inside ColorEditor), providing a hint on how to add/change a custom color is reasonable. A localizable tooltip appears, when hovering over the custom colors area on the palette tab page.

Using the code

The download contains a demo project I used for developing and testing. A ocColorEditor instance controls the color of another ColorEditorAlpha control. The ColorEditorAlpha derives from ocColorEditor, and adds a tab page to edit the alpha component of the color.

Points of interest

You guessed it by now, there is credit due: without Lutz Roeder's .NET Reflector, this article (as many others) would never have come into existence. Two future articles will describe how to replace the palette color area in a customized ColorDialog with this presented ColorEditor. How do you feel about applying unsupported practices? Does your boss allow it? The pitfalls, I'd never see?

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here