在 win32 应用程序中嵌入 cmd 并且文本不可选择

embed cmd in win32 application and text not selectable

我既写图形用户界面程序又写控制台程序。对于控制台,我使用像 \33[0m 这样的颜色输出。对于 gui,我需要编写更多代码,如果我切换到另一个 gui 库,我需要重写代码。一些简单的库(我目前正在使用)甚至没有 api 自定义文本颜色。所以我尝试使用 cmd 作为所有应用程序的输出。

问题是我无法在控制台中 select 文本,即使我将控制台默认设置为 QuickEdit 模式

代码:(如果cmd没有出现,请调整main的大小window,然后应该会出现)

#include <windows.h>

#include <iostream>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nShowCmd)
{
    static TCHAR lpszAppName[] = TEXT("HelloWin");
    HWND      hwnd;
    MSG       msg;
    WNDCLASS  wc;

    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = lpszAppName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
            lpszAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(lpszAppName,
        TEXT("The Hello Program"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL);

    ShowWindow(hwnd, nShowCmd);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{

    switch (message)
    {
    case WM_CREATE:
        {
            using namespace std;
            AllocConsole();
            freopen("CONOUT$", "w", stdout); // redirect std::cout to console
            cout << "test console" << endl;

            // get console handle
            HWND console = GetConsoleWindow();
            SetParent(console, hwnd);
            SetWindowLong(console, GWL_STYLE, WS_CHILD | WS_VISIBLE);
            // ShowWindow(console, SW_MAXIMIZE);

            DWORD prev_mode;
            GetConsoleMode(console, &prev_mode); 
            SetConsoleMode(console, prev_mode | ENABLE_QUICK_EDIT_MODE);
            cout << "aaaaaaaaaa" << endl;
            cout << "aaaaaaaaaa" << endl;
            cout << "aaaaaaaaaa" << endl;
            cout << "aaaaaaaaaa" << endl;
        }
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

如何使其select可用?还有其他可用于嵌入到我的应用程序中的替代控制台应用程序吗?(我试过 ConEmu 但没有成功)

这可以通过添加对 InvalidateRect( console, NULL, TRUE );RedrawWindow( console, NULL, NULL, RDW_INVALIDATE ); 的调用来部分解决。尝试在 WM_CREATEWM_MOVEWM_SIZE 内进行这两个调用。但是只在 WM_PAINT 中调用 RedrawWindow 否则程序似乎会冻结。

也在WM_PAINT等消息后调用DefWindowProc()。只有在确定不需要标准 windows 处理时才应该 return 0,这种情况很少见,除非响应自定义消息。

最后,控制台 window 是一个单独的进程。将一个进程的 window 嵌入另一个进程本质上是冒险的。例如,控制台通常有一个标准的 window 框架,因此文本区域不会绘制在客户端矩形的 [0,0] 处。它吸引到它的右下角。没有 window 框架,文本区域现在在 [0,0] 处绘制,但大小相同,周围的客户端矩形也是如此。在文本区域的右侧和底部留下空的未绘制 space。在 window 上设置 WS_HSCROLLWS_VSCROLL 以立即显示滚动条,在文本区域和滚动条之间显示空白 space。

一般来说,将一个进程 window 嵌入到另一个进程中并非易事。快速搜索显示了一些相关主题 here, here. If you want to redirect stdout to an child text window, without using the console, another related topic is here。如果您只想打印调试消息,请考虑使用 OutputDebugString 打印到 Visual Studio 输出 window.

SetParent documentation 声明如果您将此函数与另一个进程一起使用,则必须同步两个 Windows:

的 UISTATE

When you change the parent of a window, you should synchronize the UISTATE of both windows. For more information, see WM_CHANGEUISTATE and WM_UPDATEUISTATE.

但是您无权访问控制台的消息循环。有两个消息循环,Windows 必须阻止一些消息。你马上就会发现对焦和绘画方面的问题。控制台 window 在您单击它时不会获得焦点,或者它不会被绘制。使用 WS_CLIPCHILDREN 将改善绘画。要重定向焦点,您必须从您自己的 window 调用 SetForeground(console)SetFocus(console)(这必须在 WM_CREATE returns 之后完成,您可以在WM_LBUTTONDOWN 例如,或 PostMessage) 但是你 运行 会遇到更多问题。即使您确实可以访问其他进程,也不容易。同步线程已经够难了,同步进程会更糟。

另请参阅:拥有跨进程 parent/child 或 owner/owned window 关系是否合法?
https://blogs.msdn.microsoft.com/oldnewthing/20130412-00/?p=4683

你有更简单的选择。您可以稍微修改代码以写入 std::ostringstream 并将流粘贴到编辑控件,或将 cout 重定向到编辑控件。

下面的示例使用 RichEdit 控件来支持颜色和字体样式,大致基于 Bash 编码:

#include <sstream>
#include <string>
#include <iomanip>
#include <Windows.h>
#include <Richedit.h>

class my_stream
{
    HWND hedit;
public:
    std::wostringstream oss;

    HWND create(HWND hwnd, int x, int y, int w, int h, HINSTANCE hinst, int menu_id)
    {
        //create rich edit control
        LoadLibrary(L"Msftedit.dll");
        hedit = CreateWindow(MSFTEDIT_CLASS, 0,
            ES_READONLY | ES_MULTILINE | WS_CHILD | WS_VISIBLE, x, y, w, h, 
            hwnd, HMENU(menu_id), NULL, NULL);

        //default background color
        SendMessage(hedit, EM_SETBKGNDCOLOR, 0, (LPARAM)RGB(0, 0, 0));

        //default text color
        CHARFORMAT cf = { sizeof(cf) };
        cf.dwMask = CFM_COLOR | CFM_FACE | CFM_SIZE;
        cf.yHeight = 220;
        cf.crTextColor = RGB(255, 255, 255);
        //Consolas font is available since Vista
        wcscpy_s(cf.szFaceName, _countof(cf.szFaceName), L"Consolas"); 
        SendMessage(hedit, EM_SETSEL, (WPARAM)0, (LPARAM)-1);
        SendMessage(hedit, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&cf);
        return hedit;
    }

    template<typename T>
    my_stream& operator<<(const T& rhs)
    {
        //write to stream
        oss.str(L"");
        oss << rhs;
        std::wstring s = oss.str();

        if(s.find(L"3[") == 0)
        {
            bool bold = false;
            if(s.find(L"3[1") == 0)
            {
                bold = true;
                s[2] = L'0';
            }

            COLORREF color = RGB(255, 255, 255);
            if(s == L"3[0m") color = RGB(255, 255, 255);
            if(s == L"3[0;30m") color = RGB(0, 0, 0);//black
            if(s == L"3[0;31m") color = RGB(255, 0, 0);//red
            if(s == L"3[0;32m") color = RGB(0, 255, 0);//green
            if(s == L"3[0;33m") color = RGB(128, 64, 0);//brown
            if(s == L"3[0;34m") color = RGB(0, 128, 255);//blue
            if(s == L"3[0;35m") color = RGB(255, 0, 255);//magenta
            if(s == L"3[0;36m") color = RGB(0, 255, 255);//cyan
            if(s == L"3[0;37m") color = RGB(192, 192, 192);//light gray

            CHARFORMAT cf = { sizeof(cf) };
            cf.dwMask = CFM_BOLD | CFM_COLOR;
            cf.dwEffects = bold ? CFE_BOLD : 0;
            cf.crTextColor = color;
            SendMessage(hedit, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
            SendMessage(hedit, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&cf);
        }
        else
        {
            append_to_richedit(s.c_str());
        }

        return *this;
    }

    //this is for std::endl
    my_stream& operator<<(std::wostream& (*func)(std::wostream&))
    {
        oss.str(L"");
        oss << func;
        append_to_richedit(oss.str().c_str());
        return *this;
    }

    void append_to_richedit(const wchar_t *text)
    {
        if(text && wcslen(text))
        {
            SendMessage(hedit, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
            SendMessage(hedit, EM_REPLACESEL, (WPARAM)FALSE, (LPARAM)text);
        }
    }
};

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    static my_stream cout;
    switch(msg)
    {
    case WM_CREATE:
    {
        RECT rc;
        GetClientRect(hwnd, &rc);
        InflateRect(&rc, -10, -10);
        cout.create(hwnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
            ((LPCREATESTRUCT)lparam)->hInstance, 0);

        cout << "3[0;31m" << "red\n";
        cout << "3[1;31m" << "bold red\n";
        cout << "3[0m" << "reset\n";
        cout << "3[0;32m" << "green\n";
        cout << "3[0;34m" << std::showbase << std::hex << 17 << std::endl;
        cout << "3[1m";
        cout << L"bold, unicode ☺ ελληνική\n";
        cout << L"Win10 symbols \n";
        cout.oss.precision(3);
        cout << "numbers " << std::setw(10) << 3.1415 << std::endl;
        break;
    }

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, msg, wparam, lparam);
}

int WINAPI wWinMain(HINSTANCE hinst, HINSTANCE, LPTSTR, int)
{
    WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.hInstance = hinst;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    wcex.lpszClassName = L"Test";
    RegisterClassEx(&wcex);

    CreateWindow(wcex.lpszClassName, L"Test", WS_VISIBLE | WS_OVERLAPPEDWINDOW, 
        100, 100, 600, 400, 0, 0, hinst, 0);

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

这是一个只读的rich edit控件,应该支持鼠标选择和Ctrl+C复制。您可以子类化 richedit 控件以添加菜单功能。