您的位置:首页 > 产品设计 > UI/UE

在GUI程序中使用控制台的两种方法

2009-12-10 10:47 627 查看
原文地址:

http://student.csdn.net/space.php?uid=112600&do=blog&id=10713

http://student.csdn.net/space.php?uid=112600&do=blog&id=12217

 

 

当我们第一次接触C++时,写的第一行代码,可能就是:

Code:

//...  

  

std::cout << "Hello world" << std::endl;  

  

//...  

慢慢的,cin, cout, cerr 就成为我们的好朋友,我们除了把它们用于程序中和输入输出有关实际逻辑以外,还经常拿cout或cerr作调试工具。

——这会受到我们的老师表扬,因为真正的程序员,都是不会轻易使用单步调试的……(好的程序员,必须是一名不错的测试员)。

后来有一天,我们一步迈入了 Windows 的世界,我们来到了GUI(图形用户界面/接口)的世界,我们了解一切都是“画”出来的……我们兴奋地,热情地执行这一华丽的转身,旧日的朋友cin, cout, cerr 被抛弃了,它们站在我们很远很远的身后……像我们儿时在乡下的好伙伴目送我们进城,黯然伤神:“他们还会回来吗?”

但程序还需要调试。

1. 我们用::MessageBox(....)来显示一个字符串的值……吃力不讨好。

2. 我们把调试信息打印到文件里,但又不直观,又没有及时性。

3. 我们搞了一个单独的窗口,上面放个Edit,然后往里打印消息……好大的工程

4. Windows 觉得挺不好意思,于是搞些OutputDebugXXX之类的函数,来帮我们把调试消息打印到VC的消息窗口……用这方式?想想都脸红,交给实施人员测试时,附赠一套VC 6.0 ?

 其实,老朋友从来没有忘记我们,我们还可以继续使用控制台(console),还可以继续使用cin, cout, cerr。 我们将讲解两种方法,一种是典型的Window风格,一种是典型的linux风格……当然,都在Windows实现的。

 

1. 方法-1:内嵌式

顾名思义,我们主动在自己写的GUI程序中,创建一个控制台。

1.1 API

在Windows 的API,提供一大族Console的函数(最无聊的,比如可以设置字符颜色:) ),AllocConsole用来直接为一个进程创建一个控制台。注意,一个进程只能有一个控制台:

Remarks

A process can be associated with only one console, so the AllocConsole function fails if the calling process already has a console. A process can use the FreeConsole function to detach itself from its current console, then it can call AllocConsole to create a new console or AttachConsole to attach to another console.

怎么让“编程”与“英语”齐飞,C++与Windows API一色呢?请参看我的另一篇笔记《学习编程需要什么英语基础?》。

1.2 主要类设计

好,这一段就基本齐全了,连设计模式都有了:既然“A process can be associated with only one console”,那就来一个简单的“单例模式”吧。

Code:

struct EmbeddedConsole  

{  

public:      

    static void Need()  

    {  

        

        if (!_instance)  

        {  

            _instance = new EmbeddedConsole;  

        }  

        

    }  

      

    static void Unneed()  

    {  

        delete _instance;  

        _instance = 0;  

    }  

      

private:  

    EmbeddedConsole()  

    {  

        AllocConsole();  

          

        SetConsoleTitle("XXX程序内嵌测试控制台");  

          

        freopen("conin$", "r+t", stdin);  

        freopen("conout$", "w+t", stdout);  

        freopen("conout$", "w+t", stderr);   

    }  

      

    ~EmbeddedConsole()  

    {  

        fclose(stderr);  

        fclose(stdout);  

        fclose(stdin);  

          

        FreeConsole();  

    }  

      

    static EmbeddedConsole* _instance;   

};  

  

EmbeddedConsole* EmbeddedConsole::_instance;  

EmbeddedConsole 的构造和析构函数都是private的。我们逼着自己通过“Need()”函数来new 出这个类的对象,而不是直接在全局范围内,定义一个它的对象,是因为我们需要严格的控制这个对象的“生死”时机。C++控制台程序是“耍了手段”,替我们尽量安全地往前自动生成cout, cin, cerr 这三个对象(没错,这三个老朋友都是变量),但对于GUI程序,它们要起效,必须有一个真正的控制台——我们通过AllocConsole所做的事情。
构造函数分配一个控制台,然后重定向; 析构函数关闭三个标准控制台文件,然后释放控制台,注意次序。

构造函数中,我还特意调用了一下SetConsoleTitle,用来修改控制台标题,同时也用来体现一名像我这样“专业的”程序员所必须拥有的“骚包”劲儿。

静态成员数:instance 确保我们不会愚蠢地一直尝试去重新分配控制台(AllocConsole函数是有返回值的,这里简单地认定它一定会成功 :)。

第44 行在类之外定义了类的静态成员数据,这是必须的,除非那是一个静态常量成员内置类型数据,请参看《纠正“C++测试题的一些问题”的问题》

 

1.3 测试例子

