Raymond Chen大神(The Old New Thing博客的作者,不知道Raymond大神的Windows程序员请自行面壁)去年写过一篇blog[1],文章大意是说运行在控制台(console)下的程序可以决定自己在控制台下面的表现,如果你想搞一个状态栏,那就放手去搞吧!接着Raymond举了个在console程序中显示一个状态栏的例子,这个状态栏能够动态地显示鼠标在控制台窗口中的坐标。
其实我没太明白Raymond大神前三段到底想说什么意思……不过看了下面的例子程序,觉得还是挺有意思的,以前写console程序基本都是用来printf,还真的不会定制console的外观。毕竟console程序是纯正的PE文件,console本身也不过是个窗口而已,我们可以把console程序看做拥有默认主窗口的Windows应用程序,或者称之为Windows CUI应用程序。
下面我们就看看Raymond提供的例子代码吧。
1、各种include:
#define _UNICODE
#include <windows.h>
#include <strsafe.h> // for StringCchPrintf
2、画status bar的函数:
{
CONSOLE_SCREEN_BUFFER_INFO sbi;
if (!GetConsoleScreenBufferInfo(hScreen, &sbi))
return;
TCHAR szBuf[80];
StringCchPrintf(szBuf, 80, TEXT("Pos = %3d, %3d"), sbi.dwCursorPosition.X sbi.dwCursorPosition.Y);
DWORD dwWritten;
COORD coDest = {0, sbi.srWindow.Bottom};
WriteConsoleOutputCharacter(hScreen, szBuf, lstrlen(szBuf), coDest, &dwWritten);
}
需要注意的是CUI程序并不遵循GUI系统中的endpoint-exclusive定则[2],即CUI的坐标体系中,矩形的坐标表示不包含矩形坐标的终点坐标。比如,如果一个矩形的左上角坐标是(0,0),右下角坐标是(10,10),那么在GUI系统中,它的表示为(0,0)-(10,10),而在CUI系统中,它的表示为(0,0)-(9,9)。
3、然后看main函数:
{
HANDLE hConin = CreateFile(TEXT("CONIN$"),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
if (hConin == INVALID_HANDLE_VALUE)
return 1;
HANDLE hConout = CreateFile(TEXT("CONOUT$"),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
if (hConout == INVALID_HANDLE_VALUE)
return 1;
HANDLE hScreen = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
if (!hScreen)
return 1;
SetConsoleActiveScreenBuffer(hScreen);
首先我们获取当前console的handle,然后新建一个新的屏幕缓存,并设置其为当前console使用的缓存。
GetConsoleMode(hConin, &dwInMode);
SetConsoleCtrlHandler(NULL, TRUE);
SetConsoleMode(hConin, ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS);
下面我们保存之前的console mode,然后将其设置为允许接收鼠标输入。将console control handler设为NULL可以让我们的程序不被Ctrl+C所中断。
if (!GetConsoleScreenBufferInfo(hConout, &sbi))
return 1;
COORD coDest = {0, sbi.srWindow.Bottom - sbi.srWindow.Top};
DWORD dw;
FillConsoleOutputAttribute(hScreen,
BACKGROUND_BLUE | FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY,
sbi.srWindow.Right - sbi.srWindow.Left + 1,
coDest, &dw);
然后,我们在console屏幕底部画一个蓝色的状态栏。由于CUI程序不遵循endpoint-exclusive定则,所以Right减去Left的值是实际屏幕宽度值减1的结果,我们需要再加上1才能得到正确的屏幕宽度值。
紧接着调用
函数把我们的初始状态栏画上去。为了动态更新状态栏的状态,我们还需要借助while大法:
BOOL fContinue = TRUE;
while (fContinue && ReadConsoleInput(hConin, &ir, 1, &dw))
{
switch (ir.EventType)
{
case MOUSE_EVENT:
if (ir.Event.MouseEvent.dwEventFlags & MOUSE_MOVED)
{
SetConsoleCursorPosition(hScreen, ir.Event.MouseEvent.dwMousePosition);
DrawStatusBar(hScreen);
}
break;
case KEY_EVENT:
if (ir.Event.KeyEvent.wVirtualKeyCode == VK_ESCAPE)
{
fContinue = FALSE;
}
break;
}
}
这可以看做一个CUI版的消息循环。我们手动检测MOUSE的状态并更新鼠标坐标到状态栏,同时也监听KEY状态,这样按下ESC键我们就可以退出while循环。
最后是收尾工作了:
SetConsoleActiveScreenBuffer(hConout);
return 0;
}
程序运行效果如图所示:
我们注意到,右下角的坐标是(79,23),而不是(80,24)。
其实在MSDN中,关于console的API还有好多,Raymond举了这个例子可以作为CUI编程的一个入门示范,如果同学们想做出其他效果的话,尽情地去MSDN吧。
最后话说回来,现在有人做CUI编程么,除了大一正在学C语言的同学们?不过一般来讲大一的同学还不知道运行在Windows下的32位console应用程序和运行在CPU的虚拟8086模式下的16位DOS应用程序的区别吧……大概><
参考资料:
[1] The program running in a console decides what appears in that console
[2] Why are RECTs endpoint-exclusive?
» 转载请注明来源及链接:未来代码研究所
我大三假期的时候给冯刚他们做.NET培训的时候,用C#实现了vim的一些功能,比这个还要复杂一些。