2013/12/27

在 Console 程式中讓文字保持在同一行顯示

我們都知道我們可以在 Console 程式中以 Console.WriteLine 和  Console.Write 輸出文字到一個命令視窗裡。但是不管是 Write 或者 WriteLine 方法, 文字的走向都是向右、向下的, 從來不會回頭。因此, 如果你的輸出文字太多, 就會需要捲頁; 如果超過差不多12頁以後, 它只會保留12頁(大約是288行左右, 根據預設值), 更上方的文字通通會被截掉, 再也看不見了。

有沒有辦法讓我們既看到輸出結果, 又能讓這些輸出的文字不要佔據那麼多空間呢? 其實有兩個方法, 而且不難。第一個, 就是我們可以輸出一個 '\b' 字元 (backspace), 那麼輸出游標就會往左移動一個字元。不過, 不要以為這個字元代表「Back Space」, 就以為它會像鍵盤上的倒退鍵一樣。它實際上的行為比較像是按下向左箭頭, 而不是倒退鍵 -- 它不會消去左邊的字元。

第二個方法, 就是輸出 '\r' 字元(carriage return)。它會使得輸出游標立刻後退到該行的最左邊。這個方法比第一個方稍為好一點, 因為這樣就不需要輸出許多個 '\b' 字元。執行 Console.CursorLeft = 0; 也會得到相同的結果, 所以我把它們歸類為同一個方法。

但是這兩個方法都有相同的限制 -- 如果你的輸出文字超過一行, 也就是一旦游標進入第二行, 你無論如何都只能回到第二行的最左邊, 而不是第一行的最左邊, 即使你可能覺得第一行和第二行都是同一行, 它應該回到第一行。實際上它並非如此。

因此, 如果你要在同一行重複輸出文字的話, 你的文字就絕對不能超過一行(也就是80個字元)。然後, 既然我們可以藉著輸出 '\r' 字元回到這一行的最左邊, 我們就可以藉著輸出最多79個空白, 把這一行原有的文字抹掉, 然後又使用 '\r' 再次回到最左邊, 輸出新的文字。如此, 畫面上看起來就好像文字永遠在同一行重複輸出一樣。

或許你想問, 為什麼不是輸出80個空白, 而是79個? 那是因為如果你輸出了80個字元, 游標就會前進到第二行。那麼根據上面提到的那個限制, 你又永遠回不了第一行了。因此, 你在這一行中不管如何, 都要限制輸出的文字在79個字元(含)以內。

在這裡, 如果我提到「字元」二字, 都代表單一位元組的文字單位, 而不是雙位元組的文字單位。

運用上述原理, 我們就可以每次都透過輸出滿79個字元的文字(右邊補空白), 再讓游標退回這一行的最左邊, 重覆執行這個步驟。如此, 就能讓畫面產生永遠都在同一行輸出的效果了。

但是這麼做的話, 有一個問題必須注意。那就是中文字會佔掉兩個字元, 但是 .Net 的所有程式都會將中文字視為一個字元。因此我們必須特別注意字串中是否有中文字、有幾個中文字。我們必須分別算出中文與英數字的字數, 才能得到正確的結果。

在以下的程式中, 我寫了三個多載的方法 WriteToConsoleAtTopOfLine() :

public class Util
{
    private static ConsoleColor origColor = Console.ForegroundColor;

    /// <summary>
    /// Write console message at the most left of the original line. 
    /// </summary>
    /// <param name="isWarning">If to show this message as warning</param>
    /// <param name="length">How many characters per line</param>
    /// <param name="pattern">The format pattern of the message to be written to the console. The result string will be 
    /// trimmed to within 79 (or otherwise if specified) chars.</param>
    /// <param name="args">Pattern arguments</param>
    public static void WriteToConsoleAtTopOfLine(bool isWarning, int length, string pattern, params object[] args)
    {
        if (isWarning)
            Console.ForegroundColor = ConsoleColor.Red;
        WriteToConsoleAtTopOfLine(string.Format(pattern, args), length);
        Console.ForegroundColor = origColor;
    }

