My solution to creating a basic log window was exactly as John Knoeller suggested 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 populate a control, or write to a file, etc.
There are a few pieces to this example solution:
- The logging class itself,
Logger.
- Modification of the RichTextBox control to add scroll-to-bottom functionality after an update;
ScrollingRichTextBox.
- The main form to demonstrate its use,
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 (_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};";
}
}
}
The Logger class incorporates another class LogEntry which keeps track of the line number, timestamp, and desired color. A struct is used to build a Rich Text color table.
Next, here is the modified 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.
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.
Sample 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):
- With large values for
maximumEntries, performance is poor. This logging class was only designed for a few hundred lines of scroll-back.
- 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.)
- 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.
- The timestamp on each log entry can be made optional and allow different formats.
- 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.
Feel free to modify and improve upon this example. Feedback is welcome.