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

ConsoleWriter

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.38/5 (9投票s)

2009年1月8日

CPOL

14分钟阅读

viewsIcon

30753

downloadIcon

478

用于向 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 准备 strings 作为多行。最有用的方法之一是 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 个项目。如果你有一个大于零的 LineBufferLimitAutoRemoveCount 为零,那么一旦达到限制,缓冲区将表现为固定大小的 FIFO 队列。除非需要这种效果,否则不建议这样做,因为列表永远无法进入静止模式。(有关模式的更多信息,请参阅“尾随”部分。)
  • BandColorConsoleWriter 支持交替着色的行,如果选定的颜色足够浅,则会像列表纸一样显示为条带。默认颜色是 SystemColors.ControlLight。背景色默认为 SystemColors.Window,文本颜色默认为 SystemColors.WindowText

    如果你想要无 BandColor 的优化,例如纯色背景,则将 BandColor 设置为 BackColor 值(对控件代码感兴趣的人会注意到 ConsoleWriter.csOnPaintBackground 重写检查此相等性,并且不会尝试绘制相同颜色的条带)。

    如果需要,请设置替代的 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;
    ...
  • FontConsoleWriter 字体默认为 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 键也将重新启用列表的尾随末尾。

命令键和鼠标控制

控件以自然的方式响应使用鼠标和滚动条的滚动。按下的滚动箭头向上或向下以行增量移动显示。滚动框(拇指)的直接操作会像在滚动条轴本身内单击鼠标一样,以标准的滚动行为移动显示。控件也对鼠标滚轮活动正常响应。

可以通过按住鼠标左键(从选择应开始的位置)并移动来选择文本,以确定选择的范围。释放鼠标按钮即可结束选择。文本将保持选中状态,直到收到下一次鼠标单击。

控件开发者可以观察 OnScrollOnMouseWheel 重写,以了解其实现方式。你会注意到 VerticalScroll.SmallChange 被设置为 LineHeight,以确保可以实现一行增量。

当鼠标滚轮尝试将显示移出列表末尾时,控件将自动进入尾随模式。滚动箭头也是如此。我没有在拇指移动到列表末尾时实现此行为,因为这允许用户在查看列表末尾的值时保留在静止模式。这在控件快速接收值时尤为明显。

控件还响应以下命令键:

  • Ctrl-Home 将显示滚动到 DisplayRectangle 的顶部,并使控件保持静止模式。
  • Ctrl-End 将显示滚动到 DisplayRectangle 的末尾,并使控件保持尾随模式(单击拇指将停止尾随并进入静止模式)。
  • PageUpPageDown 以显示高度为单位滚动显示。如果 PageDown 键尝试移出列表末尾,控件将自动进入尾随模式。
  • ArrowUpArrowDown 以行高度为单位滚动显示。如果 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 中,如果 Form1EventWaiternull,则创建订阅。调用 ConsoleChild 的辅助方法 CreateForm1EventWaiter() 来处理创建线程事件处理程序并启动其执行。在 CreateForm1EventWaiter 中,会创建一个 Thread,其线程启动参数类型为 EventWaiter.EventWaiterThreadStart。这是线程启动时执行的方法。

在此示例中,线程启动参数 TypeString 被设置为 "SampleConsoleWriter.Form1EventWaiter"(EventWaiter 派生类),参数 EventWaiterEventHandler 被设置为 ConsoleChild 的处理程序 - OnEventWaiterEvent,以及 Tag 参数设置为 ConsoleChild 的 MdiParent,当然就是 Form1 - 它提供了 ConsoleChild 要订阅的事件。

如果在 OnMenuItemSubscribeClickForm1EventWaiter 不为 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 日 - 添加了剪切和粘贴功能以及后台事件接收器辅助程序
© . All rights reserved.