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

news2024/11/26 21:34:41

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

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

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

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

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

internal class Program
{
    static void Main(string[] args)
    {
        // 我们先输出三行
        Console.WriteLine("====================");
        Console.WriteLine("你好,小子");
        Console.WriteLine("====================");

        // 我们要改变的是第二行文本
        // 所以top=1
        int 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 CA1416
        static 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.coloradmin.cn/o/1320787.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MATLAB】数据拟合第12期-基于高斯核回归的拟合算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 基于高斯核回归的拟合算法是一种处理回归问题的机器学习方法。以下是该算法的简单介绍&#xff1a; 核心思想&#xff1a;高斯核回归的核心思想是利用高斯核函数对数据点进行非线性映射&a…

AudioGPT 语音技术全覆盖:语音识别、增强、分离、风格迁移等 | 开源日报 No.114

stevearc/oil.nvim Stars: 1.7k License: MIT oil.nvim 是一个类似于 vim-vinegar 的文件浏览器&#xff0c;允许您像普通 Neovim 缓冲区一样编辑文件系统。其主要功能包括支持常见插件管理器、通过适配器抽象进行所有文件系统交互以及提供 API 来执行各种操作。该项目的关键…

RNN梯度爆炸实验

前情回顾 from torch.utils.data import Dataset import torch.nn as nn import torch.nn.functional as F import os import random import torch from nndl import Accuracy from nndl import RunnerV3 from torch.utils.data import DataLoader import matplotlib.pyplot a…

零基础自学C语言|字符函数和字符串函数

在编程的过程中&#xff0c;我们经常要处理字符和字符串&#xff0c;为了方便操作字符和字符串&#xff0c;C语言标准库中提供了一系列库函数&#xff0c;接下来我们就学习一下这些函数。 &#x1f4cc;字符分类函数 C语言中有一系列的函数是专门做字符分类的&#xff0c;也就…

06_Web框架之Django三

Web框架之Django三 学习目标和内容 1、能够通过ORM模型创建数据表 2、能够通过ORM模型对数据进行操作 3、能够理解ORM模型对应关系 一、ORM概念 1、ORM介绍 对象关系映射 用于实现面向对象编程语言里不同类型系统数据之间的转换。 其就是使用面向对象的方式&#xff0c;操作…

风速预测(六)基于Pytorch的EMD-CNN-GRU并行模型

目录 前言 1 风速数据EMD分解与可视化 1.1 导入数据 1.2 EMD分解 2 数据集制作与预处理 2.1 先划分数据集&#xff0c;按照8&#xff1a;2划分训练集和测试集 2.2 设置滑动窗口大小为96&#xff0c;制作数据集 3 基于Pytorch的EMD-CNN-GRU并行模型预测 3.1 数据加载&a…

得帆信息创始人-张桐,受邀出席 BV百度风投AIGC主题论坛

近日&#xff0c;得帆信息创始人兼CEO张桐&#xff0c;作为百度风投被投代表企业创始人受邀出席“向未来&#xff0c;共成长” BV百度风投AIGC主题论坛。 与包括上海市徐汇区相关部门领导、百度集团相关事业部负责人及代表&#xff0c;以及来自国寿资本、中网投、麦顿投资的投资…

IDEA运行JSP启动后页面中文乱码

源代码截图&#xff1a; 运行结果截图&#xff1a; 在<head>标签内加入代码 <% page contentType"text/html; charsetgb2312"%> 重启服务器&#xff0c;问题已改善 ————————————————— 该文仅供学习以及参考&#xff0c;可做笔记收藏…

SQL语句整理二--Mysql

文章目录 知识点梳理&#xff1a;1. mysql 中 in 和 exists 区别2. varchar 与 char 的区别 查看表结构&#xff1a;获取当前时间&#xff1a;查看建表语句&#xff1a;修改用户密码&#xff1a;查看所有用户&#xff1a;grant命令&#xff1a;判断当前数据库有多少连接数&…

部署promethues采集kubelet数据报错:server returned HTTP status 403 Forbidden

背景 笔者尝试部署手动部署promethues去采集kubelet的node节点数据信息时报错 笔者的promethus的配置文件和promthues的clusterrole配置如下所示&#xff1a; apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata:name: prometheus rules: - apiGroups: […

钡铼无线R10A工业级路由器在工业机器人领域的创新应用

随着工业机器人的普及&#xff0c;对于高可靠性和高稳定性的网络接入设备的需求也越来越大。传统的有线网络虽然稳定&#xff0c;但在现场布置和维护上面临很多困难&#xff0c;而无线网络虽然方便&#xff0c;但受到信号干扰和传输距离限制等问题的影响。如何解决这些问题&…

在centos7上安装docker

1.CentOS安装Docker Docker CE 支持 64 位版本 CentOS 7&#xff0c;并且要求内核版本不低于 3.10&#xff0c; CentOS 7 满足最低内核的要求&#xff0c;所以我们在CentOS 7安装Docker。 1.1.卸载&#xff08;可选&#xff09; 如果之前安装过旧版本的Docker&#xff0c;可…

Python tkinter控件全集之组合选择框 ttk.ComboBox

Tkinter标准库 Tkinter是Python的标准GUI库&#xff0c;也是最常用的Python GUI库之一&#xff0c;提供了丰富的组件和功能&#xff0c;包括窗口、按钮、标签、文本框、列表框、滚动条、画布、菜单等&#xff0c;方便开发者进行图形界面的开发。Tkinter库基于Tk for Unix/Wind…

DC-5靶场

目录 DC-5靶机&#xff1a; 先进行主机发现&#xff1a; 发现文件包含&#xff1a; 上传一句话木马&#xff1a; 反弹shell&#xff1a; 提权漏洞利用&#xff1a; 下载exp&#xff1a; 第一个文件 libhax.c 第二个文件r…

Ubuntu 常用命令之 rm 命令用法介绍

rm是一个在Unix和Unix-like操作系统中的基本命令&#xff0c;用于删除文件或目录。rm的全称是“remove”。 rm命令的基本语法是 rm [options] [-r|-R] [--] file...其中&#xff0c;[options]是可选的命令行选项&#xff0c;[-r|-R]是递归删除目录和其内容&#xff0c;[--]表…

Eclipse 一直提示 loading descriptor for 的解决方法

启动eclipse之后&#xff0c;进行相关操作时&#xff0c;弹出界面&#xff0c;提示&#xff1a;loading descriptor for xxx 解决方法&#xff1a; 在Eclipse左侧的Project Explorer 最右上角有一个小钮,鼠标移上去时提示"View Menu". 你点一下,在弹出的上下文菜单中…

0基础学习VR全景平台篇第129篇:认识单反相机和鱼眼镜头

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; 一、相机 单反和微单 这里说的相机是指可更换镜头的单反/微单数码相机。那两者有何差异呢&#xff1f; 1&#xff09;取景结构差异 两者最直观的区别在于&#xff0c;微单相机…

Github2023-12-15 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2023-12-15统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量TypeScript项目3非开发语言项目3JavaScript项目1Python项目1Rust项目1PHP项目1 基于项目的学习 创建周期&am…

【Spring】10 BeanFactoryAware 接口

文章目录 1. 简介2. 作用3. 使用3.1 创建并实现接口3.2 配置 Bean 信息3.3 创建启动类3.4 启动 4. 应用场景总结 Spring 框架为开发者提供了丰富的扩展点&#xff0c;其中之一就是 Bean 生命周期中的回调接口。本文将专注于介绍一个重要的接口 BeanFactoryAware&#xff0c;探…

恒创:多链路负载均衡是什么意思

多链路负载均衡是一种网络架构技术&#xff0c;它通过将流量分散到多个网络链路上&#xff0c;以提高网络的性能和可靠性。这种技术可以应用于各种场景&#xff0c;如数据中心、云计算、企业网络等。 在多链路负载均衡中&#xff0c;流量被分配到多个网络链路上&#xff0c;以…