c# 控制檯console進度條

麥比烏斯皇發表於2019-06-05

1 說明

筆者大多數的開發在 Linux 下,多處用到進度條的場景,但又無需用到圖形化介面,所以就想著弄個 console 下的進度條顯示。

2 步驟

  1. 清行顯示

    //清行處理操作
    int currentLineCursor = Console.CursorTop;//記錄當前游標位置
    Console.SetCursorPosition(0, Console.CursorTop);//將游標至於當前行的開始位置
    Console.Write(new string(' ', Console.WindowWidth));//用空格將當前行填滿,相當於清除當前行
    Console.SetCursorPosition(0, currentLineCursor);//將游標恢復至開始時的位置

    如果要清除上一行,只需在清行處理操作前將調整游標位置提前一行,即:Console.SetCursorPosition(0, Console.CursorTop - 1);。緊接著Console.WriteLine(/*something*/);,即可實現在控制檯末尾行重複輸出。

  2. 多執行緒下的輸出顯示

    多執行緒下最容易出現的問題就是一個執行緒的輸出覆蓋其他執行緒的輸出,或者是一個執行緒緊跟著上一個執行緒輸出而沒有換行,這些情況多會導致輸出的混亂。為了,解決這樣的問題,特意實現一個專門的輸出類來進行輸出顯示:

     public class Consoler
     {
         private static string lastContext = "";//用於記錄上次寫的內容
         private static readonly object _lock = new object();//加鎖保證只有一個輸出
         public static void Write(string context)
         {
             lastContext = context;//記錄上次寫的內容
             lock (_lock)
             {
                 Console.Write(context);
             }
    
         }
         /// <summary>
         /// 覆寫
         /// </summary>
         /// <param name="context"></param>
         public static void OverWrite(string context = null)
         {
             lastContext = context;//記錄上次寫的內容
             var strLen = context?.Length ?? 0;
             //空白格的長度,考慮到內容可能超出一行的寬度,所以求餘。
             var blankLen = strLen % Console.WindowWidth;
             Console.SetCursorPosition(0, Console.CursorTop);
             //空白只需填充最後一行的剩餘位置即可。
             lock (_lock)
             {
                 Console.Write(context + new string(' ', Console.WindowWidth - blankLen));
             }
         }
    
         public static void WriteLine(string context = null)
         {
             ClearConsoleLine();//清除最後一行
             lock (_lock)
             {
                 Console.WriteLine(context);
                 if (!string.IsNullOrWhiteSpace(lastContext))
                     Console.Write(lastContext);//重新輸出最後一次的內容,否則有較明顯的閃爍
                 lastContext = null;
             }
         }
    
         public static void ClearConsoleLine(int invertedIndex = 0)
         {
             int currentLineCursor = Console.CursorTop;
             int top = Console.CursorTop - invertedIndex;
             top = top < 0 ? 0 : top;
             Console.SetCursorPosition(0, top);
             Console.Write(new string(' ', Console.WindowWidth));
             Console.SetCursorPosition(0, currentLineCursor);
         }
     }

    實際測試時,使用多 Task (模擬多執行緒)去進行輸出實驗:

    static void Main(string[] args)
     {
         Console.WriteLine("Hello World!");
    
         var task1 = Task.Run(() =>
         {
             int count = 0, w = (int)(Console.WindowWidth * 0.6), lw = 0, rw = 0;
             float p = 0;
             while (true)
             {
                 count %= 75;
                 p = count++ / 74f;
                 lw = (int)(p * w);
                 rw = w - lw;
                 Consoler.OverWrite($"from task1, [{new string('#', lw) + new string(' ', rw)}]:{p:#.00%}");
                 Thread.Sleep(100);
             }
         });
         var task2 = Task.Run(() =>
         {
             while (true)
             {
                 Consoler.WriteLine($"from task2, now:{DateTime.Now}");
                 Thread.Sleep(5000);
             }
         });
    
         var task3 = Task.Run(() =>
         {
             var rd = new Random();
             while (true)
             {
                 Consoler.WriteLine($"from task3, {new string('+', (int)(rd.NextDouble() * Console.WindowWidth))}");
                 Thread.Sleep(rd.Next(5000));
             }
         });
         Task.WaitAll(task1);
     }

    最終輸出結果:

     from task2, now:6/5/19 8:10:24 PM
     from task3, +++++++++++++++++++++++++++++++++
     from task2, now:6/5/19 8:10:29 PM
     from task3, +++++++++++++++++++++++++
     from task3, ++++
     from task2, now:6/5/19 8:10:34 PM
     from task3, +++++++++++++++++++++++
     from task3, ++++++++++++
     from task3, ++++++
     from task2, now:6/5/19 8:10:44 PM
     from task1, [###########################                     ]:58.11%

    task1 用來進行進度條的輸出,task2 和 task3 進行隨機輸出。可以看出,task1 永遠在最後一行進行進度更新,其他輸出任然可以正常進行。實現的效果和 ubuntu 下執行更新命令sudo apt-get update的輸出類似。

  3. 總結

    雖然該例子是在 c#下完成的,但在 python,c,java 中通用。為了保證輸出的有序性,程式中加了鎖,影像了多執行緒的效率,但是對於介面顯示是足夠的。如果需要高效能,那麼考慮使用類似於佇列式的非同步更新輸出顯示的方法。

相關文章