69

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.

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.

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).

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.

JYelton
  • 35,664
  • 27
  • 132
  • 191
  • That and I've had the experience of RTF causing crashes with thread synchronization that SyncLock was unable to prevent when I needed it at the time. +1 – Brett Allen Feb 03 '10 at 22:54
  • Hi JYelton. I understand I could have posted a question, but it would be great help if you could please share me the working of your log window implementation as suggested here [https://stackoverflow.com/a/2196198/5588347] – StackUseR Mar 05 '19 at 09:46
  • @AshishSrivastava It's been many years ago. Essentially the log component uses a queue as a circular buffer to store lines of text (a log class object). A timer periodically modifies the contents of a RichTextBox to display all lines in the queue. The log class object contains the line of text, but also its timestamp and a color. I'll have to search to find the code, but if I get some time I'll do so. – JYelton Apr 03 '19 at 23:09
  • @JYelton: Thank you so much. I appreciate your suggestion & try to work on it. But still it would be really helpful if I could use the code reference. – StackUseR Apr 04 '19 at 09:34
  • @AshishSrivastava I've posted an answer which provides code and explains the basic implementation of what I used. – JYelton Apr 05 '19 at 17:52

7 Answers7

33

I recommend that you don't use a control as your log at all. Instead write a log collection class 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 SendToEditControl and SendToListBox methods into my logging object. I would probably add filtering capabilities to these methods.

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.

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.

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 specific piece of UI.

John Knoeller
  • 33,512
  • 4
  • 61
  • 92
  • I like this suggestion because I'd thought about writing log events to a custom class, and have it periodically update a UI control, which would be able to better handle those occasions when a lot of events are written at once. +1 – JYelton Feb 04 '10 at 16:49
  • I assumed the "separation" part was a given. – Neil N Feb 04 '10 at 21:57
  • 1
    @Neil: In my experience most newbies seem to use controls as _data storage_ and only later realize that this is a mistake, and by then it's hard to unwind. – John Knoeller Feb 04 '10 at 22:10
  • 1
    Thanks John, I wrote a logging class and am now using it with very good results. If you or anyone else is interested in seeing it, I would be happy to share (and welcome any critiques and comments). – JYelton Feb 25 '10 at 17:09
  • 1
    It would be of great help to newbies if they see a sample code of how this logger class will be implemented. Can anyone do the honors? – dhiraj suvarna Dec 21 '17 at 03:55
  • @dhirajsuvarna I've posted an answer which shows sample code. – JYelton Apr 05 '19 at 23:05
31

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.

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.

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;
            }
        }
    }
}
Stefan
  • 431
  • 4
  • 7
  • 2
    How would you modify this so that long messages get wrapped to the next line instead of being truncated? – Chris Mar 22 '18 at 10:33
15

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:

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.

m_eiman
  • 431
  • 4
  • 8
  • 3
    I like this answer because it actually addresses the original question, which would be important for someone wanting to know how to do this on a Rich Text Box that might not have anything to do with logging. – djsumdog Aug 08 '12 at 17:23
13

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:

  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.

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:

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.

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

JYelton
  • 35,664
  • 27
  • 132
  • 191
6

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.

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

Neil N
  • 24,862
  • 16
  • 85
  • 145
  • 2
    This is what I use, too. I write log events in reverse order, inserting at the top and yanking from the bottom. Also trap any mouse-down or scrolling events so that it doesn't fight your user if an update comes through. If your log messages are longer than the window, you can use a tooltip for the extra stuff (http://stackoverflow.com/questions/192584/how-can-i-set-different-tooltip-text-for-each-item-in-a-listbox) or include a separate textbox next to the listbox to hold the details. – Ed Power Feb 03 '10 at 23:03
  • An issue would be the BeginUpdate()/EndUpdate() usage because the part of the program that sends out log events doesn't necessarily "know" whether there will be a multitude of events or not. That's not to say they couldn't be implemented, such as around the for-loop that contains all of the log-generating events. – JYelton Feb 04 '10 at 16:57
  • I used to use ListBox, but just tried ListView. It seems to operate faster and in less quirky fashion, so I may use it from now on, though I do like ListBox's ability to have variable-height or owner-draw items, Can ListView do such things? – supercat May 22 '13 at 18:11
5

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.

Daniel Pryden
  • 59,486
  • 16
  • 97
  • 135
  • Another good suggestion, I've wondered whether creating a custom or inherited UI control would be easier than trying to work with existing controls. The only issue I see is that during a "pause" the color formatting would be lost while auto-scrolling is on hold. – JYelton Feb 04 '10 at 16:54
2

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

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

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

Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • RTF has the issues he stated, and ListBox does not enable color coding. That and I would dislike not being able to highlight and copy several rows as possible in a RTF or Text box. – Brett Allen Feb 03 '10 at 22:55
  • A good point that Aequitarum makes is the ability to copy from the log. It seems various UI controls have their share of pros and cons. RTF is the current implementation and works well, but trimming it is not straight-forward. – JYelton Feb 04 '10 at 16:52