登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

BCB-DG's Blog

...

 
 
 

日志

 
 

RAD后面的故事--深入理解控制台程序II  

2009-09-30 16:30:31|  分类: Delphi |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
高级I/O

在本文上一部分,我们谈到利用Windows控制台(文本模式)子系统开发应用程序的一些基本问题。这个子系统允许你创建外观类似旧Dos操作系统的命令行应用。

这只是控制台最肤浅的应用。Windows控制台模式API允许你利用Windows保护模式环境的所有优点,远不限于旧式的MS-DOS编程技术。

底层输出

控制台窗口输出由字符单元格的一个集合组成。每个单元格都有一个字符,和与之相关联的属性。在内部,Window把输出看作是一个CHAR_INFO记录的两维数组。有几个输出函数可以用来控制输出:
function ReadConsoleOutput(hConsoleOutput: THandle; lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord; var lpReadRegion: TSmallRect): BOOL; stdcall;

function WriteConsoleOutput(hConsoleOutput: THandle; lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord;var lpWriteRegion: TSmallRect): BOOL; stdcall;

function FillConsoleOutputCharacter(hConsoleOutput: THandle; cCharacter: Char; nLength: DWORD; dwWriteCoord: TCoord;var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;

function FillConsoleOutputAttribute(hConsoleOutput: THandle; wAttribute: Word; nLength: DWORD; dwWriteCoord: TCoord;var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall;

function ReadConsoleOutputCharacter(hConsoleOutput: THandle; lpCharacter: PAnsiChar; nLength: DWORD; dwReadCoord: TCoord;var lpNumberOfCharsRead: DWORD): BOOL; stdcall;

function ReadConsoleOutputAttribute(hConsoleOutput: THandle; lpAttribute: Pointer; nLength: DWORD; dwReadCoord: TCoord;var lpNumberOfAttrsRead: DWORD): BOOL; stdcall;

function WriteConsoleOutputCharacter(hConsoleOutput: THandle; lpCharacter: PChar; nLength: DWORD; dwWriteCoord: TCoord; var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;

function WriteConsoleOutputAttribute(hConsoleOutput: THandle; lpAttribute: Pointer; nLength: DWORD; dwWriteCoord: TCoord; var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall;

前两个函数在屏幕上一个矩形区域操作。目标缓冲指针用一个CHAR_INFO结构的数组填充,这个数组包括所请求区域的单元格。其它函数作用于这些字符的一个连续序列,返回字符或属性数组。在行尾会自动换行。如果达到屏幕底端,停止处理并在最后的var参数中返回输出的字符数。

这些函数用起来还算简单。代码段一和二展示了FillConsoleOutputCharacter函数的一个普通应用:清除屏幕内容。做到这一点很简单,就是从0,0开始,用(height*width)个空格字符填充整个屏幕。这个技术兼容于Windows NT/2000和Windows 95/98,优于其它方法。

控制台,屏幕缓冲,查看窗口

到现在为止,我们的话题都基于几个假设,假设我们的程序运行于以下环境:

*目前已经有一个单一的控制台,我们要使用它;
*只有一个可供输出的屏幕;
*输出窗口和控制台窗口一样大。

这三个假设是新控制台程序的缺省值,但是并非没有其它选择。进程可以自由离开父进程控制台,创建自己的控制台窗口,也可以创建多个输出缓冲和与之相应的不同尺寸的窗口。要利用这些特性,需要理解关于在控制台显示文字的三个不同的Windows对象。

控制台本身是Windows创建的抽象对象,包括几个I/O缓冲,和一个可视的窗口,用来显示程序界面。下面是直接操作控制台的API函数列表:
function GetConsoleWindow: THandle; stdcall;function AllocConsole: BOOL; stdcall;

function FreeConsole: BOOL; stdcall;

function SetConsoleTitle(lpConsoleTitle: PChar): BOOL; stdcall;

function GetConsoleTitle(lpConsoleTitle: PChar; nSize: DWORD): DWORD; stdcall;

很不幸,GetConsoleWindow函数只工作于Windows2000。用它可以取得控制台窗口的合法句柄,然后在ShowWindow等API中传递这个句柄作为参数。在之前的操作系统中,用EnumWindows或相似的函数来取得这一信息。要找到控制台窗口的句柄,可以用合适的函数取得(或改变)控制台窗口的标题栏文字。如果未曾改变过标题栏文字,控制台窗口将用最后一个关联到它的进程命作为标题栏文字(例如DOS命令的EXE名称)。

如果应用程序已经有一个控制台,在分配一个新控制台之前,必须调用FreeConsole。在没有控制台存在的情况下调用FreeConsole没有负面影响,所以应该在创建控制台之前总是调用它。AllocConsole创建一个新控制台窗口,把标准句柄关联到窗口,在屏幕上显示它。注意,从子进程调用FreeConsole将把控制台的控制权返回给父进程,让父进程继续处理输入。(例如,从命令行执行GUI程序,GUI程序将会立即释放控制台窗口。)

控制台具有输入和输出缓冲。每个控制台程序都拥有一个独立输入缓冲,处理所有的输入事件。但是,对于输出缓冲则没有数量限制,输入缓冲被称之为"屏幕缓冲"。用一个二维字符网格来显示缓冲内容。一个和控制台窗口同样大小的单一屏幕缓冲自动被创建。下面是操作屏幕缓冲的API函数列表:
function CreateConsoleScreenBuffer(dwDesiredAccess, dwShareMode: DWORD; lpSecurityAttributes: PSecurityAttributes; dwFlags: DWORD; lpScreenBufferData: Pointer): THandle; stdcall;

function GetConsoleScreenBufferInfo(hConsoleOutput: THandle; var lpConsoleScreenBufferInfo: TConsoleScreenBufferInfo): BOOL; stdcall;

function SetConsoleActiveScreenBuffer(hConsoleOutput: THandle): BOOL; stdcall;

function SetConsoleScreenBufferSize(hConsoleOutput: THandle; dwSize: TCoord): BOOL; stdcall;

function ScrollConsoleScreenBuffer(hConsoleOutput: THandle; const lpScrollRectangle: TSmallRect; lpClipRectangle: PSmallRect; dwDestinationOrigin: TCoord; var lpFill: TCharInfo): BOOL; stdcall;

你可以创建任意多数目的屏幕缓冲,但是在同一时刻只能有一个活动的缓冲。不同的控制台输出函数都有一个屏幕缓冲句柄参数,这个句柄不一定是当前活动缓冲句柄。而用GetStdHandle得到的标准输出句柄总是指向活动控制台缓冲。

有一个有趣的问题,就是改变活动屏幕缓冲将怎样影响库函数Write和Writeln。缺省地,这些函数工作于一个全局文本文件类型变量Output。应用程序地一次执行时,这个变量被关联到标准输出,而且再不改变。记住,如果你改变活动的屏幕缓冲,用Write和Writeln输出的内容将不可见,直到切换回来。注意代码段一的WriteToScreen函数,这个函数用来代替Write和Writeln,它总是向活动的缓冲写内容。

和每个屏幕缓冲相关联的是一个查看窗口。不要和控制台窗口混淆,尽管它们之间关系密切。屏幕缓冲的查看窗口是屏幕上的可见屏幕缓冲。用以下函数操作查看窗口:
function GetLargestConsoleWindowSize(hConsoleOutput: THandle): TCoord; stdcall;
function SetConsoleWindowInfo(hConsoleOutput: THandle; bAbsolute: BOOL; const lpConsoleWindow: TSmallRect): BOOL; stdcall;

SetConsoleWindowInfo函数改变查看窗口的大小和位置。这可以用来滚动控制台窗口,只需要简单地向下移动查看窗口即可。最大窗口尺寸由关联的屏幕缓冲和用户选定的控制台字体决定。Windows提供了GetLargestConsoleWindowSize函数以取得所有这些参数,确定控制台窗口的最大尺寸。根据微软文档,试图设置比这个值大的窗口尺寸,或者超出屏幕缓冲,将会导致错误。但是,SetConsoleWindowInfo的实际行为却有异于此。在Windows 95/98和Windows NT/2000之间也存在差异。代码段一有相关示例。

在Windows NT/2000中,如果试图卷动到屏幕缓冲底端以下,会导致查看窗口缩短。当你试图通过相对坐标设置查看窗口位置,而底端坐标超出屏幕缓冲底端之外时,这种让人感到困惑的现象就会发生。在此情况下,顶端坐标调整为请求的值,但底端坐标没有改变,于是就改变了查看窗口的行数。为了避免这个问题,请使用GetConsoleScreenBufferInfo函数,然后要么计算新查看窗口的绝对坐标,要么在设置之前确定相对坐标正确,不会引起问题。

这种现象在Windows 95/98上不会发生。实际上,Windows 95/98程序绝对不能改变输出缓冲的查看窗口尺寸。试图改变查看窗口的尺寸将会使输出缓冲失效:一旦查看窗口大小与控制台窗口大小不一致,输出将不会被显示。在Windows 95/98环境运行代码段一,然后把注明部分的注释去掉,再运行一次。当第二个屏幕缓冲尺寸和第一个不一样,显示内容就会变空。相反,在Windows NT/2000下运行,当活动的屏幕缓冲变化时,控制台窗口尺寸也会发生改变,而所有缓冲中的文字都仍然被显示。

可以看到,在代码段一中没有{$APPTYPE CONSOLE}指示符,因为我们调用AllocConsole来创建控制台。这主要是基于演示的目的。通常而言,如果你确信程序总是在控制台运行,最好是让Windows自动创建它,这样性能较佳。在后文中用API创建控制台会用得更多,我们将为普通的GUI程序创建应用台窗口。

图一 代码段一的运行结果(第一个缓冲区)

控制台模式

控制台窗口行为取决于Windows为控制台设置的模式。多数情况下,缺省模式已经足够使用。但是,通过改变控制台模式,能获得更高的控制权。以下两个API函数用来查询和改变控制台模式:
function GetConsoleMode(hConsoleHandle: THandle; var lpMode: DWORD): BOOL; stdcall;

function SetConsoleMode(hConsoleHandle: THandle; dwMode: DWORD): BOOL; stdcall;

在本文的第一部分,我们讨论了两个层次的控制台输入/输出处理。微软称之为"高级"和"底层"控制台存取。对于高级和底层I/O函数,有不同的控制台模式选项影响之。传递一个位掩码到SetConsoleMode函数,可以设置或清除这些选项。SetConsoleMode可以包括任意高级和底层I/O模式选项的组合。

对于高级输入输出,可以使用以下模式:

* ENABLE_LINE_INPUT 输入由程序每次处理一行。
* ENABLE_ECHO_INPUT 输入时字符回显到屏幕
* ENABLE_PROCESSED_INPUT Windows在内部处理编辑和控制字符
* ENABLE_PROCESSED_OUTPUT Windows自动处理控制序列
* ENABLE_WRAP_AT_EOL_OUTPUT 在屏幕右端自动折行

这五个选项缺省值为打开。这种模式有时被称之为"cooked(做好的)"模式,因为Windows为你做了大多数编辑和控制符处理工作。在输入回车符之前,你的ReadFile调用被阻止,Windows会正确地处理和回显任意编辑或控制键。如果想关掉这三个输入模式,记住,假若禁止LINE_INPUT,则ECHO_INPUT模式自动被禁止。而且,试图在Windows 95/98系统关闭WRAP_AT_EOL_OUTPUT模式是无效的。

底层控制台输出使用WriteConsoleOutputCharacter或FillConsoleOutputAttribute函数,不会被任何输入或输出模式所影响,总是用较原始、直接的形式工作。但是,ReadConsoleInput和其它相关底层输入函数受以下三种输入模式影响:

* ENABLE_MOUSE_INPUT 确定是否向应用程序报告MOUSE_EVENT输入事件。
* ENABLE_WINDOW_INPUT 确定是否向应用程序报告WINDOW_BUFFER_SIZE_EVENT输入事件。
* ENABLE_PROCESSED_INPUT 自动处理[Ctrl][C]。

第三种模式意味着在[Ctrl][C]被按下时,Windows自动调用控制句柄。缺省地,第二和第三种模式是打开的,而第二种模式则关闭。注意,无论处于何种控制台模式,总会收到KEY_EVENT、FOCUS_EVENT、MENU_EVENT消息。

在现实情况中,很少需要去改变这些模式。关掉高级输入的缺省输入模式让你能更好地控制控制台输入,使用底层输入函数可以获得相同效果。而且,如我们即将谈到的,重载[Ctrl][C]是一件很简单的事。

控制台控制句柄

研究控制台输入模式时,一个常常引起关注的元素是控制台的控制句柄。这个句柄是每当某个系统级动作发生、而且可能影响到控制台时,Windows调用的函数。当控制台创建后,Windows简单地调用ExitProcess来安置一个空句柄。要安置你自己的句柄,用以下API调用:
function SetConsoleCtrlHandler(HandlerRoutine: TFNHandlerRoutine; Add: BOOL): BOOL; stdcall;

HandlerRoutine是指向一个函数的指针。这个函数按stdcall调用,接受一个DWORD参数,返回布尔值。根据Add参数的不同值,把你的自定义函数增加到注册句柄列表中,或者从中移除它。在 Windows NT/2000下,可以把NULL作为一个HandlerRoutine增加或移除。这个特殊的句柄专为忽略[Ctrl][C]而设计。

一旦系统事件发生,句柄就以被注册的相反顺序调用。以下DWORD参数是你的句柄可能收到的事件列表:

* CTRL_C_EVENT 用户按下[Ctrl][C]。
* CTRL_BREAK_EVENT 用户按下[Ctrl][Break]。
* CTRL_CLOSE_EVENT 用户试图关闭控制台窗口。
* CTRL_LOGOFF_EVENT 用户试图从系统注销。
* CTRL_SHUTDOWN_EVENT 用户试图关闭计算机。

每个句柄都有机会获得系统事件,自行处理或者传给下一个句柄。如果确信控制句柄能成功处理事件,就返回True。这样一来,事件就不会继续传递。否则,应该返回False,好让事件传递给链条中的下一个句柄。

最后三个事件,关闭、注销和关机消息,根据函数结果的不同而有不同的外部表现。如果所有句柄函数返回False,则进程将在最后一个句柄完成后立即退出。但是,如果其中任何一个句柄函数返回True,Windows就会提示用户确认是否关闭进程。用户可以选择立即结束进程,也可以选择不关闭进程。而且,如果函数执行超时,Windows也会让用户自行选择关闭进程、取消关闭或者继续等待。

如果有几个应用程序关联到同一个控制台,那么,它们都会收到传递给控制台窗口的信号。它们中的任何一个都有机会进行消息处理。此外,也可以把多个控制台程序作为一个控制台进程租对待。用CreateProcess创建新控制台窗口时情形就是这样。如果没有指定CREATE_NEW_PROCESS_GROUP标志,则控制台进程的所有子进程都属于同一个组。这样,控制台程序就能用下面这个API把信号发送给一个指定的进程组。
function GenerateConsoleCtrlEvent(dwCtrlEvent: DWORD; dwProcessGroupId: DWORD): BOOL; stdcall;

这个函数把指定的信号传递给与调用进程相关联的控制台窗口。所有其它同组的程序都会收到这个信号。这可以用来实现一个父进程同时控制多个子进程的行为。

代码段二所示的程序只实现了基本的信号处理。例中,简单地装置两个控制句柄,演示函数返回值如何影响Windows行为。运行一下就可以知道,如果没有其它句柄返回True,则会在最后执行缺省的内部处理。结果就是,无论收到什么控制信号,都关闭进程。如果这不是你想要的,就应该装置一个控制句柄,在程序开始执行时尽快返回True。

控制句柄范例程序

结论

本文讨论了控制台应用程序的一些高级特性。关于控制台编程和相关API,在话题中已基本覆盖。我们还探讨了如何取得控制台和缓冲区的底层控制权。

在本文的第三部分,我们还将研究如何利用其它的Windows API实现更有效率的控制台程序,用一些普通的Windows编程技术来操作控制台。特别地,我们还将看到如何在线程中使用控制台,创建GUI层面应用程序可以利用的消息队列,在同一个程序中使用控制台和图形窗口,用自定义句柄代替标准输入/输出句柄(包括其它文件句柄和未命名管道,在Windows程序中执行I/O重定向等)。

代码段一 屏幕缓冲区

program ScreenBuffers;

uses Windows, SysUtils;

var
hInput, hOriginal, hSecond, hActive: THandle;
arrInputRecs: array[0..9] of TInputRecord;
dwCount, dwCur: DWORD;
bQuit: Boolean = False;
coorNew: TCoord = (X: 100; Y: 100);
rectView: TSmallRect = (Left: 0; Top: 0; Right: 99; Bottom: 49);

const
OUTPUT_STRING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()[]{}';
FOREGROUND_MAGENTA = FOREGROUND_RED or FOREGROUND_BLUE;
FOREGROUND_BR_MAGENTA = FOREGROUND_MAGENTA or FOREGROUND_INTENSITY;

procedure WriteToScreen(buf: String);
var
dwCount: DWORD;
begin buf := buf + #13#10;
WriteConsole(hActive, PChar(@buf[1]), Length(buf), dwCount, nil);
end;

procedure FillOutputBuffer(hBuf: THandle);
var
dwAttr, dwCur: DWORD;
begin dwAttr := 0;
for dwCur := 0 to 100 do
begin SetConsoleTextAttribute(hSecond, dwAttr + 1);
WriteConsole(hSecond, PChar(@OUTPUT_STRING[1]), Length(OUTPUT_STRING), dwCount, nil);
dwAttr := (dwAttr + 1) mod 15;
end;
Writeln('第二个缓冲用数据填充。');
end;

procedure ClearOutputBuffer(hBuf: THandle);
var
cbsi: TConsoleScreenBufferInfo;
coorClear: TCoord;
dwWritten: DWORD;
cFill: Char;
begin GetConsoleScreenBufferInfo(hBuf, cbsi);
coorClear.X := 0;
coorClear.Y := 0;
cFill := ' ';
FillConsoleOutputCharacter(hBuf, cFill, cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten);
end;

procedure ScrollViewWindow(hBuf: THandle;
bUp: Boolean);
var
rectNew: TSmallRect;
begin if bUp then
rectNew.Top := -1
else
rectNew.Top := 1;
rectNew.Bottom := rectNew.Top;
rectNew.Left := 0;
rectNew.Right := 0;
SetConsoleWindowInfo(hBuf, False, rectNew);
end;

procedure SwitchConsole;
begin if hActive = hOriginal then
begin SetConsoleActiveScreenBuffer(hSecond);
hActive := hSecond;
end
else
begin SetConsoleActiveScreenBuffer(hOriginal);
hActive := hOriginal;
end;
end;

begin
{ 首先,释放存在的控制台,创建一个新的。}
FreeConsole;
AllocConsole;
SetConsoleTitle('Screen Buffers Demo');
{ 取得自动创建的输入输出缓冲。}
hInput := GetStdHandle(STD_INPUT_HANDLE);
hOriginal := GetStdHandle(STD_OUTPUT_HANDLE);
hActive := hOriginal;
SetConsoleTextAttribute(hActive, FOREGROUND_BR_MAGENTA);
WriteLn('取得标准输出句柄。');
{ 创建第二个屏幕缓冲。}
hSecond := CreateConsoleScreenBuffer(GENERIC_READ or GENERIC_WRITE, 0, nil, CONSOLE_TEXTMODE_BUFFER, nil);
{ *** Windows 95/98: 从此处注释掉... }
if not SetConsoleScreenBufferSize(hSecond, coorNew) then
WriteLn('error: SetConsoleScreenBufferSize() == ', GetLastError)
else
WriteLn('已经调整第二个屏幕缓冲尺寸。');
if not SetConsoleWindowInfo(hSecond, True, rectView) then
WriteLn('error: SetConsoleWindowInfo() == ', GetLastError)
else
WriteLn('Adjusted secondary screen buffer window.');
{ *** ...一直注释到这里 }
FillOutputBuffer(hSecond);
{ 处理输入循环。}
while not bQuit do
begin
ReadConsoleInput(hInput, arrInputRecs[0], 10, dwCount);
for dwCur := 0 to dwCount - 1 do
begin
case arrInputRecs[dwCur].EventType of
KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do
if bKeyDown then
if (dwControlKeyState and ENHANCED_KEY) > 9 then
case wVirtualKeyCode of
VK_UP: ScrollViewWindow(hActive, True);
VK_DOWN: ScrollViewWindow(hActive, False);
end
else
case Ord(AsciiChar) of
13: SwitchConsole;
Ord('?'):
begin WriteLn('控制台命令 ');
WriteLn(' ? - 简略帮助');
WriteLn(' C - 清除第二个输出缓冲。');
WriteLn(' F - 填充第二个输出缓冲。');
WriteLn(' Q - 退出程序。');
WriteLn(' <ent> - 切换活动屏幕缓冲。');
WriteLn(' <up> - 上卷。');
WriteLn(' <dn> - 下卷。');
end;
Ord('C'): ClearOutputBuffer(hSecond);
Ord('F'): FillOutputBuffer(hSecond);
Ord('Q'): bQuit := True;
end;
// case Ord(AsciiChar)...
end;
// case arrInputRecs...
end;
// for dwCur...
end;
// while not bQuit...
CloseHandle(hSecond);
FreeConsole;
end.

代码段二 控制句柄

Program ControlHandlers;

{$APPTYPE CONSOLE}

uses Windows;

var
bHandler1, bHandler2: Boolean;
nHandler1, nHandler2: Integer;
hStdOutput, hStdInput: THandle;
arrInputRecs: array[0..9] of TInputRecord;
dwCur, dwCount: DWORD;
cCur: Char;

const
FOREGROUND_BR_CYAN = FOREGROUND_BLUE + FOREGROUND_GREEN + FOREGROUND_INTENSITY;
FOREGROUND_BR_RED = FOREGROUND_RED + FOREGROUND_INTENSITY;

procedure ClearOutputBuffer;
var
cbsi: TConsoleScreenBufferInfo;
coorClear: TCoord;
dwWritten: DWORD;
cFill: Char;
begin
GetConsoleScreenBufferInfo(hstdOutput, cbsi);
coorClear.X := 0;
coorClear.Y := 0;
cFill := ' ';
FillConsoleOutputCharacter(hStdOutput, cFill, cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten);
end;

procedure PaintScreen;
var
coorHome: TCoord;
begin
coorHome.X := 0;
coorHome.Y := 0;
SetConsoleCursorPosition(hStdOutput, coorHome);
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
Write('<1> 第一个控制句柄状态:');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_RED);
if bHandler1 then
WriteLn('ON ')
else
WriteLn('OFF');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
Write('<2> 第二个控制句柄状态:');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_RED);
if bHandler2 then
WriteLn('ON ')
else
WriteLn('OFF');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
WriteLn('第一个控制句柄引发 ', nHandler1, ' 次。');
WriteLn('第二个控制句柄引发 ', nHandler2, ' 次。');
end;

function Handler1(dwSignal: DWORD): BOOL; stdcall;
begin Inc(nHandler1);
PaintScreen;
Result := bHandler1;
end;

function Handler2(dwSignal: DWORD): BOOL;
stdcall;
begin Inc(nHandler2);
PaintScreen;
Result := bHandler2;
end;

begin
hStdInput := GetStdHandle(STD_INPUT_HANDLE);
hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
{ 反向注册,保证第一句柄先被引发。}
SetConsoleCtrlHandler(@Handler2, True);
SetConsoleCtrlHandler(@Handler1, True);
ClearOutputBuffer;
PaintScreen;
while True do
begin ReadConsoleInput(hStdInput, arrInputRecs[0], 10, dwCount);
for dwCur := 0 to dwCount - 1 do
case arrInputRecs[dwCur].EventType of
KEY_EVENT: with arrInputRecs[dwCur].Event.KeyEvent do
begin cCur := AsciiChar;
if (not bKeyDown) and (cCur = '1') then
begin bHandler1 := not bHandler1;
PaintScreen;
end;
if (not bKeydown) and (cCur = '2') then
begin bHandler2 := not bHandler2;
PaintScreen;
end;
end;
end;
end;
end.
  评论这张
 
阅读(2020)| 评论(0)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018