WinForms ColorEditor 模态显示






4.76/5 (14投票s)
一个可以永久在窗体上显示 ColorEditor 的用户控件。
引言
.NET 的 PropertyGrid
使用的 WinForms ColorEditor
类被设计为一个下拉窗口。尽管其归类为内部框架使用,但使用它的控件已经被发布。palomraz 提交了两个包含大量背景解释的优秀 VB.NET 示例。
可能因为其弹出式特性,ColorEditor
被认为是一个“酷”的组件。本文描述了一个“不那么酷”的用户控件,它可以将 WinForms ColorEditor
永久显示在主窗体上。ColorChanged
事件用于信号用户选择的变化,并且选定的颜色和自定义颜色可以在运行时设置(预设)。这是通过修改内部属性的非支持实践(使用反射,即反射的“黑魔法”方面)来实现的。
背景
为了显示 ColorEditor
,我们必须实现两个接口,其中 IServiceProvider.GetService()
只返回我们的 IWindowsFormsEditorService
实现。
// 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
的布局如图所示
// 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 {}
}
当调用 ColorEditor.EditValue(null, provider, initialColor)
时,它执行以下操作:
- 忽略
ITypeDescriptorContext
参数。 - 从传入的
IServiceProvider
实例查询IWindowsFormEditorService
实现。 - 创建一个私有
ColorUI
类的实例,该类实现实际的用户界面并与用户交互。ColorUI.Start()
使用传入的颜色值进行初始化,并将IWindowsFormEditorService
引用存储在私有字段edSvc
中。 - 调用
IWindowsFormEditorService.DropDownControl
方法,并将ColorUI
实例传递给它。此方法将ColorUI
嵌入到窗体中,并在适当的屏幕位置显示该窗体。该方法必须阻塞,同时使用MsgWaitForMultipleObjects
API 函数分派所有消息。简而言之,它会等待直到用户完成编辑。 - 当用户选择新颜色时,
ColorEditor
调用IWindowsFormEditorService.CloseDropDown()
,该方法关闭下拉 UI 并导致IWindowsFormEditorService.DropDownControl()
返回。 ColorUI.End()
将IWindowsFormEditorService
引用置为 null。EditValue()
返回选定的颜色值。ColorUI
实例在后续的EditValue()
调用中保持有效。
是不是头疼了好几次?阅读 实现 IWindowsFormsEditorService 接口(最后一部分) 有助于我理解。
就我们而言,ColorUI
的布局
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 {}
}
一旦我们在 IWindowsFormEditorService.DropDownControl
方法中收到 ColorUI
实例,我们就通过将其添加到 UserControl.Controls
集合中来显示它。虽然用户界面由私有类组成,但我们可以访问它们的相应基类。
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];
我这里使用了 WinForms 的内部命名。调色板控件位于第一个选项卡页(美式英语:“Custom”),常用的列表框显示网页颜色。请注意,我们也可以在此处添加自己的选项卡页。
这个技巧
如上所述,下拉操作要求 IWindowsFormEditorService.DropDownControl
方法在用户完成编辑之前不能返回。我们可以省略此功能,我们的 DropDownControl
方法,从而 ColorEditor.EditValue()
将立即返回。换句话说,我们使用 EditValue()
来启动编辑器并设置(初始)颜色,但我们不能使用其返回值(我们的初始颜色)。
为了实现我们的目标,我们必须克服四个问题:
- 防止
ColorUI
在用户选择颜色后关闭。这个很简单,在我们的
IWindowsFormEditorService.CloseDropDown
方法中,我们只是不将其从UserControl
.Controls
集合中移除。 - 检索选定的颜色值
相反,我们将调用
CloseDropDown()
作为选择更改发生的指示。在选择网页或系统颜色时,我们将Listbox
.SelectedItem
属性转换为颜色值。ListBox lb = (ListBox)tab.SelectedTab.Controls[0]; Color value = (Color)lb.SelectedItem;
对于选定的调色板颜色,我们必须依赖反射(“白魔法”)。
ColorUI
暴露了一个公共属性Value
(object
),但请记住ColorUI
是一个私有类,因此我们只能访问其Control
基类。Type t = colorUI.GetType(); PropertyInfo pInfo = t.GetProperty("Value"); Color value = (Color)pInfo.GetValue(colorUI, null);
- 按请求关闭
ColorUI
(即,在处置时)。在任何选项卡页上按 Enter 键都会调用
CloseDropDown
。我们通过向活动选项卡页上的控件发送WM_KEYDOWN
消息来模拟它。在这种情况下,我们从UserControl
.Controls
集合中移除ColorUI
。请记住,任何添加的自定义选项卡页必须在关闭之前移除,否则可能会失败。 - 防止 System.Drawing.Design.dll 中出现
NullReferenceException
。如前所述,
ColorUI
在私有字段中保留了对我们IWindowsFormEditorService
的引用。作为一个行为良好的组件,它会在IWindowsFormEditorService.DropDownControl()
返回后将此引用置为 null。我们让DropDownControl()
立即返回,因此我们用无效的引用启动ColorUI
。后续用户选择,而不是调用CloseDropDown()
,将导致NullReferenceException
。因此,“黑魔法”发挥作用,通过恢复私有字段
edSvc
中的引用。Type t = colorUI.GetType(); FieldInfo fInfo = t.GetField("edSvc", BindingFlags.NonPublic | BindingFlags.Instance); fInfo.SetValue(colorUI, service);
好消息是,我们只需要在调用
EditValue()
后进行一次此操作,无论是启动编辑器时,还是在编辑器运行时设置新颜色时。一旦恢复,ColorUI
会在后续的用户输入时调用CloseDropDown()
,并且不再使引用无效。
ocColorEditor
为了使其成为一个功能齐全的用户控件,还需要进行大量的编码工作,这里我将不详细介绍。为了帮助理解主要操作,这里是 UserControl
的骨架,混合了接口声明和伪代码。
// 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
}
实例化 ColorUI
是一个 CPU 密集型任务,因此这是通过调用 ShowEditor()
来完成的,而不是在惰性构造函数中。在私有类中实现 IWindowsFormsEditorService
和 IServiceProvider
不会弄乱我们的公共接口。
用户控件大小
ColorUI
为其调色板窗口使用固定的 202 x 202 像素大小。ColorUI
的整体高度(默认为 220)会变化,因为选项卡标题会调整以适应使用的字体。ocColorEditor
将其放大 2 x 2 像素以获得最佳外观,并根据需要确保恒定的客户端大小,无论选择哪种边框样式。通过将 FixedSize
属性设置为 false
,您可以覆盖此行为并指定一个较大的尺寸以适应您的控件布局。每次调用 ColorEditor
.EditValue()
时,ColorUI
都会调整其大小。为了保持我们的尺寸设置,NativeWindow
类可以防止不必要的缩放。
Tab 键操作
Tab 键会在编辑器的选项卡页之间循环并限制选择。这是弹出组件的正确行为,但现在与其他控件在窗体上时很烦人。为了允许 Tab 键切换(AllowTabOut
属性),如果选择了第一个或最后一个选项卡页,我们必须找到并选择窗体上的下一个控件。Control.SelectNextControl()
就是为此任务而设计的,但无论我传递什么参数,它都只返回编辑器的选项卡页。因此,我诉诸于自己构建一个可选的同级控件列表及其相应的制表位位置。如果您动态加载控件,切换 AllowTabOut
属性将刷新内部列表。
自定义颜色
我通常会预加载自定义颜色以及所有非默认的应用程序颜色,因此我花了些力气来确保可以在编辑器已经运行时做到这一点。这同样需要“黑魔法”来操作私有字段。考虑到我学习到右键单击自定义颜色(我曾想知道自定义的 ColorDialog
在 ColorEditor
中做什么),提供如何添加/更改自定义颜色的提示是合理的。当鼠标悬停在调色板选项卡页上的自定义颜色区域时,会出现一个本地化的工具提示。
使用代码
下载中包含了我开发和测试时使用的演示项目。一个 ocColorEditor
实例控制着另一个 ColorEditorAlpha
控件的颜色。ColorEditorAlpha
继承自 ocColorEditor
,并添加了一个选项卡页来编辑颜色的 alpha 分量。
关注点
您现在应该猜到了,这里要归功于:如果没有 Lutz Roeder 的 .NET Reflector,本文(以及许多其他文章)将永远不会出现。两篇未来的文章将描述如何用本文介绍的 ColorEditor
替换自定义 ColorDialog
中的调色板颜色区域。您如何看待应用不受支持的做法?您的老板允许吗?我永远看不到的陷阱是什么?