最近博主在做服務程序開發的時候,用的是控制台的非圖形圖像的界面。然后采用了log4net作為日志打印組件,在配置文件里面做了一些配置項。在控制台輸出的時候,針對不同的日志級別,定義了不同的文字顏色在控制台中進行字符串輸出。然后博主聯想到winform程序也存在界面上的日志打印需求,通常情況下,程序員會調用系統自帶的文本控件做日志打印。比如TextBox、RichTextBox等等。使用此類控件作為日志打印控件的時候,需要考慮一些細節問題,常見的有跨UI線程訪問等等。既然腦海中已經產生了這樣的需求,何不自己造一個呢?
日志模塊應該是項目中常見或者必不可少的通用模塊之一了,因為日志記錄能夠提升程序的可維護性。嗯,是的,沒錯,日志記錄往往是軟件開發周期中的重要組成部分。它具有以下幾個優點:記錄一些用來作為“依據”的重要信息,記錄應用程序運行時的精確環境,記錄捕獲到的異常錯誤信息,方便開發人員盡快找到應用程序中的Bug等等。一旦在程序中加入了日志輸出代碼,程序運行過程中就能生成並輸出日志信息而無需人工干預。
1、文本的輸出格式
做日志打印的時候,針對文本輸出可能需要諸如線程信息,當前進程,當前類和方法,執行時間,堆棧信息等等。針對界面輸出可能需要用到不同顏色來區分日志級別。於是博主根據日志的不同級別,在控件里面自定義了幾個相關屬性用來控制文本的顯示(具體代碼看步驟6)。開發人員既可以在設計時可視化設置屬性值,也可以硬編碼賦值。

2、保持界面操作流暢性
UI線程大量的調用控件可能造成的界面假死,博主覺得解決這個問題的最好方式是多個線程協同分工,一方面提高程序的界面流暢性,一方面提高程序的響應速度和處理能力。UI主線程只負責從界面接收用戶的操作,后台線程則負責響應該操作。比如一個查詢動作,用戶鼠標點擊查詢按鈕,UI線程接收該點擊事件,通知后台線程對該點擊事件作出一系列包辦。后台線程靜悄悄的在后台查詢數據,然后處理數據,然后批量加載數據並更新到界面的展示控件。

3、非UI線程訪問所帶來的異常
眾所周知,winform窗體上的控件是由UI線程創建的,如果要進行跨UI線程訪問該控件,則會導致異常(InvalidOperationException)。

4、日志打印是否有序
開發基於多線程程序的時候,如果開啟VS調試,則調試起來相當麻煩,因為執行順序會在多個線程間切換,所以調試的時候,會到處亂跳。所以一般我們會用日志記錄的方式來捕獲開發人員想要的程序運行信息。那么既然這些信息有利於開發人員解決bug、優化系統,則必然得保證日志產生的有序性,這樣開發人員才知道,哪行代碼先運行,哪個bug先出現。外部線程生產日志,內部線程消費打印日志,於是就可以引入隊列來作為待處理日志的存儲容器。
5、多線程的並發訪問是否安全
由於多個線程同時並發調用控件的打印方法,其內部處理邏輯就是多個外部線程寫入內部隊列,控件內部的處理線程讀取隊列,那么這時候就得考慮線程安全的問題了。通常對於線程安全的問題,最好是將代碼段的執行封裝成原子操作。從而在源頭上規避了因多個線程之間的切換而導致該代碼段的執行結果存在二義性,也就是說我們不用考慮共享資源同步的問題。構建原子一般采用加鎖的方式,當然也可以 lock free 。
6、編碼實現
考慮到兼容性,博主決定在.Net2.0下實現這個日志打印控件。
留郵箱求源碼刷評論什么的真的好么?直接貼上全部的源碼和測試代碼,不要叫我活雷鋒,我想靜靜...O(∩_∩)O
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace WindowsForms20 { /// <summary> /// 線程安全的日志打印控件 /// Create by 陌城 /// http://www.cnblogs.com/StrangeCity/p/4356009.html /// </summary> [Description("線程安全的日志打印控件")] public class PrintLog : RichTextBox { public delegate void Action(); private readonly object _lockObj = new object(); private Queue<Action> _queueAction; private Thread _threadQueueHnadler; private AutoResetEvent _AutoResetEvent = new AutoResetEvent(true); [Browsable(false)] public new bool ReadOnly { get; private set; } [Category("外觀"), Description("普通信息的文本顏色")] public Color InfoFontColor { get; set; } [Category("外觀"), Description("警告信息的文本顏色")] public Color WarnFontColor { get; set; } [Category("外觀"), Description("錯誤信息的文本顏色")] public Color ErrorFontColor { get; set; } [Category("外觀"), Description("成功信息的文本顏色")] public Color SucceeFontColor { get; set; } public PrintLog() : base() { this.BackColor = Color.Black; base.ReadOnly = true; this.BorderStyle = BorderStyle.None; this.InfoFontColor = Color.White; this.WarnFontColor = Color.Yellow; this.ErrorFontColor = Color.Red; this.SucceeFontColor = Color.LightGreen; this._AutoResetEvent = new AutoResetEvent(true); this._queueAction = new Queue<Action>(); this._threadQueueHnadler = new Thread(() => { while (true) { if (this._queueAction.Count > 0) { lock (this._lockObj) { Action action = this._queueAction.Dequeue(); if (action != null) { action(); } } } else { this._AutoResetEvent.WaitOne(); } } }); this._threadQueueHnadler.IsBackground = true; this._threadQueueHnadler.Start(); } private void PrintMsg(string msg, Color fontColor) { msg = string.Format("{0}{1}{2}{1}{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), Environment.NewLine, msg); int p1 = this.TextLength; this.AppendText(msg); int p2 = msg.Length; this.Select(p1, p2); this.SelectionColor = fontColor; this.ScrollToCaret(); } private void Enqueue(string msg, Color fontColor) { lock (this._lockObj) { this._queueAction.Enqueue(() => { if (this.InvokeRequired) { this.Invoke(new MethodInvoker(() => { this.PrintMsg(msg, fontColor); })); } else { this.PrintMsg(msg, fontColor); } }); this._AutoResetEvent.Set(); } } public void Info(string msg) { this.Enqueue(msg, this.InfoFontColor); } public void Warn(string msg) { this.Enqueue(msg, this.WarnFontColor); } public void Error(string msg) { this.Enqueue(msg, this.ErrorFontColor); } public void Error(Exception ex) { this.Error(ex.ToString()); } public void Succee(string msg) { this.Enqueue(msg, this.SucceeFontColor); } } }
7、測試效果
private void button1_Click(object sender, EventArgs e) { this.printLog1.Clear(); //普通打印 for (int i = 0; i < 10000; i++) { this.printLog1.Error("sjdbnvkjsndvjn死到那時"); } return; //多線程並發打印 for (int i = 0; i < 1; i++) { Thread t = new Thread(() => { for (int j = 0; j < 10000; j++) { this.printLog1.Error("sjdbnvkjsndvjn死到那時" + i.ToString()); } }); t.Start(); } }

8、擴展思考
博主也是臨時造輪子,可能考慮並不周全。例如控件是否需要增加AutoSaveToFile屬性?日志內容保存到文件的功能是否可以與log4net等日志組件銜接起來,並支持適配器模式。日志控件顯示了多少條日志內容應該自動清除?博主一時半會就只想到這么多了,還請各位讀者多多指教!
