Чтение из местоположения на консоли C#

Мне нужно прочитать текст из определенного места в консоли, скажем 5,5.

Если бы мне нужно было написать в это место, это было бы просто:

Console.SetCursorPosition(5, 5);
Console.Write("My text");

есть ли способ я могу читать подобным образом?

просто для уточнения: Я не хочу останавливаться, чтобы взять ввод от пользователя, есть шанс даже, что ввод не будет от пользователя, но что-то ранее распечатано. Я буквально хочу что-то вроде: Приставка.GetCharAtLocation (5,5) или нечто подобное.

5 ответов


эта функциональность не существует. Теоретически вы можете переопределить входные и выходные потоки на консоли, чтобы сохранить свою собственную копию буфера консоли, из которого вы могли бы читать, но это было бы нетривиально (и, вероятно, не могло бы поддерживать все крайние случаи, такие как внешняя программа, подключенная к вашей консоли и чтение/запись на нее).


вот утилита кода C#, которая может читать то, что в настоящее время находится в буфере консоли (не окно, буфер):

пример использования:

class Program
{
    static void Main(string[] args)
    {
        // read 10 lines from the top of the console buffer
        foreach (string line in ConsoleReader.ReadFromBuffer(0, 0, (short)Console.BufferWidth, 10))
        {
            Console.Write(line);
        }
    }
}

утилиты:

public class ConsoleReader
{
    public static IEnumerable<string> ReadFromBuffer(short x, short y, short width, short height)
    {
        IntPtr buffer = Marshal.AllocHGlobal(width * height * Marshal.SizeOf(typeof(CHAR_INFO)));
        if (buffer == null)
            throw new OutOfMemoryException();

        try
        {
            COORD coord = new COORD();
            SMALL_RECT rc = new SMALL_RECT();
            rc.Left = x;
            rc.Top = y;
            rc.Right = (short)(x + width - 1);
            rc.Bottom = (short)(y + height - 1);

            COORD size = new COORD();
            size.X = width;
            size.Y = height;

            const int STD_OUTPUT_HANDLE = -11;
            if (!ReadConsoleOutput(GetStdHandle(STD_OUTPUT_HANDLE), buffer, size, coord, ref rc))
            {
                // 'Not enough storage is available to process this command' may be raised for buffer size > 64K (see ReadConsoleOutput doc.)
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            IntPtr ptr = buffer;
            for (int h = 0; h < height; h++)
            {
                StringBuilder sb = new StringBuilder();
                for (int w = 0; w < width; w++)
                {
                    CHAR_INFO ci = (CHAR_INFO)Marshal.PtrToStructure(ptr, typeof(CHAR_INFO));
                    char[] chars = Console.OutputEncoding.GetChars(ci.charData);
                    sb.Append(chars[0]);
                    ptr += Marshal.SizeOf(typeof(CHAR_INFO));
                }
                yield return sb.ToString();
            }
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct CHAR_INFO
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        public byte[] charData;
        public short attributes;
    }

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

    [StructLayout(LayoutKind.Sequential)]
    private struct SMALL_RECT
    {
        public short Left;
        public short Top;
        public short Right;
        public short Bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct CONSOLE_SCREEN_BUFFER_INFO
    {
        public COORD dwSize;
        public COORD dwCursorPosition;
        public short wAttributes;
        public SMALL_RECT srWindow;
        public COORD dwMaximumWindowSize;
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool ReadConsoleOutput(IntPtr hConsoleOutput, IntPtr lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpReadRegion);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr GetStdHandle(int nStdHandle);
}

упрощенная демонстрация, которая работает в Windows 10 для чтения одного символа из указанного (X, Y) положение на экране. Проверено с помощью .NET 4.7.2.1

во-первых, вот строка кода, которая заполняет консоль демонстрационной сеткой. Обратите внимание, что он должен отображаться в верхнем левом углу экрана, чтобы демо-версия работала.

        static void Populate_Console()
        {
            Console.Clear();
            Console.Write(@"
 ┌───────┐
1│C D E F│
2│G H I J│
3│K L M N│
4│O P Q R│
 └───────┘
  2 4 6 8

    ".TrimStart('\r', '\n'));
        }

это должно выглядеть это:

enter image description here

теперь давайте прочитаем некоторые символы обратно. Для начала вам понадобится собственный дескриптор консоли для стандартный вывод. Вот метод P / Invoke для его получения из для Win32:

[DllImport("kernel32", SetLastError = true)]
static extern IntPtr GetStdHandle(int num);

теперь для прохладной части; это, кажется, единственный ответ на этой странице до сих пор, который использует ReadConsoleOutputCharacter функции Win32. Хотя это не позволяет получить атрибуты цвета символов, этот подход сохраняет ли все проблемы с копированием прямоугольников и использованием CreateConsoleScreenBuffer для выделения буферов экрана и копирования между ними.

отдельные в ANSI и Unicode версии, и вам нужно вызвать правильный в зависимости от кодовой страницы, которая активна в окне консоли. Я показываю здесь обе подписи P/Invoke, но для простоты в примере я просто продолжу с в ANSI версия:

    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
    [return: MarshalAs(UnmanagedType.Bool)] //   ̲┌──────────────────^
    static extern bool ReadConsoleOutputCharacterA(
        IntPtr hStdout,   // result of 'GetStdHandle(-11)'
        out byte ch,      // A̲N̲S̲I̲ character result
        uint c_in,        // (set to '1')
        uint coord_XY,    // screen location to read, X:loword, Y:hiword
        out uint c_out);  // (unwanted, discard)

    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)] //   ̲┌───────────────────^
    static extern bool ReadConsoleOutputCharacterW(
        IntPtr hStdout,   // result of 'GetStdHandle(-11)'
        out Char ch,      // U̲n̲i̲c̲o̲d̲e̲ character result
        uint c_in,        // (set to '1')
        uint coord_XY,    // screen location to read, X:loword, Y:hiword
        out uint c_out);  // (unwanted, discard)

вы можете заметить, что я разделил маршалинг на них до минимума, необходимого для целей моего примера кода, который предназначен только для извлечения одного символа за раз. Поэтому вы, вероятно, найдете, что c_in всегда должны быть 1, из-за управляемых объявлений указателя ‘out byte ch’ и ‘out Char ch’.

это действительно все, что вам нужно; вызов соответствующей функции P / Invoke, как описано выше, в основном самоочевиден, если вы ограничиваетесь чтением одного символа. Чтобы показать это на тривиальном примере, я закончу с милой демонстрационной программой, которая считывает четыре символа из Console, по диагонали сетки, которую мы нарисовали выше.

static void Windows_Console_Readback()
{
    var stdout = GetStdHandle(-11);

    for (uint coord, y = 1; y <= 4; y++)
    {
        coord = (5 - y) * 2;        // loword  <-- X coord to read
        coord |= y << 16;           // hiword  <-- Y coord to read

        if (!ReadConsoleOutputCharacterA(
                stdout,
                out byte chAnsi,    // result: single ANSI char
                1,                  // # of chars to read
                coord,              // (X,Y) screen location to read (see above)
                out _))             // result: actual # of chars (unwanted)
            throw new Win32Exception();

        Console.Write(" " + (Char)chAnsi + " ");
    }
}

и вот оно...

enter image description here




Примечания:
1. Код может использовать некоторые функции компилятора C# из 7.2. Для Visual Студийского 2017, включите" последнюю "опцию в расширенных параметрах сборки "свойства проекта".

о:

class Program {
    static void Main( string[ ] args ) {
        CustomizedConsole.WriteLine( "Lorem Ipsum" ); //Lorem Ipsum
        Console.WriteLine( CustomizedConsole.ReadContent( 6, 5 ) ); //Ipsum
        Console.WriteLine( CustomizedConsole.GetCharAtLocation( 0, 0 ) ); //L
    }
}

static class CustomizedConsole {
    private static List<char> buffer = new List<char>();
    private static int lineCharCount = 0;

    public static void Write(string s){
        lineCharCount += s.Length;
        buffer.AddRange( s );
        Console.Write( s );
    }

    public static void WriteLine(string s ) {
        for ( int i = 0; i < Console.BufferWidth - lineCharCount - s.Length; i++ )
            s += " ";

        buffer.AddRange( s );
        Console.WriteLine( s );
        lineCharCount = 0;
    }

    public static string ReadContent( int index, int count ) {
        return new String(buffer.Skip( index ).Take( count ).ToArray());
    }

    public static char GetCharAtLocation( int x, int y ) {
        return buffer[ Console.BufferHeight * x + y ];
    }
}

EDIT:

Как сказали другие, это просто тривиальный случай, когда есть много других вещей, чтобы улучшить. Но я написал это только для начала.


как заявил @Servy, нет никаких встроенных функций (которые я знаю или могу найти), которые могут делать то, что вы хотите. Тем не менее, есть обходной путь (это немного хак, но он работал).

вы можете создать свой собственный буфер в памяти или на диске. Всякий раз, когда вы выводите на консоль, также выводите в буфер. Затем вы можете использовать свой буфер для чтения так, как вы не могли с помощью консоли.

существует два способа буфера: на диске или в памяти. Вы можно использовать Console.BufferWidth и Console.BufferHeight свойства, чтобы узнать размер буфера. Мне было проще сделать это в памяти, используя массив строк (каждая строка была строкой вывода, и в массиве было несколько строк, равных BufferHeight, Если я правильно помню). Коллега сделал то же самое на диске.

вы хотите создать метод, чтобы заменить Console.Write и Console.WriteLine, так что вы можете писать в оба буфера сразу. Что-то например:

public void MyWrite( string output ) {
    Console.Write( output );
    Array.Write( output );  // obvious pseudo-code
}

Я нашел полезным обернуть класс вокруг массива и реализовать методы для его поддержки ... затем вы можете реализовать свой GetCharAtLocation( int i, int j ) метод, а также любые другие функции, которые вам нужны есть.