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

关于控制台交互,大伙伴们也许见得最多的是进度条,就是输出一行但末尾不加 \n,而是用 \r 回到行首,然后输出新的内容,这样就做出进度条了。不过这种方法永远只能修改最后一行文本。

于是,有人想出了第二种方案——把要输出的文本存起来(用二维数组,啥的都行),每次更新输出时把屏幕内容清空重新输出。这就类似于窗口的刷新功能。缺点是文本多的时候会闪屏。

综合来说,局部覆盖是最优方案。就是我要修改某处的文本,我先把光标移到那里,覆盖掉这部分内容即可。这么一来,咱们得了解,在控制台程序中,光标是用行、列定位的。其移动的单位不是像素,是字符。比如 0 是第一行文本,1 是第二行文本……对于列也是这样。所以,(2, 4) 表示第三行的第五个字符处。这个方案是核心原理。

当然了,上述方案只是程序展示给用户看的,若配合用户的键盘输入,交互过程就完整了。

下面给大伙伴们做个演示,以便了解其原理。

internal class Program
{static void Main(string[] args){// 我们先输出三行Console.WriteLine("====================");Console.WriteLine("你好,小子");Console.WriteLine("====================");// 我们要改变的是第二行文本// 所以top=1int x = 10;do{// 重新定位光标Console.SetCursorPosition(0, 1);Console.Write("离爆炸还剩 {0} 秒", x);Thread.Sleep(1000);}while ((--x) >= 0);Console.SetCursorPosition(0, 1);Console.Write("Boom!!");Console.Read();}
}

SetCursorPosition 方法的签名如下:

public static void SetCursorPosition(int left, int top);

left 参数是指光标距离控制台窗口左边沿的位移,top 参数指定的是光标距离窗口上边沿的位移。因此,left 表示的是列,top 表示的是行。都是从 0 开始的。

你得注意的是,在覆盖旧内容的时候,要用 Write 方法,不要调用 WriteLine 方法。你懂的,WriteLine 方法会在末尾产生换行符,那样会破坏原有文本的布局的,覆写后会出现N多空白行。

咱们看看效果。

这时候会发现一个问题:输出“Boom!!”后,后面还有上一次的内容未完全清除,那是因为,新的内容文本比较短,没有完全覆写前一次的内容。咱们可以把字符串填充一下。

Console.Write("Boom!!".PadRight(Console.BufferWidth, ' '));

BufferWidth 是缓冲区宽度,即一整行文本的宽度。Buffer 指的是窗口中输出文本的一整块区域,它的面积会大于/等于窗口大小。不过,咱们好像也没必要填充那么多空格,比竟文本不长,要不,咱们就填充一部分空格好了。

Console.Write("Boom!!".PadRight(30, ' '));

30 是总长度,即字符加上填充后总长度为 30。好了,这下子就完美了。

存在的问题:直接运行控制台应用程序是一切正常的,但如果先启动 CMD,再运行程序就不行了。原因未知。

咱们也不总是让用户输入命令来交互的,也可以列一组选项,让用户去选一个。下面咱们举一例:运行后输出五个选项,用户可以按上、下箭头键来选一项,按 ESC/回车 可以退出循环。

static void Main(string[] args)
{// 下面这行是隐藏光标,这样好看一些Console.CursorVisible = false;const string Indicator = "* ";     // 前导符int indicatWidth = Indicator.Length;// 前导符长度// 先输出选项string[] options = ["雪花","梨花","豆腐花","小花","眼花"];foreach(string s in options){Console.WriteLine(s.PadLeft(indicatWidth + s.Length));}// 表示当前所选int currentSel = -1;// 表示前一个选项int prevSel = -1;ConsoleKeyInfo key;while(true){key = Console.ReadKey(true);// ESC/Enter 退出if (key.Key == ConsoleKey.Escape || key.Key == ConsoleKey.Enter){// 光标移出选项列表所在的行Console.SetCursorPosition(0, options.Length+1);break;}switch (key.Key){case ConsoleKey.UpArrow:    // 向上prevSel = currentSel;   // 保存前一个被选项索引currentSel--;break;case ConsoleKey.DownArrow:prevSel = currentSel;currentSel++;break;default:// 啥也不做break;}// 先清除前一个选项的标记if(prevSel > -1 && prevSel < options.Length){Console.SetCursorPosition(0, prevSel);Console.Write("".PadLeft(indicatWidth, ' '));}// 再看看当前项有没有超出范围if (currentSel < 0) currentSel = 0;if (currentSel > options.Length - 1) currentSel = options.Length - 1;// 设置当前选择项的标记Console.SetCursorPosition(0, currentSel);Console.Write(Indicator);}if(currentSel != -1){var selItem = options[currentSel];Console.WriteLine($"你选的是:{selItem}");}
}

首先,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处的前导符。

其他选项依此类推。

来,看看效果。

怎么样,还行吧。可是,你又想了:要是在被选中时改变一下背景色,岂不美哉。好,改一下代码。

……
// 先清除前一个选项的标记
if(prevSel > -1 && prevSel < options.Length)
{Console.SetCursorPosition(0, prevSel);// 把背景改回默认Console.ResetColor();Console.Write("".PadLeft(indicatWidth, ' ') + options[prevSel]);
}
// 再看看当前项有没有超出范围
if (currentSel < 0) currentSel = 0;
if (currentSel > options.Length - 1) currentSel = options.Length - 1;
// 设置当前选择项的标记
// 这一次不仅要写前导符,还要重新输出文本
Console.BackgroundColor = ConsoleColor.Blue;    // 背景蓝色
Console.SetCursorPosition(0, currentSel);
// 文本要重新输出
Console.Write(Indicator + options[currentSel]);
……

ResetColor 方法是重置颜色为默认值,BackgroundColor 属性设置文本背景色。颜色一旦修改,会应用到后面所输出的文本。所以当你要输出不同样式的文本前,要先改颜色。

效果很不错的。

咱们扩展一下思路,还可以实现能动态更新的表格。请看以下示例:

static void Main(string[] args)
{// 隐藏光标Console.CursorVisible = false;// 控制台窗口标题Console.Title = "万人迷赛事直通车";// 生成随机数对象,稍后用它随机生成时速Random rand = new(DateTime.Now.Nanosecond);// 第0行:标题Console.WriteLine("2023非正常人类摩托车大赛");// 第1行:分隔线Console.WriteLine("--------------------------------------------");// 第2行:表头Console.ForegroundColor = ConsoleColor.Green;Console.Write("{0,-4}", "编号");Console.Write("{0,-8}", "选手");Console.Write("{0,-5}", "颜色");Console.Write("{0,-8}\n", "实时速度(Km)");Console.ResetColor();   // 重置颜色// 数据string[][] data = [["1", "张天师", "白", "78"],["2", "王光水", "蓝", "81"],["3", "戴胃王", "红", "80"],["4", "马真帅", "黄", "77"],["5", "钟小瓶", "黑", "83"],["6", "江三鳖", "紫", "78"]];// 输出数据foreach (var dt in data){Console.Write("{0,-6}{1,-7}{2,-6}{3,-5}\n", dt[0], dt[1], dt[2], dt[3]);}// 数据列表开始行int startLine = 3;// 数据列表结束行int endLine = startLine + data.Length;// 覆写开始列int startCol = 23;// 循环更新while(true){for(int i = startLine; i < endLine; i++){// 生成随机数int num = rand.Next(60, 100);// 移动光标Console.SetCursorPosition(startCol, i);// 覆盖内容Console.Write($"{num,-5}");// 暂停一下Thread.Sleep(300);}}
}

这个例子在 while 循环内生成随机数,然后逐行更新最后一个字段的值。

运行效果如下:

下面咱们来做来好玩的进度条。

static void Main(string[] args)
{Console.CursorVisible = false;// 进度条模板string strTemplate = "[               {0,5:P0}              ]";Console.WriteLine(string.Format(strTemplate, 0.0d));for (int i = 0; i <= 100; i++){// 计算比例double pc = (double)i / 100;// 产生进度文件string pstr = string.Format(strTemplate, pc);// 两边的中括号不用覆盖var subContent = pstr[1..^1];// 总字符数int totalChars = subContent.Length;// 有多少个字符要高亮显示int highlightChars = (int)(pc * totalChars);// 定位光标Console.SetCursorPosition(1, 0);// 改变颜色Console.ForegroundColor = ConsoleColor.Black;Console.BackgroundColor = ConsoleColor.DarkYellow;// 先写前半段字符串Console.Write(subContent.Substring(0, highlightChars));// 重置颜色Console.ResetColor();// 再写后半段字符串Console.Write(subContent.Substring(highlightChars));// 暂停一下Thread.Sleep(100);}// 重置颜色Console.ResetColor();Console.WriteLine();Console.Read();
}

效果如下:

说说原理:

1、进度字符串的格式:[ 100% ],百分比显示部分固定为五个字符(格式控制符 {0,5:P0});

2、头尾的中括号是不用改变的,但[、]之间的内容需要每次刷新;

3、根据百分比算出,代表进度的字符个数。方法是 HL = 字符串总长(除去两边的中括号)× xxx%;

4、将要覆盖的字符串内容分割为两段输出。

a、第一段字符串输出前把背景色改为深黄色,前景色改为黑色。然后输出从 0 索引处起,输出 HL 个字符;b、第二段字符串输出前重置颜色,接着从索引 HL 起输出直到末尾。

随着百分比的增长,第一段字符的长度越来越长——即背景为DarkYellow 的字符所占比例更多。

现在,获取控制台窗口句柄来绘图的方式已经不能用了。不过,咱们通过字符也是可以拼接图形的。咱们看例子。

#pragma warning disable CA1416static void Main(string[] args){Console.CursorVisible = false;  // 隐藏光标Console.SetWindowSize(100, 100);Bitmap bmp = new Bitmap(32, 32);using(Graphics g = Graphics.FromImage(bmp)){g.Clear(Color.White);// 画笔Pen myPen = new(Color.Black, 1.0f);g.DrawEllipse(myPen, new Rectangle(0, 0, bmp.Width-1, bmp.Height-1));}// 逐像素访问位图// 如果遇到黑色就填字符,白色就是空格for(int h = 0; h < bmp.Height; h++){// 定位光标Console.SetCursorPosition(0, h);for (int w = 0; w < bmp.Width; w++){Color c = bmp.GetPixel(w, h);// 黑色if(c.ToArgb() == Color.Black.ToArgb()){Console.Write("**");}// 白色else{Console.Write("  ");}}}}
#pragma warning restore CA1416

控制台应用程序项目要添加以下 Nuget 包:

<ItemGroup><PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>

这是为了使用 Drawing 相关的类。我说说上面示例的原理:

1、先创建内存在的位图对象(Bitmap类);

2、用 Graphics 对象,以黑色钢笔画一个圆。注意,笔是黑色的,后面有用;

3、逐像素获取位图的颜色,映射到控制台窗口的行、列中。如果像素是黑色,就输出“**”,否则输出“  ”(两个空格)。

为什么要用两个字符呢?用一个字符它的宽度太窄,图像会变形,只好用两个字符了。汉字就不需要,一个字符即可。

咱们看看效果。

生成位图时,尺寸不要太大,不然很占屏幕。毕竟控制台是以字符来计量的,不是像素。

文章转载自:东邪独孤

原文链接:https://www.cnblogs.com/tcjiaan/p/17908891.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/285009.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序地图检索

<template><view style"background-color: #f5f5f5"><!-- 头部开始 --><viewstyle"position: fixed;left: -5rpx;right: -5rpx;z-index: 99;top: -5rpx;width: 101vw;"><view style"position: relative"><view…

三大主流前端框架介绍

在前端项目中&#xff0c;可以借助某些框架&#xff08;如React、Vue、Angular等&#xff09;来实现组件化开发&#xff0c;使代码更容易复用。此时&#xff0c;一个网页不再是由一个个独立的HTML、CSS和JavaScript文件组成&#xff0c;而是按照组件的思想将网页划分成一个个组…

Go集成elasticsearch8极简demo,光速入门

Go集成elasticsearch8极简demo,光速入门 配置go环境创件go mod工程代码实现配置go环境 编辑器添加goproxy GO111MODULE=on;GOPROXY=https://mirrors.wps.cn/go/,https://goproxy.cn,direct;GOSUMDB=off创件go mod工程 mkdir demo cd demo go mod init demo代码实现 在demo…

人工智能与底层架构:构建智能引擎的技术支柱

导言 人工智能与底层架构的交融塑造了智能系统的基石&#xff0c;是推动智能时代发展的关键动力&#xff0c;本文将深入研究人工智能在底层架构中的关键作用&#xff0c;以及它对智能引擎的技术支持&#xff0c;探讨人工智能在计算机底层架构中的作用&#xff0c;以及这一融合如…

MongoDB的数据库引用

本文主要介绍MongoDB的数据库引用。 目录 MongoDB的数据库引用 MongoDB的数据库引用 MongoDB是一种面向文档的NoSQL数据库&#xff0c;它使用BSON&#xff08;Binary JSON&#xff09;格式存储和查询数据。在MongoDB中&#xff0c;数据库引用是一种特殊的数据类型&#xff0c;…

cefsharp120.1.8(cef120.1.8,Chromium120.0.6099.109)版本升级测试,其他版本H264版本

此版本最新版cef120.1.8,Chromium120.0.6099.109 此更新包括一个高优先级安全更新 This update includes a high priority security update. 说明&#xff1a;本版本暂时不支持264&#xff0c;其他H264版本参考119,116&#xff0c;114&#xff0c;110&#xff0c;109等版本 c…

【贪心算法】之跳跃游戏

问题&#xff1a; 给定一个非负整数数组 [nums] &#xff0c;开始时位于数组的初始位置 &#xff0c;数组中每个下标对应的元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达数组的最后位置。**思路&#xff1a;**贪心算法 看不懂的可以去下面这个链接看 具体思路 …

【Proteus仿真】【Arduino单片机】电子称重秤

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器&#xff0c;使LCD1602液晶&#xff0c;矩阵按键、蜂鸣器、HX711称重模块等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示HX711称重模块检测重量…

02.微服务组件 Eureka注册中心

1.Eureka注册中心 服务提供者与消费者&#xff1a; 服务提供者:一次业务中&#xff0c;被其它微服务调用的服务。(提供接口给其它微服务)服务消费者:一次业务中&#xff0c;调用其它微服务的服务。&#xff08;调用其它微服务提供的接口)一个服务是消费者还是提供者&#xff…

清空缓存区的方法

fflush(文件指针) fflush()用于刷新相应文件的缓存区。 使用getchar()函数来清空标准输入缓存区 上面的fflush是一个函数,有些编译器不一定支持,这时候我们可以自己实现清空标准输入缓存区的操作。 代码示例: 使用scanf()的高级特性来清空标准输入缓存区 上面代码的意思是: …

分布式链路追踪 —— 基于Dubbo的traceId追踪传递

文章目录 原文链接RpcContext 上下文对象Dubbo 过滤器&#xff08;Filter&#xff09;对象基于Dubbo的traceId追踪传递实现 原文链接 RpcContext 上下文对象 在实现 Dubbo 调用之间的链路跟踪之前&#xff0c;先简单了解 RpcContext 上下文对象和 Filter 过滤器对象&#xff…

Acrel-1000DP分布式光伏系统在某重工企业18MW分布式光伏中应用——安科瑞 顾烊宇

摘 要&#xff1a;分布式光伏发电特指在用户场地附近建设&#xff0c;运行方式以用户侧自发自用、余电上网&#xff0c;且在配电系统平衡调节为特征的光伏发电设施&#xff0c;是一种新型的、具有广阔发展前景的发电和能源综合利用方式&#xff0c;它倡导就近发电&#xff0c;就…