开始学习OpenGL由于有一段时间,但是glfw只有窗口区,虽然通过某种手段(移步这里)可以加入工具栏,但仍然无法作为一个标准的GUI,而直接在MFC或Qt里面使用OpenGL API感觉有诸多制肘,各有利弊,所以打算将其嵌入GUI框架,此处以MFC为例

参考博文:https://blog.csdn.net/sunbibei/article/details/51783783

注意:本文使用的是MFC主程序调用GLFW子进程的方式嵌入窗口,略显繁琐。经道友提醒,原来也可以用多线程方式嵌入渲染,详情请移步 将GLFW窗口嵌入Win32 SDK窗口及其多线程渲染方法。其方式应该也可以用在MFC窗口中。

1、准备工作

由于要通过CreateProcess创建子进程的方式调用第三方exe程序,所以有必要知道创建的子进程信息,此处exe来自GLFW示例程序

1.1、查看打开窗口程序进程PID

windows任务管理器 -> 进程 -> 查看 -> 选择列 -> 进程勾选PID选项

如图,同一个exe窗口程序多次重复打开之后其PID是唯一的,其他信息(名称)相同,所以首先拿到以CreateProcess方法创建子进程时的进程PID

2、对CreateProcess函数进行封装

  1. /*
  2. * 创建子进程
  3. * @program 被调用进程的路径
  4. * @args 需传入的参数列表
  5. */
  6. HANDLE StartNewProcess(LPCTSTR program, LPCTSTR args) {
  7. HANDLE hProcess = NULL;
  8. PROCESS_INFORMATION pi;
  9. STARTUPINFO si;
  10. ::ZeroMemory(&si, sizeof(si));
  11. si.cb = sizeof(si);
  12. si.dwFlags = STARTF_USESHOWWINDOW;
  13. si.wShowWindow = SW_HIDE;
  14. // 创建子进程
  15. if (::CreateProcess(
  16. program, // 参数1.应用程序的名称,绝对路径,也可以是相对路径,可为NULL,若为NULL,则执行lpCommandLine
  17. (LPTSTR)args, // 参数2.命令行参数,可为NULL,一般为应用程序传参,若为NULL,函数则使用 lpApplicationName字符串为运行命令行
  18. NULL, // 参数3.进程的属性,指向一个SECURITY_ATTRIBUTES结构,结构体决定返回的句柄是否被子进程继承,一般为NULL
  19. NULL, // 参数4.线程的属性,同参数3.但是这个参数决定的是 线程 是否被继承,一般为NULL
  20. FALSE, // 参数5.是否继承父进程的属性,TRUE\FALSE ,一般为FALSE ,若为TRUE 进程中每个可被继承的打开句柄都被继承,被继承者有相同的值和访问权限
  21. , // 参数6.标志位信息,参数太多,具体见MSDN,或者百度百科,一般默认为 0
  22. NULL, // 参数7.环境变量,指向新进程的环境块,一般为NULL,为NULL则新进程使用调用进程的环境
  23. NULL, // 参数8.程序当前目录,为指定子进程的工作路径,如果是启动Exe程序,则为应用程序所在的目录
  24. &si, // 参数9.传给新进程的信息,指向新进程主窗口如何显示的STARUPINFO 结构体
  25. &pi) // 参数10.进程返回的信息,用来接收新进程识别信息的PROCESS_INFORMATION结构体
  26. ) {
  27. Sleep(); // 此处要是不等待或等待时间过短,子进程窗口未创建完成,则无法在回调中枚举到当前创建的窗口句柄
  28. TRACE("PID = >>>>> = %d, TID = >>>>> = %d, hProcess = >>> %d\n", pi.dwProcessId, pi.dwThreadId, pi.hProcess);
  29. // 枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数
  30. ::EnumWindows(&EnumWindowsProc, pi.dwThreadId);
  31. hProcess = pi.hProcess;
  32. } else {
  33. TRACE("CreateProcess failed (%d).\n", GetLastError());
  34. return ;
  35. }
  36. return hProcess; // 返回进程句柄
  37. }

任务管理器显示如下,进程PID为10172

VS打印输出

  1. d:\vsworkspace\mfcglfwtest\mfcglfwtest\mfcglfwtestview.cpp() : atlTraceGeneral - PID = >>>>> = , TID = >>>>> = , hProcess = >>>

