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

DiagnosticsTextBox:WinForms 的日志窗口

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2020年6月25日

MIT

5分钟阅读

viewsIcon

23281

downloadIcon

709

一个可重用的 Windows 窗体文本框控件,用于捕获 DEBUG 和 TRACE 输出

引言

您是否需要为您的 WinForm 应用程序添加日志窗口?我们创建了一个易于使用的用户控件,可以捕获具有线程支持的应用程序的跟踪信息。

最新的源代码可在 GitHub 获取。

背景

在使用 WinForms 进行应用程序开发时,大多数时候我们都希望在开发过程中以及发布后捕获 System.Diagnostics.DebugSystem.Diagnostics.Trace 的输出,日志在理解用户报告的问题时非常有用。随着应用程序的复杂性增加,这种需求也会增加。对于执行复杂处理的应用程序,日志窗口是一个快速有用的指示器,可以告知用户应用程序正在运行,没有冻结,这样他们就不会随意终止它。

因此,我们决定创建一个自定义控件,只需将其拖放到窗体上即可立即开始记录,而无需重写或复制任何代码行。这些控件名为 DiagnosticsTextBox DiagnosticsRichTextBox ,它们是 CodeArtEng.Diagnostics NuGet 包的一部分。

使用控件

该控件已在 GitHub 上发布,并已在 NuGet 上发布。

  • 将 NuGet 包 CodeArtEng.Diagnostics 包含到您的 WinForm 项目中。
  • DiagnosticsTextBox DiagnosticsRichTextBox 拖到您的项目中。

架构

DiagnosticsTextBox DiagnosticsRichTextBox 的设计相同。我们将基于 DiagnosticsTextBox 描述实现,它显示纯文本,并描述 DiagnosticsRichTextBox 中包含的其他方法和功能,该控件支持颜色格式化。

类概述

  • System.Diagnostcis.TraceListener:为监视跟踪和调试输出的侦听器提供抽象基类。
  • CodeArtEng.Diagnostics.TraceLogger:派生自 TraceListener,实现了 WriteWriteLine 方法。为 DiagnosticsTextBox 提供回调函数。
  • CodeArtEng.Diagnostics.TraceFileWritter:派生自 TraceListener,实现日志记录到文件。此类也可以独立使用。
  • CodeArtEng.Diagnostics.DiagnosticsTextBox:用于显示 DEBUGTRACE 消息的 TextBox 控件。

幕后

捕获和解析消息

TraceLogger 负责处理从 TraceListener 收到的所有消息,处理并将它们传递给 DiagnosticsTextBoxTraceFileWritter。TraceLogger 中实现了三个回调函数:OnMessageReceivedOnWriteOnFlush

类 TraceLogger

public override void Write(string message)
{
    OnMessageReceived(ref message);
    if (string.IsNullOrEmpty(message)) return;

    message = ParseMessage(message);
    OnWrite(message);
    IsNewLine = false;
}

类 DiagnosticsTextBox

private void Tracer_OnMessageReceived(ref string message)
{
    //Message filter implementation.
    if(MessageReceived != null)
    {
        TextEventArgs eArg = new TextEventArgs() { Message = message };
        MessageReceived.Invoke(this, eArg);
        message = eArg.Message;
    }
}

/// <summary>
/// Occur when message is received by Trace Listener.
/// </summary>
[DisplayName("MessageReceived")]
public event EventHandler<TextEventArgs> MessageReceived;

当从 Write WriteLine 接收到消息时,原始消息通过 OnMessageReceived 转发给父类。在 DiagnosticsTextBox 类中,消息然后通过 MessageReceived 事件转发到下一个级别。开发人员可以利用此事件来根据需要响应或过滤消息。

然后,消息由 ParseMessage 处理,以在将消息写入文件或文本框之前对每条传入消息进行格式化,这在 Write WriteLine 方法中完成。

private string ParseMessage(string message)
{
    string dateTimeStr = ShowTimeStamp ? AppendDateTime() : string.Empty;
    string result = IsNewLine ? dateTimeStr : string.Empty;

    if (message.Contains("\r") || message.Contains("\n"))
    {
        //Unified CR, CRLF, LFCR, LF
        message = message.Replace("\n", "\r");
        message = message.Replace("\r\r", "\r");

        string newLineFiller = new string(' ', dateTimeStr.Length);
        string[] multiLineMessage = message.Split('\r');
        result += multiLineMessage[0].Trim() + NewLineDelimiter;
        foreach (string msg in multiLineMessage.Skip(1))
            result += newLineFiller + msg.Trim() + NewLineDelimiter;

        result = result.TrimEnd();
    }
    else result += message;
    return result;
}

如果启用了 ShowTimeStamp ,则在调用 ParseMessage 时会将时间戳附加到消息。时间戳的格式使用 .NET 字符串格式在 TimeStampFormat 属性中定义。

private string AppendDateTime()
{
    switch (TimeStampStyle)
    {
        case TraceTimeStampStyle.DateTimeString: 
             return "[" + DateTime.Now.ToString(TimeStampFormat) + "] ";
        case TraceTimeStampStyle.TickCount: return "[" + DateTime.Now.Ticks.ToString() + "] ";
    }
    return "-";
}

