开始学习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函数进行封装

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

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

VS打印输出

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

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

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

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

4、进程关闭函数

/*
* 关闭进程
*/
BOOL CloseProcess() {
return TerminateProcess(handle, );
}

5、入口函数

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

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

事件处理函数

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

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

6、全局变量与函数说明

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

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

7、关键的窗口销毁事件

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

/*
* 窗口销毁
*/
void CMFCGlfwTestView::OnDestroy() {
// TODO: 在此处添加消息处理程序代码
if (CloseProcess() != ) {
TRACE("child process has been specified ... \n");
}
// 释放客户区上下文对象
if (m_pDC) {
delete m_pDC;
}
TRACE("m_pDC has been free ... \n");
CView::OnDestroy();
}

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

8、子窗口大小改变

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

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

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

9、效果展示

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

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

    // TODO:  在此添加控件通知处理程序代码
STARTUPINFO startupinfo;
memset(&startupinfo, '\0', sizeof(startupinfo));
startupinfo.cb = sizeof(startupinfo);
//设置进程创建时不显示窗口
// startupinfo.dwFlags = STARTF_USESHOWWINDOW; /*startf_useposition*/
// startupinfo.wShowWindow = SW_HIDE; char* CommandLine = new char[];
memset(CommandLine, '\0', );
// 主进程窗口句柄
HWND mainWnd = AfxGetMainWnd()->m_hWnd;
// 显示控件句柄
HWND viewWnd = GetDlgItem(IDC_STATIC)->m_hWnd;
CRect rect;
GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);
// 将参数写入命令行, 传递给马上要创建的进程
sprintf(CommandLine, "%d %d %d %d", mainWnd, viewWnd, rect.Width(), rect.Height()); BOOL b = CreateProcess("..\\Debug\\OpenCVProc.exe", CommandLine, NULL, NULL, FALSE, NULL, NULL, NULL, &startupinfo, &pi);
if (!b)
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. SQLServer 数据库索引碎片

    --改成当前库 use DB_Name --创建变量 指定要查看的表 declare @table_id int set @table_id=object_id('TableName') --执行 d ...

  2. UML 工具 Dia

    linux下一款不错的流程图工具:dia. dia是一款非常优秀的制图工具,可以用于绘制 类图,流程图等等 安装此工具: 1.打开终端(快捷键:ctrl+alt+t). 2.输入命令:$sudo ap ...

  3. 375. 猜数字大小 II

    题目: 链接:https://leetcode-cn.com/problems/guess-number-higher-or-lower-ii/ 我们正在玩一个猜数游戏,游戏规则如下: 我从 1 到 ...

  4. 一个扩展搜索API的优化过程

    概述 API 是一个服务的门面,就像衣装是人的形象一样. 优雅的 API 设计,能让业务方使用起来倍儿爽,提升开发效率,降低维护成本:糟糕的 API 设计,则让业务方遭心,陷入混沌. 本文将展示一个扩 ...

  5. js秒数倒计时

    代码 /** * 调用回调函数 * @param callback 回调函数体 * @param args 参数 */ execCallback: function (callback, args) ...

  6. ECMAScript基本语法——⑤运算符 逻辑运算符

    &&与,会短路:左边为false右边就不参与运算||或,会短路:左边为true右边就不参与运算!非, 注意:在JavaScript中,如果运算数不是运算符要求的类型,那么JavaScr ...

  7. Leetcode Week4 Find Minimum in Rotated Sorted Array II

    Question Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforeha ...

  8. Gin_中间件

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

  9. [CQOI2007] 余数求和 - 整除分块

    \(\sum_{i=1}^n\;k\;mod\;i\) Solution \(\sum_{i=1}^n\;k\;mod\;i\\=\sum_{i=1}^n(k-i\lfloor{\frac{k}{i} ...

  10. shell登录 脚本 expect

    作用 工作中,我们运行命令.脚本或程序时,这些命令.脚本或程序都需要从终端输入某些继续运行的指令,而这些输入都需要人为的手工进行. 利用expect,则可以根据程序的提示,模拟标准输入提供给程序,从而 ...