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

WinForms ColorEditor 模态显示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (14投票s)

2006 年 6 月 20 日

CPOL

6分钟阅读

viewsIcon

68003

downloadIcon

1072

一个可以永久在窗体上显示 ColorEditor 的用户控件。

Sample Image

引言

.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 暴露了一个公共属性 Valueobject),但请记住 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() 来完成的,而不是在惰性构造函数中。在私有类中实现 IWindowsFormsEditorServiceIServiceProvider 不会弄乱我们的公共接口。

用户控件大小

ColorUI 为其调色板窗口使用固定的 202 x 202 像素大小。ColorUI 的整体高度(默认为 220)会变化,因为选项卡标题会调整以适应使用的字体。ocColorEditor 将其放大 2 x 2 像素以获得最佳外观,并根据需要确保恒定的客户端大小,无论选择哪种边框样式。通过将 FixedSize 属性设置为 false,您可以覆盖此行为并指定一个较大的尺寸以适应您的控件布局。每次调用 ColorEditor.EditValue() 时,ColorUI 都会调整其大小。为了保持我们的尺寸设置,NativeWindow 类可以防止不必要的缩放。

Tab 键操作

Tab 键会在编辑器的选项卡页之间循环并限制选择。这是弹出组件的正确行为,但现在与其他控件在窗体上时很烦人。为了允许 Tab 键切换(AllowTabOut 属性),如果选择了第一个或最后一个选项卡页,我们必须找到并选择窗体上的下一个控件。Control.SelectNextControl() 就是为此任务而设计的,但无论我传递什么参数,它都只返回编辑器的选项卡页。因此,我诉诸于自己构建一个可选的同级控件列表及其相应的制表位位置。如果您动态加载控件,切换 AllowTabOut 属性将刷新内部列表。

自定义颜色

我通常会预加载自定义颜色以及所有非默认的应用程序颜色,因此我花了些力气来确保可以在编辑器已经运行时做到这一点。这同样需要“黑魔法”来操作私有字段。考虑到我学习到右键单击自定义颜色(我曾想知道自定义的 ColorDialogColorEditor 中做什么),提供如何添加/更改自定义颜色的提示是合理的。当鼠标悬停在调色板选项卡页上的自定义颜色区域时,会出现一个本地化的工具提示。

使用代码

下载中包含了我开发和测试时使用的演示项目。一个 ocColorEditor 实例控制着另一个 ColorEditorAlpha 控件的颜色。ColorEditorAlpha 继承自 ocColorEditor,并添加了一个选项卡页来编辑颜色的 alpha 分量。

关注点

您现在应该猜到了,这里要归功于:如果没有 Lutz Roeder 的 .NET Reflector,本文(以及许多其他文章)将永远不会出现。两篇未来的文章将描述如何用本文介绍的 ColorEditor 替换自定义 ColorDialog 中的调色板颜色区域。您如何看待应用不受支持的做法?您的老板允许吗?我永远看不到的陷阱是什么?

© . All rights reserved.