代码之家  ›  专栏  ›  技术社区  ›  JYelton Melchior Blausand

WinForms C中的优雅日志窗口#

  •  54
  • JYelton Melchior Blausand  · 技术社区  · 15 年前

    我正在寻找一种有效的方法来实现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时,我都会丢失颜色格式。有了一个普通的文本框,这个功能就可以正常工作了(当然,还有一些修改)。

    寻找解决这一问题的方法从未真正令人满意。有些建议按字符计数而不是RichTextBox中的行计数来修剪多余的部分。我也看到过使用的列表框,但没有成功尝试过。

    6 回复  |  直到 9 年前
        1
  •  24
  •   John Knoeller    15 年前

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

    然后编写将该集合转储到各种用户界面元素所需的一小段代码。我个人认为 SendToEditControl SendToListBox 方法。我可能会向这些方法添加过滤功能。

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

    重要的是不要将您的日志绑定到一个UI,这是一个错误。总有一天你会想无头跑步。

    从长远来看,记录器的一个好的UI可能是一个自定义控件。但在短期内,您只需要断开日志记录与 具体的 一块用户界面。

        2
  •  25
  •   Stefan    13 年前

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

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

    您可以使用一个构造函数重载来覆盖列表框中保留的行数(默认情况下为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;
                }
            }
        }
    }
    
        3
  •  11
  •   m_eiman    12 年前

    当我想再次使用RichTextBox记录彩色行时,我将把它存储在这里以帮助将来我。以下代码删除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而不包含文本内容是可以的。

        4
  •  5
  •   Daniel Pryden    15 年前

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

        5
  •  3
  •   Neil N HLGEM    15 年前

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

    有用的提示:如果您知道将同时添加/删除许多项,请使用beginupdate()和endupdate()。

        6
  •  2
  •   Cheeso    15 年前

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

    如果要自动滚动,请使用列表框。

    无论哪种情况,都要将其绑定到一个循环的行缓冲区。