Windows GUI + Console Output, Linux-style
Asked Answered
M

4

17

I have a GUI application, which I am developing cross-platform for Linux and Windows. On Linux, everything works smoothly. However, I've run into a hitch on Windows. I would like to be able to log certain messages to the console with a GUI app on Windows, Linux-style.

What I mean by Linux-style is, if the program is opened from a console, the output will go to the console, but if the program is opened, for example, through the start menu, the user will never see console output. Apparently, this is harder than it sounds on Windows.

Currently, I use the following trickery in main():

#if _WINDOWS /* Fix console output on Windows */
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
    freopen("CONOUT$","wb",stdout);
    freopen("CONOUT$","wb",stderr);
}
#endif

This allows me to create output before a window is actually opened by the program, such as responding to "--help" from the command line. However, once a window is actually initialized and opened by my program, the console is returned. I need a solution that will allow me continued access to the console throughout the life of my program, without opening a new console if none was originally used.

Metalworking answered 13/7, 2011 at 1:0 Comment(2)
related: INFO: Calling CRT Output Routines from a GUI Application (especially "more information" section)Heid
related: How do I get console output in C++ with a Windows program?Groundless
R
3

We use ::AllocConsole() instead of ::AttachConsole and it remains open throughout the app. Try that?

Regulate answered 15/7, 2011 at 21:47 Comment(0)
A
3

The best solution I have found so far is to have two executables.

  • program.exe is the GUI application.
  • program.com is a helper command-line application that spawns program.exe and passes standard I/O to it. (It's not a COM executable from DOS, it's just a renamed standard PE executable; since .com is before .exe in the default preference order of cmd.exe, you can type program and it will automatically call program.com instead of program.exe if both are in the path.)

With this setup, you can type program at the Windows command prompt, write to the standard output in program.exe, and it will correctly appear on the console; no console windows will be spawned when you open program.exe from the GUI.

Here is an example implementation of the helper program, taken from Inkscape: http://bazaar.launchpad.net/~inkscape.dev/inkscape/trunk/view/head:/src/winconsole.cpp

The helper creates three pipes, and spawns the GUI program with CreateProcess, giving it the appropriate ends of the pipes. Then it creates three threads that copy data between the pipes and the standard I/O of the helper program in an infinite loop. The helper is compiled as a console application (important) - the -mconsole switch in MinGW.

/**
 * \file
 * Command-line wrapper for Windows.
 *
 * Windows has two types of executables: GUI and console.
 * The GUI executables detach immediately when run from the command
 * prompt (cmd.exe), and whatever you write to standard output
 * disappears into a black hole. Console executables
 * do display standard output and take standard input from the console,
 * but when you run them from the GUI, an extra console window appears.
 * It's possible to hide it, but it still flashes for a fraction
 * of a second.
 *
 * To provide an Unix-like experience, where the application will behave
 * correctly in command line mode and at the same time won't create
 * the ugly console window when run from the GUI, we have to have two
 * executables. The first one, inkscape.exe, is the GUI application.
 * Its entry points are in main.cpp and winmain.cpp. The second one,
 * called inkscape.com, is a small helper application contained in
 * this file. It spawns the GUI application and redirects its output
 * to the console.
 *
 * Note that inkscape.com has nothing to do with "compact executables"
 * from DOS. It's a normal PE executable renamed to .com. The trick
 * is that cmd.exe picks .com over .exe when both are present in PATH,
 * so when you type "inkscape" into the command prompt, inkscape.com
 * gets run. The Windows program loader does not inspect the extension,
 * just like an Unix program loader; it determines the binary format
 * based on the contents of the file.
 *
 *//*
 * Authors:
 *   Jos Hirth <[email protected]>
 *   Krzysztof Kosinski <[email protected]>
 *
 * Copyright (C) 2008-2010 Authors
 *
 * Released under GNU GPL, read the file 'COPYING' for more information
 */

#ifdef WIN32
#undef DATADIR
#include <windows.h>

struct echo_thread_info {
    HANDLE echo_read;
    HANDLE echo_write;
    unsigned buffer_size;
};

// thread function for echoing from one file handle to another
DWORD WINAPI echo_thread(void *info_void)
{
    echo_thread_info *info = static_cast<echo_thread_info*>(info_void);
    char *buffer = reinterpret_cast<char *>(LocalAlloc(LMEM_FIXED, info->buffer_size));
    DWORD bytes_read, bytes_written;

    while(true){
        if (!ReadFile(info->echo_read, buffer, info->buffer_size, &bytes_read, NULL) || bytes_read == 0)
            if (GetLastError() == ERROR_BROKEN_PIPE)
                break;

        if (!WriteFile(info->echo_write, buffer, bytes_read, &bytes_written, NULL)) {
            if (GetLastError() == ERROR_NO_DATA)
                break;
        }
    }

    LocalFree(reinterpret_cast<HLOCAL>(buffer));
    CloseHandle(info->echo_read);
    CloseHandle(info->echo_write);

    return 1;
}

