我需要从控制台中的特定位置读取文本,比如 5,5。
如果我需要写信到这个位置,那就是:
Console.SetCursorPosition(5, 5);
Console.Write("My text");
有什么方法可以让我以类似的方式阅读吗?
澄清一下:我不想停下来接受用户的输入,甚至有可能输入不是来自用户,而是以前打印出来的。我真的想要某种: Console.GetCharAtLocation(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测试。
首先,这是用演示网格填充控制台的一行代码。请注意,它应该呈现在屏幕的左上角,以便演示工作。
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
分配屏幕缓冲区和在它们之间复制的所有麻烦。
有单独的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)
您可能会注意到我已经将这些封送处理减少到了示例代码所需的最低限度,该示例代码旨在一次仅获取一个字符。因此,由于托管指针声明 '<code>out byte ch' 和 '<code>out Char ch' ,您可能会发现c_in
must always be 。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 + " ");
}
}
你有它...
该功能不存在。从理论上讲,您可以覆盖控制台上的输入和输出流,以保留您自己可以读取的控制台缓冲区副本,但这并不重要(并且可能无法支持所有边缘情况,例如作为连接到您的控制台并读取/写入它的外部程序)。
算了,太麻烦了,你可以从缓冲区中读取并获取所有当前的控制台输出,但这太过分了。
我的建议是创建一个 ConsoleWriter 委托,你可以选择如何,可以是一个类或只是一个静态方法,这个 writer 会将最后一行保留在一个属性中,所以每次你使用 Console.WriteLine 时,你只需调用你的委托,使用您的实现,最后它调用 Console.WriteLine。
关于什么:
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 ];
}
}
编辑 :
正如其他人所说,这只是一个微不足道的案例,还有很多其他事情需要改进。但我写这个只是作为一个起点。
正如@Servy 所说,没有任何内置功能(我知道或可以找到)可以满足您的需求。但是,有一种解决方法(这有点像 hack,但它有效)。
您可以在内存或磁盘上创建自己的缓冲区。每当您输出到控制台时,也输出到您的缓冲区。然后,您可以使用您的缓冲区以您无法使用控制台的方式进行读取。
有两种缓冲方式:在磁盘上或在内存中。您可以使用Console.BufferWidth
和Console.BufferHeight
属性来找出您的缓冲区大小。我发现使用字符串数组在内存中执行此操作更简单(每个字符串都是一行输出,BufferHeight
如果我没记错的话,数组中有许多字符串等于 )。一位同事最终在磁盘上做了同样的事情。
您需要创建一个方法来替换Console.Write
and Console.WriteLine
,以便您可以一次写入两个缓冲区。就像是:
public void MyWrite( string output ) {
Console.Write( output );
Array.Write( output ); // obvious pseudo-code
}
我发现在数组周围包装一个类并实现支持它的方法很有帮助......然后您可以实现您的GetCharAtLocation( int i, int j )
方法以及您需要的任何其他功能。