
简介面向虚幻引擎4开发者的完整源码工程用于在蓝图中通过 C 实现打开外部可执行程序。核心基于 FPlatformProcess 的 ExecuteAndWait 接口覆盖进程启动、命令行参数传递、进程句柄获取等关键操作适合游戏内启动辅助编辑器、执行数据分析脚本等场景。压缩包共 31 个文件以 4 个 .h 与 4 个 .cpp 源码文件、2 个 .umap 和 2 个 .uasset 蓝图资产为主体辅以 .uproject、.ini、.cs、.target 配置以及编译产生的 .dll 与 .pdb 调试文件大小约 37.79MB。已有 3517 人学习下载。工程 OpenExe 目录清晰可对照 C 类定义、函数实现与蓝图事件图表理解如何将 C 函数暴露给蓝图同时涵盖重新生成 Visual Studio 项目文件的步骤方便编译调试或移植到自建项目中。1. UE4用C在蓝图里打开外部exe: 这不是黑魔法, 是一次正经的进程调用UE4项目里一旦牵扯到“和外部的程序联动”最常见到的一句话就是能不能在蓝图里点一下就打开一个exe。场景通常是启动一个配套的配置工具、拉起更新器、打开外部浏览器页面或者调试时顺手把另一个服务带起来。用C写一个BlueprintCallable函数把FPlatformProcess::CreateProc包一层再暴露成蓝图节点十几行代码就能跑通不需要任何第三方插件。下文从进程调用的选型讲起把源码、参数、蓝图接法和五个高频踩坑现场一次说透适合刚把C和蓝图打通、又要做外部程序联动的UE4开发者。2. 选型先做对: 为什么FPlatformProcess::CreateProc是UE4打开exe的正路打开外部exe这件事很多初学者第一反应是去查Windows API然后写一个只能在Windows编译的乱炖函数。UE4其实已经把这条路封装好了FPlatformProcess就是HAL层硬件抽象层给开发者留下的进程入口CreateProc能在Windows、Mac、Linux上编译通过只是平台行为有差异。对“在蓝图里打开exe”这种需求它是最短路径也是我最推荐的默认方案。2.1 三条路对比: CreateProc、ShellExecuteW、CreateProcessW差在哪先把三条路摆在一起看选型就不会纠结方式封装层级适用场景能否请求管理员权限主要成本FPlatformProcess::CreateProcUE4 HAL封装启动exe并带命令行参数最常见否参数里的子路径要自己加引号ShellExecuteWWinAPI打开文档/URL或需要runas提权启动exe是只能在Windows下编译要包含Windows头CreateProcessWWinAPI需要精细控制进程信息、管道读写否代码量翻倍STARTUPINFO和PROCESS_INFORMATION都要自己管判断逻辑很简单CreateProc是UE4替你管平台差异ShellExecuteW是Windows专属但能提权CreateProcessW是把所有控制权都给你代价是代码量。我一般只在前两种里选。补充一点FPlatformProcess还提供了LaunchURL函数底层走ShellExecute适合打开网址、文档、邮件协议但它不能顺手传一套命令行参数给exe所以“启动外部工具带参数”这个需求LaunchURL并不合适。提示如果你只是想在编辑器里打开一个网页用LaunchURL就够如果你要拉起的是exe且要传参数回到CreateProc。2.2 CreateProc的9个参数逐个拆: 三个bool决定exe窗口怎么出现先看函数签名UE4的通用版本长这样static FProcHandle CreateProc( const TCHAR* URL, // exe的完整路径 const TCHAR* Parms, // 命令行参数没有就传nullptr bool bLaunchDetached, // 是否让进程脱离游戏进程组 bool bLaunchHid, // 是否隐藏启动窗口 bool bLaunchReallyHidden, // 是否真正隐藏所有窗口 uint32* OutProcessId, // 输出进程ID不需要就传nullptr int32 PriorityModifier, // 优先级调整正常填0 const TCHAR* OptionalWorkingDir, // 工作目录nullptr表示继承游戏目录 void* PipeWrite // 管道写端用于读写子进程输出 );参数不多但真正让Windows程序员都晕的是中间三个bool。按我的经验直接记三种组合就行打开一个正常GUI程序用false、false、false窗口该出现就出现启动一个控制台程序但不想弹黑窗把bLaunchHid给true启动一个后台静默进程彻底不想看见任何窗口再把bLaunchReallyHidden也打开。Windows底层实现里bLaunchHid会映射成CREATE_NO_WINDOWbLaunchReallyHidden会通过STARTUPINFO把主窗口设为SW_HIDEbLaunchDetached对应DETACHED_PROCESS理解到这一层遇到“为什么弹了黑窗”就知道是哪根筋没搭对。剩下几个参数平时不用动OutProcessId是想要PID时传一个uint32变量的地址PriorityModifier调进程优先级正常0OptionalWorkingDir影响exe内部相对路径的解析不传就继承UE进程的工作目录PipeWrite只在你要读子进程标准输出时才需要属于少数进阶玩法。2.3 路径拼接是第一个坑: 绝对路径、空格与反斜杠在蓝图里手填一个“D:/Tools/MyTool.exe”是最直观的但项目要打包、要给别人用路径就得按规则拼。我习惯在C侧用FPaths统一处理而不是把路径计算丢给蓝图// 用ProjectDir拼绝对路径避免蓝图里手填一个随时会变的盘符 FString ToolFullPath FPaths::ConvertRelativePathToFull( FPaths::Combine(FPaths::ProjectDir(), TEXT(ThirdParty/MyTool.exe)) );FPaths::Combine会按平台正确的分隔符拼接Windows下它接受正斜杠也接受反斜杠不用纠结。ConvertRelativePathToFull会把相对路径转成绝对路径顺手把“./”这种写法清掉。这里有个前置认知必须建立打包之后Content目录里的东西会被压进.pak文件exe是没法从pak里取出来执行的。所以外部程序一律放项目根目录下的非Content目录比如ThirdParty、Tools代码里用ProjectDir去拼。另外CreateProc的URL参数是单独传的exe路径本身带空格没问题但等会你要往Params里拼其他带空格的路径时必须自己包引号这是后面避坑章会重点说的翻车点。3. 源码落地: 把C函数缝进蓝图的最小工程结构与完整代码选型定了接下来就是把代码写进工程。这里给出一个最小可编译的结构一个UBlueprintFunctionLibrary子类两个文件加一个Build.cs蓝图端就能直接搜到节点。整个模块不需要引入任何额外第三方依赖。3.1 模块怎么摆: Build.cs、头文件、cpp各管什么常见的布局是直接放进现有游戏模块或者单独建一个Runtime插件。独立插件的好处是复用和移植方便但最小验证阶段不必先建插件直接在项目的Source目录里加两个文件就行Source/OpenExeDemo/ OpenExeDemo.Build.cs OpenExeDemo.h OpenExeDemo.cpp OpenExeBPLibrary.h OpenExeBPLibrary.cppBuild.cs里依赖模块不要画蛇添足// OpenExeDemo.Build.cs using UnrealBuildTool; public class OpenExeDemo : ModuleRules { public OpenExeDemo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; // FPlatformProcess 在 Core 里BlueprintFunctionLibrary 在 Engine 里 PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore }); PrivateDependencyModuleNames.AddRange(new string[] { }); } }这里强调一个常见误区有人以为要额外加一个什么“Process”模块或者去引入第三方库。不需要。FPlatformProcess由Core模块提供UBlueprintFunctionLibrary由Engine模块提供上面这几个就够。头文件里注意generated.h必须放在所有include的最后一行这是UHT的硬性要求顺序错了编译直接报奇怪错误。3.2 UFUNCTION的BlueprintCallable写法: DisplayName与Keywords别偷懒头文件里定义蓝图函数库// OpenExeBPLibrary.h #pragma once #include CoreMinimal.h #include Kismet/BlueprintFunctionLibrary.h #include OpenExeBPLibrary.generated.h UCLASS() class OPENEXEDEMO_API UOpenExeBPLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** 传入exe绝对路径和命令行参数启动外部程序成功返回true */ UFUNCTION(BlueprintCallable, Category OpenExe, meta (DisplayName OpenExternalExe, Keywords exe process launch 启动 外部程序 进程)) static bool OpenExternalExe(const FString ExePath, const FString Params); };实现文件是重头戏// OpenExeBPLibrary.cpp #include OpenExeBPLibrary.h #include HAL/PlatformProcess.h #include Misc/Paths.h bool UOpenExeBPLibrary::OpenExternalExe(const FString ExePath, const FString Params) { // 先把相对路径转成绝对路径再检查文件是否存在 FString FullPath FPaths::ConvertRelativePathToFull(ExePath); if (!FPaths::FileExists(FullPath)) { UE_LOG(LogTemp, Warning, TEXT([OpenExe] 文件不存在: %s), *FullPath); return false; } #if PLATFORM_WINDOWS // 三个bool按显示普通GUI窗口的场景设置控制台程序要隐藏窗口再改 FProcHandle ProcHandle FPlatformProcess::CreateProc( *FullPath, // 要启动的exe绝对路径 Params.IsEmpty() ? nullptr : *Params, // 命令行参数空就传nullptr false, // bLaunchDetached: false 保持窗口可见 false, // bLaunchHid: false 不隐藏启动窗口 false, // bLaunchReallyHidden: 不需要彻底隐藏 nullptr, // 不需要拿进程ID 0, // 优先级不调整 nullptr, // 工作目录继承游戏进程 nullptr // 不读写子进程管道 ); if (!ProcHandle.IsValid()) { UE_LOG(LogTemp, Error, TEXT([OpenExe] 进程创建失败: %s), *FullPath); return false; } // 只负责拉起不等待退出所以句柄用完立刻释放避免泄漏 FPlatformProcess::CloseProc(ProcHandle); return true; #else UE_LOG(LogTemp, Warning, TEXT([OpenExe] 当前平台不支持启动外部程序)); return false; #endif }代码逻辑分三段先做路径规整和存在性检查避免CreateProc返回一个无效句柄时你去猜原因再用PLATFORM_WINDOWS宏包住平台相关调用保证其他平台编译不报错最后成功创建后立即CloseProc。这里CloseProc指的是释放UE4持有的进程句柄Windows底层对应的是CloseHandle不是杀掉进程。进程本身会继续运行只是游戏这边不再盯着它了。参数上有两个细节值得说。第一Params为空时传nullptr而不是空字符串这是通用HAL实现的约定避免个别平台的解析行为不一致。第二UE_LOG打日志很有用蓝图调用的失败现场不一定复现日志里留下“文件不存在”还是“进程创建失败”能让你少做一半的玄学排查。注意UFUNCTION头文件里就算写了C默认参数蓝图节点上也不会因此少一根引脚。蓝图永远看到ExePath和Params两个输入Params手动留空即可。3.3 蓝图侧接线: 从按钮点击到返回值判空C编译通过后蓝图里右键搜索“OpenExe”就能找到节点。接线步骤很固定在控件蓝图里给按钮的OnClicked拉一条线节点搜索框输入“OpenExe”拖出OpenExternalExe。ExePath引脚填exe的绝对路径比如D:/Tools/MyTool.exe如果连的是变量就做一个FString变量统一管理。Params引脚填命令行参数比如-silent -workdir...没有就什么都不接。Return Value接一个Branch节点失败时用PrintString把死路打出来别让玩家面对一个没反应的按钮。编辑器测试时填相对路径“看着能用”但一旦打包这条路径就失效。测试阶段就按打包后的目录结构摆文件这是最省事的习惯。如果编译后蓝图里搜不到节点常见的处理是先重新编译然后关掉当前蓝图编辑器再打开右键菜单会重新生成。别在没编译的状态下反复刷新那不是蓝图问题是C侧还没生成蓝图节点信息。4. 进程生命周期: 别让游戏卡死, 也别让句柄泄漏打开exe只是第一步。进程拉起来之后游戏代码和这个外部进程的关系才是真正容易出问题的地方卡死、句柄泄漏、重复拉起全在这一层。4.1 同步CreateProc不卡, WaitForProc才卡: 点击无效的真相有人把“点击按钮游戏卡住”归罪于CreateProc这冤枉它了。CreateProc本身只是创建进程Windows返回几乎是毫秒级的。真正让游戏卡死的是后续动作CreateProc之后紧接着调WaitForProc或者在游戏线程里写一个循环反复Sleep检查进程是否退出。外部工具跑几十分钟游戏就死几十分钟任务管理器里直接“未响应”。所以第3章的源码刻意设计成“拉起即忘”CreateProc成功、CloseProc、立刻返回。如果你确实要等外部exe做完再继续游戏流程不要用同步等待往下看Timer轮询的做法那是把等待放到游戏主循环里既不阻塞也不漏检。4.2 保存FProcHandle: 结束进程与释放句柄的正确姿势第3章的函数返回bool句柄已经释放适合“开完不管”的场景。但很多需求是要能中途结束进程的比如启动了一个长时间运行的配套服务游戏里要提供“停止”按钮。这时候FProcHandle必须留着而且要留在一个C对象上因为它不是USTRUCT不能直接穿过蓝图引脚。我的做法是写一个ActorComponent// AExeLauncherComponent.h 里的声明 UFUNCTION(BlueprintCallable, Category ExeLauncher) void Launch(const FString ExePath, const FString Params); UFUNCTION(BlueprintCallable, Category ExeLauncher) void Kill(); private: FProcHandle RunningProc; // 注意FProcHandle不是USTRUCT不能当UPROPERTY// 对应实现 void AExeLauncherComponent::Launch(const FString ExePath, const FString Params) { // 重复启动前先干掉旧进程 if (RunningProc.IsValid()) { UE_LOG(LogTemp, Warning, TEXT([ExeLauncher] 已有进程在运行先结束再启动)); FPlatformProcess::TerminateProc(RunningProc, true); FPlatformProcess::CloseProc(RunningProc); } FString FullPath FPaths::ConvertRelativePathToFull(ExePath); RunningProc FPlatformProcess::CreateProc( *FullPath, Params.IsEmpty() ? nullptr : *Params, false, false, false, nullptr, 0, nullptr, nullptr); } void AExeLauncherComponent::Kill() { if (RunningProc.IsValid()) { FPlatformProcess::TerminateProc(RunningProc, true); FPlatformProcess::CloseProc(RunningProc); RunningProc FProcHandle(); } }这里有两个容易漏的动作。TerminateProc的第二个参数是KillTree传true会把目标进程的子进程一起结束否则只杀父进程子进程会留下来变孤儿进程。另外结束之后要么置回默认构造的FProcHandle要么每次使用前都判IsValid否则下一次Launch会对着一个已失效的句柄做TerminateProc返回的是错误码而不是异常排查时特别容易绕弯。4.3 高级做法: 用Timer轮询IsProcRunning, 在进程退出时通知蓝图需要“等exe退出再通知蓝图”时我不建议写BlueprintAsyncAction那个样板代码不少单实例场景用定时器轮询反而清爽。组件里保存句柄后启动时挂一个循环定时器// 启动时挂上定时器每0.5秒检查一次 GetWorld()-GetTimerManager().SetTimer(CheckTimer, this, AExeLauncherComponent::CheckProcess, 0.5f, true); // 定时器回调 void AExeLauncherComponent::CheckProcess() { if (!RunningProc.IsValid()) { GetWorld()-GetTimerManager().ClearTimer(CheckTimer); return; } if (!FPlatformProcess::IsProcRunning(RunningProc)) { GetWorld()-GetTimerManager().ClearTimer(CheckTimer); FPlatformProcess::CloseProc(RunningProc); // OnProcessExited 是一个 BlueprintAssignable 动态多播委托 OnProcessExited.Broadcast(true); } }轮询间隔0.5秒到1秒足够不要写10毫秒这种精度IsProcRunning在Windows下查询的是进程内核对象开销不大但高频调用没意义。Timer回调天然跑在游戏线程所以Broadcast动态委托后蓝图里绑定的自定义事件可以直接操作UI不用再担心线程切换。这段代码要记得在类里包含TimerManager.h并且组件的所属Actor需要能取到GetWorld。提示IsProcRunning只查进程是否还活着查不出“exe已经起来了但卡在初始化”这种状态别拿它当健康检查。5. 避坑排查: 打包后没反应、弹黑窗、缺运行库的5个现场下面五个现场是我见过最高频的翻车点也是这个功能从“能用”到“稳定”必须跨过的坎。每条都按现象、原因、解决的顺序写照着自己的实现逐条对。5.1 打包后点击无反应: Content目录里的exe被Pak吞了现象编辑器里测试一切正常Cook加打包后装到别的机器按钮点了没有任何反应日志里出现“文件不存在”。原因开发期外部exe放在项目Content目录下编辑器里Content是真实文件夹能读到打包后Content被压缩进.pakexe根本不存在于文件系统。第3章代码里的FileExists检查会先拦截所以表现为“静默失败”。解决外部程序统一放到项目根目录的ThirdParty或Tools目录代码用FPaths::ProjectDir()拼接。打包时需要把整个第三方目录拷到安装目录常见做法是在构建脚本里做一次额外Staging或者在项目设置里用额外的部署步骤把ThirdParty同步过去。另一条省事的路是把exe路径做成可配置项第6章的做法装机时手动指定位置避免把目录结构写死在代码里。5.2 参数里带空格路径: exe起来了, 参数却被切成了两半现象exe成功启动但行为不对比如传-config D:/My Tools/config.ini工具端收到的config路径是D:/My后面的Tools/config.ini变成了另一个参数。原因CreateProc的URL参数单独传exe路径带空格安全但Params是一个整体字符串Windows在解析命令行时按空格切分参数D:/My Tools/config.ini没包引号就被切了。解决拼接Params里的子路径时手动加引号FString ConfigArg FString::Printf(TEXT(-config \%s\), *ConfigFullPath);别指望Windows自动帮你处理它不猜。凡是带空格的路径参数一律用双引号包住没有例外。5.3 一心想等exe退出: 蓝图挂着, 游戏先“未响应”现象点击按钮后游戏画面卡住任务管理器显示未响应外部exe退出后游戏又恢复。原因网上很多旧教程在CreateProc之后直接调WaitForProc这个调用在游戏主线程上阻塞到子进程退出。如果外部程序是个常驻服务游戏就永远卡着。解决只拉起不等待。需要知道退出时机时用第4.3章的Timer轮询IsProcRunning把等待从阻塞变成周期检查。也不要在蓝图里搞“循环Delay查文件是否存在”的伪轮询蓝图的Delay不占线程但延时循环的时序很难控制不如定时器干净。5.4 目标机器报缺VCRUNTIME140.dll: Microsoft Visual C Redistributable没装现象开发机上双击外部exe一切正常拷给同事或玩家后系统弹窗提示找不到VCRUNTIME140.dll或者其他VCRUNTIME相关错误exe根本起不来。原因这个外部exe是用Visual Studio编译的C程序动态链接到VC运行库。目标机器如果没有安装对应版本的Microsoft Visual C Redistributable (x64)就会缺运行库。这是外部程序自己的部署问题UE4侧代码不用背锅但整体联调时它表现为“游戏里点了没反应”特别容易被误判成蓝图接错了。解决发布包内捎带对应版本的VC Redistributable安装包比如常见的vc_redist.x64.exe。更省心的是在编译外部exe时把运行库改成静态链接/MT免除目标机器的运行库依赖。验证方法是在一台干净的Windows虚拟机里跑一遍完整流程不要只在开发机上测。5.5 快速连点按钮: 同一个exe被拉起七八份现象按钮没做防连点或者用户手滑双击任务管理器里同一个外部工具冒出一堆进程后启动的还可能因为资源占用失败。原因第3章的OpenExternalExe是“拉起即忘”设计每次调用都无条件CreateProc不判活。它满足“开完不管”的需求但扛不住重复触发。解决改用第4.2章的组件方案Launch前先判断RunningProc.IsValid()并且IsProcRunning(RunningProc)还活着就直接跳过。测试时用命令看一下现场tasklist /FI IMAGENAME eq MyTool.exe这个命令按镜像名过滤进程列表能看到到底拉起了几份。把这句写进自己的验收清单比肉眼盯任务管理器靠谱。6. 收尾技巧: 把exe路径换成配置项, 并用tasklist验证进程真起来了到了这一步核心功能已经通了。最后再塞两个让这个功能“好维护”的技巧一是把exe路径挪到配置文件二是把验证方法从“看返回值”升级成“查进程真身”。6.1 把外部exe路径写进GGameIni: 不重新编译也能换工具不要把所有路径写死在C源码里尤其是发行后可能要替换工具版本的场景。用GConfig读写INI很简单#include Misc/ConfigCacheIni.h FString ToolPath; if (!GConfig-GetString(TEXT(/Script/OpenExeDemo), TEXT(ExternalToolPath), ToolPath, GGameIni)) { // 没配置就回退到工程目录下的默认位置 ToolPath FPaths::Combine(FPaths::ProjectDir(), TEXT(ThirdParty/MyTool.exe)); }GGameIni对应项目的Game.ini写入则用GConfig-SetString加GConfig-Flush。这样升级外部工具时只替换exe文件或改一行ini不用重新编译工程。想做得更完整可以用UDeveloperSettings做一个项目设置面板原理相同只是把ini操作包了一层编辑器界面。6.2 用tasklist检查进程: 返回值只代表“创建成功”, 不代表“跑起来了”最后一个习惯拿到true返回值别急着说功能完成了。CreateProc的true只表示进程创建API调用成功exe内部初始化、缺运行库、闪退这些情况返回值一概不管。我自己现在的验证习惯是点击按钮后先看UE日志有没有“进程创建失败”再用tasklist确认进程存在再跑一遍带参数的完整流程最后在任务管理器里结束进程确认游戏的进程管理逻辑也跟着清理干净。这一套走完才算“真的起来了”。说回这个功能本身我现在的习惯是先确认三件事再动笔exe放在哪个目录、要不要等待退出、目标机器是不是干净环境。这个顺序是我自己翻车翻出来的第一版把exe塞进Content第二版没处理运行库依赖第三版才把整条链路稳定下来。路径交给配置、句柄管好、窗口参数按场景设对这个功能就能从“能用”变成“好维护”。希望帮到你。本文还有配套的精品资源点击获取