1 说明
笔者大多数的开发在 Linux 下,多处用到进度条的场景,但又无需用到图形化界面,所以就想着弄个 console 下的进度条显示。
2 步骤
-
清行显示
//清行处理操作 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*/);
,即可实现在控制台末尾行重复输出。 -
多线程下的输出显示
多线程下最容易出现的问题就是一个线程的输出覆盖其他线程的输出,或者是一个线程紧跟着上一个线程输出而没有换行,这些情况多会导致输出的混乱。为了,解决这样的问题,特意实现一个专门的输出类来进行输出显示:
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
的输出类似。 -
总结
虽然该例子是在 c#下完成的,但在 python,c,java 中通用。为了保证输出的有序性,程序中加了锁,影像了多线程的效率,但是对于界面显示是足够的。如果需要高性能,那么考虑使用类似于队列式的异步更新输出显示的方法。