WinMain で開始するウィンドウアプリケーションをコマンドラインアプリケーションとして実行する

はじめに

普通のウィンドウアプリケーションをコマンドラインアプリケーションとしても実行させたい場合にとりうる方法として、1 つは、WinMain のかわりに、main で開始するコマンドラインアプリケーションから、ウィンドウを表示する方法が考えられます。

この方法は簡単ですが、実行時に常にコンソールウィンドウが表示されるので、普通のウィンドウアプリケーションとしては実行するのは難しそうです。(GetConsoleWindow function とかを駆使して、非表示にする方法もあるかもしれませんが、試していません)

なので、この記事では、WinMain で開始したまま必要に応じてコンソールを用意することにより、ウィンドウアプリケーションのまま、コマンドラインアプリケーションとしても実行できるようにする方法を解説します。

必要に応じてコンソールを用意する

まず、専用のコンソールを起動時に用意するのが一番簡単です。例えば、コマンドライン引数で /console が指定されている場合のみ専用のコンソールを作成します。専用のコンソールを作成するには、AllocConsole function を使用します。

この方法はとても簡単ですが、普通のコンソールアプリケーションの場合、コンソールから起動した場合には、起動元のコンソールが流用され、新しいコンソールは作成されません。このような挙動を実現するには、AttachConsole function を使用します。

::AttachConsole(ATTACH_PARENT_PROCESS)

結局、必要に応じてコンソールを用意するコードは下記のようになります。

if (::AttachConsole(ATTACH_PARENT_PROCESS) == FALSE)
{
  if (!force)
  {
    m_lastError = ::GetLastError();
    return;
  }
  if (::AllocConsole() == FALSE)
  {
    m_lastError = ::GetLastError();
    return;
  }
}

bool force には、親コンソールが無い場合に、自前で用意するかどうかをあらかじめ設定しておくフラグです。DWORD m_lastError には、失敗した場合、0 以外の値 (エラーコード) が格納されます。また、このコードにはありませんが、AllocConsole、または、AttachConsole で用意したコンソールは、FreeConsole function により開放できます。

stdin, stdout, stderr を用意する

コンソールを用意しただけでは、printfstd::coutstd::wcout などに必要な、stdin, stdout, stderr はまだ、使用できる状態にはなっていません。stdin などを用意したコンソールに繋げるには、freopen_s を使用します。

FILE* fpOut = NULL;
::freopen_s(&fpOut, "CONOUT$", "w", stdout);

FILE* fpErr = NULL;
::freopen_s(&fpErr, "CONOUT$", "w", stderr);

FILE* fpIn = NULL;
::freopen_s(&fpIn, "CONIN$", "r", stdin);

freopen_s で開いたファイルは使用後に閉じる必要があるので、下記のようなクラスに閉じるロジックを封じておくと便利です。

#pragma once

class CStdFile
{
public:
  CStdFile() : m_fp() {}
  ~CStdFile() { Close(); }

  void Reopen(const char* path, const char* mode, FILE* std)
  {
    // for stdin, stdout, stderr //
    Close();
    ::freopen_s(&m_fp, path, mode, std);
  }
  
  void Close()
  {
    if (m_fp != NULL)
    {
      ::fclose(m_fp);
      m_fp = NULL;
    }
  }
  
protected:
  FILE* m_fp;
};

ウィンドウアプリケーションでも、コンソールからリダイレクトされている場合、すでに、開かれている場合があるので、その場合は、reopen_s しません。結局、stdout などを reopen するコードは下記のようになります。m_stdout、m_stderr、m_stdin は前で定義した、CStdFile クラスのインスタンスです。

if (_fileno(stdout) < 0)
  m_stdout.Reopen("CONOUT$", "w", stdout);
if (_fileno(stderr) < 0)
  m_stderr.Reopen("CONOUT$", "w", stderr);
if (_fileno(stdin) < 0)
  m_stdin.Reopen("CONIN$", "r", stdin);

ウィンドウを表示しない場合

例えば、/console を指定すると、メインウィンドウを表示しないアプリケーションは、ウィンドウを非表示で作成した後、ShowWindow 関数 しなければよいだけなので簡単です。注意点としては、SetWindowPos 関数 を呼び出すときに、SWP_NOACTIVATE を指定しなかったり、ダイアログのウィンドウプロシージャーの WM_INITDIALOG message (Windows) で、TRUE を返したりすると、非表示のウィンドウにもフォーカスが移る!ので注意が必要です。見えないウィンドウにフォーカスが移動している場合、キーボードに反応して、ユーザーからして見ると意味不明なアクションが起きるので、注意が必要です。例えば、メインダイアログの最初のコントロールがカレントドライブをフォーマットするボタンだったと想像してみてください。恐ろしいですね。

コンソールから実行する場合

以上で、実装は終わりですが、まだコマンドラインアプリケーションと挙動が異なる部分があります。それは、コンソールから実行した場合、コマンドラインアプリケーションは実行が終了するまで待つのに対し、ウィンドウアプリケーションでは、終了を待たずに、次のコマンドを受け付ける状態になってしまう点です。

この状態のコンソールに AttachConsole した場合、標準出力結果も見づらくなりますし、標準入力の受け付けもおかしくなるので、困りものですが、これをウィンドウアプリケーションの実装でどうにかする方法は、よくわかりませんでした。

この状態は、Start コマンドに /wait オプションを指定してウィンドウアプリケーションを実行すると回避できます。また、バッチファイルから起動すると、Start コマンドを使用しなくても、待ってくれるみたいなので、そちらの方が簡単です。

start /wait WindowApplicationSample.exe /console

同様に、vbs から呼び出す場合は、Wscript.Shell オブジェクトの Run メソッド最後の引数を True にすれば、待つことができます。

Dim wsh
Set wsh = Wscript.CreateObject("Wscript.Shell")

Dim ret
ret = wsh.Run("%comspec% /c " & chr(34) & cmdLine & chr(34), 1, True)

となりのページ

このサイトについて

このサイトのページへのリンクは自由に行っていただいてかまいません。
このサイトで公開している全ての画像、プログラム、文書の無断転載を禁止します。

連絡先

ここをクリック すると表示されるページから作者へメールで連絡できます。

共有