来一个使用例子。请在Code::Blocks(或你用的其它编程IDE),通过"Win32 GUI project”向导,生成一个应用,提问窗口类型时,请选择“Dialog”, 编译运行后,应该出来一个有两个按钮的对话框。点击“test”按钮,应该出一个消息框(MessageBox),这就是我们要改掉的地方,加入以上代码,全部代码如下:

Code:

#define WIN32_LEAN_AND_MEAN  

  

#include <windows.h>  

#include <iostream>  

#include <string>  

  

#include "resource.h"  

  

HINSTANCE hInst;  

  

struct EmbeddedConsole  

{  

public:      

    static void Need()  

    {  

         

        if (!_instance)  

        {  

            _instance = new EmbeddedConsole;  

        }  

       

    }  

      

    static void Unneed()  

    {  

        delete _instance;  

        _instance = 0;  

    }  

      

private:  

    EmbeddedConsole()  

    {  

        AllocConsole();  

        SetConsoleTitle("XXX程序内嵌测试控制台");  

          

        freopen("conin$", "r+t", stdin);  

        freopen("conout$", "w+t", stdout);  

        freopen("conout$", "w+t", stderr);   

    }  

      

    ~EmbeddedConsole()  

    {  

        fclose(stderr);  

        fclose(stdout);  

        fclose(stdin);  

          

        FreeConsole();  

    }  

      

    static EmbeddedConsole* _instance;   

};  

  

EmbeddedConsole* EmbeddedConsole::_instance;  

  

BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)  

{  

    std::string str;  

      

    switch (uMsg)  

    {  

        case WM_INITDIALOG:  

            /* 

             * TODO: Add code to initialize the dialog. 

             */  

            return TRUE;  

  

        case WM_CLOSE:  

            EndDialog(hwndDlg, 0);  

            return TRUE;  

  

        case WM_COMMAND:  

            switch (LOWORD(wParam))  

            {  

                    /** TODO: Add more control ID's, when needed. 

                     */  

                case IDC_BTN_QUIT:  

                    EndDialog(hwndDlg, 0);  

                      

                    EmbeddedConsole::Unneed(); //不要了!  

                    return TRUE;  

  

                case IDC_BTN_TEST:  

                    EmbeddedConsole::Need(); //我要!  

                    std::cout << "please input :";  

                    std::cin >> str;  

                    std::cerr << str << std::endl;  

                    ::MessageBox(hwndDlg, str.c_str(), "Information"  

                        , MB_ICONINFORMATION);  

                    return TRUE;  

            }  

    }  

  

    return FALSE;  

}  

  

  

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)  

{  

    hInst = hInstance;  

  

    // The user interface is a modal dialog box  

    return DialogBox(hInstance, MAKEINTRESOURCE(DLG_MAIN), NULL, (DLGPROC)DialogProc);  

}  

第82行,是用户按下“test”按钮时,要执行的代码。

我们先在83行通过Need()函数,确保控制台生成。然后就是我们熟悉的 cin, cout , cerr 的使用了,当然这里的cerr用得没有什么逻辑。

第76行,那是用户按下"quit"按钮时,要执行的代码。我们只是调用了Unneed()函数,表示我们在程序退出前,释放掉之前创建的控制台。

对了,如果你害怕在发布发行版时,忘了屏掉这个功能,可以通过宏来检测编译版本,如果发现它是release版时,就什么也别做。由于不同的编译器用于判断编译版本,有不同的宏名称,所以我就不写了。

 

1.4 截图



 

如果仅是为了通过控制来辅助调试,这不是我最喜欢的方法,因为它违反了我们推崇的“高内聚,低耦合”的原则(这里扯了一点这方面的内容)。另外一个方法是什么呢?

 

 

2. 方法-2:外挂式

先接前一篇《在GUI程序中使用控制台的两种方法-方法.1》。方法1采用的是“内嵌式”,即由GUI程序自身申请一个控制台,然后在上面输入输出,用到一个API:“ [b]AllocConsole  [/b]”。

2.1 API

这一次我们使用另一个函数:“[b]AttachConsole[/b]”:

Attach的意思很明显:“贴上,依附上,缠上,赖上……:( ”,而它的参数也很直白: 进程ID。 这样,一个GUI程序,完全可以“缠上”另一个进程的控制台(假设它有的话)。

Code:

BOOL WINAPI AttachConsole(  

  __in  DWORD dwProcessId  

);  

 

 

那么怎么得到另一个进程的ID呢?有点难,但如果是要取“父进程”的ID,就方便多了——事实上是不用取,只要填-1就可以了。

 

2.2 “外挂式”思路

一个进程可以启动另一个进程,后者就称为前者的“子进程”,而前者则称为“父进程”。这个关系可以一直传递下去,即子进程也可以有自己的子进程。

基本上, 电脑用户通过操作系统运行一个程序,比如画笔,比如浏览器,比如Word,都是以一个叫“Explorer.exe”的子进程身份启动的。而我们在IDE里调试程序,则程序会以调试器的子进程运行……,可见子进程其实是相当常见的。现在,我们事先写一个控制台的程序,假设称为P程序,以后当S程序(通常是一个GUI程序)需要附加的控制台来输入输出时(效果像方法.1),我们就用P程序来启动S程序,S程序中则通过“AttachConsole(-1)”来“挂”到其父程序(也就是P)的控制台。完成调试之后,就把P程序丢一边,以普通方式运行S程序,则AttachConsole(-1)失败,那些用于调试的cout/cin自然失效。

 

2.3 GUI 测试例子

我们先来写GUI程序的一个例子,也就上面说的子进程S程序。代码和方法1中的几乎一样的,为了简单,我们干脆只写出WinMain函数。

Code:

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)  

