Visual Studio、JBuilder 等 IDE 編譯程式時,輸出視窗會即時顯示編譯器、連結器的文字訊息,卻不會彈出 DOS 視窗。原理是 IDE 在背景啟動命令列工具,並即時捕獲其標準輸出到 GUI 介面。本文詳細說明如何實作這項核心技術。
GUI 程式無主控台,無法直接重導向標準輸出。解決方案:編寫一個「中介程式」,負責啟動目標工具並重導向其輸出,再透過匿名管道即時傳回 GUI 程式。

圖:VS 6.0 Output 視窗執行 javac.exe 編譯 Java 程式

圖:中介程式架構示意圖
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <windows.h>
using namespace std;
void exit_friendly(void) { puts("請不要單獨執行 wspawn."); exit(0); }
int main(int argc, char *argv[])
{
HANDLE hWrite = NULL;
DWORD dwWrited;
int i = 0, ret = 0, len = 0;
char psBuffer[256];
FILE* child_output;
string command_line = "";
// 解析命令列:wspawn -h <管道控制代碼> <真正命令> [參數...]
if (argc < 2) exit_friendly();
if (!stricmp(argv[1], "-h"))
{
if (argc < 4) exit_friendly();
hWrite = (HANDLE)atoi(argv[2]);
i = 3;
}
else i = 1;
for (; i < argc; i++) { command_line += argv[i]; command_line += " "; }
// 使用 _popen 建立子行程並重導向 stdout
if ((child_output = _popen(command_line.c_str(), "rt")) == NULL) exit(1);
while (!feof(child_output))
{
if (fgets(psBuffer, 255, child_output) != NULL)
{
if (hWrite)
{
len = strlen(psBuffer);
WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL); // 先寫長度
WriteFile(hWrite, psBuffer, len, &dwWrited, NULL); // 再寫內容
}
else printf(psBuffer); // 無管道時直接輸出
}
}
len = 0; // 寫入 0 表示結束
if (hWrite) WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL);
return _pclose(child_output);
}
建立 MFC 對話框專案,加入多行編輯框 IDC_EDIT1(屬性勾選 Multiline, Vertical scroll, Auto VScroll, Want return),對應變數 CEdit m_edit1。
// CMyIDEDlg.h
protected:
static UINT myThread(LPVOID pParam);
// CMyIDEDlg.cpp
void CMyIDEDlg::OnOK()
{
AfxBeginThread(myThread, this);
InvalidateRect(NULL);
UpdateWindow();
}
UINT CMyIDEDlg::myThread(LPVOID pParam)
{
PROCESS_INFORMATION pi;
STARTUPINFO siStartInfo;
SECURITY_ATTRIBUTES saAttr;
CString Output, tmp;
char command_line[200];
DWORD dwRead;
char* buf; int len;
HANDLE hRead, hWrite;
CMyIDEDlg* pDlg = (CMyIDEDlg*)pParam;
// 建立可繼承的匿名管道
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
if (!CreatePipe(&hRead, &hWrite, &saAttr, 0))
{ AfxMessageBox("建立管道失敗"); return 0; }
// 準備 wspawn 命令列,傳入寫入端控制代碼
memset(&pi, 0, sizeof(pi));
sprintf(command_line, "wspawn -h %d cl /?", (unsigned int)hWrite); // 測試用 cl.exe /?
ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.wShowWindow = SW_HIDE;
siStartInfo.dwFlags = STARTF_USESHOWWINDOW;
if (!CreateProcess(NULL, command_line, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &pi))
{ AfxMessageBox("呼叫 wspawn 失敗"); return 0; }
// 讀取管道並即時顯示
if (!ReadFile(hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0) return 0;
while (len)
{
buf = new char[len + 1];
memset(buf, 0, len + 1);
if (!ReadFile(hRead, buf, len, &dwRead, NULL) || dwRead == 0) return 0;
tmp = buf;
tmp.Replace("\n", "\r\n"); // 換行相容編輯框
Output += tmp;
pDlg->m_edit1.SetWindowText(Output);
pDlg->InvalidateRect(NULL);
pDlg->UpdateWindow();
delete[] buf;
if (!ReadFile(hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0) return 0;
}
WaitForSingleObject(pi.hProcess, 30000);
CloseHandle(hRead); CloseHandle(hWrite);
return 0;
}

圖:myIDE 執行畫面,即時顯示 cl.exe /? 說明文字
_popen() 僅能重導向 stdout 或 stdin。若需同時捕獲 stderr(如 javac 錯誤輸出),須改用 _pipe()、_dup() 等傳統 API 完整實作。cl.exe 在系統 PATH 中,或修改命令列為完整路徑。