c# - System.Console 作为带有输入行的日志窗口

标签 c# scroll console-application

我正在编写一个控制台应用程序,它需要用户在文本滚动时在底行输入。这个想法是让文本滚动并在底部留下一个输入行。我想要文本编辑功能(箭头键、插入、删除等)。我也希望能够拥有静态的“状态行”(不受滚动影响的行)。

一个真实世界的例子是 Irssi: enter image description here

在我的代码中,我连接到 NLog 并将其输出写入屏幕,同时还向用户提供输入行。这是通过在写入时“暂停输入”来完成的:使用 Console.MoveBufferArea向上移动文本、禁用光标、重新定位光标、写入日志文本、将光标重新定位回输入行并启用光标。它几乎可以工作,但存在一些问题:

  • 速度很慢。在我写 20-30 行的情况下,应用程序会显着变慢。 (可以通过缓冲传入来解决,但不会解决滚动速度问题。)
  • 溢出的行(即异常堆栈跟踪)在屏幕的最底部留下一行。
  • 当文本向上滚动时,溢出的行会(部分)被覆盖。这也会弄乱输入行。
  • 向上/向下滚动不起作用。

是否有图书馆可以帮我做这件事? 如果不是那么我该如何解决速度问题?如何修复滚动?

首选跨平台解决方案。

public class InputConsole
{
    private static readonly object _bufferLock = new object();

    private static int _windowWidth = Console.BufferWidth;
    private static int _windowHeight = Console.BufferHeight;
    private static int _windowLeft = Console.WindowLeft;
    private static int _windowTop = Console.WindowTop;

    public InputConsole()
    {    
        MethodCallTarget target = new MethodCallTarget();
        target.ClassName = typeof(InputConsole).AssemblyQualifiedName;
        target.MethodName = "LogMethod";
        target.Parameters.Add(new MethodCallParameter("${level}"));
        target.Parameters.Add(new MethodCallParameter("${message}"));
        target.Parameters.Add(new MethodCallParameter("${exception:format=tostring}"));
        target.Parameters.Add(new MethodCallParameter("[${logger:shortName=true}]"));

        SimpleConfigurator.ConfigureForTargetLogging(target, LogLevel.Trace);

        try
        {

            Console.SetWindowSize(180, 50);
            Console.SetBufferSize(180, 50);
            _windowWidth = Console.BufferWidth;
            _windowHeight = Console.BufferHeight;
        }
        catch (Exception exception)
        {
            Console.WriteLine("Unable to resize console: " + exception);
        }

    }

    public void Run()
    {

        string input;
        do
        {
            lock (_bufferLock)
            {
                Console.SetCursorPosition(0, _windowHeight - 1);
                Console.Write("Command: ");
                Console.CursorVisible = true;
            }

            Console.BackgroundColor = ConsoleColor.Black;
            Console.ForegroundColor = ConsoleColor.Yellow;

            input = Console.ReadLine();

            lock (_bufferLock)
            {
                Console.CursorVisible = false;
            }

        } while (!string.Equals(input, "quit", StringComparison.OrdinalIgnoreCase));
    }

    public static void LogMethod(string level, string message, string exception, string caller)
    {

        if (Console.BufferHeight == _windowHeight)
            Console.MoveBufferArea(_windowLeft, _windowTop + 1, Console.BufferWidth, Console.BufferHeight - 2, _windowLeft, _windowTop);

        var fgColor = ConsoleColor.White;
        var bgColor = ConsoleColor.Black;

        switch (level.ToUpper())
        {
            case "TRACE":
                fgColor = ConsoleColor.DarkGray;
                break;
            case "DEBUG":
                fgColor = ConsoleColor.Gray;
                break;
            case "INFO":
                fgColor = ConsoleColor.White;
                break;
            case "WARNING":
                fgColor = ConsoleColor.Cyan;
                break;
            case "ERROR":
                fgColor = ConsoleColor.White;
                bgColor = ConsoleColor.Red;
                break;
        }

        var str = string.Format("({0})  {1} {2} {3}", level.ToUpper(), caller, message, exception);
        WriteAt(_windowLeft, _windowHeight - 3, str, fgColor, bgColor);
    }

