【.NET】控制台应用程序的各种交互玩法

打印 上一主题 下一主题

主题 504|帖子 504|积分 1512

老周是一个不喜欢做界面的码农,所以很多时候能用控制台交互就用控制台交互,既方便又占资源少。有大伙伴可能会说,控制台全靠打字,不好交互。那不一定的,像一些选项类的交互,可以用键盘按键(如方向键),可比用鼠标快得多。当然了,要是要触控的话,是不太好用,只能做UI了。
关于控制台交互,大伙伴们也许见得最多的是进度条,就是输出一行但末尾不加 \n,而是用 \r 回到行首,然后输出新的内容,这样就做出进度条了。不过这种方法永远只能修改最后一行文本。
于是,有人想出了第二种方案——把要输出的文本存起来(用二维数组,啥的都行),每次更新输出时把屏幕内容清空重新输出。这就类似于窗口的刷新功能。缺点是文本多的时候会闪屏。
综合来说,局部覆盖是最优方案。就是我要修改某处的文本,我先把光标移到那里,覆盖掉这部分内容即可。这么一来,咱们得了解,在控制台程序中,光标是用行、列定位的。其移动的单位不是像素,是字符。比如 0 是第一行文本,1 是第二行文本……对于列也是这样。所以,(2, 4) 表示第三行的第五个字符处。这个方案是核心原理。
当然了,上述方案只是程序展示给用户看的,若配合用户的键盘输入,交互过程就完整了。
下面给大伙伴们做个演示,以便了解其原理。
  1. internal class Program
  2. {
  3.     static void Main(string[] args)
  4.     {
  5.         // 我们先输出三行
  6.         Console.WriteLine("====================");
  7.         Console.WriteLine("你好,小子");
  8.         Console.WriteLine("====================");
  9.         // 我们要改变的是第二行文本
  10.         // 所以top=1
  11.         int x = 10;
  12.         do
  13.         {
  14.             // 重新定位光标
  15.             Console.SetCursorPosition(0, 1);
  16.             Console.Write("离爆炸还剩 {0} 秒", x);
  17.             Thread.Sleep(1000);
  18.         }
  19.         while ((--x) >= 0);
  20.         Console.SetCursorPosition(0, 1);
  21.         Console.Write("Boom!!");
  22.         Console.Read();
  23.     }
  24. }
复制代码
SetCursorPosition 方法的签名如下:
  1. public static void SetCursorPosition(int left, int top);
复制代码
left 参数是指光标距离控制台窗口左边沿的位移,top 参数指定的是光标距离窗口上边沿的位移。因此,left 表示的是列,top 表示的是行。都是从 0 开始的。
你得注意的是,在覆盖旧内容的时候,要用 Write 方法,不要调用 WriteLine 方法。你懂的,WriteLine 方法会在末尾产生换行符,那样会破坏原有文本的布局的,覆写后会出现N多空白行。
咱们看看效果。
 
这时候会发现一个问题:输出“Boom!!”后,后面还有上一次的内容未完全清除,那是因为,新的内容文本比较短,没有完全覆写前一次的内容。咱们可以把字符串填充一下。
  1. Console.Write("Boom!!".PadRight(Console.BufferWidth, ' '));
复制代码
BufferWidth 是缓冲区宽度,即一整行文本的宽度。Buffer 指的是窗口中输出文本的一整块区域,它的面积会大于/等于窗口大小。不过,咱们好像也没必要填充那么多空格,比竟文本不长,要不,咱们就填充一部分空格好了。
  1. Console.Write("Boom!!".PadRight(30, ' '));
复制代码
30 是总长度,即字符加上填充后总长度为 30。好了,这下子就完美了。

