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

ColorEditorEx - An extension to the ColorEditor to support translucent colors

0.00/5 (No votes)
15 Jul 2006 1  
Shows a way to extend the ColorEditor class to set alpha values on Colors within the Visual Studio editor, by revealing its internals.

Sample Image - ColorEditorEx.png

Contents

Introduction

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.

Background

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.

Understanding the ColorEditor

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.

Extending the ColorEditor

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.

Using the code

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.

Conclusion

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.

History

  • July 15th, 2006 - Version 1.0.0
    • Initial release.

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