【.NET】控制台應用程式的各種交互玩法

来源:https://www.cnblogs.com/tcjiaan/archive/2023/12/17/17908891.html
-Advertisement-
Play Games

老周是一個不喜歡做界面的碼農,所以很多時候能用控制台交互就用控制台交互,既方便又占資源少。有大伙伴可能會說,控制台全靠打字,不好交互。那不一定的,像一些選項類的交互,可以用鍵盤按鍵(如方向鍵),可比用滑鼠快得多。當然了,要是要觸控的話,是不太好用,只能做UI了。 關於控制台交互,大伙伴們也許見得最多 ...


老周是一個不喜歡做界面的碼農,所以很多時候能用控制台交互就用控制台交互,既方便又占資源少。有大伙伴可能會說,控制台全靠打字,不好交互。那不一定的,像一些選項類的交互,可以用鍵盤按鍵(如方向鍵),可比用滑鼠快得多。當然了,要是要觸控的話,是不太好用,只能做UI了。

關於控制台交互,大伙伴們也許見得最多的是進度條,就是輸出一行但末尾不加 \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、逐像素獲取點陣圖的顏色,映射到控制台視窗的行、列中。如果像素是黑色,就輸出“**”,否則輸出“  ”(兩個空格)。

為什麼要用兩個字元呢?用一個字元它的寬度太窄,圖像會變形,只好用兩個字元了。漢字就不需要,一個字元即可。

咱們看看效果。

生成點陣圖時,尺寸不要太大,不然很占屏幕。畢竟控制台是以字元來計量的,不是像素。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SpringCloud 文章推薦:Eureka:Spring Cloud服務註冊與發現組件(非常詳細) (biancheng.net) 概述 Spring Cloud 是一個服務治理平臺,是若幹個框架的集合,提供了全套的分散式系統解決方案。包含了:服務註冊與發現、配置中心、服務網關、智能路由、負載均 ...
  • 在使用pip安裝Python軟體包時,有時會遇到與 SSL/TLS 相關的問題。一種常見情況是在使用VPN時出現以下錯誤信息 ValueError: check_hostname requires server_hostname: ValueError: check_hostname require ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹自定義`Dialog`組件的常用方法及靈活運用。自定義對話框需要解決的問題是,如何讓父窗體與子窗體進行數據交換,要... ...
  • 在C++中使用SQLite資料庫需要使用SQLite的C/C++介面。以下是一個簡單的示例,演示如何在C++中使用SQLite,並提供了常見的查詢、增加、修改和刪除功能。為了使用SQLite,你需要下載SQLite的C/C++介面,並鏈接到你的項目中。 首先,確保你已經下載了SQLite的C/C++ ...
  • 在買賣二手車的過程中,準確的估值是非常重要的。而快速獲取準確的二手車估值需要大量的數據和計算,這對於個人來說可能是非常困難的。然而,現在有一種API介面可以幫助我們快速獲取準確的二手車估值,讓我們省時省力。 這個API介面是由挖數據平臺提供的。挖數據平臺是一個專註於數據挖掘和分析的平臺,在汽車行業有 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹標準對話框`QInputDialog`、`QFileDialog `這兩種對話框組件的常用方法及靈活運用。在 Qt... ...
  • #include <stdio.h> #include <stdlib.h> #include <malloc.h> #include <time.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #define N ...
  • 一、引言 使用基本的套接字編程技術,以一對基本的TCP協議通信程式為基礎,模擬比特洪流(BitTorrent)的分散傳輸技術完成一個文件的正確傳輸,使用標準C語言編程。本實驗的目的並不是做一個實用的網路程式,而是更好地理解套接字編程原理和P2P技術,重點在特定條件下的實驗方案的設計並予以實現。 盡可 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...