Вопрос: Как сделать графическую оболочку для консольного приложения?

Лучший ответ: Можно запустить процесс и его потоки ввода-вывода забрать себе, а не в консоль. Тогда вы будете получать его текст, а так же сможете отправлять ему.
https://docs.microsoft.com/ru-ru/dotnet/api/system...

Худший ответ: Вот мой код. Не законченный (завязанный на моей собственной библиотеке абстрактных потоков, не реализована функция flush, поток ошибок намеренно не перенаправлен), но относительно действующий.spoilerclass ConStream : public st::Stream
{
public:
    ConStream();
    ~ConStream() override;
    bool open(
            const std::wstring& aFname,
            const std::wstring& aCmdLine,
            const std::wstring& aLog);

    // Console does not have remainder.
    st::Pos remainder() override { return 0; }
    // Only when we bumped into end.
    size_t eof() override { return fIsEof; }
    size_t readNe (void* aBuf, size_t aSize) override;
    size_t writeNe(const void* aBuf, size_t aSize) override;
    void flush() override;
    bool isOpen() override { return fIsOpen; }

    bool readln(std::string& s);
    void endInput();

    // Shall we implement close?
    //void close() override {}

    // Implemented capabilities
    unsigned long caps() const override
        { return st::cRead | st::cWrite | st::cMultiplex | st::cRealtime
            | st::cSizeUnaware; }
private:
    st::AsyncFile fLog;
    bool fIsOpen = false;
    bool fIsEof = false;
    bool fFoundCr = false;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    HANDLE hStdOutInside, hStdOutOutside;
    HANDLE hStdInInside, hStdInOutside;
    //HANDLE hStdErrInside, hStdErrOutside;
    //bool readln(std::string& s);
};


ConStream::ConStream()
{
    SECURITY_ATTRIBUTES sa;
      // Security attributes
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = nullptr;
    sa.bInheritHandle = true;  // Help says the handle will be inherited

    SECURITY_DESCRIPTOR sd;
    if (isWinNT()) {       // security initialization for Windows NT
        InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
        SetSecurityDescriptorDacl(&sd, true, nullptr, false);
        sa.lpSecurityDescriptor = &sd;
    }

    // Stdout pipe
    // read = outside, write = inside
    CreatePipe(&hStdOutOutside, &hStdOutInside, &sa, 0);
    SetHandleInformation(hStdOutOutside, HANDLE_FLAG_INHERIT, 0);

    // Stderr pipe
    // read = outside, write = inside
    //CreatePipe(&hStdErrOutside, &hStdErrInside, &sa, 1024*64);  // enough for stderr?
    //SetHandleInformation(hStdErrOutside, HANDLE_FLAG_INHERIT, 0);

    // Stdin pipe
    // read = inside, write = outside
    CreatePipe(&hStdInInside, &hStdInOutside, &sa, 1024*10);
    SetHandleInformation(hStdInOutside, HANDLE_FLAG_INHERIT, 0);
}


void ConStream::endInput()
{
    if (hStdInOutside != INVALID_HANDLE_VALUE) {
        CloseHandle(hStdInOutside);
        hStdInOutside = INVALID_HANDLE_VALUE;
    }
}


ConStream::~ConStream()
{
    if (fIsOpen) {
        if (WaitForSingleObject(pi.hProcess, 2000) == WAIT_TIMEOUT) {
            TerminateProcess(pi.hProcess, 0);
        }

        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
    }
    CloseHandle(hStdInInside);
    endInput();
    //CloseHandle(hStdErrOutside);
    //CloseHandle(hStdErrInside);
    CloseHandle(hStdOutOutside);
    CloseHandle(hStdOutInside);
}


bool ConStream::open(
        const std::wstring& aFname,
        const std::wstring& aCmdLine,
        const std::wstring& aLog)
{
    if (fIsOpen)
        return false;

    // Fill startup info
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    size_t n = aFname.length() + aCmdLine.length() + 10;

    si.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
    si.hStdOutput = hStdOutInside;
    si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
    si.hStdInput = hStdInInside;
    si.wShowWindow = SW_HIDE;

    std::wstring ws;
    ws.reserve(n);
    ws += L'"';
    ws += aFname;
    ws += L"\" ";
    ws += aCmdLine;

    fIsOpen = CreateProcess(
            nullptr,        // lpApplicationName
            &*ws.begin(),   // lpCommandLine
            nullptr,        // lpProcessAttributes
            nullptr,        // lpThreadAttributes
            TRUE,           // bInheritHandles
            0,              // dwCreationFlags
            nullptr,        // lpEnvironment
            nullptr,        // lpCurrentDirectory
            &si,            // lpStartupInfo
            &pi );          // lpProcessInformation

    if (fIsOpen && !aLog.empty())
        fLog.open(aLog.c_str(), st::File::omNew);

    return fIsOpen;
}


void ConStream::flush()
{

}


size_t ConStream::readNe (void* aBuf, size_t aSize)
{
    if (!fIsOpen)
        return 0;

    DWORD nRead;
    if (!ReadFile(hStdOutOutside, aBuf, static_cast<DWORD>(aSize), &nRead, nullptr))
        return 0;

    if (fLog.isOpen())
        fLog.write(aBuf, nRead);

    return nRead;
}


// Implemented write
size_t ConStream::writeNe(const void* aBuf, size_t aSize)
{
    if (!fIsOpen)
        return 0;

    DWORD nWritten;
    if (!WriteFile(hStdInOutside, aBuf, static_cast<DWORD>(aSize), &nWritten, nullptr))
        return 0;
    return nWritten;
}


bool ConStream::readln(std::string& s)
{
    if (!fIsOpen)
        return false;

    s.clear();
    enum { SZ = 256, TIME = 30 };
    char buf[SZ + 2];
    DWORD nRead, nm1, nm2;

    while (true) {
        if (!PeekNamedPipe(hStdOutOutside, buf, SZ, &nRead, &nm1, &nm2))
            throwWinError();
        // Read nothing — maybe, the program has finished?
        if (nRead == 0) {
            if (WaitForSingleObject(pi.hProcess, TIME) == WAIT_OBJECT_0) {
                // Try again
                if (!PeekNamedPipe(hStdOutOutside, buf, SZ, &nRead, &nm1, &nm2))
                    throwWinError();
                if (nRead == 0)
                    return !s.empty();
                // otherwise fall out
            }
        }
        const char* start = buf;
        const char* end = buf + nRead;
        if (fFoundCr && *start == '\n') {
            ++start;
            fFoundCr = false;
        }
        for (const char* p = start; p != end; ++p) {
            switch (*p) {
            case '\r':
                fFoundCr = true;
                // fall through
            case '\n':
                s.append(start, p);
                // Read until CR
                ++p;
                skipRead(p - buf);
                return true;
            default: ;
            }
        }
        // Copy the entire buffer and go on
        s.append(start, end);
        skipRead(end - buf);
    }
}