LineEditor 控件 – 基于行的可视化输入/输出
一个用于输出和可选输入的基于行的控件,以及关于如何从 .NET UserControl 创建自定义控件的讨论。
引言
可视化应用程序的常见要求是“ListBox++”——一个基于行的日志,允许对条目进行一些区分。最常见的一种,也是该类第一个版本所满足的,是能够拥有不同颜色的不同行(错误行显示为红色等)。
此控件通过允许行自行渲染并控制自身高度,以通用方式解决了此问题。它提供了一个基本的 Line
类,该类以默认字体和指定颜色显示自身,一个显示包含图像的行的类,以及一个允许用户修改其文本内容的类。它还允许您创建可以具有任何显示形式的自定义行。
它也是一个关于如何通过继承 System.Windows.Forms
中的 UserControl
类来创建有用的自定义控件的研究。要直接跳到如何创建良好控件的信息,请点击此处。
可以在我的游戏大厅客户端中找到该控件的示例用法。游戏列表、游戏信息面板、玩家列表和聊天/日志区域都是 LineEditor
。
用途
该控件以单个 .cs 文件提供,一旦编译成 DLL,就可以以通常方式添加到 Visual Studio、SharpDevelop 等中,作为自定义控件出现,您可以将其拖放到窗体上。或者,您可以使用构造函数手动添加控件
LineEditor lineEditor = new LineEditor();
someContainer.Controls.Add(lineEditor);
该控件公开了几个属性,允许您自定义视觉效果及其与用户的交互方式
Editable
:用户是否可以编辑接受用户输入的行。默认为false
。ShowSelection
:是否使用当前 Windows 选择颜色突出显示选定行。无论开或关,选定行周围都会放置一个虚线矩形来标识它。默认为true
。BottomAligned
:添加新行时是否调整滚动,以保持与控件底部的相同偏移量。这对于日志或命令提示符样式的用法很有用,其中最后一个项目应在大部分时间可见。默认为true
。Selectable
:用户是否可以选择控件。
当然,您也可以使用 UserControl
提供的所有标准颜色和样式选项。
该控件最简单的用法是仅使用 Line
类以编程方式添加行。如果您希望添加默认行(仅包含单一颜色的文本),可以使用 AddLine
方法
lineEditor.AddLine(Color.Blue, "A test line"); // the simplest way
lineEditor.AddLine(Color.Green, "With some data", new int[]{3, 4}); // attach data
您可以将不被控件使用的任意数据附加到行,但您可以将其用于存储您自己的行信息。
要添加其他类型的行,您可以使用 Lines
属性,它是一个 LineCollection
。(如果我不是很久以前在 .NET 1.1 下编写了这个类,这就会简单地是一个 List<line>
。)与所有集合一样,它提供了接受 Line
实例的 Add
、Insert
和 Remove
方法
lineEditor.Lines.Add(new Line(Color.Blue, "Another test"));
lineEditor.Lines.Add(new ImageLine(Image.FromFile("test.png")));
// remember to set Editable to true
lineEditor.Lines.Add(new EditableLine(Color.Green, "Editable text"));
lineEditor.Lines.Insert(new Line(Color.Black, "At the front"), 0);
除了简单的文本 Line
,还提供了 ImageLine
(将显示 Image
的任何实例)和 EditableLine
(用户可以修改其文本)。
自定义行
许多您可能希望生成的专业行可以通过简单地创建 Bitmap
、在其上绘图并使用 ImageLine
来实现。但是,如果您希望生成交互式或复杂的行,您可以从 Line
类继承并修改其行为。EditableLine
类是一个很好的指南,因为它是一个相对复杂的类,修改了 Line
的许多方面。以下是您需要重写的方法
public virtual void Paint(Graphics g, Font font, Color c, int ypos)
public virtual int GetHeight(Font font)
public virtual Font GetFont(Font font)
public virtual Line CopyTo(Line li)
和public virtual object Clone()
这是最重要的虚拟方法,它绘制行。您会得到一个用于绘制的字体,与此行关联的颜色(如果选中并且 ShowSelection
为 true
,则可能是 Windows 选择突出显示颜色),以及此行开始的 Y 位置。通常不应在 ypos
上方或 ypos+GetHeight(font)
下方绘制。建议您观察 protected indent
成员作为您使用的最小 X 坐标,以确保您的行与控件中的其他行对齐。
给定一个用于绘制的字体,返回行的高度。默认情况下,返回该字体设置下文本所占用的空间。
给定控件请求的字体,选择一个用于绘制的字体。覆盖此方法允许您使用与其它行不同的字体绘制一行。
允许行被克隆。将您添加到自定义类的任何属性添加到 CopyTo()
,并以与 EditableLine
类似的方式重写 Clone
public override object Clone(){ return CopyTo(new MyCustomLine(Color, Text)); }
public virtual void InsertText(string text)
定义文本如何插入。默认情况下,它被附加。
您可以随时查看和修改 Text
属性。如果您希望您的行响应用户输入,还有一些您需要重写的方法
public virtual bool OnKeyDown(KeyEventArgs e){return false;}
public virtual bool OnKeyUp(KeyEventArgs e){return false;}
public virtual bool OnKeyPress(KeyPressEventArgs e){return false;}
如果您已处理按键并希望抑制其可能引起的默认操作,请从这些方法返回 true
。请记住,您需要将控件的 Editable
属性设置为 true
才能获取按键事件。鼠标方法将在后续版本中以类似模式添加。
如果您想提供用户可以编辑的行,您可能希望继承自 EditableLine
而不是 Line
。
编写用户控件
这个控件直接继承自 UserControl
,对我来说,了解如何从这个基类编写一个可用的控件非常有启发性。.NET 使这相对容易,但有一些地方让我感到有点困难。
入门
要做的第一件事是通过覆盖 OnPaint
来绘制你的控件。每次你的控件需要绘制时都会调用它,所以我尝试通过缓存 Pen
和 Brush
来提高效率。你还需要创建一个 InitializeComponent
方法(必须使用美式拼写),以确保该控件可以作为自定义控件出现在你的 IDE 中,并在构造函数中调用它。在这个方法中,你应该放置
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.DoubleBuffer | ControlStyles.Selectable |
ControlStyles.StandardClick , true)
请查看 Control.SetStyle
的文档,以确定您确切需要的样式,但 UserPaint
和 Selectable
对于大多数控件来说是必不可少的。
如果您正在创建一个绘图可能很复杂的控件,您可能会考虑缓存整个要绘制的图像,并在需要时单独更新它。我在我的地质绘图器的地图控件中使用了这种技术,它可能需要在复杂的地图上绘制数千个贝塞尔段,在我的 Abaria 游戏查看器中,它必须绘制数百个带光照的 3D 三角形。但对于大多数控件,例如这个控件,您可以轻松地在 Paint
中执行所有计算。
鼠标交互
为了让用户与您的控件交互,您需要对鼠标做出反应。通常,您会希望使用鼠标按下选择一个项目,并可能使用鼠标移动拖动项目或显示悬停高亮(但请记住,这样做会强制进行多次重绘,因此请确保您的 Paint
代码高效)。
您需要重写的方法是
protected override void OnMouseDown(MouseEventArgs e)
protected override void OnMouseUp(MouseEventArgs e) // maybe
protected override void OnMouseMove(MouseEventArgs e) // maybe
典型的鼠标处理程序就像 LineEditor
中的这个一样
protected override void OnMouseDown(MouseEventArgs e){
base.OnMouseDown(e);
if(!Selectable) return;
Focus();
SelectedIndex = GetIndexAt(e.X, e.Y);
Invalidate();
}
您应该始终调用 base.Whatever
(e),因为不这样做会导致默认行为不会发生,并且可能会发生奇怪的事情。GetIndexAt
方法是一个常见的需求,它返回鼠标下方项目的索引。
要实现拖动,您将在 OnMouseDown/Up
中设置一个开关,并在鼠标按下时在 OnMouseMove
中执行拖动。由于此控件中未实现该技术,因此我没有 C# 示例。
设置 StandardClick
样式(参见“第一步”)会导致触发 Click
和 DoubleClick
事件,因此通过在鼠标按下时设置选定项目,您可以使这些事件变得有用。
键盘
乍一看,使用键盘也很容易。与鼠标一样,需要重写三个方法才能获得键盘功能
protected override void OnKeyDown(KeyEventArgs e)
protected override void OnKeyUp(KeyEventArgs e)
protected override void OnKeyPress(KeyPressEventArgs e)
KeyPress
用于可打印字符,以及不知何故的退格键 (8) 和回车键 (13)。KeyDown
和 KeyUp
适用于所有按键(如果按键持续按下,KeyDown
会重复),并告知您按键和修饰符(Shift、Alt、Ctrl)。
然而,为了接受所有相关的键和字符,您需要阻止框架“劫持”它们用于控制助记符(那些带下划线的字母,您可以通过 Alt 键激活一个控件)。为此,您需要重写另外两个方法
protected override bool IsInputKey(Keys k)
protected override bool IsInputChar(char c)
给定的键是否由本控件处理。如果您希望在 OnKeyUp
/Down
中看到此键,则返回 true
。如果您不想以不同于其正常状态的方式处理该键,则应始终 return base.IsInputKey(k)
。在 LineEditor
的情况下,箭头键返回 true
。
给定字符是否应由控件处理。如果您希望在 OnKeyPress
中看到此键,则返回 true
——如果您不这样做,它可能会被控制助记符劫持。对于像这样的文本控件,您总是希望从此方法返回 true
。
一旦能够处理按键事件,控件内的导航就留给读者作为练习了——尽管您可能会从 EditableLine.KeyDown
方法中获得一些灵感。重要的一点是跟踪光标位置,以便您可以在正确的位置插入、删除和修改文本。为了测量部分字符串以正确放置插入符号,您应该使用 Graphics.MeasureCharacterRanges
方法(不是 Graphics.MeasureString
),就像 EditableLine.Paint
中的这个摘录一样
StringFormat sf = new StringFormat();
sf.SetMeasurableCharacterRanges (
new CharacterRange[]{ new CharacterRange(0, ci) } );
sf.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
cx = indent + (ci > 0 ? g.MeasureCharacterRanges(
s, font,
new RectangleF(0, 0, Host.Parent.ClientSize.Width, font.Height), sf
)[0].GetBounds(g).Right : 0);
滚动
另一个常见要求是当控件变得太小或其内容对于可用空间来说太大时,会出现滚动条。我的技术是在类中包含一个 ScrollBar
实例,该实例已适当地停靠(在本例中为 DockStyle.Right
),并在需要时显示。
// in InitializeComponent
scrollbar = new VScrollBar();
scrollbar.Dock = DockStyle.Right;
scrollbar.Visible = false;
scrollbar.Scroll += new ScrollEventHandler(ScrollbarMoved);
this.Controls.Add(scrollbar);
每当可能影响滚动条的事物发生变化时(在 OnResize
中,或修改控件内容时;在本例中,通过添加或删除行),您都应该调用一个 RecalculateScrollbar
方法,该方法类似于 LineEditor
中的这个
private void RecalculateScrollbar(){
int bottom = 0;
foreach(Line line in lines) bottom += line.GetHeight(Font);
if(bottom < ClientSize.Height){
scrollbar.Visible = false;
scrollbar.Value = 0;
return;
}
scrollbar.Visible = true;
scrollbar.Maximum = bottom;
scrollbar.LargeChange = ClientSize.Height;
scrollbar.SmallChange = Font.Height;
lastSBValue = scrollbar.Value;
}
拥有一个 VisibleWidth
属性也很有用
public int VisibleWidth {
get { return ClientSize.Width - (scrollbar.Visible ? scrollbar.Width : 0); }
}
...您可以在计算何时换行、何时添加水平滚动条等方面使用它。
滚动条的事件处理程序通常只需调用 Invalidate
来强制控件重绘;应在 Paint
中检查滚动条的值以在正确位置绘制项目。
本例处理垂直滚动条(可能是最常见的),但水平滚动条非常相似。唯一需要注意的是,如果您同时拥有两个滚动条,则应将右下角的“死区”涂成背景色并缩短两个滚动条。