    public static void WriteAt(int left, int top, string s, ConsoleColor foregroundColor = ConsoleColor.White, ConsoleColor backgroundColor = ConsoleColor.Black)
    {
        lock (_bufferLock)
        {
            var currentBackgroundColor = Console.BackgroundColor;
            var currentForegroundColor = Console.ForegroundColor;
            Console.BackgroundColor = backgroundColor;
            Console.ForegroundColor = foregroundColor;
            int currentLeft = Console.CursorLeft;
            int currentTop = Console.CursorTop;
            var currentVisible = Console.CursorVisible;
            Console.CursorVisible = false;
            Console.SetCursorPosition(left, top);
            Console.Write(s);
            Console.SetCursorPosition(currentLeft, currentTop);
            Console.CursorVisible = currentVisible;
            Console.BackgroundColor = currentBackgroundColor;
            Console.ForegroundColor = currentForegroundColor;
        }

    }
}

最佳答案

对 Windows 中的文本控制台进行进一步研究,我似乎很难让它运行得更快。通过具有较低重绘率(低于 WriteConsoleOutput)的自定义实现,我能够获得超过 Console.WriteLine 10 倍的速度提升。

然而,由于 Console.WriteLine 强制执行“当我们到达底部时滚动所有内容”,我使用的是 Console.MoveBufferArea .测试表明,我对 MoveBufferArea 的实现(包含在我的原始问题中)比 Console.WriteLine 慢了大约 90 倍。然而,在我使用 WriteConsoleOutput 的新实现中,我的速度比 MoveBufferedArea 提高了 1356 倍

由于很难找到有关它的信息,我在 blog post 中详细说明了我的发现。 .我还将代码附加到此答案以供后代使用。

我编写了一个允许我滚动单个框的类。我还实现了一个线路输入系统来模拟标准 Console.ReadLine();。请注意,此实现缺少 home/end-support(尽管很容易修复)。

请注意,要从中获得任何速度提升,您必须设置 box.AutoRedraw = false; 并定期手动调用 box.Draw();。使用 box.AutoRedraw = true;(在每个 Write() 上调用 Draw())这个解决方案实际上比 Console.WriteLine 慢 30 倍比 MoveBufferArea 快 3 倍。

使用示例:

_logBox = new InputConsoleBox(0, 0, (short)Console.BufferWidth, (short)(Console.BufferHeight - 2), InputConsoleBox.Colors.LightWhite, InputConsoleBox.Colors.Black);
_statusBox = new InputConsoleBox(0, (short)(Console.BufferHeight - 3), (short)Console.BufferWidth, 1, InputConsoleBox.Colors.LightYellow, InputConsoleBox.Colors.DarkBlue);
_inputBox = new InputConsoleBox(0, (short)(Console.BufferHeight - 2), (short)Console.BufferWidth, 1, InputConsoleBox.Colors.LightYellow, InputConsoleBox.Colors.Black);
_statusBox.WriteLine("Hey there!");
_inputBox.InputPrompt = "Command: ";

// If you are okay with some slight flickering this is an easy way to set up a refresh timer
_logBox.AutoDraw = false;
_redrawTask = Task.Factory.StartNew(async () =>
{
    while (true)
    {
        await Task.Delay(100);
        if (_logBox.IsDirty)
            _logBox.Draw();
    }
});

// Line input box
var line = _inputBox.ReadLine(); // Blocking while waiting for <enter>

代码:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

public class InputConsoleBox
{
    #region Output
    #region Win32 interop
    private const UInt32 STD_OUTPUT_HANDLE = unchecked((UInt32)(-11));
    private const UInt32 STD_ERROR_HANDLE = unchecked((UInt32)(-12));

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern IntPtr GetStdHandle(UInt32 type);
    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern SafeFileHandle CreateFile(
        string fileName,
        [MarshalAs(UnmanagedType.U4)] uint fileAccess,
        [MarshalAs(UnmanagedType.U4)] uint fileShare,
        IntPtr securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        [MarshalAs(UnmanagedType.U4)] int flags,
        IntPtr template);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool WriteConsoleOutput(
        SafeFileHandle hConsoleOutput,
        CharInfo[] lpBuffer,
        Coord dwBufferSize,
        Coord dwBufferCoord,
        ref SmallRect lpWriteRegion);

    [StructLayout(LayoutKind.Sequential)]
    private struct Coord
    {
        public short X;
        public short Y;

        public Coord(short X, short Y)
        {
            this.X = X;
            this.Y = Y;
        }
    };

