程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 編寫自己的IDE:如何在圖形界面中實時捕獲控制台程序的標准輸出

編寫自己的IDE:如何在圖形界面中實時捕獲控制台程序的標准輸出

編輯:關於VC++

編寫自己的"IDE"-- 如何在圖形界面中實時捕獲控制台程序的標准輸出.

IDE是集成開發環境(Integrated Development Environment)的簡稱。印象裡有很多出色的IDE,比如JBuilder和Kylix,比如Visual Studio。不知大家是否留意過,大多數IDE本身只提供代碼編輯、工程管理等人機交互功能,我們在IDE中編譯代碼、調試程序時,IDE需要調用命令行的編譯器、調試器完成相應的操作。例如,使用Visual Studio編譯C++程序時,我們會在IDE下方的Output窗口中看到編譯和連接的全過程,雖然我們看不到彈出的DOS窗口,但實際上是IDE先後啟動了Microsoft C++編譯器cl.exe和連接器link.exe這兩個命令行程序,而cl.exe和link.exe的輸出又實時反映到了IDE的Output窗口中。還有,我們可以在Visual Studio中配置自己需要的工具程序(比如特殊的編譯器),然後讓Visual Studio在適當的時候運行這些工具,並將工具程序的輸出實時顯示到Output窗口中。下圖是我在Visual Studio 6.0的Output窗口中運行J2SDK的javac.exe編譯java源程序並顯示程序中語法錯誤的情形:

也就是說,大多數IDE工具都可以在集成環境中調用特定的命令行程序(WIN32裡更確切的說法是控制台程序),然後實時捕獲它們的輸出(這多半是輸出到標准的stdout和stderr流裡的東西),並將捕獲到的信息顯示在圖形界面的窗口中。

這顯然是一種具備潛在價值的功能。利用這一技術,我們至少可以

1. 編寫出自己的IDE,如果我們有足夠的耐心的話;

2. 在我們自己的應用程序裡嵌入全文檢索功能(調用Borland C++裡的grep.exe工具),或者壓縮和解壓縮功能(調用控制台方式的壓縮解壓程序,比如arj.exe、pkzip.exe等);

3. 連接其他人編寫的,或者我們自己很久以前編寫的控制台程序——我經常因為難以調用一個功能強大但又沒有源碼的控制台程序而苦惱萬分。

這樣好的功能是如何實現的呢?

首先,如果我們想做的是用一個控制台程序調用另一個控制台程序,那就再簡單不過了。我們只消把父進程的stdout重定向到某個匿名管道的WRITE端,然後啟動子進程,這時,子進程的stdout因為繼承的關系也連在了管道的WRITE端,子進程的所有標准輸出都寫入了管道,父進程則在管道的另一端隨時“偵聽”——這一技術叫做輸入輸出的重定向。

可現在的問題是,GUI方式的Windows程序根本沒有控制台,沒有stdin、stdout之類的東西,子進程又是別人寫好的東西無法更改,這重定向該從何談起呢?

還有另外一招:我們可以直接在調用子進程時用命令行中的管道指令“>”將子進程的標准輸出重定向到一個文件,子進程運行完畢後再去讀取文件內容。這種方法當然可行,但它的問題是,我們很難實時監控子進程的輸出,如果子進程不是隨時刷新stdout的話,那我們只能等一整塊數據實際寫入文件之後才能看到運行結果;況且,訪問磁盤文件的開銷也遠比內存中的管道操作來得大。

我這裡給出的方案其實很簡單:既然控制台程序可以調用另一個控制台程序並完成輸入輸出的重定向,那我們完全可以編寫一個中介程序,這個中介程序調用我們需要調用的工具程序並隨時獲取該程序的輸出信息,然後直接將信息用約定的進程間通訊方式(比如匿名管道)傳回GUI程序,就象下圖中這樣:

圖中,工具程序和中介程序都是以隱藏的方式運行的。工具程序原本輸出到stdout的信息被重定向到中介程序開辟的管道中,中介程序再利用GUI程序創建的管道將信息即時傳遞到GUI程序的一個後台線程裡,後台線程負責刷新GUI程序的用戶界面(使用後台線程的原因是,只有這樣才可以保證信息在GUI界面中隨時輸出時不影響用戶正在進行的其他操作,就象我們在Visual Studio中執行耗時較長的編譯功能那樣)。

我寫的中介程序名字叫wSpawn,這個名字來自Visual Studio裡完成類似功能的中介程序VcSpawn(你可以在Visual Studio的安裝目錄中找到它)。我的wSpawn非常簡單,它利用系統調用_popen()同時完成創建子進程和輸入輸出重定向兩件工作。GUI程序則使用一種特殊的命令行方式調用wSpawn:

wspawn –h <n> <command> [arg1] [arg2] ...

其中,-h後跟的是GUI程序提供的管道句柄,由GUI程序自動將其轉換為十進制數字,wSpawn運行時將信息寫入該句柄中,隨後的內容是GUI程序真正要執行的命令行,例如調用C++編譯器cl.exe的方式大致如下:

wspawn –h 1903 cl /Id:\myInclude Test.cpp

wspawn.cpp的程序清單如下:

#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 = "";
  // 檢查命令行,如存在管道句柄,則將其轉換為HANDLE類型
  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創建子進程並重定向其標准輸出到文件指針中
  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)
      {
        // 將子進程的標准輸出寫入管道,提供給自己的父進程
        // 格式是先寫數據塊長度(0表示結束),再寫數據塊內容
        len = strlen(psBuffer);
        WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL);
        WriteFile(hWrite, psBuffer, len, &dwWrited, NULL);
      }
      else
        // 如命令行未提供管道句柄,則直接打印輸出
        printf(psBuffer);
    }
  }
   // 寫“0”表示所有數據都已寫完
  len = 0;
  if (hWrite)
    WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL);
   return _pclose( child_output );
}

下面,我們就利用wSpawn程序,寫一個簡單的“IDE”工具。我們選擇Visual Studio 6.0作為開發環境(本文給出的代碼也在Visual Studio.NET 7.0中做過測試)。首先,創建Visual C++工程myIDE,工程類型為MFC AppWizard(EXE)中的Dialog based類型,即創建了一個主窗口為對話框的GUI程序。工程myIDE的主對話框類是CMyIDEDlg。現在我們要在資源編輯器中為主對話框添加一個足夠大的多行編輯框(Edit Box),它的控制ID是IDC_EDIT1,必須為IDC_EDIT1設置以下屬性:

Multiline, Horizontal scroll, Auto HScroll, Vertical scroll, Auto VScroll, Want return

然後用ClassWizard為IDC_EDIT1添加一個對應的成員變量(注意變量的類型要選CEdit型而非字符串CString型)

CEdit m_edit1;

使用ClassWizard為“確定”按鈕添加消息響應方法OnOK(),編輯該方法:

void CMyIDEDlg::OnOK()
{
  AfxBeginThread(myThread, this);
  InvalidateRect(NULL);
  UpdateWindow();
}

也就是說,我們在“確定”按鈕按下時,啟動了後台線程myThread(),那麼,myThread()到底做了些什麼呢?我們先在CMyIDEDlg類的頭文件myIDEDlg.h中加上一個成員函數聲明:

protected:
  static UINT myThread(LPVOID pParam);

然後,在CMyIDEDlg類的實現文件myIDEDlg.cpp裡添加myThread()的實現代碼:

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;
  // 創建與wSpawn.exe通訊的可繼承的匿名管道
  saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
  saAttr.bInheritHandle = TRUE;
  saAttr.lpSecurityDescriptor = NULL;
  if (!CreatePipe(&hRead, &hWrite, &saAttr, 0))
  {
    AfxMessageBox("創建管道失敗");
    return 0;
  }
  // 准備wSpawn的命令行,在命令行給出寫管道句柄和要wSpawn執行的命令
  memset(&pi, 0, sizeof(pi));
  sprintf(command_line, "wspawn -h %d cl /?", (unsigned int)hWrite);
  // 子進程以隱藏方式運行
  ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
  siStartInfo.cb = sizeof(STARTUPINFO);
  siStartInfo.wShowWindow = SW_HIDE;
  siStartInfo.dwFlags = STARTF_USESHOWWINDOW;
  // 創建wSpawn子進程
  if (!CreateProcess( NULL, command_line, NULL, NULL, TRUE,
            0, NULL, NULL, &siStartInfo, &pi))
  {
    AfxMessageBox("調用wSpawn時失敗");
    return 0;
  }
  // 讀管道,並顯示wSpawn從管道中返回的輸出信息
  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;
    // 將返回信息中的"\n"替換為Edit Box可識別的"\r\n"
    tmp = buf;
    tmp.Replace("\n", "\r\n");
    Output += tmp;
    // 將結果顯示在Edit Box中,並刷新對話框
    pDlg->m_edit1.SetWindowText(Output);
    pDlg->InvalidateRect(NULL);
    pDlg->UpdateWindow();
    delete[] buf;
    if(!ReadFile( hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0)
      return 0;
  }
  // 等待wSpawn結束
  WaitForSingleObject(pi.hProcess, 30000);
  // 關閉管道句柄
  CloseHandle(hRead);
  CloseHandle(hWrite);
  return 0;
}

很簡單,不是嗎?後台線程創建一個匿名管道,然後以隱藏方式啟動wSpawn.exe並將管道句柄通過命令行傳給wSpawn.exe,接下來只要從管道裡讀取信息就可以了。現在我們可以試著編譯運行myIDE.exe了,記住要把myIDE.exe和wSpawn.exe放在同一目錄下。還有,我在myThread()函數中寫死了傳給wSpawn.exe的待執行的命令行是“cl /?”,這模擬了一次典型的編譯過程,如果你不打算改變這一行代碼的話,那一定要注意在你的計算機上,C++編譯器cl.exe必須位於環境變量PATH指明的路徑裡,否則wSpawn.exe可就找不到cl.exe了。下面是myIDE程序的運行結果:

補充一點,上面給出的wSpawn利用_popen()完成子進程創建和輸入輸出重定向,這一方法雖然簡單,但只能重定向子進程的stdout或stdin,如果還需要重定向子進程的stderr的話(Java編譯器javac就利用stderr輸出結果信息),那我們就不能這麼投機取巧了。根據以上討論,你一定可以使用傳統的_pipe()、_dup()等系統調用,寫出功能更完整的新版wSpawn來,我這裡就不再羅嗦了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved