【问题标题】:Elegant Log Window in WinForms C#WinForms C# 中的优雅日志窗口
【发布时间】:2011-01-12 20:35:03
【问题描述】:

我正在寻找有关为 Windows 窗体应用程序实现日志窗口的有效方法的想法。过去我已经使用 TextBox 和 RichTextBox 实现了几个,但我仍然对这些功能并不完全满意。

此日志旨在为用户提供各种事件的最近历史记录,主要用于数据收集应用程序,其中人们可能会好奇特定事务是如何完成的。在这种情况下,日志不需要是永久的,也不需要保存到文件中。

首先,提出一些要求:

  • 高效快捷;如果数百行连续快速写入日志,则需要消耗最少的资源和时间。
  • 能够提供多达 2000 行左右的可变回滚。没有必要再长了。
  • 突出显示和颜色是首选。不需要字体效果。
  • 达到回滚限制时自动修剪线条。
  • 添加新数据时自动滚动。
  • 奖励但不是必需的:在手动交互期间暂停自动滚动,例如用户正在浏览历史记录。

到目前为止我一直在使用什么来编写和修剪日志:

我使用以下代码(我从其他线程调用):

// rtbLog is a RichTextBox
// _MaxLines is an int
public void AppendLog(string s, Color c, bool bNewLine)
{
    if (rtbLog.InvokeRequired)
    {
        object[] args = { s, c, bNewLine };
        rtbLog.Invoke(new AppendLogDel(AppendLog), args);
        return;
    }
    try
    {
        rtbLog.SelectionColor = c;
        rtbLog.AppendText(s);
        if (bNewLine) rtbLog.AppendText(Environment.NewLine);
        TrimLog();
        rtbLog.SelectionStart = rtbLog.TextLength;
        rtbLog.ScrollToCaret();
        rtbLog.Update();
    }
    catch (Exception exc)
    {
        // exception handling
    }
}

private void TrimLog()
{
    try
    {
        // Extra lines as buffer to save time
        if (rtbLog.Lines.Length < _MaxLines + 10)
        {
            return;
        }
        else
        {
            string[] sTemp = rtxtLog.Lines;
            string[] sNew= new string[_MaxLines];
            int iLineOffset = sTemp.Length - _MaxLines;
            for (int n = 0; n < _MaxLines; n++)
            {
                sNew[n] = sTemp[iLineOffset];
                iLineOffset++;
            }
            rtbLog.Lines = sNew;
        }
    }
    catch (Exception exc)
    {
        // exception handling
    }
}

这种方法的问题是,每当调用 TrimLog 时,我都会丢失颜色格式。使用常规的 TextBox 就可以了(当然需要稍作修改)。

对此问题的解决方案的搜索从未真正令人满意。有些人建议通过字符数而不是 RichTextBox 中的行数来修剪多余的部分。我也见过使用 ListBox,但没有成功尝试过。

【问题讨论】:

  • 那,我有过 RTF 导致线程同步崩溃的经验,当我需要它时,SyncLock 无法阻止它。 +1
  • 嗨 JYelton。我知道我可以发布一个问题,但如果您能按照此处的建议与我分享您的日志窗口实现的工作,那将是非常有帮助的 [stackoverflow.com/a/2196198/5588347]
  • @AshishSrivastava 很多年前了。本质上,日志组件使用队列作为循环缓冲区来存储文本行(日志类对象)。计时器定期修改 RichTextBox 的内容以显示队列中的所有行。日志类对象包含文本行、时间戳和颜色。我必须搜索才能找到代码,但如果我有时间我会这样做。
  • @JYelton:非常感谢。我很感激你的建议并尝试解决它。但是,如果我可以使用代码参考,它仍然会非常有帮助。
  • @AshishSrivastava 我已经发布了一个答案,它提供了代码并解释了我使用的基本实现。

标签: c# winforms logging


【解决方案1】:

我会说 ListView 非常适合这个(在细节查看模式下),它正是我在一些内部应用程序中使用它的目的。

有用的提示:如果您知道要一次添加/删除大量项目,请使用 BeginUpdate() 和 EndUpdate()。

【讨论】:

  • 这也是我用的。我以相反的顺序编写日志事件,在顶部插入并从底部拉出。还捕获任何鼠标向下或滚动事件,以便在更新通过时不会与您的用户发生冲突。如果您的日志消息比窗口长,您可以使用工具提示来显示额外内容 (stackoverflow.com/questions/192584/…) 或在列表框旁边添加一个单独的文本框来保存详细信息。
  • 一个问题是 BeginUpdate()/EndUpdate() 的使用,因为发送日志事件的程序部分不一定“知道”是否会有大量事件。这并不是说它们无法实现,例如围绕包含所有日志生成事件的 for 循环。
  • 我以前用过ListBox,只是尝试了ListView。它似乎运行得更快且不那么古怪,所以我可以从现在开始使用它,虽然我确实喜欢 ListBox 具有可变高度或所有者绘制项目的能力,ListView 可以做这样的事情吗?
【解决方案2】:

如果您想要突出显示和颜色格式,我建议使用 RichTextBox。

如果要自动滚动,请使用 ListBox。

在任何一种情况下,都将其绑定到行的循环缓冲区。

【讨论】:

  • RTF 有他所说的问题,并且 ListBox 不启用颜色编码。那我不喜欢不能在 RTF 或文本框中尽可能高亮和复制多行。
  • Aequitarum 提出的一个优点是能够从日志中复制。似乎各种 UI 控件各有利弊。 RTF 是当前的实现并且运行良好,但修剪它不是直截了当的。
【解决方案3】:

我建议您根本不要使用控件作为日志。而是编写一个日志 collection 类,它具有您想要的属性(不包括显示属性)。

然后编写将集合转储到各种用户界面元素所需的少量代码。就个人而言,我会将SendToEditControlSendToListBox 方法放入我的日志记录对象中。我可能会为这些方法添加过滤功能。

您可以根据需要尽可能频繁地更新 UI 日志,从而为您提供最佳性能,更重要的是,当日志快速更改时,您可以减少 UI 开销。

重要的是不要将您的日志记录绑定到一个 UI,这是一个错误。有一天你可能想无头运行。

从长远来看,一个好的记录器用户界面可能是一个自定义控件。但在短期内,您只想断开您的日志记录与任何特定 UI 片段。

【讨论】:

  • 我喜欢这个建议,因为我考虑过将日志事件写入自定义类,并让它定期更新 UI 控件,这样可以更好地处理大量事件发生的情况一下子写的。 +1
  • 我认为“分离”部分是给定的。
  • @Neil:根据我的经验,大多数新手似乎都将控件用作数据存储,后来才意识到这是一个错误,到那时就很难放松了。跨度>
  • 谢谢约翰,我写了一个日志类,现在使用它,效果很好。如果您或其他任何人有兴趣看到它,我很乐意分享(并欢迎任何批评和 cmets)。
  • 如果新手看到这个记录器类将如何实现的示例代码,将会有很大的帮助。任何人都可以做这个荣誉吗?
【解决方案4】:

我最近实现了类似的东西。我们的方法是保留回滚记录的环形缓冲区,然后手动绘制日志文本(使用 Graphics.DrawString)。然后,如果用户想要向后滚动、复制文本等,我们有一个“暂停”按钮,它可以翻转回普通的 TextBox 控件。

【讨论】:

  • 另一个好建议,我想知道创建自定义或继承的 UI 控件是否比尝试使用现有控件更容易。我看到的唯一问题是,在“暂停”期间,颜色格式会在自动滚动暂停时丢失。
【解决方案5】:

这是我根据我不久前写的一个更复杂的记录器汇总的东西。

这将支持基于日志级别的列表框颜色,支持 Ctrl+V 和右键单击以复制为 RTF,并处理从其他线程到列表框的日志记录。

您可以使用构造函数重载之一覆盖 ListBox 中保留的行数(默认为 2000)以及消息格式。

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Text;

namespace StackOverflow
{
    public partial class Main : Form
    {
        public static ListBoxLog listBoxLog;
        public Main()
        {
            InitializeComponent();

            listBoxLog = new ListBoxLog(listBox1);

            Thread thread = new Thread(LogStuffThread);
            thread.IsBackground = true;
            thread.Start();
        }