    [StructLayout(LayoutKind.Explicit)]
    private struct CharUnion
    {
        [FieldOffset(0)]
        public char UnicodeChar;
        [FieldOffset(0)]
        public byte AsciiChar;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct CharInfo
    {
        [FieldOffset(0)]
        public CharUnion Char;
        [FieldOffset(2)]
        public ushort Attributes;

        public CharInfo(char @char, ushort attributes)
        {
            this.Char = new CharUnion();
            Char.UnicodeChar = @char;
            Attributes = attributes;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SmallRect
    {
        public short Left;
        public short Top;
        public short Right;
        public short Bottom;
    }
    #endregion
    #region Colors Enum

    private const int HighIntensity = 0x0008;
    private const ushort COMMON_LVB_LEADING_BYTE = 0x0100;
    private const ushort COMMON_LVB_TRAILING_BYTE = 0x0200;
    private const ushort COMMON_LVB_GRID_HORIZONTAL = 0x0400;
    private const ushort COMMON_LVB_GRID_LVERTICAL = 0x0800;
    private const ushort COMMON_LVB_GRID_RVERTICAL = 0x1000;
    private const ushort COMMON_LVB_REVERSE_VIDEO = 0x4000;
    private const ushort COMMON_LVB_UNDERSCORE = 0x8000;
    private const ushort COMMON_LVB_SBCSDBCS = 0x0300;
    [Flags]
    public enum Colors : int
    {
        Black = 0x0000,
        DarkBlue = 0x0001,
        DarkGreen = 0x0002,
        DarkRed = 0x0004,
        Gray = DarkBlue | DarkGreen | DarkRed,
        DarkYellow = DarkRed | DarkGreen,
        DarkPurple = DarkRed | DarkBlue,
        DarkCyan = DarkGreen | DarkBlue,
        LightBlue = DarkBlue | HighIntensity,
        LightGreen = DarkGreen | HighIntensity,
        LightRed = DarkRed | HighIntensity,
        LightWhite = Gray | HighIntensity,
        LightYellow = DarkYellow | HighIntensity,
        LightPurple = DarkPurple | HighIntensity,
        LightCyan = DarkCyan | HighIntensity
    }

    #endregion // Colors Enum

    private readonly CharInfo[] _buffer;
    private readonly List<CharInfo> _tmpBuffer;
    private readonly short _left;
    private readonly short _top;
    private readonly short _width;
    private readonly short _height;
    private ushort _defaultColor;
    private int _cursorLeft;
    private int _cursorTop;
    private static SafeFileHandle _safeFileHandle;
    /// <summary>
    /// Automatically draw to console.
    /// Unset this if you want to manually control when (and what order) boxes are writen to consoles - or you want to batch some stuff.
    /// You must manually call <c>Draw()</c> to write to console.
    /// </summary>
    public bool AutoDraw = true;
    public bool IsDirty { get; private set; }

    public InputConsoleBox(short left, short top, short width, short height, Colors defaultForegroundColor = Colors.Gray, Colors defaultBackgroundColor = Colors.Black)
    {
        if (left < 0 || top < 0 || left + width > Console.BufferWidth || top + height > Console.BufferHeight)
            throw new Exception(string.Format("Attempting to create a box {0},{1}->{2},{3} that is out of buffer bounds 0,0->{4},{5}", left, top, left + width, top + height, Console.BufferWidth, Console.BufferHeight));

        _left = left;
        _top = top;
        _width = width;
        _height = height;
        _buffer = new CharInfo[_width * _height];
        _defaultColor = CombineColors(defaultForegroundColor, defaultBackgroundColor);
        _tmpBuffer = new List<CharInfo>(_width * _height); // Assumption that we won't be writing much more than a screenful (backbufferfull) in every write operation


        //SafeFileHandle h = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
        if (_safeFileHandle == null)
        {
            var stdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
            _safeFileHandle = new SafeFileHandle(stdOutputHandle, false);
        }

        Clear();
        Draw();
    }

    public void Clear()
    {
        for (int y = 0; y < _height; y++)
        {
            for (int x = 0; x < _width; x++)
            {
                var i = (y * _width) + x;
                _buffer[i].Char.UnicodeChar = ' ';
                _buffer[i].Attributes = _defaultColor;
            }
        }
        IsDirty = true;
        // Update screen
        if (AutoDraw)
            Draw();
    }

    public void Draw()
    {
        IsDirty = false;
        var rect = new SmallRect() { Left = _left, Top = _top, Right = (short)(_left + _width), Bottom = (short)(_top + _height) };
        bool b = WriteConsoleOutput(_safeFileHandle, _buffer,
            new Coord(_width, _height),
            new Coord(0, 0), ref rect);
    }

    private static ushort CombineColors(Colors foreColor, Colors backColor)
    {
        return (ushort)((int)foreColor + (((int)backColor) << 4));
    }

    public void SetCursorPosition(int left, int top)
    {
        if (left >= _width || top >= _height)
            throw new Exception(string.Format("Position out of bounds attempting to set cursor at box pos {0},{1} when box size is only {2},{3}.", left, top, _width, _height));

        _cursorLeft = left;
        _cursorTop = top;
    }

    public void SetCursorBlink(int left, int top, bool state)
    {
        Console.SetCursorPosition(left, top);
        Console.CursorVisible = state;
        //// Does not work
        //var i = (top * _width) + left;
        //if (state)
        //    _buffer[i].Attributes = (ushort)((int)_buffer[i].Attributes & ~(int)COMMON_LVB_UNDERSCORE);
        //else
        //    _buffer[i].Attributes = (ushort)((int)_buffer[i].Attributes | (int)COMMON_LVB_UNDERSCORE);

        //if (AutoDraw)
        //    Draw();
    }

    public void WriteLine(string line, Colors fgColor, Colors bgColor)
    {
        var c = _defaultColor;
        _defaultColor = CombineColors(fgColor, bgColor);
        WriteLine(line);
        _defaultColor = c;
    }

    public void WriteLine(string line)
    {
        Write(line + "\n");
    }

    public void Write(string text)
    {
        Write(text.ToCharArray());
    }

    public void Write(char[] text)
    {
        IsDirty = true;
        _tmpBuffer.Clear();
        bool newLine = false;

        // Old-school! Could definitively have been done more easily with regex. :)
        var col = 0;
        var row = -1;
        for (int i = 0; i < text.Length; i++)
        {

            // Detect newline
            if (text[i] == '\n')
                newLine = true;
            if (text[i] == '\r')
            {
                newLine = true;
                // Skip following \n
                if (i + 1 < text.Length && text[i] == '\n')
                    i++;
            }

            // Keep track of column and row
            col++;
            if (col == _width)
            {
                col = 0;
                row++;

                if (newLine) // Last character was newline? Skip filling the whole next line with empty
                {
                    newLine = false;
                    continue;
                }
            }

            // If we are newlining we need to fill the remaining with blanks
            if (newLine)
            {
                newLine = false;

                for (int i2 = col; i2 <= _width; i2++)
                {
                    _tmpBuffer.Add(new CharInfo(' ', _defaultColor));
                }
                col = 0;
                row++;
                continue;
            }
            if (i >= text.Length)
                break;

            // Add character
            _tmpBuffer.Add(new CharInfo(text[i], _defaultColor));
        }

        var cursorI = (_cursorTop * _width) + _cursorLeft;

        // Get our end position
        var end = cursorI + _tmpBuffer.Count;

        // If we are overflowing (scrolling) then we need to complete our last line with spaces (align buffer with line ending)
        if (end > _buffer.Length && col != 0)
        {
            for (int i = col; i <= _width; i++)
            {
                _tmpBuffer.Add(new CharInfo(' ', _defaultColor));
            }
            col = 0;
            row++;
        }

        // Chop start of buffer to fit into destination buffer
        if (_tmpBuffer.Count > _buffer.Length)
            _tmpBuffer.RemoveRange(0, _tmpBuffer.Count - _buffer.Length);
        // Convert to array so we can batch copy
        var tmpArray = _tmpBuffer.ToArray();

        // Are we going to write outside of buffer?
        end = cursorI + _tmpBuffer.Count;
        var scrollUp = 0;
        if (end > _buffer.Length)
        {
            scrollUp = end - _buffer.Length;
        }

        // Scroll up
        if (scrollUp > 0)
        {
            Array.Copy(_buffer, scrollUp, _buffer, 0, _buffer.Length - scrollUp);
            cursorI -= scrollUp;
        }
        var lastPos = Math.Min(_buffer.Length, cursorI + tmpArray.Length);
        var firstPos = lastPos - tmpArray.Length;

        // Copy new data in
        Array.Copy(tmpArray, 0, _buffer, firstPos, tmpArray.Length);

        // Set new cursor position
        _cursorLeft = col;
        _cursorTop = Math.Min(_height, _cursorTop + row + 1);

        // Write to main buffer
        if (AutoDraw)
            Draw();
    }
    #endregion

    #region Input
    private string _currentInputBuffer = "";
    private string _inputPrompt;
    private int _inputCursorPos = 0;
    private int _inputFrameStart = 0;
    // Not used because COMMON_LVB_UNDERSCORE doesn't work
    //private bool _inputCursorState = false;
    //private int _inputCursorStateChange = 0;
    private int _cursorBlinkLeft = 0;
    private int _cursorBlinkTop = 0;

    public string InputPrompt
    {
        get { return _inputPrompt; }
        set
        {
            _inputPrompt = value;
            ResetInput();
        }
    }

    private void ResetInput()
    {
        SetCursorPosition(0, 0);
        _inputCursorPos = Math.Min(_currentInputBuffer.Length, _inputCursorPos);

        var inputPrompt = InputPrompt + "[" + _currentInputBuffer.Length + "] ";

        // What is the max length we can write?
        var maxLen = _width - inputPrompt.Length;
        if (maxLen < 0)
            return;

        if (_inputCursorPos > _inputFrameStart + maxLen)
            _inputFrameStart = _inputCursorPos - maxLen;
        if (_inputCursorPos < _inputFrameStart)
            _inputFrameStart = _inputCursorPos;

        _cursorBlinkLeft = inputPrompt.Length + _inputCursorPos - _inputFrameStart;

        //if (_currentInputBuffer.Length - _inputFrameStart < maxLen)
        //    _inputFrameStart--;


        // Write and pad the end
        var str = inputPrompt + _currentInputBuffer.Substring(_inputFrameStart, Math.Min(_currentInputBuffer.Length - _inputFrameStart, maxLen));
        var spaceLen = _width - str.Length;
        Write(str + (spaceLen > 0 ? new String(' ', spaceLen) : ""));

        UpdateCursorBlink(true);

    }
    private void UpdateCursorBlink(bool force)
    {
        // Since COMMON_LVB_UNDERSCORE doesn't work we won't be controlling blink
        //// Blink the cursor
        //if (Environment.TickCount > _inputCursorStateChange)
        //{
        //    _inputCursorStateChange = Environment.TickCount + 250;
        //    _inputCursorState = !_inputCursorState;
        //    force = true;
        //}
        //if (force)
        //    SetCursorBlink(_cursorBlinkLeft, _cursorBlinkTop, _inputCursorState);
        SetCursorBlink(_left + _cursorBlinkLeft, _top + _cursorBlinkTop, true);
    }

    public string ReadLine()
    {
        Console.CursorVisible = false;
        Clear();
        ResetInput();
        while (true)
        {
            Thread.Sleep(50);

            while (Console.KeyAvailable)
            {
                var key = Console.ReadKey(true);

                switch (key.Key)
                {
                    case ConsoleKey.Enter:
                        {
                            var ret = _currentInputBuffer;
                            _inputCursorPos = 0;
                            _currentInputBuffer = "";
                            return ret;
                            break;
                        }
                    case ConsoleKey.LeftArrow:
                        {
                            _inputCursorPos = Math.Max(0, _inputCursorPos - 1);
                            break;
                        }
                    case ConsoleKey.RightArrow:
                        {
                            _inputCursorPos = Math.Min(_currentInputBuffer.Length, _inputCursorPos + 1);
                            break;
                        }
                    case ConsoleKey.Backspace:
                        {
                            if (_inputCursorPos > 0)
                            {
                                _inputCursorPos--;
                                _currentInputBuffer = _currentInputBuffer.Remove(_inputCursorPos, 1);
                            }
                            break;
                        }
                    case ConsoleKey.Delete:
                        {
                            if (_inputCursorPos < _currentInputBuffer.Length - 1)
                                _currentInputBuffer = _currentInputBuffer.Remove(_inputCursorPos, 1);
                            break;
                        }

                    default:
                        {
                            var pos = _inputCursorPos;
                            //if (_inputCursorPos == _currentInputBuffer.Length)
                            _inputCursorPos++;
                            _currentInputBuffer = _currentInputBuffer.Insert(pos, key.KeyChar.ToString());
                            break;
                        }
                }
                ResetInput();

            }

            // COMMON_LVB_UNDERSCORE doesn't work so we use Consoles default cursor
            //UpdateCursorBlink(false);

        }
    }

    #endregion

}

关于c# - System.Console 作为带有输入行的日志窗口,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31740747/

相关文章:

ios - UIScrollView页面负方向

c# - .NET应用程序将作为控制台应用程序运行,但不能作为Windows窗体运行,调试工作

c# - 即使控制台应用程序退出,线程也保持事件状态

c# - Windows 窗体 - 奇怪的自动滚动行为

javascript - Jquery Autocomplete()/Catcomplete() 函数不起作用

c# - 将一个表达式作为参数传递到另一个表达式中

jquery - CSS 叠加效果未关闭

jQuery 滚动结束和最后一个子元素选择器

c# - C# Windows 控制台应用程序如何判断它是否以交互方式运行

c# - Linq 拼图 : How do I convert this foreach to linq syntax?