过程无误,由PROCESS_INFORMATION 结构体对象pi拿到进程pid和句柄,两个结构体详情查看这里

3、枚举桌面窗口句柄的回调函数

  1. /*
  2. * 回调函数, 枚举获取窗口句柄
  3. * @hwnd 枚举获取的顶层窗口句柄
  4. * @param 进程名称
  5. */
  6. BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param) {
  7. DWORD pID; // 进程ID
  8. DWORD TpID = GetWindowThreadProcessId(hwnd, &pID); // 返回值为创建窗口的线程ID
  9. if (TpID == (DWORD)param) {
  10. TRACE("TpID = %d, > ===== > param = %d, > ===== > hwnd = %d\n", TpID, param, hwnd);
  11. apphwnd = hwnd;
  12. return FALSE; // 停止枚举,返回FALSE
  13. }
  14. return TRUE; // 继续枚举,返回TRUE
  15. }

4、进程关闭函数

  1. /*
  2. * 关闭进程
  3. */
  4. BOOL CloseProcess() {
  5. return TerminateProcess(handle, );
  6. }

5、入口函数

此处加入了一个工具栏按钮事件,点击触发此函数,客户区位置rect和上下文m_pDC定义为CXXXView类的成员

  1. public:
  2. CClientDC *m_pDC = NULL; // 客户区上下文
  3. CRect rect;

事件处理函数

  1. void CMFCGlfwTestView::OnGlfwsimple() {
  2. // TODO: 在此添加命令处理程序代码
  3. handle = StartNewProcess(_T("..\\Debug\\simple.exe"), NULL);
  4. // 获取客户区位置
  5. GetClientRect(&rect);
  6. // 更改窗口的位置和尺寸,此处填满父窗口
  7. ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), false);
  8. // ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE); // 更改窗口属性
  9. // 获取客户区上下文
  10. m_pDC = new CClientDC(this);
  11. HDC c_DC = m_pDC->GetSafeHdc();
  12. HWND viewWnd = WindowFromDC(c_DC);
  13. TRACE("child_hwnd >> %d, parent_hwnd >> %d\n", apphwnd, viewWnd);
  14. ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE);
  15. // 获取客户区所在窗口句柄
  16. ::SetParent(apphwnd, viewWnd);
  17. }

关键在于通过::SetParent(apphwnd, viewWnd)函数设置子窗口的父窗口为单文档客户区,所以要用WindowFromDC(c_DC)函数经客户区上下文拿到客户区句柄。

6、全局变量与函数说明

上面代码涉及到两个全局变量子窗口句柄和子进程句柄,这两个变量被定义在CXXXView.cpp中而不是头文件中,还有2、3、4三个函数也是直接在CXXXView.cpp中定义而没有进行头文件声明,因为将其作为CXXXView类的成员或对象成员都会使得::EnumWindows(&EnumWindowsProc, pi.dwThreadId)枚举函数参数错误,暂未找到解决之法。

  1. HWND apphwnd; // 子窗口句柄
  2. HANDLE handle; // 进程句柄

7、关键的窗口销毁事件

子进程窗口应该随着父窗口的销毁而关闭,所以需要为当前类添加一个OnDestroy消息处理函数,在其中调用上面定义的进程关闭函数

  1. /*
  2. * 窗口销毁
  3. */
  4. void CMFCGlfwTestView::OnDestroy() {
  5. // TODO: 在此处添加消息处理程序代码
  6. if (CloseProcess() != ) {
  7. TRACE("child process has been specified ... \n");
  8. }
  9. // 释放客户区上下文对象
  10. if (m_pDC) {
  11. delete m_pDC;
  12. }
  13. TRACE("m_pDC has been free ... \n");
  14. CView::OnDestroy();
  15. }

注意:由于是通过该函数关闭子进程,所以正常点击主窗口上的×按钮是没问题的,但是通过VS的停止调试按钮关闭程序不会触发该函数,子进程仍会在后台运行,需要手动关闭

8、子窗口大小改变

要使得父窗口大小变化时子窗口随之改变,需要再给当前类添加一个OnSize消息处理函数

  1. /*
  2. * 窗口大小改变监听
  3. */
  4. void CMFCGlfwTestView::OnSize(UINT nType, int cx, int cy) {
  5. CView::OnSize(nType, cx, cy);
  6. // TODO: 在此处添加消息处理程序代码
  7. GetClientRect(&rect);
  8. ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), true);
  9. }