存在的问题:直接运行控制台应用程序是一切正常的,但如果先启动 CMD,再运行程序就不行了。原因未知。
咱们也不总是让用户输入命令来交互的,也可以列一组选项,让用户去选一个。下面咱们举一例:运行后输出五个选项,用户可以按上、下箭头键来选一项,按 ESC/回车 可以退出循环。
  1. static void Main(string[] args)
  2. {
  3.     // 下面这行是隐藏光标,这样好看一些
  4.     Console.CursorVisible = false;
  5.     const string Indicator = "* ";     // 前导符
  6.     int indicatWidth = Indicator.Length;// 前导符长度
  7.     // 先输出选项
  8.     string[] options = [
  9.         "雪花",
  10.         "梨花",
  11.         "豆腐花",
  12.         "小花",
  13.         "眼花"
  14.     ];
  15.     foreach(string s in options)
  16.     {
  17.         Console.WriteLine(s.PadLeft(indicatWidth + s.Length));
  18.     }
  19.     // 表示当前所选
  20.     int currentSel = -1;
  21.     // 表示前一个选项
  22.     int prevSel = -1;
  23.     ConsoleKeyInfo key;
  24.     while(true)
  25.     {
  26.         key = Console.ReadKey(true);
  27.         // ESC/Enter 退出
  28.         if (key.Key == ConsoleKey.Escape || key.Key == ConsoleKey.Enter)
  29.         {
  30.             // 光标移出选项列表所在的行
  31.             Console.SetCursorPosition(0, options.Length+1);
  32.             break;
  33.         }
  34.         switch (key.Key)
  35.         {
  36.             case ConsoleKey.UpArrow:    // 向上
  37.                 prevSel = currentSel;   // 保存前一个被选项索引
  38.                 currentSel--;
  39.                 break;
  40.             case ConsoleKey.DownArrow:
  41.                 prevSel = currentSel;
  42.                 currentSel++;
  43.                 break;
  44.             default:
  45.                 // 啥也不做
  46.                 break;
  47.         }
  48.         // 先清除前一个选项的标记
  49.         if(prevSel > -1 && prevSel < options.Length)
  50.         {
  51.             Console.SetCursorPosition(0, prevSel);
  52.             Console.Write("".PadLeft(indicatWidth, ' '));
  53.         }
  54.         // 再看看当前项有没有超出范围
  55.         if (currentSel < 0) currentSel = 0;
  56.         if (currentSel > options.Length - 1) currentSel = options.Length - 1;
  57.         // 设置当前选择项的标记
  58.         Console.SetCursorPosition(0, currentSel);
  59.         Console.Write(Indicator);
  60.     }
  61.     if(currentSel != -1)
  62.     {
  63.         var selItem = options[currentSel];
  64.         Console.WriteLine($"你选的是:{selItem}");
  65.     }
  66. }
复制代码
首先,CursorVisible 属性设置为 false,隐藏光标,这样用户在操作过程看不见光标闪动,会友好一些。毕竟我们这里不需要用户输入内容。
选项内容是通过字符串数组来定义的,先在屏幕上输出,然后在 while 循环中分析用户按的是不是上、下方向键。向上就让索引 -1,向下就让索引 +1。为什么要定义一个 prevSel 变量呢?因为这是单选项,同一时刻只能选一个,被选中的项前面会显示“* ”。当选中的项切换后,前一个被选的项需要把“* ”符号清除掉,然后再设置新选中的项前面的“* ”。所以,咱们需要一个变量来暂时记录上一个被选中的索引。
如果你的程序逻辑复杂,这些功能可以封装一下,比如用某结构体记录选择状态,或者干脆加上事件处理,当按上、下键后调用相关的委托触发事件。这里我为了让大伙伴们看得舒服一些,就不封装那么复杂了。
运作过程是这样的:
1、初始时,一个没选上;
2、按【向下】键,此时当前被选项变成0(即第一项),上一个被选项仍然是 -1;
3、前一个被选项是-1,无需清除前导字符;
4、设置第0行(0就是刚被选中的)的前导符,即在行首覆写上“* ”;
5、继续按【向下】键,此时被选项为 1,上一个被选项为 0;
6、清除上一个被选项0的前导符,设置当前项1的前导符;
7、如果按【向上】键,当前选中项变回0,上一个被选项是1;
8、清除1处的前导符,设置0处的前导符。
其他选项依此类推。
来,看看效果。

