WinForms C# 中优雅的日志窗口

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2196097/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-07 00:18:30  来源:igfitidea点击:

Elegant Log Window in WinForms C#

c#winformslogging

提问by JYelton

I am looking for ideas on an efficient way to implement a log window for a windows forms application. In the past I have implemented several using TextBox and RichTextBox but I am still not totally satisfied with the functionality.

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

This log is intended to provide the user with a recent history of various events, primarily used in data-gathering applications where one might be curious how a particular transaction completed. In this case, the log need not be permanent nor saved to a file.

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

First, some proposed requirements:

首先,提出一些要求:

  • Efficient and fast; if hundreds of lines are written to the log in quick succession, it needs to consume minimal resources and time.
  • Be able to offer a variable scrollback of up to 2000 lines or so. Anything longer is unnecessary.
  • Highlighting and color are preferred. Font effects not required.
  • Automatically trim lines as the scrollback limit is reached.
  • Automatically scroll as new data is added.
  • Bonus but not required: Pause auto-scrolling during manual interaction such as if the user is browsing the history.
  • 高效快速;如果将数百行快速连续写入日志,则需要消耗最少的资源和时间。
  • 能够提供多达 2000 行左右的可变回滚。任何更长的时间都是不必要的。
  • 突出显示和颜色是首选。不需要字体效果。
  • 达到回滚限制时自动修剪线条。
  • 添加新数据时自动滚动。
  • 奖励但不是必需的:在手动交互期间暂停自动滚动,例如用户正在浏览历史记录。

What I have been using so far to write and trim the log:

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

I use the following code (which I call from other threads):

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

// 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
    }
}

The problem with this approach is that whenever TrimLog is called, I lose color formatting. With a regular TextBox this works just fine (with a bit of modification of course).

这种方法的问题在于,无论何时调用 TrimLog,我都会丢失颜色格式。使用常规的 TextBox,这可以正常工作(当然需要进行一些修改)。

Searches for a solution to this have never been really satisfactory. Some suggest to trim the excess by character count instead of line count in a RichTextBox. I've also seen ListBoxes used, but haven't successfully tried it.

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

采纳答案by John Knoeller

I recommend that you don't use a control as your log at all. Instead write a log collectionclass that has the properties you desire (not including the display properties).

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

Then write the little bit of code that is needed to dump that collection to a variety of user interface elements. Personally, I would put SendToEditControland SendToListBoxmethods into my logging object. I would probably add filtering capabilities to these methods.

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

You can update the UI log only as often as it makes sense, giving you the best possible performance, and more importantly, letting you reduce the UI overhead when the log is changing rapidly.

您可以仅在有意义的情况下更新 UI 日志,从而为您提供最佳性能,更重要的是,可以在日志快速更改时减少 UI 开销。

The important thing is not to tie your logging to a piece of UI, that's a mistake. Someday you may want to run headless.

重要的是不要将您的日志记录与某个 UI 联系起来,这是一个错误。有一天你可能想要无头奔跑。

In the long run, a good UI for a logger is probably a custom control. But in the short run, you just want to disconnect your logging from any specificpiece of UI.

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

回答by Neil N

I would say ListView is perfect for this (in Detail viewing mode), and its exactly what I use it for in a few internal apps.

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

Helpful tip: use BeginUpdate() and EndUpdate() if you know you will be adding/removing a lot of items at once.

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

回答by Cheeso

If you want highlighting and color formatting, I'd suggest a RichTextBox.

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

If you want the auto scrolling, then use the ListBox.

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

In either case bind it to a circular buffer of lines.

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

回答by Daniel Pryden

I recently implemented something similar. Our approach was to keep a ring buffer of the scrollback records and just paint the log text manually (with Graphics.DrawString). Then if the user wants to scroll back, copy text, etc., we have a "Pause" button that flips back to a normal TextBox control.

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

回答by Stefan

Here is something I threw together based on a much more sophisticated logger I wrote a while ago.

