WinForms ColorEditor Displayed Modeless






4.76/5 (14 votes)
A usercontrol that can show the ColorEditor permanently on a form.
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 anIWindowsFormEditorService
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 theIWindowsFormEditorService
reference in a private field,edSvc
. - Calls the
IWindowsFormEditorService.DropDownControl
method, passing it theColorUI
instance. This method embedsColorUI
inside a form, and shows the form at an appropriate screen location. The method must block, while dispatching all messages using theMsgWaitForMultipleObjects
API function. Simply put, it waits until the user finishes editing. - When the user selects a new color, the
ColorEditor
callsIWindowsFormEditorService.CloseDropDown()
, which closes the drop-down UI and causesIWindowsFormEditorService.DropDownControl()
to return. ColorUI.End()
nulls theIWindowsFormEditorService
reference.EditValue()
returns the selected color value.- The
ColorUI
instance remains valid for furtherEditValue()
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 ourUserControl
.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 theListbox
.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 propertyValue
(object
), but rememberColorUI
is a private class, and so we only can access itsControl
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 aWM_KEYDOWN
message to the control on the active tab page. In this case, we removeColorUI
from ourUserControl
.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 ourIWindowsFormEditorService
in a private field. As a well-behaving component, it nulls this reference, afterIWindowsFormEditorService.DropDownControl()
returns. We letDropDownControl()
return immediately, thus we launchColorUI
with an invalid reference. Subsequent user selection, instead of callingCloseDropDown()
, will result in aNullReferenceException
.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
callsCloseDropDown()
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?