C++利用CerateProcess创建WPF进程并通过命名管道通讯

引言

原因是我需要在C++程序中调用另外一个WPF窗体打开或则关闭,进程之前通过通讯协议进行交互。由于使用不同语言开发,两者都比较复杂不方便重写,最方便的方法就是使用进程间通信,WPF窗体应用程序根据消息进行Show/Hide/Exit操作。

函数介绍CreateProcess

cpp 复制代码
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之后由主进程控制它继续执行。

cpp 复制代码
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

指定新进程的主窗体特性(指定窗体大小、初始位置、控制台背景颜色字体颜色、窗口标题等信息)、标准输入、输出、错误设备句柄等

cpp 复制代码
typedef struct _STARTUPINFOW {
    DWORD   cb;//结构的大小,以字节为单位。
    LPWSTR  lpReserved;//保留,必须为 NULL。
    LPWSTR  lpDesktop;//指向一个以空字符结尾的字符串,指定桌面名称。
    LPWSTR  lpTitle;//指向一个以空字符结尾的字符串,指定新进程的窗口标题。
    DWORD   dwX;//指定窗口的初始位置X
    DWORD   dwY;//指定窗口的初始位置Y
    DWORD   dwXSize;/指定窗口的初始大小X
    DWORD   dwYSize;//指定窗口的初始大小Y
    DWORD   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返回给主进程。参数的定义如下:

cpp 复制代码
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参数就代表子进程的进程句柄和主线程句柄,父进程可以使用这两个参数做对应的业务逻辑。

例子

cpp 复制代码
#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::endl
    std::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;
}