【问题标题】:Read from location on console C#从控制台 C# 上的位置读取
【发布时间】:2012-09-10 16:03:18
【问题描述】:

我需要从控制台中的特定位置读取文本,例如 5,5。

如果我需要写信到这个位置,那就是:

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

有什么方法可以用类似的方式阅读吗?

只是为了澄清: 我不想停下来接受用户的输入,甚至有可能输入不是来自用户,而是以前打印出来的。我真的想要某种: Console.GetCharAtLocation(5,5) 或类似的东西。

【问题讨论】:

  • 这个控制台应用程序可以修改吗?
  • 是的,我正在开发控制台应用程序,如果这就是您的要求。
  • 需要从实际窗口读取还是从缓冲区读取?
  • 通常很少需要这样做。如果用户提供了您需要稍后引用的输入,您可能应该将该输入存储在内存中的某个地方。也许如果您可以更具体地提出有关您的问题的新问题,那么有人可以提供替代解决方案。
  • 伙计们,我正在尝试从托管在 WPF 应用程序中的控制台应用程序中读取数据。到目前为止,我可以使用 ReadConsoleOutput 从 wpf 外部的控制台应用程序读取数据,但它只能从 ActiveConsole 缓冲区读取。我如何从 WPF 应用程序中托管的控制台读取数据。请帮忙!

标签: c# console


【解决方案1】:

这是一个 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);
}

【讨论】:

  • 我试过这个,但是报错:“System.IO.IOException: 'The handle is invalid.”
  • @AndrásGeiszl - 我刚刚对其进行了测试,它仍然可以正常工作,使用 .NET 4 到 4.7,或 .NET Core、win10 x64 或 x32,使用 cmd.exe 或其他控制台(如 ConEmu )。
  • 旧线程,但它仍然可能对某人有所帮助。找到您和其他有关此的文章,并为此建立了一个小型图书馆。如果其他人想快速开始这样做,请加入github.com/jrnker/Proxmea.ConsoleHelper
【解决方案2】:

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          ".Trim());
}

应该是这样的:

现在让我们读回一些字符。首先,您需要 stdout 的本机控制台句柄。下面是从 Win32 获取它的 P/Invoke 方法:

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

现在是最酷的部分;到目前为止,这似乎是此页面上唯一使用ReadConsoleOutputCharacter Win32 函数的答案。虽然它不能让您获得字符颜色属性,但这种方法确实省去了处理复制矩形和必须使用CreateConsoleScreenBuffer 分配屏幕缓冲区并在它们之间复制的所有麻烦。

有单独的 AnsiUnicode 版本,您需要根据控制台窗口中的活动代码页调用正确的版本。我在这里展示了两个 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)

您可能会注意到我已将这些封送处理减少到了示例代码所需的最低限度,该示例代码旨在一次仅获取一个字符。因此,由于托管指针声明“out byte ch”和“out Char ch”,您可能会发现c_in 必须始终为1

这就是你真正需要的;如果您限制自己阅读单个字符,则如上所述调用适当的 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 + " ");
    }
}

你有它...

【讨论】:

  • 我在输出字节 chAnsi ==> 出现错误的表达式术语字节。我正在使用 Visual Studio 2015 .Net Framework 4.6.2
  • @FaranSaleem 该示例使用 C# 7 功能。您可以单独声明“out”变量,而不是内联,或者升级到 VS2017 或 VS2019。
  • 好的,知道了。我还有一个嵌入在另一个 WPF 应用程序中的控制台应用程序。我正在将句柄传递给它,但它说句柄无效。使用 -11 我只能在当前控制台窗口上写字。我该如何解决这个问题?
  • @FaranSaleem 如果您将控制台作为新进程生成,则需要使用通过ProcessStartInfo 选项明确请求和捕获的stdio 句柄。反过来,这也仅在 ProcessStartInfo.UseShellExecute 选项设置为 false 时才有效。
  • 嗯,你能给我举个例子吗?基本上,我对此没有太多了解。我正在使用 wpf 窗口,在其中我托管控制台应用程序,如果我使用 std_output_handle 到 -11 以外的任何其他句柄,它会给我一个异常,说句柄无效。所以,你能帮我举个例子吗?
