WinForms中优雅的日志窗口C#

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

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

首先,一些提出的要求:

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

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

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

// 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,这个工作很好(当然有一些修改)。

search这个解决scheme从来没有真正令人满意。 有些人build议修剪多余的字符数,而不是在RichTextBox中的行数。 我也看过使用ListBox,但没有成功地尝试过。

我build议你不要使用控件作为你的日志。 而是编写一个具有所需属性的日志集合类(不包括显示属性)。

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

您只需要经常更新UI日志,就可以获得最佳的性能,更重要的是,可以在日志快速更改时减lessUI开销。

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

从长远来看,logging器的良好用户界面可能是一个自定义控件。 但是从短期来看,你只是想断开你的日志logging与任何特定的用户界面。

以前我写的一个更复杂的logging器,这是我扔在一起的东西。

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

您可以覆盖保留在列表框中的行数(默认为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; } } } } 

当我想使用RichTextBox再次logging彩色线条时,我将把它作为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而没有文本内容是可以的。

我会说ListView是完美的(在详细查看模式),而它正是我用它在一些内部的应用程序。

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

我最近实现了类似的东西。 我们的方法是保持回滚logging的环形缓冲区,并手动绘制日志文本(使用Graphics.DrawString)。 然后,如果用户想要回滚,复制文本等,我们有一个“暂停”button,可以翻转回正常的文本框控件。

如果你想突出显示和颜色格式,我会build议一个RichTextBox。

如果你想自动滚动,然后使用列表框。

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