C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

C++/Windows: WinMain/wWinMain な Windows デスクトップ向けアプリで std::cout / std::wcout とかをコンソールに直接出したいときのメモ

Visual StudioWindows デスクトップアプリを作る場合は WinMain / wWinMain がエントリーポイントのプロジェクトを扱うのが一般的です。コンソールアプリとして作って GLFW から画面を出したり imguiGUIを作ったり、コンソールアプリの実行時に Win32 API を叩いてウィンドウハンドルを操作して…というのは特別理由が無ければふつーはしません。

そうすると、 std::cout / std::wcout はじめ標準出力、標準エラー出力、標準入力を扱う仕組みが "コンソールへ文字列を出したい" という点では期待動作はしなくなります。でも、出したい時は:

// (A1) OutputDebugStringA / OutputDebugStringW
// ; 少し趣旨と違うけれどこれでいいなら、いいんじゃない的な代替手段。使うのかんたん
// #include <debugapi.h> // <-- windows.h に含まれるのでふつーは手いんくるーど要りません
OutputDebugStringW( _T( "ヘルシオ・ホットクック美味しいです♡" ) );
// (A2) std::cout が動く。でも使うまでに少しの手間が必要です
#include <iostream>
#include <fcntl.h> // _O_TEXT
#include <io.h> // _open_osfhandle

void somewhere_in_your_code()
{
    // プロセスにコンソールを割り当てる(ref:A2-1)
    AllocConsole();
    
    // コンソールで使う文字コードをUTF8へ切り替える
    //   切り替えないと日本では ANSI -> CP932 のままになります
    SetConsoleOutputCP(CP_UTF8);

    // 現在の std::cout, std::wcout が接続されたストリーム(=FILE*) stream_std_out を開く
    auto stream_std_out = []()
    {
      // internal-step-1:
      // 標準デバイス(=STD_OUTPUT_HANDLE=標準出力を示すフラグ)の"Windowsにおけるハンドル"(=wh_std_out)を取得
      auto wh_std_out = GetStdHandle(STD_OUTPUT_HANDLE); // (ref:A2-2)

      // internal-step-2:
      // 既存の標準出力のWindowsにおけるハンドル(=wh_std_out)を
      // CRT(C run-time)のファイル・ディスクリプター(=fd_std_out)として開く
      //   note: このファイル・ディスクリプターをRAII的に閉じる必要はありません (ref:A2-2-a)
      auto fd_std_out = _open_osfhandle((intptr_t)wh_std_out, _O_TEXT); // (ref:A2-3)

      // internal-step-3: 
      // 直前に開かれたCRTのファイル・ディスクリプターをCRTのストリーム(FILE*)として開く
      return _fdopen(fd_std_out, "w"); // (ref:A2-4)
    }();

    // 現在の std::cerr, std::wcerr が接続された stream を開く
    auto stream_std_error = []()
    {
      // ↑の cout/wcout と同様
      auto wh_std_error = GetStdHandle(STD_ERROR_HANDLE);
      auto fd_std_error = _open_osfhandle((intptr_t)wh_std_error, _O_TEXT);
      return _fdopen(fd_std_error, "w");
    }();

    // 現在の std::cin, std::wcin が接続された stream を開く
    auto stream_std_in = []()
    {
      // ↑↑の cout/wcout と同様
      auto wh_std_in = GetStdHandle(STD_INPUT_HANDLE);
      auto fd_std_in = _open_osfhandle((intptr_t)wh_std_in, _O_TEXT);
      return _fdopen(fd_std_in, "r");
    }();

    // C++ の標準ストリーム系(iostream系なやつら)を C の標準ストリーム系(cstdio系的なやつら)と同期する
    std::ios::sync_with_stdio( true ); // (ref:A2-5)


    // 現在のCRTストリームに接続されたCRTハンドルを閉じつつ、新たにCRTストリームへCRTハンドルを接続する
    freopen_s(&stream_std_out  , "CONOUT$", "w", stdout); // (ref:A2-6)
    freopen_s(&stream_std_error, "CONOUT$", "w", stderr);
    freopen_s(&stream_std_in   , "CONIN$" , "r", stdin );

#if 0
    // VS2019で著者が試した限りでは不要ですが、VS2005からしばらくでは必要かもしれません。
    // C++ の標準ストリームオブジェクトのエラー状態をクリアーする
    //   有効な標準ストリームが設定される前に扱おうとするとエラーフラグが立つ対策。
    //   VS2005以降ではコンソールへの読み書きの有無によらず常に実行時にエラーフラグが立つので必要「かも」しれません
    //   (著者は古いVSでわざわざ試していないので確認していませんが参考 ref:A2 によると必要な事が起こるかもしれないので一応メモへ残します)
    std::wcout.clear(); std::cout.clear(); // (ref:A2-7)
    std::wcerr.clear(); std::cerr.clear();
    std::wcin.clear();  std::cin.clear();
    std::wclog.clear(); std::clog.clear();
#endif

    // そうして、ようやくですが std::clog など使えるようになります
    std::cout << u8"UTF-8で🍵どーぞ" << std::endl;
    std::cout << u8"⛄🎄📦🎍🌸" << std::endl;
}

