65.9K
CodeProject 正在变化。 阅读更多。
Home

ColorEditorEx - ColorEditor 的扩展, 支持半透明颜色

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (18投票s)

2006年7月15日

CPOL

5分钟阅读

viewsIcon

54855

downloadIcon

1026

展示了一种扩展 ColorEditor 类的方法,以便在 Visual Studio 编辑器中为颜色设置 alpha 值,通过揭示其内部机制。

Sample Image - ColorEditorEx.png

目录

引言

在开发一组支持半透明颜色和丰富设计时支持的控件时,我很快发现 Visual Studio 中常规的颜色编辑器缺乏设置 alpha 值的能力。因此,我总是必须在代码中定义这些颜色,更糟糕的是,我的组件用户也必须这样做。我搜索了一段时间,也找到了一些用于定义带有 alpha 值的颜色的控件(例如 MultiTabColorPicker[^]),但没有一个准备好用作 Visual Studio 中的编辑器。所以我决定自己构建一个,我认为扩展框架中内置的编辑器是可行的途径。

我的最终解决方案代码量不多,但我希望您会发现这篇文章很有用,它描述了我为实现目标所采取的步骤。

背景

作为开始,我将首先介绍 Visual Studio 设计器也使用的 PropertiesControl 的工作原理。

通常,PropertiesControl 显示它当前显示的对象的 public 属性。这可以通过适当的属性来修改,但这并非本文的主题。当用户单击属性的值字段时,会显示一个小控件,允许用户编辑该值。正如你们许多人可能已经知道的那样,这可以是从普通的 TextBox,到填充了不同值的 ComboBox,再到编辑颜色时的复杂控件。

具体显示什么由一个 UITypeEditor 类定义。为了解析使用哪个类,PropertiesControl 首先查看属性是否设置了 EditorAttribute,该属性告诉它显示哪个编辑器。如果没有找到,它会查找属性的 Type 或其任何基类定义中是否存在此属性。由于 Color class 在其 EditorAttribute 中定义了 ColorEditor,因此类型为 Color 的每个属性默认都会显示此编辑器。

因此,很明显我必须通过为每个需要 alpha 值的 Color 类型属性设置 EditorAttribute 来覆盖此默认行为。所以让我们构建一个新的 UITypeEditor

理解 ColorEditor

如上所述,我不想重写整个 ColorEditor 仅仅是为了添加一些小的功能。所以第一步是理解它。为此,我使用了 Reflector[^]。对于任何想要理解 .Net 框架内部机制的人来说,这是一个很棒的工具。

截图显示了 ColorEditor 类的整体结构。GetPaintValueSupportedPaintValue 方法用于在属性值字段的文本前面绘制一个小块。对于 ColorEditor,这是一个用选定颜色填充的简单矩形。我将在文章后面回到这些。GetEditStyle 定义了 ColorEditor 显示一个下拉字段,编辑器将在其中显示。最后,当用户单击下拉字段时,会调用 EditValue 方法,从而显示用于编辑值的控件。

所以这个是最有趣的。使用 Reflector 反汇编它会揭示以下代码

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;
}

为了完成我的工作,我至少需要 override 这个方法,并扩展 ColorUI 控件,该控件通过 service1.DropDownControl(this.colorUI) 调用显示。这可能很容易,但 ColorUIColorEditor 类中一个私有的嵌套控件,因此不能直接使用。为了能够扩展它,我决定使用反射机制。

扩展 ColorEditor

为了方便使用,我首先围绕隐藏的 ColorUI 类构建了一个包装器。它应该涵盖解析类型、实例化它以及发布所需的方法。

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]);
   }

现在我可以定义我自己的继承自 ColorEditor 并重写 EditValueUITypeEditor 了。

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;
}
    

正如你所见,差异相当小。到目前为止我所做的一切就是用我的公共包装器 ColorUIWrapper 替换 private class ColorUI。现在,我已经准备好扩展 ColorUI class 了。为此,我添加了一个包含 TrackBarLabelPanel 到每个被创建并停靠在右侧的 ColorUI 实例。我还扩展了我的 Value 属性和 Start 方法,以便从 TrackBar 获取和设置 alpha 值。这个以及一些适当的调整大小逻辑非常简单,因为 Reflector 吐出了我需要知道的每一个细节。最终的 ColorUIWrapper 看起来如下。

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();
   }
}

还记得我提到的 PaintValue 吗?在实现主要目标后,我最终在 ColorEditorEx 类中自定义了绘制行为。

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);
}

请注意,我不需要重写 PaintValueSupported,因为 ColorEditor 类已经这样做了并且返回 true。在绘制实际颜色之前绘制一个基础背景应该更容易直观地把握透明度级别。

使用代码

所以最后的问题是,如何在你的项目中如何使用它。如果你已经定义了自己的 Color 类型属性,那么很简单。

[Editor(typeof(Design.ColorEditorEx), typeof(System.Drawing.Design.UITypeEditor))]
public Color MyColor {
   get { return _myColor; }
   set { _myColor = value; }
}
    

这就是全部。重新编译并查看你的属性。它将以新的外观显示。

在我的例子中,我制作了一个用户控件,它应该支持半透明的 BackColorForeColor。这两个属性都可以被重写,所以你可以添加 EditorAttribute

[Editor(typeof(Design.ColorEditorEx), typeof(System.Drawing.Design.UITypeEditor))]
public override Color ForeColor {
   get { return base.ForeColor; }
   set { base.ForeColor = value; }
}

请注意,将 BackColor 设置为具有指定 alpha 值的颜色会导致异常,除非你在相应控件的构造函数中的某个地方调用 base.SetStyle(ControlStyles.SupportsTransparentBackColor, true)

结论

我希望您喜欢我深入 ColorEditor 的旅程。所展示的技术在您需要扩展 .Net 框架的内部机制被隐藏的许多情况下都很有用。

历史

  • 2006年7月15日 - 版本 1.0.0
    • 初始发布。
© . All rights reserved.