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

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

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

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

Есть ли способ, которым я могу читать аналогичным образом?

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

Ответ 1

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

Ответ 2

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

Использование образца:

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);
}

Ответ 3

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

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

        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

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

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

Теперь о крутой части; кажется, это единственный ответ на этой странице, который использует ReadConsoleOutputCharacter Win32 ReadConsoleOutputCharacter. Хотя он не позволяет получить атрибуты цвета символа, этот подход избавляет от всех проблем, связанных с копированием прямоугольников и необходимостью использовать 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. Код может использовать некоторые функции компилятора С# из 7.2.Для Visual Studion 2017 включите "последний" параметр в дополнительных параметрах сборки "Свойства проекта".

Ответ 4

Как насчет:

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:

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

Ответ 5

Как заявил @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 ), а также любые другие функции, которые вам там нужны.