【解决方案3】:

此功能不存在。从理论上讲,您可以覆盖控制台上的输入和输出流,以保留您自己可以读取的控制台缓冲区副本,但这并不重要(并且可能无法支持所有边缘情况,例如作为连接到您的控制台并对其进行读/写的外部程序)。

【讨论】:

  • 这有点烦人。哦,好吧,令人失望的答案(当然不是你的错),但我想并不是每条彩虹最后都有一罐金子。感谢大家的帮助,我想我现在会开始考虑其他选择:P
【解决方案4】:

算了,太麻烦了,你可以从缓冲区中读取并获取所有当前控制台输出,但这太过分了。

我的建议是创建一个 ConsoleWriter 委托,您可以选择如何,可以是一个类或只是一个静态方法,并且这个 writer 会将最后一行保留在一个属性中,因此每次您使用 Console.WriteLine 时,您只需调用您的委托,通过您的实现,最后它调用 Console.WriteLine。

【讨论】:

    【解决方案5】:

    正如@Servy 所说,没有任何内置功能(我知道或可以找到)可以满足您的需求。但是,有一种解决方法(这有点像 hack,但确实有效)。

    您可以在内存或磁盘上创建自己的缓冲区。每当您输出到控制台时,也输出到您的缓冲区。然后,您可以使用您的缓冲区以您无法使用控制台的方式进行读取。

    有两种缓冲方式:在磁盘上或在内存中。您可以使用Console.BufferWidthConsole.BufferHeight 属性来找出您的缓冲区大小。我发现使用字符串数组在内存中执行此操作更简单(每个字符串都是一行输出,如果我没记错的话,数组中有许多字符串等于BufferHeight)。一位同事最终在磁盘上做了同样的事情。

    您需要创建一个方法来替换Console.WriteConsole.WriteLine,这样您就可以同时写入两个缓冲区。比如:

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

    我发现在数组周围封装一个 Class 并实现支持它的方法很有帮助...然后您可以实现 GetCharAtLocation( int i, int j ) 方法以及您需要的任何其他功能。

    【讨论】:

    • 正如我所提到的,您需要能够处理光标移动的情况(由用户和在代码中),您需要能够处理一般的用户输入,缓冲区正在调整大小,整个缓冲区中是否存在空字符,异步编程中潜在的竞争条件(如果大约同时有两次写入,您的内部缓冲区是否总是与控制台缓冲区相同?)等等。这就是为什么我有说这是一个非常重要的问题。
    • 我认为你的意思是“如果你需要能够处理......”;-)但是是的,如果是这样的话,这对至少可以说。很多这些条件都是可以解决的,如果我编写的处理这些问题的代码不是私有财产,我会发布它。我确实同意,对于他正在处理的任何情况,可能都有更好的解决方案。
    • 好吧,只要阅读 OP 的帖子,他就清楚地在代码中以编程方式设置光标,并且他正在处理用户输入和编程输出(用户在提供输入时也会经常移动光标;不这样做将是一个很大的烦恼)。目前尚不清楚应用程序是否是单线程的,或者是否会写入足够的文本来填充缓冲区,但这似乎是一个值得编码的情况。
    【解决方案6】:

    怎么样:

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

    编辑:

    正如其他人所说,这只是一个微不足道的案例,还有很多其他事情需要改进。但是我写这个只是作为一个起点。

    【讨论】:

    • CustomizedConsole 缓冲区大于实际控制台缓冲区时会发生什么?
    • 你不处理手动移动光标的情况,用户移动光标的情况,退格字符,阅读输入,你没有考虑到有整个缓冲区中的空字符(您的代码在尝试读取未读取的位置时出现越界错误)。等等。它实际上只涵盖了最微不足道的情况。
    猜你喜欢
    • 2023-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-19
    • 2010-12-28
    相关资源
    最近更新 更多