每次父窗口改变后获取窗体大小,重新设置子窗口大小。

9、效果展示

注意:子窗口本身具有按键监听函数,按ESC可关闭子窗口,该功能嵌入MFC后依然有效,由于没有设置点击打开子窗口的次数,所以多次打开会重叠,无法关闭前面进程,需要手动关闭。

由于子窗口是创建后再被移动缩放到指定位置的,所以会有闪烁,可以先创建隐藏窗口-->移动到指定位置-->窗口显示,在创建子进程时将父窗口的位置和大小传给子进程,在子进程main函数中接收

  1. // TODO: 在此添加控件通知处理程序代码
  2. STARTUPINFO startupinfo;
  3. memset(&startupinfo, '\0', sizeof(startupinfo));
  4. startupinfo.cb = sizeof(startupinfo);
  5. //设置进程创建时不显示窗口
  6. // startupinfo.dwFlags = STARTF_USESHOWWINDOW; /*startf_useposition*/
  7. // startupinfo.wShowWindow = SW_HIDE;
  8.  
  9. char* CommandLine = new char[];
  10. memset(CommandLine, '\0', );
  11. // 主进程窗口句柄
  12. HWND mainWnd = AfxGetMainWnd()->m_hWnd;
  13. // 显示控件句柄
  14. HWND viewWnd = GetDlgItem(IDC_STATIC)->m_hWnd;
  15. CRect rect;
  16. GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);
  17. // 将参数写入命令行, 传递给马上要创建的进程
  18. sprintf(CommandLine, "%d %d %d %d", mainWnd, viewWnd, rect.Width(), rect.Height());
  19.  
  20. BOOL b = CreateProcess("..\\Debug\\OpenCVProc.exe", CommandLine, NULL, NULL, FALSE, NULL, NULL, NULL, &startupinfo, &pi);
  21. if (!b)
  22. MessageBox("创建进程失败!");

10、进程间通信

既然是通过子进程方式调用OpenGL程序,那么某些复杂交互应该需要进程间通信,关于C++进程间通信的方法比较多,常见的管道、共享内存等,作为一个新手,我这里选择了UDP通信方式,即在exe程序中加个线程函数,启动一个UDPServer,然后在MFC程序中需要的位置启动UDPClient,很简单就可以实现两者之间通信了。

需要注意的是UDP通信有数据大小限制(网络MTU,1500-20-8=1472),对于基本的传输应该是可以满足的,有需要可以改成TCP通信。

关于C++ UDP通信的Demo各种博客比较多,就不复制代码了。

11、更好的方式 dear imgui

参考文章:现代OpenGL教程(一):绘制三角形(imgui+OpenGL3.3)

imgui 是一个开源的GUI框架,见 github,对opengl,d3d图形接口,glfw,glut、sdl等界面库进行了一定的封装,可以看看其演示程序(直接点击下载)。

写这篇glfw嵌入MFC的文章的初衷是为了给glfw窗口添加菜单栏,以便实现更好的交互功能。因为以前一直不明白为什么GLFW库只有一个窗口,就不能像MFC,Qt等框架一样也带上其他界面元素,直到看到dear imageui的演示程序,忽然悟到原来菜单栏,按钮,输入框等界面元素其实与窗体本身没有必然关系(Window != UI,窗口是UI的基础),是后来绘制出来的。MFC,Qt框架通过其自带的UI设计器很方便的在窗口上可视化绘制这些UI元素。GLFW只是一个绘图板,没有UI设计器,就需要自己绘制元素,写事件交互。而dear imgui就是再窗口的基础上用图形接口做了这些事情。下面是其demo演示:

这个OpenGL 3 + GLFW的演示程序只有623K。没有任dll依赖。里面有各种常用界面元素的演示。

总结:如果是更侧重于图形渲染的程序,那么这种方式可能是更好的选择,满足轻量级、性能、跨平台。如果是侧重于一般的条条框框,数据展示,Qt和MFC显然更快速方便。

GLFW + MFC 可能更适合将 GLFW 程序作为 MFC/Qt程序的插件使用的场景,而我的初衷是更好的使用GLFW,所以。。。放弃这种做法,就当留个踩坑记录。