int main()
{
    // structs that will store information for our I/O threads
    echo_thread_info stdin = {NULL, NULL, 4096};
    echo_thread_info stdout = {NULL, NULL, 4096};
    echo_thread_info stderr = {NULL, NULL, 4096};
    // handles we'll pass to inkscape.exe
    HANDLE inkscape_stdin, inkscape_stdout, inkscape_stderr;
    HANDLE stdin_thread, stdout_thread, stderr_thread;

    SECURITY_ATTRIBUTES sa;
    sa.nLength=sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor=NULL;
    sa.bInheritHandle=TRUE;

    // Determine the path to the Inkscape executable.
    // Do this by looking up the name of this one and redacting the extension to ".exe"
    const int pathbuf = 2048;
    WCHAR *inkscape = reinterpret_cast<WCHAR*>(LocalAlloc(LMEM_FIXED, pathbuf * sizeof(WCHAR)));
    GetModuleFileNameW(NULL, inkscape, pathbuf);
    WCHAR *dot_index = wcsrchr(inkscape, L'.');
    wcsncpy(dot_index, L".exe", 4);

    // we simply reuse our own command line for inkscape.exe
    // it guarantees perfect behavior w.r.t. quoting
    WCHAR *cmd = GetCommandLineW();

    // set up the pipes and handles
    stdin.echo_read = GetStdHandle(STD_INPUT_HANDLE);
    stdout.echo_write = GetStdHandle(STD_OUTPUT_HANDLE);
    stderr.echo_write = GetStdHandle(STD_ERROR_HANDLE);
    CreatePipe(&inkscape_stdin, &stdin.echo_write, &sa, 0);
    CreatePipe(&stdout.echo_read, &inkscape_stdout, &sa, 0);
    CreatePipe(&stderr.echo_read, &inkscape_stderr, &sa, 0);

    // fill in standard IO handles to be used by the process
    PROCESS_INFORMATION pi;
    STARTUPINFOW si;

    ZeroMemory(&si,sizeof(STARTUPINFO));
    si.cb = sizeof(STARTUPINFO);
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput = inkscape_stdin;
    si.hStdOutput = inkscape_stdout;
    si.hStdError = inkscape_stderr;

    // spawn inkscape.exe
    CreateProcessW(inkscape, // path to inkscape.exe
                   cmd, // command line as a single string
                   NULL, // process security attributes - unused
                   NULL, // thread security attributes - unused
                   TRUE, // inherit handles
                   0, // flags
                   NULL, // environment - NULL = inherit from us
                   NULL, // working directory - NULL = inherit ours
                   &si, // startup info - see above
                   &pi); // information about the created process - unused

    // clean up a bit
    LocalFree(reinterpret_cast<HLOCAL>(inkscape));
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    CloseHandle(inkscape_stdin);
    CloseHandle(inkscape_stdout);
    CloseHandle(inkscape_stderr);

    // create IO echo threads
    DWORD unused;
    stdin_thread = CreateThread(NULL, 0, echo_thread, (void*) &stdin, 0, &unused);
    stdout_thread = CreateThread(NULL, 0, echo_thread, (void*) &stdout, 0, &unused);
    stderr_thread = CreateThread(NULL, 0, echo_thread, (void*) &stderr, 0, &unused);

    // wait until the standard output thread terminates
    WaitForSingleObject(stdout_thread, INFINITE);

    return 0;
}

#endif
Allocate answered 21/8, 2015 at 11:21 Comment(0)
R
0

I remember reading something about this and if I remember correctly the solution was to add a gui to a console project instead of adding a console to a gui project because the latter could only be done by opening a new console.

Relax answered 13/7, 2011 at 5:23 Comment(0)
C
0

I think you should create a console application and then check who initialized the process (probably cmd.exe) and depending on that you can hide the console window. Then you create a window in it... the donwside on this is that the console window may be open for a moment until you hide it and it's gonna look very ugly, I suppose. Opening later the console doesn't have that issue but I don't know if the stdout redirects to it as it does in console apps or if you have to set it somehow, or maybe you have to redirect in every call... no, there must be a better way!

Comprehend answered 13/7, 2011 at 8:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.