        private void LogStuffThread()
        {
            int number = 0;
            while (true)
            {
                listBoxLog.Log(Level.Info, "A info level message from thread # {0,0000}", number++);
                Thread.Sleep(2000);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Debug, "A debug level message");
        }
        private void button2_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Verbose, "A verbose level message");
        }
        private void button3_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Info, "A info level message");
        }
        private void button4_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Warning, "A warning level message");
        }
        private void button5_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Error, "A error level message");
        }
        private void button6_Click(object sender, EventArgs e)
        {
            listBoxLog.Log(Level.Critical, "A critical level message");
        }
        private void button7_Click(object sender, EventArgs e)
        {
            listBoxLog.Paused = !listBoxLog.Paused;
        }
    }

    public enum Level : int
    {
        Critical = 0,
        Error = 1,
        Warning = 2,
        Info = 3,
        Verbose = 4,
        Debug = 5
    };
    public sealed class ListBoxLog : IDisposable
    {
        private const string DEFAULT_MESSAGE_FORMAT = "{0} [{5}] : {8}";
        private const int DEFAULT_MAX_LINES_IN_LISTBOX = 2000;

        private bool _disposed;
        private ListBox _listBox;
        private string _messageFormat;
        private int _maxEntriesInListBox;
        private bool _canAdd;
        private bool _paused;

        private void OnHandleCreated(object sender, EventArgs e)
        {
            _canAdd = true;
        }
        private void OnHandleDestroyed(object sender, EventArgs e)
        {
            _canAdd = false;
        }
        private void DrawItemHandler(object sender, DrawItemEventArgs e)
        {
            if (e.Index >= 0)
            {
                e.DrawBackground();
                e.DrawFocusRectangle();

                LogEvent logEvent = ((ListBox)sender).Items[e.Index] as LogEvent;

                // SafeGuard against wrong configuration of list box
                if (logEvent == null)
                {
                    logEvent = new LogEvent(Level.Critical, ((ListBox)sender).Items[e.Index].ToString());
                }

                Color color;
                switch (logEvent.Level)
                {
                    case Level.Critical:
                        color = Color.White;
                        break;
                    case Level.Error:
                        color = Color.Red;
                        break;
                    case Level.Warning:
                        color = Color.Goldenrod;
                        break;
                    case Level.Info:
                        color = Color.Green;
                        break;
                    case Level.Verbose:
                        color = Color.Blue;
                        break;
                    default:
                        color = Color.Black;
                        break;
                }

                if (logEvent.Level == Level.Critical)
                {
                    e.Graphics.FillRectangle(new SolidBrush(Color.Red), e.Bounds);
                }
                e.Graphics.DrawString(FormatALogEventMessage(logEvent, _messageFormat), new Font("Lucida Console", 8.25f, FontStyle.Regular), new SolidBrush(color), e.Bounds);
            }
        }
        private void KeyDownHandler(object sender, KeyEventArgs e)
        {
            if ((e.Modifiers == Keys.Control) && (e.KeyCode == Keys.C))
            {
                CopyToClipboard();
            }
        }
        private void CopyMenuOnClickHandler(object sender, EventArgs e)
        {
            CopyToClipboard();
        }
        private void CopyMenuPopupHandler(object sender, EventArgs e)
        {
            ContextMenu menu = sender as ContextMenu;
            if (menu != null)
            {
                menu.MenuItems[0].Enabled = (_listBox.SelectedItems.Count > 0);
            }
        }

        private class LogEvent
        {
            public LogEvent(Level level, string message)
            {
                EventTime = DateTime.Now;
                Level = level;
                Message = message;
            }

            public readonly DateTime EventTime;

            public readonly Level Level;
            public readonly string Message;
        }
        private void WriteEvent(LogEvent logEvent)
        {
            if ((logEvent != null) && (_canAdd))
            {
                _listBox.BeginInvoke(new AddALogEntryDelegate(AddALogEntry), logEvent);
            }
        }
        private delegate void AddALogEntryDelegate(object item);
        private void AddALogEntry(object item)
        {
            _listBox.Items.Add(item);

            if (_listBox.Items.Count > _maxEntriesInListBox)
            {
                _listBox.Items.RemoveAt(0);
            }

            if (!_paused) _listBox.TopIndex = _listBox.Items.Count - 1;
        }
        private string LevelName(Level level)
        {
            switch (level)
            {
                case Level.Critical: return "Critical";
                case Level.Error: return "Error";
                case Level.Warning: return "Warning";
                case Level.Info: return "Info";
                case Level.Verbose: return "Verbose";
                case Level.Debug: return "Debug";
                default: return string.Format("<value={0}>", (int)level);
            }
        }
        private string FormatALogEventMessage(LogEvent logEvent, string messageFormat)
        {
            string message = logEvent.Message;
            if (message == null) { message = "<NULL>"; }
            return string.Format(messageFormat,
                /* {0} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
                /* {1} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss"),
                /* {2} */ logEvent.EventTime.ToString("yyyy-MM-dd"),
                /* {3} */ logEvent.EventTime.ToString("HH:mm:ss.fff"),
                /* {4} */ logEvent.EventTime.ToString("HH:mm:ss"),

                /* {5} */ LevelName(logEvent.Level)[0],
                /* {6} */ LevelName(logEvent.Level),
                /* {7} */ (int)logEvent.Level,

                /* {8} */ message);
        }
        private void CopyToClipboard()
        {
            if (_listBox.SelectedItems.Count > 0)
            {
                StringBuilder selectedItemsAsRTFText = new StringBuilder();
                selectedItemsAsRTFText.AppendLine(@"{\rtf1\ansi\deff0{\fonttbl{\f0\fcharset0 Courier;}}");
                selectedItemsAsRTFText.AppendLine(@"{\colortbl;\red255\green255\blue255;\red255\green0\blue0;\red218\green165\blue32;\red0\green128\blue0;\red0\green0\blue255;\red0\green0\blue0}");
                foreach (LogEvent logEvent in _listBox.SelectedItems)
                {
                    selectedItemsAsRTFText.AppendFormat(@"{{\f0\fs16\chshdng0\chcbpat{0}\cb{0}\cf{1} ", (logEvent.Level == Level.Critical) ? 2 : 1, (logEvent.Level == Level.Critical) ? 1 : ((int)logEvent.Level > 5) ? 6 : ((int)logEvent.Level) + 1);
                    selectedItemsAsRTFText.Append(FormatALogEventMessage(logEvent, _messageFormat));
                    selectedItemsAsRTFText.AppendLine(@"\par}");
                }
                selectedItemsAsRTFText.AppendLine(@"}");
                System.Diagnostics.Debug.WriteLine(selectedItemsAsRTFText.ToString());
                Clipboard.SetData(DataFormats.Rtf, selectedItemsAsRTFText.ToString());
            }

        }

        public ListBoxLog(ListBox listBox) : this(listBox, DEFAULT_MESSAGE_FORMAT, DEFAULT_MAX_LINES_IN_LISTBOX) { }
        public ListBoxLog(ListBox listBox, string messageFormat) : this(listBox, messageFormat, DEFAULT_MAX_LINES_IN_LISTBOX) { }
        public ListBoxLog(ListBox listBox, string messageFormat, int maxLinesInListbox)
        {
            _disposed = false;

            _listBox = listBox;
            _messageFormat = messageFormat;
            _maxEntriesInListBox = maxLinesInListbox;

            _paused = false;

            _canAdd = listBox.IsHandleCreated;

            _listBox.SelectionMode = SelectionMode.MultiExtended;

            _listBox.HandleCreated += OnHandleCreated;
            _listBox.HandleDestroyed += OnHandleDestroyed;
            _listBox.DrawItem += DrawItemHandler;
            _listBox.KeyDown += KeyDownHandler;

            MenuItem[] menuItems = new MenuItem[] { new MenuItem("Copy", new EventHandler(CopyMenuOnClickHandler)) };
            _listBox.ContextMenu = new ContextMenu(menuItems);
            _listBox.ContextMenu.Popup += new EventHandler(CopyMenuPopupHandler);

            _listBox.DrawMode = DrawMode.OwnerDrawFixed;
        }

        public void Log(string message) { Log(Level.Debug, message); }
        public void Log(string format, params object[] args) { Log(Level.Debug, (format == null) ? null : string.Format(format, args)); }
        public void Log(Level level, string format, params object[] args) { Log(level, (format == null) ? null : string.Format(format, args)); }
        public void Log(Level level, string message)
        {
            WriteEvent(new LogEvent(level, message));
        }

        public bool Paused
        {
            get { return _paused; }
            set { _paused = value; }
        }

        ~ListBoxLog()
        {
            if (!_disposed)
            {
                Dispose(false);
                _disposed = true;
            }
        }
        public void Dispose()
        {
            if (!_disposed)
            {
                Dispose(true);
                GC.SuppressFinalize(this);
                _disposed = true;
            }
        }
        private void Dispose(bool disposing)
        {
            if (_listBox != null)
            {
                _canAdd = false;

                _listBox.HandleCreated -= OnHandleCreated;
                _listBox.HandleCreated -= OnHandleDestroyed;
                _listBox.DrawItem -= DrawItemHandler;
                _listBox.KeyDown -= KeyDownHandler;

                _listBox.ContextMenu.MenuItems.Clear();
                _listBox.ContextMenu.Popup -= CopyMenuPopupHandler;
                _listBox.ContextMenu = null;

                _listBox.Items.Clear();
                _listBox.DrawMode = DrawMode.Normal;
                _listBox = null;
            }
        }
    }
}

