ConsoleWriter
用于向 UI 写入文本的控件

引言
有时能够将信息逐行地传达给用户会很有用。需要提供一个形成跟踪信息列表。大多数开发者都会熟悉的一个例子就是 Visual Studio 在构建、加载、“查找结果”等期间的“输出视图窗格”。
构建此类控件的常用方法是修改常用控件之一,但我认为有一个现成且专为此任务构建的控件会更方便,所以我决定填补我工具箱中的空白,并基于 UserControl
编写一个。它是一个比 GUI 控件更早出现的基本界面设备,并且它仍然是一个有趣且令人愉快的开发任务。
这个控件的第一个主要修订版,主要增加了对剪切和粘贴的支持,还包括一个改进的演示程序和一个后台事件接收器辅助程序,用于构建更健壮和响应更快的控制台主机。这可能对那些构建服务器控制台的人有帮助,因为这类程序可能需要运行很长时间,而对控件管理资源的能力的信心,特别是对可能涉及大量活动的控件,是重要的。有关更多详细信息,请参阅示例应用程序部分。
未来的计划包括持久化,支持将日志保存到文件和自动截断日志到文件。
对控件开发感兴趣的人可能会发现一些控件代码很有用。在这方面,我演示了使用 AutoScroll 结合绘制文本等。这些代码可能有助于那些学习使用 AutoScroll 功能的人。它还演示了剪切和粘贴功能的实现。那些希望通过鼠标或键盘操作字符串的人,希望会发现源代码很有用。
创建和使用 ConsoleWriter 控件
要将该控件集成到你的 C# Forms 项目中,请将文件 ConsoleWriter.cs 添加到你的项目中。你可以引用 Elements.ConsoleWriter
,或者在你的代码文件顶部添加 using Elements;
,这样你就可以只引用该类。
要编写你自己的 ConsoleWriter
成员,你可以这样做:
//MyForm.cs
using Elements;
//declare a variable of type ConsoleWriter
private ConsoleWriter m_ConsoleWriter;
//provide a property accessor (optional)
public ConsoleWriter MyConsoleWriter
{
get
{
return m_ConsoleWriter;
}
private set
{
m_ConsoleWriter = value;
}
}
public class MyForm()
{
//create and add it to the controls
MyConsoleWriter = new ConsoleWriter();
MyConsoleWriter.Dock = DockStyle.Fill;
Controls.Add(MyConsoleWriter);
}
如果你使用窗体设计器,一旦将 ConsoleWriter
添加到你的项目中,你应该会在工具箱中找到它。你可以从那里将其拖放到设计图面上。
ConsoleWriter 方法
该控件派生自 UserControl
,因此继承的功能在相关时适用于 ConsoleWriter
。
ConsoleWriter
本身暴露了四个方法:
Add(string text)
:此方法接受一个string
参数,即你想输出到ConsoleWriter
的文本。
此向控件添加输出的方法的原理是,用户可以发送单行文本,也可以发送包含多行文本。Add
函数解析输入文本中的换行符,并将每一行作为新行处理。有许多方法可以使用 .NET 准备string
s 作为多行。最有用的方法之一是System.Text
中的StringBuilder
。这允许你构建一个string
,并包含一个AppendLine
方法,该方法会在其ToString()
方法提供的string
输出中嵌入换行符。Clear()
:这是一个无参数方法,它指示ConsoleWriter
清空控制台。CopySelectedToClipboard()
:这是一个无参数方法,它指示ConsoleWriter
将选定的文本复制到系统剪贴板。SelectAll()
:这是一个无参数方法,它指示ConsoleWriter
选择控件中的所有文本。
ConsoleWriter 属性
ConsoleWriter
暴露了三个属性:
LineBufferLimit
:为了能够向后滚动查看之前的输出,控件会保留一个行缓冲区。你可以将其设置为一个正整数值以满足你的需求。默认值为 1000。如果将其设置为零或更低,则缓冲区将无限制地增长。调用Clear()
会清空缓冲区。缓冲区是一个SortedDictionary
,它保存控件绘制文本从当前滚动位置所需的所有信息。AutoRemoveCount
:如果LineBufferLimit
大于零(例如,默认的 1000),当达到该限制时,将从缓冲区顶部移除AutoRemoveCount
(默认值为 250)个项目。因此,在默认模式(1000/250)下,一旦达到LineBufferLimit
,缓冲区中将始终有 750 到 1000 个项目。如果你有一个大于零的LineBufferLimit
但AutoRemoveCount
为零,那么一旦达到限制,缓冲区将表现为固定大小的 FIFO 队列。除非需要这种效果,否则不建议这样做,因为列表永远无法进入静止模式。(有关模式的更多信息,请参阅“尾随”部分。)BandColor
:ConsoleWriter
支持交替着色的行,如果选定的颜色足够浅,则会像列表纸一样显示为条带。默认颜色是SystemColors.ControlLight
。背景色默认为SystemColors.Window
,文本颜色默认为SystemColors.WindowText
。如果你想要无
BandColor
的优化,例如纯色背景,则将BandColor
设置为BackColor
值(对控件代码感兴趣的人会注意到 ConsoleWriter.cs 的OnPaintBackground
重写检查此相等性,并且不会尝试绘制相同颜色的条带)。如果需要,请设置替代的
BandColor
值。此代码示例将控件设置为纯显示。//MyForm.cs ... MyConsoleWriter.BandColor = ConsoleWriter.BackColor; ...
派生颜色属性
- 使用
BackColor
属性设置替代背景颜色,同样使用派生属性ForeColor
设置更改文本颜色。//MyForm.cs ... ConsoleWriter.BackColor = System.Drawing.Color.White; ConsoleWriter.BandColor = System.Drawing.Color.MintCream; ConsoleWriter.ForeColor = System.Drawing.Color.Black; ...
Font
:ConsoleWriter
字体默认为 Courier New 8.25,这是一种固定字符宽度的字体,可提供标准的外观和易于阅读的输出。你可以通过在构造时设置Font
属性来为控件选择任何字体,例如,对于由设计器托管的组合,这些属性在 Visual Studio 的属性任务窗格中可用。//MyForm.cs ... MyConsoleWriter.Font = new System.Drawing.Font("Arial", 11.25f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); ...
尾随
ConsoleWriter
有两种模式:静止模式或尾随模式。如果是尾随模式,控件会将最新输出保持在屏幕底部可见,这意味着窗口会自动滚动,并且超出 ClientRectangle
高度的旧行会滚出屏幕。对如何使用与 AutoScroll 相关的函数实现这一点感兴趣的人可以在控件的 InternalRefresh
方法中看到。将检查 AutoScrollMinSize
属性,如果需要,则重置该属性,并且如果控件处于尾随模式,则将 AutoScrollPosition
设置为与最后一行(即最新一行)对应。
当处于静止模式时,控件位于 AutoScroll DisplayRectangle
的固定点 - 这允许程序用户滚动查看当前行缓冲区。
这些功能对控件用户来说相当直观。控件开始时处于尾随模式,因此会显示最新输出并自动滚动。如果用户向上滚动显示,则控件将自动进入静止模式。
当控件向下滚动到列表末尾时,尾随模式将自动重新启用。按 Ctrl+End 键也将重新启用列表的尾随末尾。
命令键和鼠标控制
控件以自然的方式响应使用鼠标和滚动条的滚动。按下的滚动箭头向上或向下以行增量移动显示。滚动框(拇指)的直接操作会像在滚动条轴本身内单击鼠标一样,以标准的滚动行为移动显示。控件也对鼠标滚轮活动正常响应。
可以通过按住鼠标左键(从选择应开始的位置)并移动来选择文本,以确定选择的范围。释放鼠标按钮即可结束选择。文本将保持选中状态,直到收到下一次鼠标单击。
控件开发者可以观察 OnScroll
和 OnMouseWheel
重写,以了解其实现方式。你会注意到 VerticalScroll.SmallChange
被设置为 LineHeight
,以确保可以实现一行增量。
当鼠标滚轮尝试将显示移出列表末尾时,控件将自动进入尾随模式。滚动箭头也是如此。我没有在拇指移动到列表末尾时实现此行为,因为这允许用户在查看列表末尾的值时保留在静止模式。这在控件快速接收值时尤为明显。
控件还响应以下命令键:
- Ctrl-Home 将显示滚动到
DisplayRectangle
的顶部,并使控件保持静止模式。 - Ctrl-End 将显示滚动到
DisplayRectangle
的末尾,并使控件保持尾随模式(单击拇指将停止尾随并进入静止模式)。 - PageUp 和 PageDown 以显示高度为单位滚动显示。如果 PageDown 键尝试移出列表末尾,控件将自动进入尾随模式。
- ArrowUp 和 ArrowDown 以行高度为单位滚动显示。如果 ArrowDown 键尝试移出列表末尾,控件将自动进入尾随模式。
- Ctrl-A 选择控件中的所有文本。
- Ctrl-C 将任何选定的文本复制到
ClipBoard
。
控件开发者可以观察 ProcessCmdKey
重写,以了解其实现方式。
示例应用程序 - ConsoleWriterSample.exe
为了帮助你评估和理解 ConsoleWriter
的工作原理和行为,我创建了一个示例 MDI 应用程序。它的唯一目的是演示一个或多个 ConsoleWriter
的工作。
为了向 ConsoleWriter
提供持续的信息流,应用程序的主窗体有一个计时器。应用程序的子窗口都包含一个 ConsoleWriter
,它们可以订阅主窗体发布的计时器事件。当主窗体接收到计时器的 elapsed 事件时,它会将此事件广播给订阅的子窗口,子窗口再将此信息写入 ConsoleWriter
。为了向 ConsoleWriter
提供额外的测试输出,子窗体还支持两个命令。一个是列出当前环境,另一个是列出当前加载的模块。
你可以通过主窗体选项菜单下的对话框来更改主窗体计时器的间隔周期。你可以在 ConsoleChild Create
方法中以编程方式更改 ConsoleWriter
控件的设置。
在此控件的原始版本中,我将 DoEvents
方法放在 Add
方法中,目的是在多个 ConsoleWriter
高负荷使用时释放 UI。这是因为我发现在使用具有多个 ConsoleWriter
的示例应用程序时,它们都以高频率(例如 100 毫秒间隔)接收消息,UI 可能会冻结或卡住。回想起来,DoEvents
不是一个好主意,因为它可能导致托管程序出现问题,我认为这会损害控件的完整性。它也没有解决我遇到的一个更大问题——从高速事件源断开连接和处理时。尽管进行了 Disposed 和 Disposing 测试,并多次尝试查找健壮的技术,但我仍然遇到了对已处理对象进行调用的异常。
我最终找到了解决这两个问题的方法,在一个我很久以前开发的用于方便托管线程的类中。我使用这个线程托管类作为 Form1
计时器事件的事件接收器,并作为 ConsoleWriter
的事件源。这种技术解决了 UI 问题和事件分离问题。在我测试中,我没有遇到任何高于 100 毫秒的问题(我的测试使用了 9 个打开的 ConsoleWriter
以此速率接收)。下一节将介绍 Thread
类以及应用程序如何使用它来实现更健壮的 ConsoleWriter
设置。
菜单和按钮中使用的位图是公共领域作品,可在 www.famfamfam.com 找到。
EventWaiter 线程辅助类
EventWaiter
是一个基类助手,你可以从中继承并编写自己的线程启动类。有些人可能会发现它有用,作为一个通用的多线程实用类。
EventWaiter
类是一个完全可选的类,你可以在编写极端条件或防御性程序时使用它来实现更健壮的控制台托管环境,或者当你认为在另一个线程上托管事件接收器会帮助你的程序时使用。
如果例如将有许多控制台,并且它们将承受高强度和持续的负载,那么它特别有用。在这种情况下,EventWaiter
类可以帮助 UI 线程保持可用,以便进行用户输入和干预,这对于这些类型的程序很重要。例如,可能需要关闭控制台,但如果 UI 没有响应以允许发出断开连接的命令 - 那么程序将无法正常工作,或者可能根本没有响应。
EventWaiter
类为你提供了一个托管在线程中的事件接收器,用于接收你的外部事件源,同时又为你提供了一个内部事件源,用于你的 ConsoleWriter
。该线程可以安全地关闭,允许你在处理 ConsoleWriter
或附加新事件源之前从源断开连接。
接下来的示例解释了 EventWaiter
在示例应用程序中的用法。
正如示例应用程序说明中所解释的,每个托管 ConsoleWriter
的子 MDI 窗口 (ConsoleChild
) 都可以订阅 Form1
发布的计时器事件。而不是直接在应用程序的 UI 线程中订阅,我们使用一个派生自 EventWaiter
类的类在单独的后台线程上接收这些事件。
在 ConsoleChild.cs 中,事件 OnMenuItemSubscribeClick
接收用户菜单指令以开始接收事件。
ConsoleChild
有一个类型为 Form1EventWaiter
的成员,这是一个派生自 EventWaiter
的线程事件处理程序类。
在 OnMenuItemSubscribeClick
中,如果 Form1EventWaiter
为 null
,则创建订阅。调用 ConsoleChild
的辅助方法 CreateForm1EventWaiter()
来处理创建线程事件处理程序并启动其执行。在 CreateForm1EventWaiter
中,会创建一个 Thread
,其线程启动参数类型为 EventWaiter.EventWaiterThreadStart
。这是线程启动时执行的方法。
在此示例中,线程启动参数 TypeString
被设置为 "SampleConsoleWriter.Form1EventWaiter
"(EventWaiter
派生类),参数 EventWaiterEventHandler
被设置为 ConsoleChild
的处理程序 - OnEventWaiterEvent
,以及 Tag
参数设置为 ConsoleChild
的 MdiParent,当然就是 Form1
- 它提供了 ConsoleChild
要订阅的事件。
如果在 OnMenuItemSubscribeClick
中 Form1EventWaiter
不为 null
,则存在事件订阅,因此会调用 UnSubscribe
。在 UnSubscribe
中,调用 Form1EventWaiter.DetachEventWaiterEvent
并传入 OnEventWaiterEvent
处理程序以进行分离。
现在看一下 OnEventWaiterEvent
。第一个明显的问题是处理程序中的代码不直接处理事件。那是因为我们的事件到达的是一个非主 UI 线程。事件需要通过使用“委托”在 UI 线程上“调用”方法来编组,该委托构成一个调用签名。在这种情况下,会使用事件参数调用 HandleEventWaiterEvent
。
HandleEventWaiterEvent
很重要,不仅是为了处理 Form1
上的订阅事件,也是为了处理运行新线程的一些要求。
在 HandleEventWaiterEvent
中,eventWaiterEventArgs.Message
被切换为其 string
值。线程类在初始化后发送消息 "FirstRun
"。线程已准备好启动,因此 ConsoleWriter
将发送者对象参数强制转换为 Form1EventWaiter
成员,并调用 Form1EventWaiter.Wait();
和 Form1EventWaiter.Go();
来启动事件。
当调用 UnSubscribe
时,事件会分离,并且在 HandleEventWaiterEvent
中收到线程类消息 "Detached
"。这意味着 ConsoleWriter
现在可以安全地调用 Form1EventWaiter.Stop();
和 Form1EventWaiter.Dispose();
线程类消息 "Form1Event
" 在派生的 Form1EventWaiter
类中定义,并且是已订阅的 Form1Event
的到达。在这种情况下,ConsoleWriter
将消息添加到其 ConsoleWriter
。
历史
- 版本 1.0.0 发布:2009 年 1 月 8 日
- 版本 1.0.1 发布:2009 年 1 月 17 日 - 修复了
OnResize
重写代码中可能导致刷新问题的 bug - 版本 2.0.0 发布:2010 年 4 月 11 日 - 添加了剪切和粘贴功能以及后台事件接收器辅助程序