在上一篇文章,我介绍了如何编写模态对话框属性编辑器,这篇文章我将介绍如何编写下拉式属性编辑器。下拉式(
DropDown
)属性编辑器和模态对话框属性编辑器的不同之处就是,当你点击属性值修改的时候,模态对话框编辑器是弹出一个模态对话框,而下拉式属性编辑器却是在紧贴着属性值的地方显示一个下拉的控件。不知道大家注意到了没有,这里我说的是显示一个下拉的控件,而这个控件也是需要你去开发的,接下来我还是以
Scope
属性为例,介绍一下具体的实现。
首先我们要创建一个用于编辑属性的控件,在本系列文章的开始,我们介绍了自定义控件有三种类型:复合控件,扩展控件,自定义控件。在本例中我们制作一个复合控件(
Compsite control
),复合控件的开发比较简单,不在本系列文章的讲解范围,我简单做个介绍,在
Solution
浏览器里右键点击
CustomControlSample
工程选择
Add->User Control…,
输入文件名
ScopeEditorControl.cs
。我们做的这个复合控件上一篇文章介绍的模态对话框所包含子控件基本一样,除了用于确认和取消的按钮,如下图:

由于我们取消了用于确认和取消的按钮,并且是一个下拉的编辑器控件,在出现下面三种情况的时候下拉的编辑器控件会关闭:用户敲了回车,用户敲了
ESC
键,用户点击了编辑器以外的地方。当下拉编辑器控件关闭的时候我们就需要更新属性的值。下边是这个控件的代码:
using
System;
using
System.Collections.Generic;
using
System.ComponentModel;
using
System.Drawing;
using
System.Data;
using
System.Text;
using
System.Windows.Forms;

namespace
CustomControlSample

