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
#define _UNICODE
#include <windows.h>
#include <strsafe.h> // for StringCchPrintf

2、画status bar的函数:

void DrawStatusBar(HANDLE hScreen)
{
    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函数:

int main(int argc, WCHAR **argv)
{
    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使用的缓存。

    DWORD dwInMode;
    GetConsoleMode(hConin, &dwInMode);
    SetConsoleCtrlHandler(NULL, TRUE);
    SetConsoleMode(hConin, ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS);

下面我们保存之前的console mode,然后将其设置为允许接收鼠标输入。将console control handler设为NULL可以让我们的程序不被Ctrl+C所中断。

    CONSOLE_SCREEN_BUFFER_INFO sbi;
    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才能得到正确的屏幕宽度值。

紧接着调用

    DrawStatusBar(hScreen);

函数把我们的初始状态栏画上去。为了动态更新状态栏的状态,我们还需要借助while大法:

    INPUT_RECORD ir;
    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循环。

最后是收尾工作了:

    SetConsoleMode(hConin, dwInMode);
    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?

» 转载请注明来源及链接:未来代码研究所

Related Posts:

One Response to “为console程序添加底部状态栏”

  • 冬大少爷 says:

    我大三假期的时候给冯刚他们做.NET培训的时候,用C#实现了vim的一些功能,比这个还要复杂一些。

Leave a Reply

World Line
Time Machine
Online Tools