此外,ParseMessage 还负责在添加时间戳时对多行消息进行对齐。通过计算时间戳占用的字符数插入前导空格来实现文本对齐。这种对齐方式在固定宽度字体(如 Courier New)下效果最佳。

在 DiagnosticsTextBox 中显示消息

我们希望文本框尽快用最新消息更新,但又不能阻塞主线程直到主窗体无响应。考虑到 Write WriteLine 可以从主线程或工作线程调用,我们不能直接将消息写入 TextBox,因为 TextBox 只能由 MainThread 更新。

DiagnosticsTextBox 中引入了一个 MessageBuffer 来捕获所有传入的消息,并以定义的计时器间隔更新到文本框控件,该值可通过 RefreshInterval 属性配置。每次计时器滴答时,文本框都会用 MessageBuffer 中的消息更新。添加了一个锁以防止 MessageBuffer 被同时修改。

private void refreshTimer_Tick(object sender, EventArgs e)
{
    lock (LockObject)
    {
        //Transfer from Message Buffer to Diagnostics Text Box without locking main thread.
        if (MessageBuffer.Length == 0) return;
        this.AppendText(MessageBuffer);
        MessageBuffer = "";

内存消耗

我们在部署 DiagnosticTextBox 的第一个版本时遇到的一个问题是内存消耗随时间增加。对于运行数小时甚至数天的应用程序,最终会触发内存溢出异常。然后我们注意到这是由文本框被所有日志消息填满引起的。

为了提高性能并最大限度地减少总内存消耗,我们添加了一个名为 DisplayBufferSize 的属性来定义文本框中显示的最大行数。这是 refreshTimer_Tick 方法的一部分。我们使用 Array.Copy 来最大化性能。用户仍然可以选择将 DisplayBufferSize 设置为 0 以始终显示所有消息。

        if (DisplayBufferSize <= 0) return;

        if (Lines.Length > DisplayBufferSize)
        {
            string[] data = new string[DisplayBufferSize];
            Array.Copy(Lines, Lines.Length - DisplayBufferSize, data, 0, DisplayBufferSize);
            Lines = data;
        }
        SelectionStart = Text.Length;
        ScrollToCaret();
    }
}

彩色格式化

DiagnosticsTextBox DiagnosticsRichTextBox 之间的区别在于后者“丰富”在颜色方面。一个名为 AddFormattingRule 的附加方法,用于根据字典中匹配的字符串定义特定行的字体颜色。

public void AddFormattingRule(string containString, Color color)
{
    if (SyntaxTable.ContainsKey(containString)) return;
    SyntaxTable.Add(containString, color);
}

每当触发 TextChanged 事件时,都会扫描新添加的行,并根据格式规则进行格式化。对于每一行,这可能不是最高效的实现,但它简单易读,无需直接操作富文本框中的 RTF 格式。

请注意,我们使用 LastSelection 来跟踪最后更新的行,以便在下一次 TextChanged 事件发生时跳过已处理过的行。此外,还使用了一个 Updating 标志来防止在 FormatText 方法中更新文本框内容时发生递归调用。

private void DiagnosticsRichTextBox_TextChanged(object sender, EventArgs e)
{
    FormatText();
}

private void FormatText()
{
    if (Updating) return;

    Updating = true;
    try
    {
        int startLine = GetLineFromCharIndex(LastSelection);
        for (int x = startLine; x < Lines.Length; x++)
        {
            int lineStart = GetFirstCharIndexFromLine(x);
            int lineEnd = GetFirstCharIndexFromLine(x + 1) - 1;
            SelectionStart = lineStart; 
            SelectionLength = lineEnd < 0 ? 0 : lineEnd - lineStart + 1;

            if (AutoResetFormat)
                SelectionColor = LastFontColor = ForeColor;
            else
                SelectionColor = LastFontColor;

            foreach (KeyValuePair<string, Color> entry in SyntaxTable)
            {
                if (Lines[x].Contains(entry.Key))
                {
                    SelectionColor = LastFontColor = entry.Value;
                    break;
                }
            }
        }
        SelectionStart = LastSelection = Text.Length;
        ScrollToCaret();
    }
    finally { Updating = false; }
}

不幸的是,没有简单的方法可以从富文本框中删除行而不影响其之前的格式。当富文本框中的行数达到定义的 DisplayBufferSize 时,整个富文本框必须重新扫描并再次更新格式。

if (DisplayBufferSize <= 0) return;

if (Lines.Length > DisplayBufferSize)
{
    string[] data = new string[DisplayBufferSize];
    Array.Copy(Lines, Lines.Length - DisplayBufferSize, data, 0, DisplayBufferSize);
    Lines = data;
    LastSelection = 0;
    FormatText();
}

关注点

我们在开发此控件时面临的一个挑战是,我们不能在任何类中调用 WriteWriteLine,这会导致递归调用。我们只能利用 Visual Studio 中的断点来单步调试代码以识别错误。我们希望这个工具能使所有使用 WinForms 的开发人员受益,从而实现更快的开发和调试。

历史

  • 2020年6月25日:初始版本
© . All rights reserved.