{
public partial class ScopeEditorControl : UserControl

{
private Scope _oldScope;
private Scope _newScope;
private Boolean canceling;
public ScopeEditorControl(Scope scope)

{
_oldScope = scope;
_newScope = scope;
InitializeComponent();
}

public Scope Scope

{
get

{
return _newScope;
}
}

private void textBox1_Validating(object sender, CancelEventArgs e)

{
try

{
Int32.Parse(textBox1.Text);

}
catch (FormatException)

{
e.Cancel = true;
MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

private void textBox2_Validating(object sender, CancelEventArgs e)

{
try

{
Int32.Parse(textBox2.Text);
}
catch (FormatException)

{
e.Cancel = true;
MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

protected override bool ProcessDialogKey(Keys keyData)

{
if (keyData == Keys.Escape)

{
_oldScope = _newScope;
canceling = true;
}
return base.ProcessDialogKey(keyData);
}

private void ScopeEditorControl_Leave(object sender, EventArgs e)

{
if (!canceling)

{
_newScope.Max = Convert.ToInt32(textBox1.Text);
_newScope.Min = Convert.ToInt32(textBox2.Text);
}
}

private void ScopeEditorControl_Load(object sender, EventArgs e)

{
textBox1.Text = _oldScope.Max.ToString();
textBox2.Text = _oldScope.Min.ToString();
}


}
}
和模态对话框编辑器一样,开发环境并不会直接调用我们的编辑器控件,而是用过UITypeEditor类的派生来实现编辑器的调用,所以我们必须实现一个下拉式编辑器。代码如下:
using
System;
using
System.ComponentModel;
using
System.Drawing.Design;
using
System.Windows.Forms.Design;
using
System.Windows.Forms;

namespace
CustomControlSample

{
public class ScopeDropDownEditor : UITypeEditor

{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)

{
if (context != null && context.Instance != null)

{
return UITypeEditorEditStyle.DropDown;
}

return base.GetEditStyle(context);
}

public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)

{
IWindowsFormsEditorService editorService = null;

if (context != null && context.Instance != null && provider != null)

{
editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (editorService != null)

{
MyListControl control = (MyListControl)context.Instance;
ScopeEditorControl editorControl = new ScopeEditorControl(control.Scope);
editorService.DropDownControl(editorControl);
value = editorControl.Scope;
return value;
}
}

return value;
}

}
}
看过上一篇文章的朋友应该对这段代码很熟悉,是的,这两个编辑器的代码只有几行不同之处,在GetEditStyle方法中,我们返回的是UITypeEditorEditStyle.DropDown,而不是UITypeEditorEditStyle.Modal,表明我们的编辑器是一个下拉式的编辑器。在EditValue中的不同之处是,我们使用DropDownControl方法来显示编辑器。编辑器制作完毕,我们把Scope以前的编辑器替换成下拉式编辑器,如下:
[Browsable(
true
)]
[Editor(
typeof
(ScopeDropDownEditor),
typeof
(UITypeEditor))]
public
Scope Scope

{
get

{
return _scope;
}
set

{
_scope = value;
}
}
现在
build CustomControlSample
工程,然后切换到测试工程查看
Scope
属性。当我们点击属性的值,在属性值的后边出现了一个按钮:
当点击这个按钮的时候,下拉的属性编辑器出现了:
好了,属性的编辑到这里就讲完了。
本系列的前面几篇文章讲解了如何来定义属性以及更有效的编辑属性,接下来我要讲一下控件属性的默认值。如果我们希望自己开发的控件更易于被其它开发者使用,那么提供默认值是非常值得的。
如果你为属性设定了默认值,那么当开发者修改了属性的值,这个值在Property Explorer中将会以粗体显示。VS为属性提供一个上下文菜单,允许程序员使用控件把值重置为默认值。当VS进行控件的串行化时,他会判断那些值不是默认值,只有不是默认值的属性才会被串行化,所以为属性提供默认值时可以大大减少串行化的属性数目,提高效率。
那么VS怎么知道我们的属性值不是默认值了呢?我们需要一种机制来通知VS默认值。实现这种机制有两种方法:
对于简单类型的属性,比如Int32,Boolean等等这些Primitive类型,你可以在属性的声明前设置一个DefaultValueAttribute,在Attribute的构造函数里传入默认值。
对于复杂的类型,比如Font,Color,你不能够直接将这些类型的值传递给Attibute的构造函数。相反你应该提供Reset<PropertyName> 和ShouldSerialize<PropertyName>方法,比如ResetBackgroundColor(),ShouldSerializeBackgroundColor()。VS能够根据方法的名称来识别这种方法,比如Reset<PropertyName>方法把重置为默认值,ShouldSerialize<PropertyName>方法检查属性是否是默认值。过去我们把它称之为魔术命名法,应该说是一种不好的编程习惯,可是现在微软依然使用这种机制。我还是以前面几篇文章使用的例子代码。
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.Windows.Forms;
using
System.ComponentModel;
using
System.Drawing;

namespace
CustomControlSample

{
public class FirstControl : Control

{

private String _displayText=”Hello World!”;
private Color _textColor=Color.Red;

public FirstControl()

{

}

// ContentAlignment is an enumeration defined in the System.Drawing
// namespace that specifies the alignment of content on a drawing
// surface.
private ContentAlignment alignmentValue = ContentAlignment.MiddleLeft;

[
Category("Alignment"),
Description("Specifies the alignment of text.")
]
public ContentAlignment TextAlignment

{

get

{
return alignmentValue;
}
set

{
alignmentValue = value;

// The Invalidate method invokes the OnPaint method described
// in step 3.
Invalidate();
}
}


[Browsable(true)]
[DefaultValue(“Hello World”)]
public String DisplayText


{
get


{
return _displayText;
}
set


{
_displayText =value;
Invalidate();
}
}

[Browsable(true)]
public Color TextColor


{
get


{
return _textColor;
}
set


{
_textColor=value;
Invalidate();
}
}

public void ResetTextColor()


{
TextColor=Color.Red;
}

public bool ShouldSerializeTextColor()


{
return TextColor!=Color.Red;
}

protected override void OnPaint(PaintEventArgs e)

{
base.OnPaint(e);
StringFormat style = new StringFormat();
style.Alignment = StringAlignment.Near;
switch (alignmentValue)

{
case ContentAlignment.MiddleLeft:
style.Alignment = StringAlignment.Near;
break;
case ContentAlignment.MiddleRight:
style.Alignment = StringAlignment.Far;
break;
case ContentAlignment.MiddleCenter:
style.Alignment = StringAlignment.Center;
break;
}

// Call the DrawString method of the System.Drawing class to write
// text. Text and ClientRectangle are properties inherited from
// Control.
e.Graphics.DrawString(
DisplayText,
Font,
new SolidBrush(TextColor),
ClientRectangle, style);

}
}
}
在上面的代码中,我增加了两个属性,一个是DisplayText,这是一个简单属性,我们只需要在它的声明前添加一个DefaultValue Attribute就可以了。另外一个是TextColor属性,这个复杂类型的属性,所以我们提供了ResetTextColor和ShouldSerializeTextColor来实现默认值。
默认值的实现就讲完了,但是有一点不要忽视了,你设定了默认值,就应该相应的初始化这些属性,比如我们例子中的代码:
private
String _displayText
=
”Hello World
!
”;
private
Color _textColor
=
Color.Red;
前面的一些文章绝大部分都是要讲控件的设计时的行为,既然涉及到这么多的设计时行为的代码编写,那么就有必要就一下如何来调试控件的设计行为。
调试控件的设计时行为和调试DLL的方式非常的相似,因为DLL是不能够单独运行的,而一般的控件也会在一个DLL里。当然如果你不考虑类的可复用性而把控件写在一个Windows Application里面也无可厚非,这样调试倒也变的简单了。但是我们还是要考虑更通常的情况。一般来说,我们调试DLL时,都是创建一个可独立运行的应用程序,在这个应用程序里引用你希望调试的DLL工程,在DLL工程的代码里设置断点,然后调试。所以,调试这一类东西,首要的问题就是找到一个调用它的宿主。调试控件的设计时行为什么样的宿主最好呢,当然是Visual studio了,visual studio里提供了非常全面的设计时支持。下来我就来演示一下具体的做法。
首先将你要测试的控件所在的工程设为启动工程。在Solution Explorer里右键点击控件所在的工程,在菜单里选择属性(Properties)进入工程属性设置界面,点击“Debug”页面,将Start Action 选为“Start External Program”,接下来点击后边的选择按钮选中你的Visual Studio的可执行程序,我的Visual Studio程序位于“D:/Program Files/Microsoft Visual Studio 8/Common7/IDE/devenv.exe”,你可以根据自己的情况选择。如下图:

在设置完以后工程属性以后,在需要调试的地方设置断点,然后点击F5或者点击工具栏的运行按钮。当点击以后,visual studio会运行起来,在运行起来的Visual studio里面打开一个应用你这个Assembly的工程,在这个工程里切换到Form设计器界面,选中你的控件,然后编辑你所要调设的功能,比如,你要调试一个控件的属性的Editor,你在这个editor类里设置断点,接着在属性浏览器里编辑这个属性,程序就会停在你设置的断点。
最近真的真的太忙了,以至于一个多月都没哟更新我的blog。昨天晚上,一个网上的朋友看了我的ToolBox的文章,问我一个问题,他说如何让ToolBox控件也能响应键盘操作,也就是用Up,down按键来选择工具箱控件里的Item,他添加了键盘事件,但是不起作用。一开始做这个控件的时候也只是演示一下控件的制作过程,只用了很短的时间做了一个,只考虑了用鼠标选取,没有考虑键盘操作,我想要添加键盘操作无非重载KeyDown事件,针对Up,Down做一些响应就可以了。可是添加了重载了OnKeyDown事件后,结果和那位朋友所说的一样,没有任何作用,我设了断点,调试了一下,发现KeyDown根本捕获不到Up,Down按键的点击,是什么原因呢,是不是忘记设控件的风格以便让它能够获得焦点?于是,我使用了语句:
SetStyle(ControlStyles.Selectable,
true
);
依然没有效果,当我们在控件上按下Down键的时候,另一个控件获得了焦点。这时Up,Down按钮只是起到了导航的作用就像Tab键一样。
接下来,我在测试工程的窗体上放置了一个ListBox控件做一个对比,其实ToolBox和ListBox在界面表现上有相似之处,就是都有子Item,并且在ListBox上点击Down是起作用的,ListBox并没有失去焦点,这说明这时Up,Down按键没有成为导航键。我想Windows一定是对默认的导航键Up,Down,Left,Right有默认的处理,除非你希望你的控件希望自己处理这些键。用反汇编工具看了一下ListBoxControl控件的源代码,发现一个有趣的函数:
protected
override
bool
IsInputKey(Keys keyData)

{
if ((keyData & Keys.Alt) == Keys.Alt)

{
return false;
}
switch ((keyData & Keys.KeyCode))

{
case Keys.Prior:
case Keys.Next:
case Keys.End:
case Keys.Home:
return true;
}
return base.IsInputKey(keyData);
}
在这里面,ListBoxControl允许Prior,Next,End,Home成为有效的输入键,接着一路跟下去,看看WinForm控件的基类Control的这个函数是如何处理的:
[UIPermission(SecurityAction.InheritanceDemand, Window
=
UIPermissionWindow.AllWindows)]
protected
virtual
bool
IsInputKey(Keys keyData)

{
if ((keyData & Keys.Alt) != Keys.Alt)

{
int num = 4;
switch ((keyData & Keys.KeyCode))

{
case Keys.Left:
case Keys.Up:
case Keys.Right:
case Keys.Down:
num = 5;
break;

case Keys.Tab:
num = 6;
break;
}
if (this.IsHandleCreated)

{
return ((((int) this.SendMessage(0x87, 0, 0)) & num) != 0);
}
}
return false;
}
注意这一行return ((((int) this.SendMessage(0x87, 0, 0)) & num) != 0);0x87是什么windows消息呢,打开WinUser.h文件,发现是WM_GETDLGCODE,在MSDN中的描述是这样的:
The WM_GETDLGCODE message is sent to the window procedure associated with a control. By default, the system handles all keyboard input to the control; the system interprets certain types of keyboard input as dialog box navigation keys. To override this default behavior, the control can respond to the WM_GETDLGCODE message to indicate the types of input it wants to process itself.
也就是说windows用这个消息来判断哪些类型的输入交给控件本身来处理。然后,我注意到,对于方向导航键,函数都给于一个值5与this.SendMessage(0x87, 0, 0))的返回值进行与操作,那么this.SendMessage(0x87, 0, 0))的返回值都可能是什么值呢,WinUser.h中是这样声明的:
/**/
/*
* Dialog Codes
*/
#define
DLGC_WANTARROWS 0x0001 /* Control wants arrow keys */
#define
DLGC_WANTTAB 0x0002 /* Control wants tab keys */
#define
DLGC_WANTALLKEYS 0x0004 /* Control wants all keys */
#define
DLGC_WANTMESSAGE 0x0004 /* Pass message to control */
#define
DLGC_HASSETSEL 0x0008 /* Understands EM_SETSEL message */
#define
DLGC_DEFPUSHBUTTON 0x0010 /* Default pushbutton */
#define
DLGC_UNDEFPUSHBUTTON 0x0020 /* Non-default pushbutton */
#define
DLGC_RADIOBUTTON 0x0040 /* Radio button */
#define
DLGC_WANTCHARS 0x0080 /* Want WM_CHAR messages */
#define
DLGC_STATIC 0x0100 /* Static item: don't include */
#define
DLGC_BUTTON 0x2000 /* Button item: can be checked */
5最贴切的表达就是DLGC_WANTMESSAGE | DLGC_WANTARROWS,也就是将方向键发送给控件处理,对于6呢,也就是DLGC_WANTMESSAGE| DLGC_WANTTAB,将Tab键发送给控件处理。
从这段代码里和控件实际的行为我们可以得出一个结论,那就是,控件本身是不处理方向键和Tab键的,因为他们有默认的行为,也就是支持焦点在窗体的控件之间转换。如果你想要处理这些导航键,那么结论很简单,就是重载IsInputKey方法,它是一个保护类型的虚方法。
在ToolBox控件的代码里重载IsinputKey方法:
protected
override
bool
IsInputKey(Keys keyData)

{
if ((keyData & Keys.Alt) == Keys.Alt)

{
return false;
}
switch ((keyData & Keys.KeyCode))

{
case Keys.Up:
case Keys.Down:
return true;
}
return base.IsInputKey(keyData);

}
当用户点击的键是Up,Down的时候,返回true,这时我们的OnKeyDown方法里就可以捕获到Up,Down的点击事件了。