f:id:USAGI-WRP:20191207033750p:plain

…悲しい…。(通常の目的は達成できていますが…)

    std::cout << u8"UTF-8で🍵どーぞ" << std::endl;
    std::cout << u8"⛄🎄📦🎍🌸" << std::endl;

↑モダン・ウェブ・ブラウザーは優秀です…(たぶん、すべての絵文字が見えているでしょう…)。

f:id:USAGI-WRP:20191207034806p:plain

↑WSLの端末エミュレーターcmdより優秀です…。

f:id:USAGI-WRP:20191207035842p:plain

Windows Terminal (Preview)-0.7.3382.0 (A-2-8) は惜しい感じです。入力(IMEで「おちゃ」と入力して変換、確定)はハテナ・ダイヤモンドですが、echoからの標準出力の表示では期待動作して絵文字を表示できました。…これがOSのコンソールの標準に置き換わってくれると、内部的には cmd であれ powershell であれでもUNICODEハッピーな絵文字も使えて人生がだいぶ楽しくなると思うので20年以内にそうなってほしいなーくらいにゆるっと期待したいと思います。(現時点では少なくとも「Windowns Terminalは既存のコンソールを置き換えるものではない」的な事をMicrosoftは言ってたような気はします)

若干、はなしがそれましたが、目的は達成できています。

注意点として、 A-2 の方法で出したコンソールのウィンドウには画面右上におなじみのバツ印ボタンがついています。これを押すと、もともとのデスクトップアプリのプロセスも終了します。

APIを駆使してこのボタンを消したり、あるいはそもそもcmdではない端末エミュレーターへ標準出力をパイプしたり、そういう変態的な事もいちおう WindowsAPI を駆使すればたぶんできますが、素直にログウィンドウを作るなりライブラリーでどうにかするなり、ログをファイルかメモリーかデータベースか何れにせよどこかへ出すなりバッファリングするなりしておいて別プロセスで読み出せるようにするなり…。そもそもどうして std::cout とかを出したかったのか、その要求を考え直さないとただの変態的でテクニカルなソフトウェア・モダン・アートのごみができると思います…。

…でも🍣とかログに出したいですよね…2019年にもなって…。ぅぅ…。

おまけ: ↑の状況とはある意味では逆転して、コンソールアプリだけどエラーメッセージのダイアログを出したい、場合には?

// (O1) でんっ!って出るやつです
#include <crtdbg.h> // おまけのおまけ: MSVC++ では <string> ( --> <xstring> --> <iosfwd> --> <yvals.h> --> <crtdbg.h> ) とインクルードされたりもします
_RPTFW0( _CRT_ERROR, _T( "今夜はパッチェリ・アラビアータにしよう。Hは発音しなでくださいとか日本語に求めないでね!" ) );

_RPT マクロ "群" なので、たくさんのマクロのセットです。 W の有無は Widechar/ascii のそれ、F の有無はファイル名と行番号を自動挿入する/しない、末尾の数字はオプションの引数の数、の組み合わせです。第1引数でエラーの他にワーニングも出せます。注意として、この関数はデバッグ版のランタイム・ライブラリーにしか含まれないので、カオスでアーティスティックでテクニカルな事をしない限りは通常デバッグビルド構成でしか使用できません。

f:id:USAGI-WRP:20191207005603p:plain

参考