關(guān)于控制臺(tái)交互,大伙伴們也許見(jiàn)得最多的是進(jìn)度條,就是輸出一行但末尾不加 \n,而是用 \r 回到行首,然后輸出新的內(nèi)容,這樣就做出進(jìn)度條了。不過(guò)這種方法永遠(yuǎn)只能修改最后一行文本。
于是,有人想出了第二種方案——把要輸出的文本存起來(lái)(用二維數(shù)組,啥的都行),每次更新輸出時(shí)把屏幕內(nèi)容清空重新輸出。這就類似于窗口的刷新功能。缺點(diǎn)是文本多的時(shí)候會(huì)閃屏。
綜合來(lái)說(shuō),局部覆蓋是最優(yōu)方案。就是我要修改某處的文本,我先把光標(biāo)移到那里,覆蓋掉這部分內(nèi)容即可。這么一來(lái),咱們得了解,在控制臺(tái)程序中,光標(biāo)是用行、列定位的。其移動(dòng)的單位不是像素,是字符。比如 0 是第一行文本,1 是第二行文本……對(duì)于列也是這樣。所以,(2, 4) 表示第三行的第五個(gè)字符處。這個(gè)方案是核心原理。
當(dāng)然了,上述方案只是程序展示給用戶看的,若配合用戶的鍵盤輸入,交互過(guò)程就完整了。
下面給大伙伴們做個(gè)演示,以便了解其原理。
internal class Program
{
static void Main(string[] args)
{
// 我們先輸出三行
Console.WriteLine("====================");
Console.WriteLine("你好,小子");
Console.WriteLine("====================");
// 我們要改變的是第二行文本
// 所以top=1
int x = 10;
do
{
// 重新定位光標(biāo)
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 參數(shù)是指光標(biāo)距離控制臺(tái)窗口左邊沿的位移,top 參數(shù)指定的是光標(biāo)距離窗口上邊沿的位移。因此,left 表示的是列,top 表示的是行。都是從 0 開(kāi)始的。
你得注意的是,在覆蓋舊內(nèi)容的時(shí)候,要用 Write 方法,不要調(diào)用 WriteLine 方法。你懂的,WriteLine 方法會(huì)在末尾產(chǎn)生換行符,那樣會(huì)破壞原有文本的布局的,覆寫后會(huì)出現(xiàn)N多空白行。
咱們看看效果。
這時(shí)候會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:輸出“Boom!!”后,后面還有上一次的內(nèi)容未完全清除,那是因?yàn)椋碌膬?nèi)容文本比較短,沒(méi)有完全覆寫前一次的內(nèi)容。咱們可以把字符串填充一下。
Console.Write("Boom!!".PadRight(Console.BufferWidth, ' '));
BufferWidth 是緩沖區(qū)寬度,即一整行文本的寬度。Buffer 指的是窗口中輸出文本的一整塊區(qū)域,它的面積會(huì)大于/等于窗口大小。不過(guò),咱們好像也沒(méi)必要填充那么多空格,比竟文本不長(zhǎng),要不,咱們就填充一部分空格好了。
Console.Write("Boom!!".PadRight(30, ' '));
30 是總長(zhǎng)度,即字符加上填充后總長(zhǎng)度為 30。好了,這下子就完美了。
存在的問(wèn)題:直接運(yùn)行控制臺(tái)應(yīng)用程序是一切正常的,但如果先啟動(dòng) CMD,再運(yùn)行程序就不行了。原因未知。
咱們也不總是讓用戶輸入命令來(lái)交互的,也可以列一組選項(xiàng),讓用戶去選一個(gè)。下面咱們舉一例:運(yùn)行后輸出五個(gè)選項(xiàng),用戶可以按上、下箭頭鍵來(lái)選一項(xiàng),按 ESC/回車 可以退出循環(huán)。
static void Main(string[] args)
{
// 下面這行是隱藏光標(biāo),這樣好看一些
Console.CursorVisible = false;
const string Indicator = "* "; // 前導(dǎo)符
int indicatWidth = Indicator.Length;// 前導(dǎo)符長(zhǎng)度
// 先輸出選項(xiàng)
string[] options = [
"雪花",
"梨花",
"豆腐花",
"小花",
"眼花"
];
foreach(string s in options)
{
Console.WriteLine(s.PadLeft(indicatWidth + s.Length));
}
// 表示當(dāng)前所選
int currentSel = -1;
// 表示前一個(gè)選項(xiàng)
int prevSel = -1;
ConsoleKeyInfo key;
while(true)
{
key = Console.ReadKey(true);
// ESC/Enter 退出
if (key.Key == ConsoleKey.Escape || key.Key == ConsoleKey.Enter)
{
// 光標(biāo)移出選項(xiàng)列表所在的行
Console.SetCursorPosition(0, options.Length+1);
break;
}
switch (key.Key)
{
case ConsoleKey.UpArrow: // 向上
prevSel = currentSel; // 保存前一個(gè)被選項(xiàng)索引
currentSel--;
break;
case ConsoleKey.DownArrow:
prevSel = currentSel;
currentSel++;
break;
default:
// 啥也不做
break;
}
// 先清除前一個(gè)選項(xiàng)的標(biāo)記
if(prevSel > -1 && prevSel < options.Length)
{
Console.SetCursorPosition(0, prevSel);
Console.Write("".PadLeft(indicatWidth, ' '));
}
// 再看看當(dāng)前項(xiàng)有沒(méi)有超出范圍
if (currentSel < 0) currentSel = 0;
if (currentSel > options.Length - 1) currentSel = options.Length - 1;
// 設(shè)置當(dāng)前選擇項(xiàng)的標(biāo)記
Console.SetCursorPosition(0, currentSel);
Console.Write(Indicator);
}
if(currentSel != -1)
{
var selItem = options[currentSel];
Console.WriteLine($"你選的是:{selItem}");
}
}
首先,CursorVisible 屬性設(shè)置為 false,隱藏光標(biāo),這樣用戶在操作過(guò)程看不見(jiàn)光標(biāo)閃動(dòng),會(huì)友好一些。畢竟我們這里不需要用戶輸入內(nèi)容。
選項(xiàng)內(nèi)容是通過(guò)字符串?dāng)?shù)組來(lái)定義的,先在屏幕上輸出,然后在 while 循環(huán)中分析用戶按的是不是上、下方向鍵。向上就讓索引 -1,向下就讓索引 +1。為什么要定義一個(gè) prevSel 變量呢?因?yàn)檫@是單選項(xiàng),同一時(shí)刻只能選一個(gè),被選中的項(xiàng)前面會(huì)顯示“* ”。當(dāng)選中的項(xiàng)切換后,前一個(gè)被選的項(xiàng)需要把“* ”符號(hào)清除掉,然后再設(shè)置新選中的項(xiàng)前面的“* ”。所以,咱們需要一個(gè)變量來(lái)暫時(shí)記錄上一個(gè)被選中的索引。
如果你的程序邏輯復(fù)雜,這些功能可以封裝一下,比如用某結(jié)構(gòu)體記錄選擇狀態(tài),或者干脆加上事件處理,當(dāng)按上、下鍵后調(diào)用相關(guān)的委托觸發(fā)事件。這里我為了讓大伙伴們看得舒服一些,就不封裝那么復(fù)雜了。
運(yùn)作過(guò)程是這樣的:
1、初始時(shí),一個(gè)沒(méi)選上;
2、按【向下】鍵,此時(shí)當(dāng)前被選項(xiàng)變成0(即第一項(xiàng)),上一個(gè)被選項(xiàng)仍然是 -1;
3、前一個(gè)被選項(xiàng)是-1,無(wú)需清除前導(dǎo)字符;
4、設(shè)置第0行(0就是剛被選中的)的前導(dǎo)符,即在行首覆寫上“* ”;
5、繼續(xù)按【向下】鍵,此時(shí)被選項(xiàng)為 1,上一個(gè)被選項(xiàng)為 0;
6、清除上一個(gè)被選項(xiàng)0的前導(dǎo)符,設(shè)置當(dāng)前項(xiàng)1的前導(dǎo)符;
7、如果按【向上】鍵,當(dāng)前選中項(xiàng)變回0,上一個(gè)被選項(xiàng)是1;
8、清除1處的前導(dǎo)符,設(shè)置0處的前導(dǎo)符。
其他選項(xiàng)依此類推。
來(lái),看看效果。
怎么樣,還行吧。可是,你又想了:要是在被選中時(shí)改變一下背景色,豈不美哉。好,改一下代碼。
……
// 先清除前一個(gè)選項(xiàng)的標(biāo)記
if(prevSel > -1 && prevSel < options.Length)
{
Console.SetCursorPosition(0, prevSel);
// 把背景改回默認(rèn)
Console.ResetColor();
Console.Write("".PadLeft(indicatWidth, ' ') + options[prevSel]);
}
// 再看看當(dāng)前項(xiàng)有沒(méi)有超出范圍
if (currentSel < 0) currentSel = 0;
if (currentSel > options.Length - 1) currentSel = options.Length - 1;
// 設(shè)置當(dāng)前選擇項(xiàng)的標(biāo)記
// 這一次不僅要寫前導(dǎo)符,還要重新輸出文本
Console.BackgroundColor = ConsoleColor.Blue; // 背景藍(lán)色
Console.SetCursorPosition(0, currentSel);
// 文本要重新輸出
Console.Write(Indicator + options[currentSel]);
……
ResetColor 方法是重置顏色為默認(rèn)值,BackgroundColor 屬性設(shè)置文本背景色。顏色一旦修改,會(huì)應(yīng)用到后面所輸出的文本。所以當(dāng)你要輸出不同樣式的文本前,要先改顏色。
效果很不錯(cuò)的。
咱們擴(kuò)展一下思路,還可以實(shí)現(xiàn)能動(dòng)態(tài)更新的表格。請(qǐng)看以下示例:
static void Main(string[] args)
{
// 隱藏光標(biāo)
Console.CursorVisible = false;
// 控制臺(tái)窗口標(biāo)題
Console.Title = "萬(wàn)人迷賽事直通車";
// 生成隨機(jī)數(shù)對(duì)象,稍后用它隨機(jī)生成時(shí)速
Random rand = new(DateTime.Now.Nanosecond);
// 第0行:標(biāo)題
Console.WriteLine("2023非正常人類摩托車大賽");
// 第1行:分隔線
Console.WriteLine("--------------------------------------------");
// 第2行:表頭
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("{0,-4}", "編號(hào)");
Console.Write("{0,-8}", "選手");
Console.Write("{0,-5}", "顏色");
Console.Write("{0,-8}\n", "實(shí)時(shí)速度(Km)");
Console.ResetColor(); // 重置顏色
// 數(shù)據(jù)
string[][] data = [
["1", "張?zhí)鞄?, "白", "78"],
["2", "王光水", "藍(lán)", "81"],
["3", "戴胃王", "紅", "80"],
["4", "馬真帥", "黃", "77"],
["5", "鐘小瓶", "黑", "83"],
["6", "江三鱉", "紫", "78"]
];
// 輸出數(shù)據(jù)
foreach (var dt in data)
{
Console.Write("{0,-6}{1,-7}{2,-6}{3,-5}\n", dt[0], dt[1], dt[2], dt[3]);
}
// 數(shù)據(jù)列表開(kāi)始行
int startLine = 3;
// 數(shù)據(jù)列表結(jié)束行
int endLine = startLine + data.Length;
// 覆寫開(kāi)始列
int startCol = 23;
// 循環(huán)更新
while(true)
{
for(int i = startLine; i < endLine; i++)
{
// 生成隨機(jī)數(shù)
int num = rand.Next(60, 100);
// 移動(dòng)光標(biāo)
Console.SetCursorPosition(startCol, i);
// 覆蓋內(nèi)容
Console.Write($"{num,-5}");
// 暫停一下
Thread.Sleep(300);
}
}
}
這個(gè)例子在 while 循環(huán)內(nèi)生成隨機(jī)數(shù),然后逐行更新最后一個(gè)字段的值。
運(yùn)行效果如下:
下面咱們來(lái)做來(lái)好玩的進(jìn)度條。
static void Main(string[] args)
{
Console.CursorVisible = false;
// 進(jìn)度條模板
string strTemplate = "[ {0,5:P0} ]";
Console.WriteLine(string.Format(strTemplate, 0.0d));
for (int i = 0; i <= 100; i++)
{
// 計(jì)算比例
double pc = (double)i / 100;
// 產(chǎn)生進(jìn)度文件
string pstr = string.Format(strTemplate, pc);
// 兩邊的中括號(hào)不用覆蓋
var subContent = pstr[1..^1];
// 總字符數(shù)
int totalChars = subContent.Length;
// 有多少個(gè)字符要高亮顯示
int highlightChars = (int)(pc * totalChars);
// 定位光標(biāo)
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();
}
效果如下:
說(shuō)說(shuō)原理:
1、進(jìn)度字符串的格式:[ 100% ],百分比顯示部分固定為五個(gè)字符(格式控制符 {0,5:P0});
2、頭尾的中括號(hào)是不用改變的,但[、]之間的內(nèi)容需要每次刷新;
3、根據(jù)百分比算出,代表進(jìn)度的字符個(gè)數(shù)。方法是 HL = 字符串總長(zhǎng)(除去兩邊的中括號(hào))× xxx%;
4、將要覆蓋的字符串內(nèi)容分割為兩段輸出。
a、第一段字符串輸出前把背景色改為深黃色,前景色改為黑色。然后輸出從 0 索引處起,輸出 HL 個(gè)字符;
b、第二段字符串輸出前重置顏色,接著從索引 HL 起輸出直到末尾。
隨著百分比的增長(zhǎng),第一段字符的長(zhǎng)度越來(lái)越長(zhǎng)——即背景為DarkYellow 的字符所占比例更多。
現(xiàn)在,獲取控制臺(tái)窗口句柄來(lái)繪圖的方式已經(jīng)不能用了。不過(guò),咱們通過(guò)字符也是可以拼接圖形的。咱們看例子。
#pragma warning disable CA1416
static void Main(string[] args)
{
Console.CursorVisible = false; // 隱藏光標(biāo)
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));
}
// 逐像素訪問(wèn)位圖
// 如果遇到黑色就填字符,白色就是空格
for(int h = 0; h < bmp.Height; h++)
{
// 定位光標(biāo)
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
控制臺(tái)應(yīng)用程序項(xiàng)目要添加以下 Nuget 包:
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>
這是為了使用 Drawing 相關(guān)的類。我說(shuō)說(shuō)上面示例的原理:
1、先創(chuàng)建內(nèi)存在的位圖對(duì)象(Bitmap類);
2、用 Graphics 對(duì)象,以黑色鋼筆畫一個(gè)圓。注意,筆是黑色的,后面有用;
3、逐像素獲取位圖的顏色,映射到控制臺(tái)窗口的行、列中。如果像素是黑色,就輸出“**”,否則輸出“? ”(兩個(gè)空格)。
為什么要用兩個(gè)字符呢?用一個(gè)字符它的寬度太窄,圖像會(huì)變形,只好用兩個(gè)字符了。漢字就不需要,一個(gè)字符即可。
咱們看看效果。
生成位圖時(shí),尺寸不要太大,不然很占屏幕。畢竟控制臺(tái)是以字符來(lái)計(jì)量的,不是像素。
文章轉(zhuǎn)載自:東邪獨(dú)孤
原文鏈接:https://www.cnblogs.com/tcjiaan/p/17908891.html文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-767093.html
體驗(yàn)地址:引邁 - JNPF快速開(kāi)發(fā)平臺(tái)_低代碼開(kāi)發(fā)平臺(tái)_零代碼開(kāi)發(fā)平臺(tái)_流程設(shè)計(jì)器_表單引擎_工作流引擎_軟件架構(gòu)文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-767093.html
到了這里,關(guān)于【.NET】控制臺(tái)應(yīng)用程序的各種交互玩法的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!