使用 API 清除控制台屏幕






4.81/5 (8投票s)
了解如何通过 API 调用清除控制台屏幕。此外,还将学习一些控制台技术,例如在屏幕上移动文本。
概述
除了清除控制台屏幕,本课程还将向您介绍一些关于 PInvoke、封送 和 内存管理 的知识。此外,您还将学习其他技术,例如清除屏幕的特定部分,以及更改光标位置。更重要的是,您将深入了解 IL,看看 System.Console.Clear()
方法是如何实现的。更重要的是,您将学习如何逆向工程 .NET 程序集并发现其中的代码。此外,示例应用程序展示了如何使用 Win32 API 调用对控制台执行 I/O 操作,以及如何显示/隐藏控制台光标。更重要的是,它教您如何在控制台屏幕上移动文本。
目录
本文目录
- 概述
- 目录
- 引言
- GetStdHandle() 函数
- 关于封送的说明
- GetConsoleScreenBufferInfo() 函数
- 关于内存管理的说明
- 内存内部概览
- FillConsoleOutputCharacter() 函数
- SetConsoleCursorPosition() 函数
- 整合
- 清除控制台屏幕
- .NET 库内部概览
- 参考文献
- 代码示例(Tiny Console Library)
- 总结
引言
System.Console
中添加了一个新方法,即用于清除控制台屏幕的 Clear()
方法。对于 2.0 之前的版本,或者如果您需要“硬编码”方式,甚至如果您需要对清除过程有更多控制,例如清除屏幕的特定部分,则必须深入研究 Win32 API 并调用特殊函数来实现。通过四个 Win32 API 函数可以清除控制台屏幕:GetStdHandle()
、GetConsoleScreenBufferInfo()
、FillConsoleOutputCharacter()
和 SetConsoleCursorPosition()
。值得注意的是,所有这些函数都位于 Kernel32.dll 库中。请确保 .NET 2.0 及更高版本中的 System.Console.Clear()
方法最初调用这四个函数——以及更多函数。
GetStdHandle() 函数
GetStdHandle()
。GetStdHandle()
仅返回标准输入、输出、错误设备(.NET 方法论中的“流”)的句柄。此函数接受一个参数,指定我们要检索句柄的标准设备(流)。此函数在 C 中的语法如下:HANDLE GetStdHandle(
DWORD nStdHandle
);
nStdHandle 参数可以接受以下三个值之一:- STD_INPUT_HANDLE = -10
指定标准输入设备(流)。 - STD_OUTPUT_HANDLE = -11
指定标准输出设备(流)。 - STD_ERROR_HANDLE = -12
指定标准错误设备(流)。它始终是输出设备(流)。
PInvoke 是 Platform Invocations 的缩写。它是 .NET 与其“女友”Win32 API 进行互操作的机制。
GetStdHandle()
的 PInvoke 方法如下——代码假定您已为 System.Runtime.InteropServices
命名空间添加了 using 语句:[DllImport("Kernel32.dll")]
public static extern IntPtr GetStdHandle(int nStdHandle);
PInvoke 方法需要 DllImport
属性,以便您可以指定函数所在的 DLL。此外,当您需要更改方法名称时,DllImport
扮演着非常重要的角色。如果您需要更改 PInvoke 方法的名称,则必须将 DllImportAttribute
的 EntryPoint
属性设置为 DLL 中函数的原始名称。如果您未设置此属性,则 .NET 将在 DLL 中搜索该方法,如果找不到,将抛出 System.EntryPointNotFoundException
异常。“static
”和“extern
”是 PInvoke 方法必需的修饰符。由于某些数据类型在 .NET 中不存在,因此您需要找到替代项。换句话说,您需要将非托管的 Win32 数据类型封送到新的托管 .NET 类型。因此,我们将 HANDLE
封送为 System.IntPtr
,将 DWORD
封送为 System.Int32
。控制台应用程序只有一个输出句柄(输入和错误设备也一样),因此请勿使用 CloseHandle()
函数关闭已打开的句柄,否则将导致您无法再向控制台写入,除非您重新打开它。
关于封送的说明
DWORD
,由于 DWORD
是一个 32 位无符号整数,我们必须将其封送为 System.UInt32
。但是,我们在 GetStdHandle()
中将其封送为 System.Int32
!虽然无符号整数(包括 DWORD
)不接受负数,但 GetStdHandle()
需要 DWORD
。实际上,这是可以的,因为负值可以以特殊方式存储在 DWORD
中。例如,-10 存储为 FFFFFFF6,-11 存储为 FFFFFFF5,依此类推。这就是 GetStdHandle()
实际执行的操作。此外,您可能会注意到我们将 HANDLE
封送为 System.IntPtr
,因为 IntPtr
是最适合任何 Win32 句柄的类型。此外,.NET 通过抽象的 SafeHandle
和 CriticalHandle
类支持托管句柄。值得一提的是,System.Runtime.InteropServices.MarshalAsAttribute
属性可以对封送过程产生显著影响。参考部分可以找到描述封送过程和 Win32 数据类型的良好资源。
托管代码是运行在 CLR(通用语言运行时)中的 .NET 代码,因为 CLR 管理和控制此代码。相反,非托管代码是指除托管代码以外的代码。您可以在 .NET 中编写在 CLR 外部运行的代码,例如直接内存管理,并且该代码被称为不安全代码,因为它容易出错并可能导致不可预测的行为。此外,MC++ 允许您将非托管代码与托管代码一起编写。
GetConsoleScreenBufferInfo() 函数
GetConsoleScreenBufferInfo()
。此方法检索有关特定控制台屏幕缓冲区(换句话说,设备)的信息。这些信息包括控制台屏幕缓冲区大小、控制台窗口的位置和边界,以及屏幕内的光标位置。此函数定义如下:BOOL GetConsoleScreenBufferInfo(
HANDLE hConsoleOutput,
[out] SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo
);
struct CONSOLE_SCREEN_BUFFER_INFO {
COORD dwSize;
COORD dwCursorPosition;
WORD wAttributes;
SMALL_RECT srWindow;
COORD dwMaximumWindowSize;
}
struct COORD {
SHORT X;
SHORT Y;
}
struct SMALL_RECT {
SHORT Left;
SHORT Top;
SHORT Right;
SHORT Bottom;
}
冗长,是吧?是的,没错。除了 GetConsoleScreenBufferInfo()
之外,还有三个结构,因为函数的第二个参数的类型是第一个结构,而第一个结构又引用第二个和第三个结构。CONSOLE_SCREEN_BUFFER_INFO
结构代表控制台信息。这些信息包括:- 控制台屏幕缓冲区的大小,以字符行和列为单位。
- 控制台屏幕中的光标位置,以字符行和列为单位。
- 写入控制台的字符特征,例如前景色和背景色。
- 控制台窗口的位置和边界。
- 考虑到字体大小和屏幕缓冲区大小,控制台窗口的最大尺寸。
什么是屏幕缓冲区和窗口大小,它们之间有什么区别?启动命令提示符,右键单击任务栏中的图标,然后在属性对话框中转到“布局”选项卡。花些时间玩弄此选项卡中的值。窗口大小是控制台窗口的大小——就像任何其他窗口一样。屏幕缓冲区大小决定了控制台屏幕可以容纳的文本量,因此您总是会看到垂直滚动条,因为缓冲区高度大于窗口高度。这是 PInvoke 方法和封送类型,因为 .NET 中没有这些三个结构:
[DllImport("Kernel32.dll")]
public static extern int GetConsoleScreenBufferInfo
(IntPtr hConsoleOutput,
out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
[StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
public COORD dwSize;
public COORD dwCursorPosition;
public ushort wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
[StructLayout(LayoutKind.Sequential)]
public struct COORD
{
public short X;
public short Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}
由于 .NET 中没有这些三个结构,也没有任何替代项,因此我们必须对其进行封送,我们已手动创建它,并将非托管映射到托管数据类型。由于这些对象的内存布局非常重要,因此我们添加了 StructLayout
属性并指定了 LayoutKind.Sequential
,它告知封送器该结构将在内存中按顺序布局,因此第一个字段排在第二个字段之前,依此类推。您还可能注意到 WORD
非托管数据类型是无符号 32 位整数,因此我们将其封送为 System.UInt32
。同样,SHORT
是有符号 16 位整数,因此很容易将其封送为 System.Int16
。有关 Win32 数据类型的更多信息,请参阅参考部分。BOOL
定义为 32 位有符号整数,因此我们已将函数的返回值封送为 Int32,而不是Boolean
,因为Boolean
只占用 4 位。它与Boolean
配合使用效果很好,但使用Int32
更好。不过,如果您想使用布尔值,用MarshalAs
属性修饰返回值会很有帮助。
此外,使用System.Runtime.InteropServices.InAttribute
和System.Runtime.InteropServices.OutAttribute
向封送器提供注释非常有效。代码示例对此进行了演示。
关于内存管理的说明
System.Object
直接或间接继承的对象是基于堆的(如大多数对象)。您还知道堆对象由垃圾回收器 (GC) 管理,它们可能会在内存中保留很长时间——实际上即使您多次调用 System.GC
。另一方面,栈对象在其作用域结束时会立即从内存中删除。此外,您需要知道栈是向下填充的。请参见图 1。CONSOLE_SCREEN_BUFFER_INFO
必须布局为 dwSize
在 dwCursorPosition
之前,依此类推。请参见图 2。CONSOLE_SCREEN_BUFFER_INFO
结构,以及它将如何渲染。请参见图 3。- 对象按创建顺序向下存储在栈中。
- 包含对象的对象也按声明顺序向下存储。
- 每个对象都有一个大小。该大小由其包含的对象决定(如果它不是基本类型的话)。
您可以通过两种方式获取对象的大小:现在,我们知道sizeof
关键字和System.Runtime.InteropServices.Marshal.SizeOf()
方法。推荐使用第二种方法。您知道为什么吗?尝试思考,然后回答。
StructLayout
属性为什么是必需的,以及为什么需要顺序布局。但是如果您想更改字段的顺序呢?答案很简单。将 LayoutKind
改为 Explicit
,并用 FieldOffset
属性修饰每个字段,指定其在结构开始处的偏移量。在以下代码中,字段已反转,但它们使用 FieldOffset
属性在内存中完美布局:[StructLayout(LayoutKind.Explicit)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
[FieldOffset(18)]
public COORD dwMaximumWindowSize;
[FieldOffset(10)]
public SMALL_RECT srWindow;
[FieldOffset(8)]
public ushort wAttributes;
[FieldOffset(4)]
public COORD dwCursorPosition;
[FieldOffset(0)]
public COORD dwSize;
}
如果将此外,从插图——特别是最后一张图——中,我们还可以使我们的LayoutKind
设置为Auto
,则无法使用该类型与非托管代码进行互操作。尽管如此,如果您省略了整个StructLayoutAttribute
。
CONSOLE_SCREEN_BUFFER_INFO
结构如下所示,并删除其他两个结构:[StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
public short dwSizeX;
public short dwSizeY;
public short dwCursorPositionX;
public short dwCursorPositionY;
public ushort wAttributes;
public short srWindowLeft;
public short srWindowTop;
public short srWindowRight;
public short srWindowBottom;
public short dwMaximumWindowSizeX;
public short dwMaximumWindowSizeY;
}
听起来很奇怪,不是吗?您也可以将还可以将两个或多个字段联合到一个字段中。例如,将两个 16 位整数LayoutKind
设置为Explicit
并开始工作。
dwSizeX
和 dwSizeY
合并为一个 32 位整数 dwSize
。它会很好地工作!此外,您可以在代码中使用 System.Collections.Specialized.BitVector32
结构将其拆分。内存内部概览
[StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
public COORD dwSize;
public COORD dwCursorPosition;
public ushort wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
[StructLayout(LayoutKind.Sequential)]
public struct COORD
{
public short X;
public short Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}
接下来,我们将初始化结构并填充一些数据,以查看它在内存中的存储方式。unsafe static void Main()
{
CONSOLE_SCREEN_BUFFER_INFO info
= new CONSOLE_SCREEN_BUFFER_INFO();
info.dwSize = new COORD();
info.dwSize.X = 0xA1;
info.dwSize.Y = 0xA2;
info.dwCursorPosition = new COORD();
info.dwCursorPosition.X = 0xB1;
info.dwCursorPosition.Y = 0xB2;
info.wAttributes = 0xFFFF;
info.srWindow = new SMALL_RECT();
info.srWindow.Left = 0xC1;
info.srWindow.Top = 0xC2;
info.srWindow.Right = 0xC3;
info.srWindow.Bottom = 0xC4;
info.dwMaximumWindowSize = new COORD();
info.dwMaximumWindowSize.X = 0xD1;
info.dwMaximumWindowSize.Y = 0xD2;
uint memoryAddress =
(uint)&info;
Console.WriteLine(
"Memory Address: 0x{0:X}",
memoryAddress);
Console.WriteLine("Press any key . . .");
Console.ReadKey(true);
// You can add a break point on the last line,
// or you can use this function to break the code.
System.Diagnostics.Debugger.Break();
}
此代码假定您已从项目属性的“生成”选项卡中启用了不安全代码。它还假定您通过按 F5 或从“调试”菜单中选择“开始调试”来运行代码。现在,在中断代码后,单击“调试”->“窗口”->“内存”->“内存 1”打开内存窗口。图 4 显示了如何打开内存窗口。图 5 显示了已打开的内存窗口。单击图片可放大。
FillConsoleOutputCharacter() 函数
BOOL FillConsoleOutputCharacter(
HANDLE hConsoleOutput,
TCHAR cCharacter,
DWORD nLength,
COORD dwWriteCoord,
[out] LPDWORD lpNumberOfCharsWritten
);
此函数接受四个输入参数和一个输出参数,并返回一个确定函数是否成功的值。如果返回值非零(true),则函数成功,否则失败(GetConsoleBufferInfo()
和 SetConsoleCursorPosition()
也如此)。这五个参数是:- hConsoleOutput
一个已打开的控制台输出设备的句柄,用于写入。 - cCharacter
用于填充缓冲区部分的字符。 - nLength
要写入的字符数。 - dwWriteCoord
一个 COORD 结构,定义开始写入的位置(第一个单元格)。 - lpNumberOfCharsWritten:一个输出参数,确定写入的字符数。
COORD
结构,因为我们之前已经创建了它。[DllImport("Kernel32.dll")]
public static extern int FillConsoleOutputCharacter
(IntPtr hConsoleOutput, char cCharacter, uint nLength,
COORD dwWriteCoord, out uint lpNumberOfCharsWritten);
请注意,非托管数据类型 DWORD 和LPDOWRD
已封送为System.UInt32
。有关非托管数据类型的更多信息,请参阅参考部分。
SetConsoleCursorPosition() 函数
BOOL SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD dwCursorPosition
);
此函数接受两个参数:第一个是已打开的控制台输出设备的句柄。第二个是指定新光标位置的值。请注意,新光标位置必须在控制台屏幕缓冲区内。此函数的 PInvoke 方法是:[DllImport("Kernel32.dll")]
public static extern int SetConsoleCursorPosition
(IntPtr hConsoleOutput, COORD dwCursorPosition);
整合
public const int STD_OUTPUT_HANDLE = -11;
public const char WHITE_SPACE = ' ';
[DllImport("Kernel32.dll")]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("Kernel32.dll")]
public static extern int GetConsoleScreenBufferInfo
(IntPtr hConsoleOutput,
out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
[DllImport("Kernel32.dll")]
public static extern int FillConsoleOutputCharacter
(IntPtr hConsoleOutput, char cCharacter, uint nLength,
COORD dwWriteCoord, out uint lpNumberOfCharsWritten);
[DllImport("Kernel32.dll")]
public static extern int SetConsoleCursorPosition
(IntPtr hConsoleOutput, COORD dwCursorPosition);
[StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
public COORD dwSize;
public COORD dwCursorPosition;
public ushort wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
[StructLayout(LayoutKind.Sequential)]
public struct COORD
{
public short X;
public short Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}
清除控制台屏幕
static void Main()
{
Console.WriteLine("Writing some text to clear.");
Console.WriteLine("Press any key to clear . . . ");
Console.ReadKey(true);
ClearConsoleScreen();
}
public static void ClearConsoleScreen()
{
// Getting the console output device handle
IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
// Getting console screen buffer info
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(handle, out info);
// Discovering console screen buffer info
Console.WriteLine("Console Buffer Info:");
Console.WriteLine("--------------------");
Console.WriteLine("Cursor Position:");
Console.WriteLine("t{0}, {1}",
info.dwCursorPosition.X, info.dwCursorPosition.Y);
// Is this information right?
Console.WriteLine("Maximum Window Size:");
Console.WriteLine("t{0}, {1}",
info.dwMaximumWindowSize.X,
info.dwMaximumWindowSize.Y);
// Is this information right?
Console.WriteLine("Screen Buffer Size:");
Console.WriteLine("t{0}, {1}",
info.dwSize.X, info.dwSize.Y);
Console.WriteLine("Screen Buffer Bounds:");
Console.WriteLine("t{0}, {1}, {2}, {3}",
info.srWindow.Left, info.srWindow.Top,
info.srWindow.Right, info.srWindow.Bottom);
Console.WriteLine("--------------------");
// Location of which to begin clearing
COORD location = new COORD();
location.X = 0;
location.Y = 0;
// What about clearing starting from
// the second line
// location.Y = 1;
// The number of written characters
uint numChars;
FillConsoleOutputCharacter(handle, WHITE_SPACE,
(uint)(info.dwSize.X * info.dwSize.Y),
location, out numChars);
// The new cursor location
COORD cursorLocation = new COORD();
cursorLocation.X = 0;
cursorLocation.Y = 0;
SetConsoleCursorPosition(handle, cursorLocation);
}
我们还可以进一步编写代码来清除控制台屏幕缓冲区的特定部分,试试这段代码:static void Main()
{
// Require the user to enter his password
AuthenticateUser();
}
public static void AuthenticateUser()
{
Console.WriteLine("Please enter your password:");
Console.Write("> "); // Two characters right
string input = Console.ReadLine();
while (input != "MyPassword")
{
COORD location = new COORD();
// The third character
location.X = 2;
// The second line
location.Y = 1;
ClearConsoleScreen(location);
input = Console.ReadLine();
}
// User authenticated
Console.WriteLine("Authenticated!");
}
public static void ClearConsoleScreen
(COORD location)
{
// Getting the console output device handle
IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
// Getting console screen buffer info
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(handle, out info);
// The number of written characters
uint numChars;
FillConsoleOutputCharacter(handle, WHITE_SPACE,
(uint)(info.dwSize.X * info.dwSize.Y),
location, out numChars);
SetConsoleCursorPosition(handle, location);
}
深入了解 .NET 库
System.Console.Clear()
方法的。这个库是每个 .NET 应用程序都必须引用的核心库,它定义了每个应用程序必需的核心类和组件。此外,它还包含 CLR(通用语言运行时)所必需的类。如果您使用 .NET 2.0 或更高版本,您可以继续本节,否则,您可以跳过本节,因为 Clear()
方法是 .NET 2.0 的新增功能。
MSIL、CIL 和 IL 都指同一件事:中间语言。
MSCORLIB 代表 Microsoft Common Object Runtime Library。
使用 IL 反汇编器
现在打开 System 命名空间,然后向下进入 System.Console
类,双击 Clear()
方法以显示 IL 指令。看看 Clear()
方法是如何实现的。
使用其他工具
如果 MSIL 看起来很奇怪,您可以尝试其他完美的工具,它们可以将您的程序集逆向工程成您喜欢的语言(例如 C# 或 VB.NET)。一些著名的工具是 Lutz Roeder's .NET Reflector 和 XenoCode Fox。
对我来说,我更喜欢第一个工具,因为它更快、更简单,并且支持许多功能。
当然,您可以反射(逆向工程)一个程序集并从其中的代码中学习有用的技巧。
参考文献
示例代码(Tiny Console Library)
此示例代码演示了控制台应用程序的一些隐藏功能,例如在屏幕上移动文本和清除屏幕的特定部分。
摘要
因此,您已经学习了如何使用 Win32 API 清除控制台屏幕。此外,您还学习了许多技术,包括 PInvoke、封送、内存管理,以及如何逆向工程 .NET 程序集并提取其代码。更重要的是,您在通过 .NET 处理非托管代码时学到了许多可应用的思路。请务必查看示例应用程序——它为您提供了许多 .NET Framework SDK(甚至我认为许多其他 SDK)中找不到的功能。它说明了许多技术,包括如何清除屏幕的特定部分以及如何移动屏幕上的文本。此外,它还通过 Win32 API 和 .NET 展示了所有常见的控制台操作,例如读取和写入屏幕缓冲区。D