    /// <summary>
    /// Write console message at the most left of the original line. 
    /// </summary>
    /// <param name="length">How many characters per line</param>
    /// <param name="pattern">The format pattern of the message to be written to the console. The result string will be 
    /// trimmed to within 79 (or otherwise if specified) chars.</param>
    /// <param name="args">Pattern arguments</param>
    public static void WriteToConsoleAtTopOfLine(int length, string pattern, params object[] args)
    {
        WriteToConsoleAtTopOfLine(false, length, pattern, args);
    }

    /// <summary>
    /// Write console message at the most left of the original line. 
    /// </summary>
    /// <param name="message">The message to be written to the console. It will be trimmed to within 79 (or otherwise if specified) chars.</param>
    /// <param name="length">How many characters per line</param>
    public static void WriteToConsoleAtTopOfLine(string message, int length)
    {
        message = message.Trim();
        int eCount, cCount;
        int l = countStringLength(message, out eCount, out cCount);
        if (l > length)
            message = cutMessage(message, length); // Limit it to be at most 80 chars
        else
            message = message + new string(' ', length - l);
        Console.Write("\r" + message);
    }

    static string cutMessage(string input, int length)
    {
        if (string.IsNullOrEmpty(input.Trim())) // || 40 > maxLength) // 防呆
            return string.Empty;        
        int i = 0, j = 0;
        while (j < maxLength)
        {
            i++;
            j += input[i].ContainsChinese() ? 2 : 1;
        }
        return input.Substring(0, i);
    }

    /// <summary>
    /// Count the actual length of the string
    /// </summary>
    /// <param name="input">The string to be checked</param>
    /// <param name="cCount">How many Chinese chars there are</param>
    /// <param name="eCount">How many alphanumeric chars there are</param>
    static int countStringLength(string input, out int eCount, out int cCount)
    {
        eCount = 0;
        cCount = 0;
        foreach (char c in input)
            if (c.ContainsChinese())
                cCount++;
            else
                eCount++;
        return cCount * 2 + eCount;
    }
}

(程式下載)

其中 WriteToConsoleAtTopOfLine() 方法的參數 isWarning 代表是不是要讓這行文字變成紅色; pattern 和 args 是對應 String.Format() 的必要參數; 如果只傳入 message 的話, 表示你不需要將字串格式化。

countStringLength() 方法則會算出字串的總長度, 並回傳中文字數 (cCount) 和英數字的字數 (eCount)。

以下則是 ContainsChinese() 公用函式, 我把它寫成擴充方法 :

public static class ext
{
    /// <summary>
    /// Determin if the input string contains any Chinese Big5 characters
    /// </summary>
    /// <param name="input">The input string</param>
    /// <returns>If the input string contains any Chinese, return true, else false</returns>
    public static bool ContainsChinese(this string input)
    {
        int charCode = 0,
            upperBound = Convert.ToInt32("9fff", 16),
            lowerBound = Convert.ToInt32("4e00", 16);
        for (int i = 0; i < input.Length; i++)
        {
            charCode = Convert.ToInt32(Convert.ToChar(input.Substring(i, 1)));
            if (charCode >= lowerBound && charCode < upperBound)
                return true;
        }
        return false;
    }

    /// <summary>
    /// Determin if the input string contains any Chinese Big5 characters
    /// </summary>
    /// <param name="input">The input character</param>
    /// <returns>If the input string contains any Chinese, return true, else false</returns>
    public static bool ContainsChinese(this char input)
    {
        int charCode = 0,
            upperBound = Convert.ToInt32("9fff", 16),
            lowerBound = Convert.ToInt32("4e00", 16);
        charCode = Convert.ToInt32(input);
        if (charCode >= lowerBound && charCode < upperBound)
            return true;
        return false;
    }
}

(程式下載)

如此, 當我們在使用時, 只需要呼叫 WriteToConsoleAtTopOfLine() 方法 (有三個多載, 請注意參數順序), 就可以在同一行進行輸出了。不過, 再提醒一下, 你的輸出訊息不能超過 79 個字元, 否則會被強制截斷。

在這個 ContinsChinese 方法中, 我只讓它能夠判斷 Big5 中文字, 其它字集的文字判斷起來不一定正確。請讀者特別留意。

 

沒有留言:

張貼留言