引言
原因是我需要在C++程序中調用另外一個WPF窗體打開或則關閉,進程之前通過通訊協議進行交互。由于使用不同語言開發,兩者都比較復雜不方便重寫,最方便的方法就是使用進程間通信,WPF窗體應用程序根據消息進行Show/Hide/Exit操作。
函數介紹CreateProcess
BOOL CreateProcess(LPCWSTR lpApplicationName,//指向可執行模塊名稱的指針LPWSTR lpCommandLine,//指向命令行字符串的指針。LPSECURITY_ATTRIBUTES lpProcessAttributes,//指向 SECURITY_ATTRIBUTES 結構的指針,指定新進程的安全屬性。LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向 SECURITY_ATTRIBUTES 結構的指針,指定新線程的安全屬性。BOOL bInheritHandles,//如果為 TRUE,新進程將繼承調用進程的句柄。DWORD dwCreationFlags,//指定附加的、用來控制優先類和進程的創建的標志。LPVOID lpEnvironment,//指向新進程的環境塊的指針。如果為 NULL,新進程將使用調用進程的環境。LPCWSTR lpCurrentDirectory,//指向新進程的當前目錄的指針。如果為 NULL,新進程將使用調用進程的當前目錄。LPSTARTUPINFOW lpStartupInfo,//指向 STARTUPINFOW 結構的指針,指定新進程的主窗口特性。LPPROCESS_INFORMATION lpProcessInformation//指向 PROCESS_INFORMATION 結構的指針,接收新進程的標識符和句柄。
);
1、lpApplicationName
即將啟動的exe程序路徑,該參數是一個字符串。我們可以傳相對路徑或者絕對路徑,Windows在啟動的時候,會按照一定的順序查找exe。
1、如果該參數傳遞的是exe全路徑,操作系統會直接啟動指定的全路徑exe,如果找不到要啟動的exe文件,CreateProcess會啟動失敗。
2、查找主進程exe同級目錄下是否存在要啟動的exe文件。
3、查詢主進程的當前目錄下是否存在要啟動的exe文件,一般情況下,主進程的當前目錄和主進程exe是同一個目錄,但是也不絕對,我們可以手動修改程序的當前目錄。
4、查詢Window系統目錄下是否存在要啟動的exe文件,就是GetSystemDirectory獲取到的文件夾。
5、查詢Window目錄下是否存在要啟動的exe文件。
6、查詢環境變量Path所表示的那些目錄下,數據存在要啟動的exe文件。
從上面的流程看,操作系統查找要指定的exe文件是一個很復雜的流程,所以如果條件允許,我們建議傳遞全路徑。如果不允許,也至少應該是將exe文件放到主進程exe的相對目錄下,這也是我經常采用的一種方式。
2、lpCommandLine
表示要啟動的進程需要接受的命令行參數。
3、lpProcessAttributes、lpThreadAttributes、bInheritHandles
這兩個參數代表子進程的進程安全屬性和線程安全屬性,都指向SECURITY_ATTRIBUTES的一個結構體,一般情況下,我們可以傳NULL,表示子進程使用默認的進程安全屬性和 線程安全屬性。bInheritHandles表示子進程是否可以繼承父進程的句柄(父進程設置了允許繼承的安全屬性)。如果設置為TRUE,則表示可以繼承。
4、dwCreationFlags
參數用于指定創建新進程的時候的一些附加標志,用于控制新進程的一些行為,下面是常用的一些標識:
1、CREATE_NEW_CONSOLE:為新進程創建一個新的控制臺窗口。上面的代碼我們使用了這個表示,主進程和新進程是兩個控制臺窗口,如果沒有這個flag的話,主進程和新創建的進程共用一個 控制臺程序。
2、CREATE_NO_WINDOW:不要為新進程啟動一個窗口。如果我們需要創建一個在后臺運行沒有界面的進程的話,可以使用這個flag。
3、CREATE_SUSPENDED:創建新進程,不要立即執行,將進程掛起,直到調用ResumeThread函數的時候,才開始調用進程。如果我們創建多個子進程之后,需要有一個統一的同步策略,由主進程統一控制多個子進程的執行順序的話,可以使用這個flag,在上面的代碼上稍微做一點修改,新增一個flag和新增兩行代碼,運行起來,你會發現新創建的進程不會立即執行,而是等待5s之后由主進程控制它繼續執行。
BOOL ret = CreateProcess(lpApplicationName,lpCommandLine,NULL,NULL,FALSE,CREATE_NEW_CONSOLE | CREATE_SUSPENDED,NULL,NULL,&si,&pi);
4、CREATE_UNICODE_ENVIRONMENT:創建Unicode字符的環境變量,如果使用了該flag,那么?lpEnvironment?
參數指向的環境變量塊將使用Unicode,否則將使用ANSI字符。
5、CREATE_DEFAULT_ERROR_MODE:每個進程都有自己的一個錯誤模式,可以通過SetErrorMode?
函數設置,默認情況下,新進程繼承父進程的錯誤模式,如果使用該flag,新進程將使用默認的錯誤模式。
6、DETACHED_PROCESS:新進程和父進程分離,不繼承父進程的控制臺。該flag會阻止子進程訪問父進程的控制臺窗口,一般我們也很少使用該flag。這個flag和CREATE_NO_WINDOW?作用一致,不能同時使用,否則程序會報錯。
5、lpEnvironment
默認情況下,子進程將繼承父進程的環境變量。如果想給子進程單獨設置環境變量塊,可以傳遞該參數。
6、lpCurrentDirectory
設置子進程的運行目錄,如果為 NULL,新進程將使用調用進程的當前目錄。當子進程有許多依賴項是務必要填入應用程序exe的絕對路徑。
7、lpStartupInfo
指定新進程的主窗體特性(指定窗體大小、初始位置、控制臺背景顏色字體顏色、窗口標題等信息)、標準輸入、輸出、錯誤設備句柄等
typedef struct _STARTUPINFOW {DWORD cb;//結構的大小,以字節為單位。LPWSTR lpReserved;//保留,必須為 NULL。LPWSTR lpDesktop;//指向一個以空字符結尾的字符串,指定桌面名稱。LPWSTR lpTitle;//指向一個以空字符結尾的字符串,指定新進程的窗口標題。DWORD dwX;//指定窗口的初始位置XDWORD dwY;//指定窗口的初始位置YDWORD dwXSize;/指定窗口的初始大小XDWORD dwYSize;//指定窗口的初始大小YDWORD dwXCountChars;//指定屏幕緩沖區的寬度,以字符為單位。DWORD dwYCountChars;//指定屏幕緩沖區的高度,以字符為單位。DWORD dwFillAttribute;//指定新控制臺窗口的文本和背景顏色。DWORD dwFlags;//指定有效的 STARTUPINFO 成員。WORD wShowWindow;//指定窗口顯示狀態。WORD cbReserved2;//保留,必須為 0。LPBYTE lpReserved2;//保留,必須為 NULL。HANDLE hStdInput;//新進程的標準輸入句柄HANDLE hStdOutput;//新進程的標準輸出句柄HANDLE hStdError;//新進程的標準錯誤設備句柄
} STARTUPINFOW, *LPSTARTUPINFOW;
8、lpProcessInformation
lpProcessInformation是一個輸出參數,進程創建之后,會把子進程的進程句柄、線程句柄、進程id、線程id返回給主進程。參數的定義如下:
typedef struct _PROCESS_INFORMATION {HANDLE hProcess;HANDLE hThread;DWORD dwProcessId;DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
關于進程Id和線程Id需要注意的是:Windows有一個ID編號池,用于給進程和線程分配ID,并且這個ID池中的ID永遠不會重復,這意味著永遠不會有進程ID和線程ID重復。不過當進程線程推出的時候,這個進程的ID會回到ID池,后續可能會分配給別的進程。
試想一下。我用進程111創建了一個子進程222,子進程222的父進程確實是111,但是因為某些原因進程111被回收,并且重新分配給了一個其他的進程,如果這個時候,子進程再拿著父進程的ID:111去處理業務的話,就會出現意想不到的錯誤。同理,線程ID也是如此。所以,一般情況下,我們習慣用句柄操作具體業務,很少會使用ID來處理業務。那么hProcess
參數和hThread
參數就代表子進程的進程句柄和主線程句柄,父進程可以使用這兩個參數做對應的業務邏輯。
例子
#include <windows.h>
#include <iostream>
#include <string>
#include <tchar.h>
#include <thread>
bool SendCommandToWpf(const std::wstring& command)
{HANDLE hPipe;DWORD dwWritten;// 等待管道可用while (1){hPipe = CreateFile(TEXT("\\\\.\\pipe\\WpfConsoleCommunicationPipe"), // 管道名稱GENERIC_READ | GENERIC_WRITE,0, // 不共享NULL, // 默認安全屬性OPEN_EXISTING, // 打開已存在的管道0, // 默認屬性NULL); // 不指定模板文件// 如果管道連接成功,退出循環if (hPipe != INVALID_HANDLE_VALUE)break;// 如果錯誤不是ERROR_PIPE_BUSY,則失敗if (GetLastError() != ERROR_PIPE_BUSY){std::cout << "無法打開管道. 錯誤代碼: " << GetLastError() << std::endl;return false;}// 所有管道實例都忙,等待20秒if (!WaitNamedPipe(TEXT("\\\\.\\pipe\\WpfConsoleCommunicationPipe"), 20000)){std::cout << "無法在20秒內連接管道." << std::endl;return false;}}// 管道連接成功,設置讀寫模式DWORD dwMode = PIPE_READMODE_MESSAGE;if (!SetNamedPipeHandleState(hPipe, // 管道句柄&dwMode, // 新的管道模式NULL, // 不設置最大字節數NULL)) // 不設置最大超時時間{std::cout << "設置管道模式失敗. 錯誤代碼: " << GetLastError() << std::endl;CloseHandle(hPipe);return false;}// 發送命令if (!WriteFile(hPipe, // 管道句柄command.c_str(), // 消息(command.size() + 1) * sizeof(wchar_t), // 消息長度(包含null終止符)&dwWritten, // 實際寫入的字節數NULL)) // 不重疊I/O{std::cout << "寫入管道失敗. 錯誤代碼: " << GetLastError() << std::endl;CloseHandle(hPipe);return false;}CloseHandle(hPipe);return true;
}void StartWpfApplication()
{STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));// 替換為你的WPF應用程序路徑std::wstring wpfAppPath = L"D:/程序代碼/WPF-Window.exe";std::wstring RunDir = L"D:/程序代碼/";if (!CreateProcess(&wpfAppPath[0], // 應用程序名稱NULL, // 命令行NULL, // 進程安全屬性NULL, // 線程安全屬性FALSE, // 不繼承句柄REALTIME_PRIORITY_CLASS,// 創建新控制臺窗口以便查看輸出NULL, // 使用父進程環境塊&RunDir[0], // 使用父進程起始目錄&si, // 啟動信息&pi)) // 進程信息{std::cout << "創建進程失敗. 錯誤代碼: " << GetLastError() << std::endl;return;}// 關閉不需要的句柄CloseHandle(pi.hProcess);CloseHandle(pi.hThread);std::this_thread::sleep_for(std::chrono::seconds(10));//確保進行已啟動
}int main()
{// 啟動WPF應用程序StartWpfApplication();std::cout << "控制臺程序已啟動. 輸入命令(Show/Hide/Exit):" << std::endlstd::wstring command;while (std::getline(std::wcin, command)){if (command == L"Exit"){SendCommandToWpf(command);break;}else if (command == L"Show" || command == L"Hide"){if (!SendCommandToWpf(command)){std::cout << "發送命令失敗." << std::endl;}}else{std::cout << "未知命令. 可用命令: Show, Hide, Exit" << std::endl;}}return 0;
}