怎么样,还行吧。可是,你又想了:要是在被选中时改变一下背景色,岂不美哉。好,改一下代码。
  1. ……
  2. // 先清除前一个选项的标记
  3. if(prevSel > -1 && prevSel < options.Length)
  4. {
  5.     Console.SetCursorPosition(0, prevSel);
  6.     // 把背景改回默认
  7.     Console.ResetColor();
  8.     Console.Write("".PadLeft(indicatWidth, ' ') + options[prevSel]);
  9. }
  10. // 再看看当前项有没有超出范围
  11. if (currentSel < 0) currentSel = 0;
  12. if (currentSel > options.Length - 1) currentSel = options.Length - 1;
  13. // 设置当前选择项的标记
  14. // 这一次不仅要写前导符,还要重新输出文本
  15. Console.BackgroundColor = ConsoleColor.Blue;    // 背景蓝色
  16. Console.SetCursorPosition(0, currentSel);
  17. // 文本要重新输出
  18. Console.Write(Indicator + options[currentSel]);
  19. ……
复制代码
ResetColor 方法是重置颜色为默认值,BackgroundColor 属性设置文本背景色。颜色一旦修改,会应用到后面所输出的文本。所以当你要输出不同样式的文本前,要先改颜色。
效果很不错的。

 
咱们扩展一下思路,还可以实现能动态更新的表格。请看以下示例:
  1. static void Main(string[] args)
  2. {
  3.     // 隐藏光标
  4.     Console.CursorVisible = false;
  5.     // 控制台窗口标题
  6.     Console.Title = "万人迷赛事直通车";
  7.     // 生成随机数对象,稍后用它随机生成时速
  8.     Random rand = new(DateTime.Now.Nanosecond);
  9.     // 第0行:标题
  10.     Console.WriteLine("2023非正常人类摩托车大赛");
  11.     // 第1行:分隔线
  12.     Console.WriteLine("--------------------------------------------");
  13.     // 第2行:表头
  14.     Console.ForegroundColor = ConsoleColor.Green;
  15.     Console.Write("{0,-4}", "编号");
  16.     Console.Write("{0,-8}", "选手");
  17.     Console.Write("{0,-5}", "颜色");
  18.     Console.Write("{0,-8}\n", "实时速度(Km)");
  19.     Console.ResetColor();   // 重置颜色
  20.     // 数据
  21.     string[][] data = [
  22.         ["1", "张天师", "白", "78"],
  23.         ["2", "王光水", "蓝", "81"],
  24.         ["3", "戴胃王", "红", "80"],
  25.         ["4", "马真帅", "黄", "77"],
  26.         ["5", "钟小瓶", "黑", "83"],
  27.         ["6", "江三鳖", "紫", "78"]
  28.     ];
  29.     // 输出数据
  30.     foreach (var dt in data)
  31.     {
  32.         Console.Write("{0,-6}{1,-7}{2,-6}{3,-5}\n", dt[0], dt[1], dt[2], dt[3]);
  33.     }
  34.     // 数据列表开始行
  35.     int startLine = 3;
  36.     // 数据列表结束行
  37.     int endLine = startLine + data.Length;
  38.     // 覆写开始列
  39.     int startCol = 23;
  40.     // 循环更新
  41.     while(true)
  42.     {
  43.         for(int i = startLine; i < endLine; i++)
  44.         {
  45.             // 生成随机数
  46.             int num = rand.Next(60, 100);
  47.             // 移动光标
  48.             Console.SetCursorPosition(startCol, i);
  49.             // 覆盖内容
  50.             Console.Write($"{num,-5}");
  51.             // 暂停一下
  52.             Thread.Sleep(300);
  53.         }
  54.     }
  55. }
复制代码
这个例子在 while 循环内生成随机数,然后逐行更新最后一个字段的值。
运行效果如下:

 
下面咱们来做来好玩的进度条。
[code]static void Main(string[] args){    Console.CursorVisible = false;    // 进度条模板    string strTemplate = "[               {0,50}              ]";    Console.WriteLine(string.Format(strTemplate, 0.0d));    for (int i = 0; i
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表