【讨论】:

  • 你将如何修改它,以便长消息被包装到下一行而不是被截断?
【解决方案6】:

当我想使用 RichTextBox 再次记录彩色线条时,我会将其存储在这里作为对 Future Me 的帮助。以下代码删除 RichTextBox 中的第一行:

if ( logTextBox.Lines.Length > MAX_LINES )
{
  logTextBox.Select(0, logTextBox.Text.IndexOf('\n')+1);
  logTextBox.SelectedRtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1053\\uc1 }";
}

我花了很长时间才发现将 SelectedRtf 设置为 "" 不起作用,但将其设置为没有文本内容的“正确”RTF 是可以的。

【讨论】:

  • 我喜欢这个答案,因为它实际上解决了原始问题,这对于想知道如何在可能与日志记录无关的富文本框上执行此操作的人来说非常重要。跨度>
【解决方案7】:

我创建基本日志窗口的解决方案与John Knoeller 在他的回答中建议的完全一样。避免将日志信息直接存储在 TextBox 或 RichTextBox 控件中,而是创建一个可用于填充控件或写入文件等的日志记录类。

这个示例解决方案有几个部分:

  1. 日志记录类本身,Logger
  2. 修改 RichTextBox 控件以在更新后添加滚动到底部功能; ScrollingRichTextBox
  3. 演示其使用的主要形式,LoggerExample