MFC单文档视图中嵌入GLFW窗口的更多相关文章

  1. MFC单文档视图程序简介

    在视图应用程序中,应用程序的数据由文档对象代表,数据的视图由视图对象代表.MFC的Cdocument类是文档对象的基类,Cview类是视图对象的基类.应用程序的主窗口,其操作功能在MFC的Cframe ...

  2. MFC单文档视图拆分窗口和相关链接

    第一步:准备2个视图类(如CTViewOne, CTViewTwo) 第二步:在CMainFrame类的头文件中添加数据成员变量: //MainFrm.h protected: CSplitterWn ...

  3. DotNetBar中dotNetBarManager设置窗口 实现单文档视图界面

    本人想设计一个但文档视图的界面: |--------------------------------------------------------------- |   Ribbon Bar |-- ...

  4. MFC单文档

    一.创建并运行MFC单文档程序 1.创建单文档程序 这里使用的是VS2017.首先,打开VS2017,选择文件->新建->项目,然后选择Visual C++ -> MFC /ATL& ...

  5. MFC单文档框架分析及执行流程(转)

    原文转自 https://blog.csdn.net/u011619422/article/details/40402705 首先来分析一下MFC单文档类的结构: 它包括如下几个类: CAboutDl ...

  6. VC-基础:MFC单文档程序架构解析

    MFC单文档程序架构解析 这里我以科院杨老师的单文档程序来分析一下MFC单文档的程序架构,纯属个人见解,不当之处烦请指教! 首先我们了解到的是 图(一) theApp 是唯一一个在程序形成的时候就存在 ...

  7. MFC单文档程序架构解析

    MFC单文档程序架构解析 MFC单文档程序架构解析 这里我以科院杨老师的单文档程序来分析一下MFC单文档的程序架构,纯属个人见解,不当之处烦请指教! 首先我们了解到的是 图(一) theApp 是唯一 ...

  8. VC++ MFC单文档应用程序SDI下调用glGenBuffersARB(1, &pbo)方法编译通过但执行时出错原因分析及解决办法:glewInit()初始化的错误

    1.问题症状 在VC++环境下,利用MFC单文档应用程序SDI下开发OpenGL程序,当调用glGenBuffersARB(1, &pbo)方法编译通过但执行时出错,出错代码如下: OpenG ...

  9. MFC单文档程序结构

    MFC单文档程序结构三方面: Doc MainFrame View

随机推荐

  1. PTA-德州扑克 题解

    于2020/02/24记录. 德州扑克属实是个带难题.本题解简单易懂,命名合理,应该比较好理解. 题目如下: 最近,阿夸迷于德州扑克.所以她找到了很多人和她一起玩.由于人数众多,阿夸必须更改游戏规则: ...

  2. Win10下安装tensorflow详细过程

    首先声明几点: 安装tensorflow是基于Python的,并且需要从Anaconda仓库中下载. 所以我们的步骤是:先下载Anaconda,再在Anaconda中安装一个Python,(你的电脑里 ...

  3. 858. Prim算法求最小生成树(模板)

    给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数. 求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible. 给定一张边带权的无向图G=(V, E),其中V表示 ...

  4. 4 Values whose Sum is 0 UVA 1152

    题目链接:https://vjudge.net/problem/UVA-1152 这题题意就是在四个集合内.每个集合分别里挑一个数a,b,c,d,求a+b+c+d=0有多少种选法. 暴力的话就是四重循 ...

  5. 专项:Vuejs面试题集合

    参考网络资源:https://segmentfault.com/a/1190000012315822 1.active-class是哪个组件的属性? 答:active-class是vue-router ...

  6. Gin_中间件

    gin可以构建中间件,但它只对注册过的路由函数起作用 对于分组路由,嵌套使用中间件,可以限定中间件的作用范围 中间件分为全局中间件,单个路由中间件和群组中间件 gin中间件必须是一个 gin.Hand ...

  7. ArcGis Server manager 忘记用户名和密码

    ArcGIS 10.1及以后版本重置Server Manager账户密码:(1)找到arcgis server的安装目录,目录指向\ArcGIS\Server\tools\passwordreset文 ...

  8. 解决lucene更新删除无效的问题

    个人博客 地址:http://www.wenhaofan.com/article/20180921233809 问题描述 在使用deleteDocuments,updateDocument方法根据id ...

  9. ES6标准入门(第三版).pdf----推荐指数⭐⭐⭐⭐⭐

    链接: https://pan.baidu.com/s/13RHsyTMNx7s1oMqQeYCm3Q 提取码: ikg3 -------------------------------------- ...

  10. JS图片轮换

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...