这是我根据我不久前写的一个更复杂的记录器拼凑起来的东西。

This will support color in the list box based on log level, supports Ctrl+V and Right-Click for copying as RTF, and handles logging to the ListBox from other threads.

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

You can override the number of lines retained in the ListBox (2000 by default) as well as the message format using one of the constructor overloads.

您可以使用构造函数重载之一覆盖 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;
            }
        }
    }
}

回答by m_eiman

I'll store this here as a help to Future Me when I want to use a RichTextBox for logging colored lines again. The following code removes the first line in a RichTextBox:

当我想再次使用 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 }";
}

It took me way too long to figure out that setting SelectedRtf to just "" didn't work, but that setting it to "proper" RTF with no text content is ok.

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

回答by JYelton

My solution to creating a basic log window was exactly as John Knoellersuggested in his answer. Avoid storing log information directly in a TextBox or RichTextBox control, but instead create a logging class which can be used to populatea control, or write to a file, etc.

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

There are a few pieces to this example solution:

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

  1. The logging class itself, Logger.
  2. Modification of the RichTextBox control to add scroll-to-bottom functionality after an update; ScrollingRichTextBox.
  3. The main form to demonstrate its use, LoggerExample.
  1. 日志类本身,Logger.
  2. 修改 RichTextBox 控件以在更新后添加滚动到底部功能;ScrollingRichTextBox.
  3. 演示其使用的主要形式,LoggerExample.

First, the logging class:

首先,日志类:

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 (_log)
            {
                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};";
        }
    }
}

The Logger class incorporates another class LogEntrywhich keeps track of the line number, timestamp, and desired color. A struct is used to build a Rich Text color table.

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

Next, here is the modified RichTextBox:

接下来,这里是修改后的 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));
        }
    }
}

All I am doing here is inheriting a RichTextBox and adding a "scroll to bottom" method. There are various other questions about how to do this on StackOverflow, from which I derived this approach.

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

Finally, an example of using this class from a form:

最后,从表单中使用此类的示例:

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();
        }
    }
}

This form is created with two timers, one to generate log entries pseudo-randomly, and one to populate the RichTextBox itself. In this example, the log class is instantiated with 100 lines of scroll-back. The RichTextBox control colors are set to have a black background with white and various color foregrounds. The timer to generate text is at a 100ms interval while the one to update the log window is at 1000ms.

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

Sample output:

示例输出:

Logger Example Output

记录器示例输出

It is far from perfect or finished, but here are some caveats and things that could be added or improved (some of which I have done in later projects):

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

  1. With large values for maximumEntries, performance is poor. This logging class was only designed for a few hundred lines of scroll-back.
  2. Replacing the text of the RichTextBox can result in flickering. I always keep the refresh timer at a relatively slow interval. (One second in this example.)
  3. Adding to #2 above, some of my projects check if the log has any new entries before redrawing the RichTextBox content, to avoid unnecessarily refreshing it.
  4. The timestamp on each log entry can be made optional and allow different formats.
  5. There is no way to pause the log in this example, but many of my projects do provide a mechanism for pausing the scrolling behavior, to allow users to manually scroll, select, and copy text from the log window.
  1. 值较大时maximumEntries,性能较差。这个日志类只是为几百行回滚而设计的。
  2. 替换 RichTextBox 的文本会导致闪烁。我总是将刷新计时器保持在相对较慢的间隔。(本例中为一秒。)
  3. 添加到上面的#2,我的一些项目在重绘 RichTextBox 内容之前检查日志是否有任何新条目,以避免不必要地刷新它。
  4. 每个日志条目上的时间戳可以是可选的,并允许使用不同的格式。
  5. 在这个例子中没有办法暂停日志,但我的许多项目确实提供了一种暂停滚动行为的机制,允许用户从日志窗口手动滚动、选择和复制文本。

Feel free to modify and improve upon this example. Feedback is welcome.

随意修改和改进这个例子。欢迎反馈。