一、日志类:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;

namespace Logger
{
    /// <summary>
    /// A circular buffer style logging class which stores N items for display in a Rich Text Box.
    /// </summary>
    public class Logger
    {
        private readonly Queue<LogEntry> _log;
        private uint _entryNumber;
        private readonly uint _maxEntries;
        private readonly object _logLock = new object();
        private readonly Color _defaultColor = Color.White;

        private class LogEntry
        {
            public uint EntryId;
            public DateTime EntryTimeStamp;
            public string EntryText;
            public Color EntryColor;
        }

        private struct ColorTableItem
        {
            public uint Index;
            public string RichColor;
        }

        /// <summary>
        /// Create an instance of the Logger class which stores <paramref name="maximumEntries"/> log entries.
        /// </summary>
        public Logger(uint maximumEntries)
        {
            _log = new Queue<LogEntry>();
            _maxEntries = maximumEntries;
        }

        /// <summary>
        /// Retrieve the contents of the log as rich text, suitable for populating a <see cref="System.Windows.Forms.RichTextBox.Rtf"/> property.
        /// </summary>
        /// <param name="includeEntryNumbers">Option to prepend line numbers to each entry.</param>
        public string GetLogAsRichText(bool includeEntryNumbers)
        {
            lock (_logLock)
            {
                var sb = new StringBuilder();

                var uniqueColors = BuildRichTextColorTable();
                sb.AppendLine($@"{{\rtf1{{\colortbl;{ string.Join("", uniqueColors.Select(d => d.Value.RichColor)) }}}");

                foreach (var entry in _log)
                {
                    if (includeEntryNumbers)
                        sb.Append($"\\cf1 { entry.EntryId }. ");

                    sb.Append($"\\cf1 { entry.EntryTimeStamp.ToShortDateString() } { entry.EntryTimeStamp.ToShortTimeString() }: ");

                    var richColor = $"\\cf{ uniqueColors[entry.EntryColor].Index + 1 }";
                    sb.Append($"{ richColor } { entry.EntryText }\\par").AppendLine();
                }
                return sb.ToString();
            }
        }

        /// <summary>
        /// Adds <paramref name="text"/> as a log entry.
        /// </summary>
        public void AddToLog(string text)
        {
            AddToLog(text, _defaultColor);
        }

        /// <summary>
        /// Adds <paramref name="text"/> as a log entry, and specifies a color to display it in.
        /// </summary>
        public void AddToLog(string text, Color entryColor)
        {
            lock (_logLock)
            {
                if (_entryNumber >= uint.MaxValue)
                    _entryNumber = 0;
                _entryNumber++;
                var logEntry = new LogEntry { EntryId = _entryNumber, EntryTimeStamp = DateTime.Now, EntryText = text, EntryColor = entryColor };
                _log.Enqueue(logEntry);

                while (_log.Count > _maxEntries)
                    _log.Dequeue();
            }
        }

        /// <summary>
        /// Clears the entire log.
        /// </summary>
        public void Clear()
        {
            lock (_logLock)
            {
                _log.Clear();
            }
        }

