While working on a set of controls which had support for translucent colors and rich design time support, I soon discovered that the regular editor for colors in Visual Studio lacked the ability to set alpha values. Thus, I always had to define such colors within my code and worse the users of my component would also have to do this. I searched a while and also found some controls for defining colors with alpha values (like the MultiTabColorPicker[^]) but none of it was ready to be used as an editor within Visual Studio. So I decided to build one on my own and I thought extending the one built into the framework should be the way to go.
My final solution doesn't have much code but I hope you will find this article useful which describes what steps I took to achieve my goal.
As a startup, I will first describe some background on how the PropertiesControl
which is also used by the Visual Studio designer works.
Generally, the PropertiesControl
shows all public
properties of the object it is currently showing. This can be altered by appropriate attributes, but this is not the theme of this article. When the user clicks into the value field of a property, a small control is shown which enables the user to edit the value. As everyone of you might know already, this can be anything from a plain TextBox
, over a ComboBox
filled with different values up to a complex control like the one when editing colors.
What exactly is shown is defined by an UITypeEditor
class. To resolve which one to use, the PropertiesControl
first looks if an EditorAttribute
is set to the property which tells him what editor to show. If he doesn't find one, he will look if he finds this attribute in the class definition of the Type
of the property or any of its base classes. As the Color class
has defined ColorEditor
in its EditorAttribute
every property of type Color
will by default show this editor.
So it is clear that I will have to overwrite this default behavior by setting the EditorAttribute
on each of my properties with Color
type which need to have alpha values. So lets build a new UITypeEditor
.
As mentioned above I didn't want to rewrite the whole ColorEditor
just to add some small functionality. So the first step was understand it. For this, I used Reflector[^]). It's a great tool for everyone who wants to understand the internals of the .Net framework.
The screenshot shows the overall structure of the ColorEditor
class. The GetPaintValueSupported
and PaintValue
methods are used to paint a small block in front of the text in the property value field. In the case of the ColorEditor
this is a simple rectangle filled with the chosen color. I will return to those later in the article. The GetEditStyle
defines that the ColorEditor
shows a drop down field in which the editor will be shown. Finally the EditValue
method gets called when the user clicks into the dropdown field and thus shows the control for editing the value.
So this one is the most interesting. Disassembling it with Reflector revealed the following code:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
if (provider != null)
{
IWindowsFormsEditorService service1 =
(IWindowsFormsEditorService)provider.GetService(
typeof(IWindowsFormsEditorService));
if (service1 == null)
{
return value;
}
if (this.colorUI == null)
{
this.colorUI = new ColorEditor.ColorUI(this);
}
this.colorUI.Start(service1, value);
service1.DropDownControl(this.colorUI);
if ((this.colorUI.Value != null) &&
(((Color) this.colorUI.Value) != Color.Empty))
{
value = this.colorUI.Value;
}
this.colorUI.End();
}
return value;
}
To get my work done, I would at least have to override
this method and extend the ColorUI
control which gets shown with the service1.DropDownControl(this.colorUI)
call. That could have been easy but ColorUI
is a private nested control within the ColorEditor
class and thus can not be used directly. To be able to extend it I decided to use reflection mechanisms.
For easier usage, I first built a wrapper around the hidden ColorUI
class. It should cover resolving the type, instantiating it and publish the needed methods:
public class ColorUIWrapper
{
private Control _control;
private MethodInfo _startMethodInfo;
private MethodInfo _endMethodInfo;
private PropertyInfo _valuePropertyInfo;
public ColorUIWrapper(ColorEditorEx colorEditor)
{
Type colorUiType = typeof(ColorEditor).GetNestedType("ColorUI",
BindingFlags.CreateInstance | BindingFlags.NonPublic);
ConstructorInfo constructorInfo = colorUiType.GetConstructor(
new Type[] { typeof(ColorEditor) });
_control = (Control)constructorInfo.Invoke(new object[] { colorEditor });
_startMethodInfo = _control.GetType().GetMethod("Start");
_endMethodInfo = _control.GetType().GetMethod("End");
_valuePropertyInfo = _control.GetType().GetProperty("Value");
}
public Control Control
{
get { return _control; }
}
public object Value
{
get { return _valuePropertyInfo.GetValue(_control, new object[0]); }
}
public void Start(IWindowsFormsEditorService service, object value)
{
_startMethodInfo.Invoke(_control, new object[] { service, value });
}
public void End()
{
_endMethodInfo.Invoke(_control, new object[0]);
}
Now I could define my own UITypeEditor
inheriting from ColorEditor
and overriding EditValue
:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
if (provider != null)
{
IWindowsFormsEditorService service =
(IWindowsFormsEditorService)provider.GetService(
typeof(IWindowsFormsEditorService));
if (service == null)
return value;
if (_colorUI == null)
_colorUI = new ColorUIWrapper(this);
_colorUI.Start(service, value);
service.DropDownControl(_colorUI.Control);
if ((_colorUI.Value != null) && (((Color) _colorUI.Value) != Color.Empty))
{
value = _colorUI.Value;
}
_colorUI.End();
}
return value;
}
As you can see, the differences are rather small. All what I had done so far was replacing the private class ColorUI
with my public wrapper ColorUIWrapper
. Now, I was ready to extend the ColorUI class
. For this, I added a Panel
containing a TrackBar
and a Label
to every ColorUI
instance being created and docked it to the right. I also extended the my Value
property and the Start
method to get and set the alpha value from and to the TrackBar
. This and some proper resizing logic was easy since Reflector spit out every detail I needed to know of. The final ColorUIWrapper
looks like the following:
public class ColorUIWrapper
{
private Control _control;
private MethodInfo _startMethodInfo;
private MethodInfo _endMethodInfo;
private PropertyInfo _valuePropertyInfo;
private TrackBar _tbAlpha;
private Label _lblAlpha;
private bool _inSizeChange = false;
public ColorUIWrapper(ColorEditorEx colorEditor)
{
Type colorUiType = typeof(ColorEditor).GetNestedType("ColorUI",
BindingFlags.CreateInstance | BindingFlags.NonPublic);
ConstructorInfo constructorInfo = colorUiType.GetConstructor(
new Type[] { typeof(ColorEditor) });
_control = (Control)constructorInfo.Invoke(new object[] { colorEditor });
Panel alphaPanel = new Panel();
alphaPanel.BackColor = SystemColors.Control;
alphaPanel.Dock = DockStyle.Right;
alphaPanel.Width = 28;
_control.Controls.Add(alphaPanel);
_tbAlpha = new TrackBar();
_tbAlpha.Orientation = Orientation.Vertical;
_tbAlpha.Dock = DockStyle.Fill;
_tbAlpha.TickStyle = TickStyle.None;
_tbAlpha.Maximum = byte.MaxValue;
_tbAlpha.Minimum = byte.MinValue;
_tbAlpha.ValueChanged += new EventHandler(OnTrackBarAlphaValueChanged);
alphaPanel.Controls.Add(_tbAlpha);
_lblAlpha = new Label();
_lblAlpha.Text = "0";
_lblAlpha.Dock = DockStyle.Bottom;
_lblAlpha.TextAlign = ContentAlignment.MiddleCenter;
alphaPanel.Controls.Add(_lblAlpha);
_startMethodInfo = _control.GetType().GetMethod("Start");
_endMethodInfo = _control.GetType().GetMethod("End");
_valuePropertyInfo = _control.GetType().GetProperty("Value");
_control.SizeChanged += new EventHandler(OnControlSizeChanged);
}
public Control Control
{
get { return _control; }
}
public object Value
{
get
{
object result = _valuePropertyInfo.GetValue(_control, new object[0]);
if (result is Color)
result = Color.FromArgb(_tbAlpha.Value, (Color)result);
return result;
}
}
public void Start(IWindowsFormsEditorService service, object value)
{
if (value is Color)
_tbAlpha.Value = ((Color)value).A;
_startMethodInfo.Invoke(_control, new object[] { service, value });
}
public void End()
{
_endMethodInfo.Invoke(_control, new object[0]);
}
private void OnControlSizeChanged(object sender, EventArgs e)
{
if (_inSizeChange)
return;
try
{
_inSizeChange = true;
TabControl tabControl = (TabControl)_control.Controls[0];
Size size = tabControl.TabPages[0].Controls[0].Size;
Rectangle rectangle = tabControl.GetTabRect(0);
_control.Size = new Size(_tbAlpha.Width + size.Width,
size.Height + rectangle.Height);
}
finally
{
_inSizeChange = false;
}
}
private void OnTrackBarAlphaValueChanged(object sender, EventArgs e)
{
_lblAlpha.Text = _tbAlpha.Value.ToString();
}
}
Remember when I mentioned PaintValue
? Having achieved my primary goal I finally customized the painting behavior in the ColorEditorEx
class:
public override void PaintValue(PaintValueEventArgs e)
{
if (e.Value is Color && ((Color)e.Value).A < byte.MaxValue)
{
int oneThird = e.Bounds.Width / 3;
using (SolidBrush brush = new SolidBrush(Color.White))
{
e.Graphics.FillRectangle(brush, new Rectangle(
e.Bounds.X, e.Bounds.Y, oneThird, e.Bounds.Height - 1));
}
using (SolidBrush brush = new SolidBrush(Color.DarkGray))
{
e.Graphics.FillRectangle(brush, new Rectangle(
e.Bounds.X + oneThird, e.Bounds.Y, oneThird, e.Bounds.Height - 1));
}
using (SolidBrush brush = new SolidBrush(Color.Black))
{
e.Graphics.FillRectangle(brush, new Rectangle(
e.Bounds.X + oneThird * 2, e.Bounds.Y,
e.Bounds.Width - oneThird * 2, e.Bounds.Height - 1));
}
}
base.PaintValue(e);
}
Note that I didn't have to override PaintValueSupported
because the ColorEditor
class did this already and returns true
. Painting a rudimentary background before the actual color gets drawn should make it easier to visually grab the translucency level.
So finally the questions arises on how to use this in your projects. If you have defined your own properties of type Color
than its easy:
[Editor(typeof(Design.ColorEditorEx), typeof(System.Drawing.Design.UITypeEditor))]
public Color MyColor {
get { return _myColor; }
set { _myColor = value; }
}
This is all. Recompile and look at your property. It will show up in a new look.
In my case I made a user control which should enable translucent BackColor
and ForeColor
. Both properties can be overridden so you can add the EditorAttribute
.
[Editor(typeof(Design.ColorEditorEx), typeof(System.Drawing.Design.UITypeEditor))]
public override Color ForeColor {
get { return base.ForeColor; }
set { base.ForeColor = value; }
}
Note that setting BackColor
to a color with a specified alpha value will result in an exception unless you call base.SetStyle(ControlStyles.SupportsTransparentBackColor, true)
somewhere in the constructor of the corresponding control.
I hope you enjoyed my journey through the depth of the ColorEditor
. The demonstrated technique can be useful in many situations where you need to extend the .Net framework at points where its developers hided the required internals.
- July 15th, 2006 - Version 1.0.0