{  

    hInst = hInstance;  

  

    if (0 == AttachConsole((DWORD)-1))  

    {  

        ::MessageBox(0, "AttachConsole fail", "msg", 0);  

    }  

    else  

    {  

        freopen("conin$", "r+t", stdin);    

        freopen("conout$", "w+t", stdout);    

        freopen("conout$", "w+t", stderr);     

  

        ::MessageBox(0, "AttachConsole OK", "msg", 0);          

    }  

      

    std::cout << std::hex << _WIN32_WINNT << std::endl;  

      

    // The user interface is a modal dialog box  

    return DialogBox(hInstance, MAKEINTRESOURCE(DLG_MAIN), NULL, (DLGPROC)DialogProc);  

}  

当“AttachConsole”失败时,返回0,为了测试,我故意让它弹出一个消息框,实际程序当然不应这样扰民。当成功时,我们还是调用freopen来绑定cin,cout,cerr。

注意: 第18,我立即用cout 输出一个宏(整数)的值(采用16进制),这个宏 _WIN32_WINNT 在有些IDE里,默认是0x40的,而AttachConsole的文档里写到:

To compile an application that uses this function, define _WIN32_WINNT as 0x0501 or later. For more information, see Using the Windows Headers.

所以,我们必须通过IDE预定好_WIN32_WINNT,让它变成0x50好了。对于Code::Blocks和 VC,这都是很简单的事,你应该知道如何做。

上面的代码需要一些宏定义和头文件包含:

Code:

#define WIN32_LEAN_AND_MEAN  

 

#include <iostream>  

#include <windows.h>  

 

2.4 带控制台的父进程

GUI程序默认不带控制台,所以我们现在写一个控制台程序,用它来调用要调试的GUI程序。这本来是一件再简单不过的事了。只要用C语言的system程序就可以了,并且是跨平台的——问题是微软大叔总爱干些半拉子的事。Windows从DOS起家,都过去多少年了,它对控制台支持还是怪怪的,所以system函数在它身上,居然不支持带空格的路径,我们只好找CreateProcess来出气。

新建一个控制台程序:

Code:

int main(int argc, char** argv)  

{  

    if (argc < 2)  

    {  

        cout << "Usage:" << argv[0] << " <filename> <args ...>" << endl;  

        return 1;  

    }  

  

    cout << argv[1] << endl;  

  

    //return system(argv[1]); linux下就这样...  

  

    STARTUPINFO si;  

    ZeroMemory(&si, sizeof(si));  

    si.cb = sizeof(si);  

      

    PROCESS_INFORMATION pi;  

    ZeroMemory(π, sizeof(pi));  

      

    ::CreateProcess (argv[1]  

        , NULL  

        , NULL  //LPSECURITY_ATTRIBUTES  

        , NULL  //LPSECURITY_ATTRIBUTES  

        , FALSE  //bInheritHandles  

        , 0     //dwCreationFlags  

        , NULL  //lpEnvironment  

        , NULL  //LPCTSTR  

        , &si   //LPSTARTUPINFO  

        , π); //LPPROCESS_INFORMATION  

      

    ::WaitForSingleObject(pi.hProcess, INFINITE);  

      

     DWORD exitCode = 0;  

      

     int ret;  

      

     if (!::GetExitCodeProcess(pi.hProcess, &exitCode))  

     {  

         ret = -1;  

     }  

     else  

     {  

         ret = exitCode;  

     }  

      

    ::CloseHandle(pi.hProcess);  

    ::CloseHandle(pi.hThread);  

      

     return ret;  

}  

    

 我们必须用WaitForSingleObject来调待子进程退出后,父进程才能退出。我们还用GetExitCodeProcess来得到子程序的退出代码。

说到CreateProcess ,我们还可做更多的事,比如我们可以倒过来,用它来创建一个带有控制台(也就是说,天生有cout,cin,cerr)的程序,然后抓取它的输出(cout, cerr),甚至也可以连通它的输入(cin),这样就可以倒过来,为一个控制台程序,挂一个GUI界面了,典型的的案例,比如WinRAR最初的版本。当然,今天我们谈的是 如何为一个GUI程序,挂一个控制台,那么,这就完事了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息