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.
public interface IServiceProvider
{
object GetService(Type serviceType);
}
public interface IWindowsFormsEditorService
{
void CloseDropDown();
void DropDownControl(Control control);
DialogResult ShowDialog(Form dialog);
}
ColorEditor
's layout, as far it concerns us, is shown here:
public class ColorEditor : UITypeEditor
{
public ColorEditor();
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value);
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);
public void Start(IWindowsFormsEditorService edSvc, object value);
public void End();
public object Value { get; }
private IWindowsFormsEditorService edSvc;
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:
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);
}
public void CloseEditor()
{
service.CloseDropDownInternal();
}
private void service_ColorUIAvailable(object sender,
EditorServiceEventArgs e)
{
if (e.ColorUI != null)
{
if (colorUI == null)
{
colorUI = e.ColorUI;
Controls.Add(colorUI);
}
}
else
{
colorUI = null;
service = null;
}
}
private void service_ColorChanged(object sender, EventArgs e)
{
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;
}
public object GetService(Type serviceType)
{
return this;
}
public void DropDownControl(Control control)
{
ColorUIAvailable(this,
new EditorServiceEventArgs(control));
}
public void CloseDropDown()
{
if (!closeEditor)
ColorChanged(this, EventArgs.Empty);
else
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?