        private Dictionary<Color, ColorTableItem> BuildRichTextColorTable()
        {
            var uniqueColors = new Dictionary<Color, ColorTableItem>();
            var index = 0u;

            uniqueColors.Add(_defaultColor, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(_defaultColor) });

            foreach (var c in _log.Select(l => l.EntryColor).Distinct().Where(c => c != _defaultColor))
                uniqueColors.Add(c, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(c) });

            return uniqueColors;
        }

        private string ColorToRichColorString(Color c)
        {
            return $"\\red{c.R}\\green{c.G}\\blue{c.B};";
        }
    }
}

Logger 类包含另一个类LogEntry,它跟踪行号、时间戳和所需颜色。结构体用于构建富文本颜色表。

接下来,这里是修改后的RichTextBox:

using System;
using System.Runtime.InteropServices;

namespace Logger
{
    public class ScrollingRichTextBox : System.Windows.Forms.RichTextBox
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SendMessage(
            IntPtr hWnd,
            uint Msg,
            IntPtr wParam,
            IntPtr LParam);

        private const int _WM_VSCROLL = 277;
        private const int _SB_BOTTOM = 7;

        /// <summary>
        /// Scrolls to the bottom of the RichTextBox.
        /// </summary>
        public void ScrollToBottom()
        {
            SendMessage(Handle, _WM_VSCROLL, new IntPtr(_SB_BOTTOM), new IntPtr(0));
        }
    }
}

我在这里所做的只是继承一个 RichTextBox 并添加一个“滚动到底部”方法。关于如何在 StackOverflow 上执行此操作还有其他各种问题,我从中得出了这种方法。

最后,一个从表单中使用这个类的例子:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Logger
{
    public partial class LoggerExample : Form
    {
        private Logger _log = new Logger(100u);
        private List<Color> _randomColors = new List<Color> { Color.Red, Color.SkyBlue, Color.Green };
        private Random _r = new Random((int)DateTime.Now.Ticks);

        public LoggerExample()
        {
            InitializeComponent();
        }

        private void timerGenerateText_Tick(object sender, EventArgs e)
        {
            if (_r.Next(10) > 5)
                _log.AddToLog("Some event to log.", _randomColors[_r.Next(3)]);
        }

        private void timeUpdateLogWindow_Tick(object sender, EventArgs e)
        {
            richTextBox1.Rtf = _log.GetLogAsRichText(true);
            richTextBox1.ScrollToBottom();
        }
    }
}

此表单由两个计时器创建,一个用于伪随机生成日志条目,另一个用于填充 RichTextBox 本身。在这个例子中,日志类是用 100 行回滚来实例化的。 RichTextBox 控件颜色设置为黑色背景,白色和各种颜色的前景。生成文本的计时器间隔为 100 毫秒,更新日志窗口的计时器间隔为 1000 毫秒。

示例输出:

它远非完美或完成,但这里有一些警告和可以添加或改进的东西(其中一些我在以后的项目中做了):

  1. maximumEntries 的值较大时,性能较差。此日志记录类仅设计用于几百行回滚。
  2. 替换 RichTextBox 的文本会导致闪烁。我总是将刷新计时器保持在相对较慢的时间间隔。 (本例中为一秒。)
  3. 除了上面的#2,我的一些项目在重绘 RichTextBox 内容之前检查日志是否有任何新条目,以避免不必要地刷新它。
  4. 可以将每个日志条目上的时间戳设为可选,并允许使用不同的格式。
  5. 在此示例中无法暂停日志,但我的许多项目确实提供了暂停滚动行为的机制,以允许用户手动滚动、选择和复制日志窗口中的文本。

请随意修改和改进此示例。欢迎反馈。

【讨论】:

  • 非常感谢 JYelton。这实际上有很大帮助。真的很抱歉给您带来麻烦。
  • @JYelton 我相信AddToLog(string text, Color entryColor)方法中的锁应该是lock(_logLock),而不是lock(_log)
  • 非常感谢您提供此解决方案。你有任何关于如何处理手动滚动/选择的例子吗?
  • @mertcanb 我没有任何容易访问的东西。我换了职业,从软件转到了硬件;对不起!
  • @JYelton 没问题。恭喜你搬家了!
猜你喜欢
  • 2010-12-08
  • 2014-04-29
  • 1970-01-01
  • 1970-01-01
  • 2021-09-15
  • 2021-01-07
  • 2010-11-25
  • 2023-03-02
  • 1970-01-01
相关资源
最近更新 更多