cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程

OpenGL开发记录 专栏收录该内容
2 篇文章 0 订阅

MFC虽然很老, 不美观, 不跨平台, 但是在Windows系统中, 利用MFC做功能验证的界面, 还是很快很方便的. 因为它老, 所以有很多解决方案可以利用, 因为它是MS提供的界面库, 所以在Windows上很容易实现, 并且和Windows系统结合很紧密. 比如说, 窗口消息等, 在MFC中是很方便实现的. 基于上面的种种原因, 利用MFC作为功能验证的一个”壳” 是很好的工具.

当然, 难免就会遇到不少工程问题. 例如利用glfwCreateWindow创建出来的窗口, 怎么让它嵌入到MFC中. 以及经常使用OpenCV的朋友, 利用cv::namedWindow函数, 创建的图像/视频显示窗口也是弹出式的, 怎么让它嵌入在MFC中的某个位置. 以及, 有时候想创建一个多进程程序, 让创建的进程嵌入在MFC中运行等.

1. 准备工作

首先, 你首先得有 glfw 的源码 , OpenCV库, 以及一个Visual Studio(我使用的是VS2013). 另外, 在VS13中, MFC已经抛弃了多字节字符集, 如果在MFC工程中想要使用多字节字符集, 需要下载一个多字节字符集支持包. 下载好了, 安装即可.

然后, 创建一个基于对话框的MFC工程, 创建好功能后, 编辑界面, 简单的添加一个控件就行, 我添加的是Picture Control, 在工具箱里面拖进来调整大小就好. 再给 <开始> 按键添加一个按键响应函数. 双击<开始> 按键就行了. 界面示图如下:

 


UI


最后, 在工程里面配置一下OpenCV相关的包含目录和库目录以及依赖项.

 

2. OpenCV窗口嵌入MFC

对当前我要分享问题感兴趣的朋友, 应该不会对OpenCV的配置有问题吧. 如果有问题的话, 搜索一下, CSDN上面也有很多人对相关问题由详细的描述.

在前面添加的<开始>按键响应函数中, 添加入下述代码.

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

<code class="language-cpp hljs ">#include <opencv2\opencv.hpp>

void CaboutMFCDlg::OnBnClickedButton1()

{

    // TODO:  在此添加控件通知处理程序代码

    CRect rect;

    // IDC_STATIC是刚刚在界面中加入的Picture Control的ID

    GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);

 

    // 创建cv窗口并重置窗口大小

    cv::namedWindow("view", cv::WINDOW_NORMAL);

    cv::resizeWindow("view", rect.Width(), rect.Height());

 

    // 设置依附关系, 将cv窗口嵌入MFC主要是下述代码起作用了.

    HWND hWnd = (HWND)cvGetWindowHandle("view");

    HWND hParent = ::GetParent(hWnd);

    ::SetParent(hWnd, GetDlgItem(IDC_STATIC)->m_hWnd);

    ::ShowWindow(hParent, SW_HIDE);

 

    // 循环读取文件夹中的图片并显示. 仅仅作为功能验证而已.

    cv::Mat img;

    int index = 0;

    char filename[128] = { 0 };

    while (true) {

        sprintf_s(filename, "..\\DragonBaby\\0%03d.jpg", ++index);

        img = cv::imread(filename);

        if ((img.cols <= 0) || (img.rows <= 0)) {

            break;

        }

        cv::imshow("view", img);

        cv::waitKey(30);

    }

 

    cv::destroyWindow("view");

}</opencv2\opencv.hpp></code>

其中真正关键的代码就六行, 别的都是一些可有可无的代码. 当然, 这只是一个简单的示例而已. 当你要使用OpenCV时, 肯定不单是为了这样循环查看图片而已. 但, 通过上面的示例可以给我们一个启发, 就是完全可以将OpenCV的处理进程与界面分离, 两者相互没有过多的影响. MFC只是作为一个”壳”用来展示而已. 因此, 可以将上述代码再进行完善一下.

在该解决方案下, 再创建一个命令行工程. 配置好OpenCV. 因为我们要使用命令行参数进行参数传递, 所以需要把工程的改为使用多字节字符集. 更改方式: 右击工程名–> 属性 –> 配置属性 –> 常规 –> 字符集, 选择使用多字节字符集.

好, 下面开始写代码, 整理如下:
首先还是改MFC中按键响应函数, 修改如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

<code class="language-cpp hljs ">PROCESS_INFORMATION pi;

void CaboutMFCDlg::OnBnClickedButton1()

{

    // 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[128];

    memset(CommandLine, '\0', 128);

    // 主进程窗口句柄

    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("创建进程失败!");

</code>

然后, 在新创建的命令行工程中, 添加下述代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

<code class="language-cpp hljs ">#include <opencv2\opencv.hpp>

#include <windows.h>

int _tmain(int argc, _TCHAR* argv[])

{

    int width = 0;

    int height = 0;

    HWND mainWnd = NULL;

    HWND viewWnd = NULL;

    char* commandline = GetCommandLine();

    // 从命令行中获取主进程传递来的参数

    sscanf(commandline, "%d %d %d %d", &mainWnd, &viewWnd, &width, &height);

 

    // 创建cv窗口并重置窗口大小

    cv::namedWindow("view", cv::WINDOW_NORMAL);

    cv::resizeWindow("view", width, height);

 

    // 设置依附关系, 将cv窗口嵌入MFC主要是下述代码起作用了.

    HWND hWnd = (HWND)cvGetWindowHandle("view");

    HWND hParent = ::GetParent(hWnd);

    ::SetParent(hWnd, viewWnd);

    ::ShowWindow(hParent, SW_HIDE);

 

    // 循环读取文件夹中的图片并显示. 仅仅作为功能验证而已.

    cv::Mat img;

    int index = 0;

    char filename[128] = { 0 };

    while (true) {

        sprintf_s(filename, "..\\DragonBaby\\0%03d.jpg", ++index);

        img = cv::imread(filename);

        if ((img.cols <= 0) || (img.rows <= 0)) {

            break;

        }

        cv::imshow("view", img);

        cv::waitKey(30);

    }

    cv::destroyWindow("view");

 

    return 0;

}</windows.h></opencv2\opencv.hpp></code>

分别编译, 然后就运行MFC程序, 点击开始, 效果如下:

效果1

作为调试用时, 命令行的调试信息输出是必不可少的. 所以难看的黑框就只有先忍着吧. 到最后展示阶段, 该黑框可以将按键响应函数中两行注释掉的代码打开注释即可让难看的黑框不再弹出来了. 最终结果如下所示:


结果


代码中需要说明的三点, 首先<喎�"/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPiwg1NrD/MHu0NCyzsr9tKu13cqxLCC0sL/avuSx+jxjb2RlPkhXTkQ8L2NvZGU+ysfX986qPGNvZGU+aW50PC9jb2RlPsC0tKu13bXELiC+38zlss68+zxhIGhyZWY9"https://msdn.microsoft.com/zh-cn/library/cc438768%28v=vs.71%29.aspx">MSDN该段说明, 可以知道, 在Win32中, HWND是32位的一个ID. 所以可以使用"%d"格式来进行传输. 其次, 在MFC的按键响应函数中, 有一个变量PROCESS_INFORMATION pi是作为全局变量放在函数体外面的. 原因是创建新进程之后, 难免会涉及到通信问题, 最简单的办法就是窗口消息. 通过该变量可以实现消息传递. 使用PostThreadMessage(pi.dwThreadId, WM_TEST, wParam, lParam)函数传递消息到新进程中, 其中WM_TEST是自定义的消息. 最后, 在命令行进程中, 命令行第一个参数, 是MFC进程的窗口句柄, 可以利用该句柄发送消息到MFC进程. 使用SendMessage(mainWnd, WM_TEST, wParam, lParam)函数.

 

3. GLFWwindow嵌入MFC

3.1 配置GLFW

之前在一个小项目中, 用到了TI所提供的DLP-ALC-LIGHTCRAFTER-SDK-2.0(简称DLP), 该SDK提供源码, 通过结构光projector + Point Grey摄像头进行扫描, 得到点云, 然后进行三维重建. 需要创建一个界面用于展示. 而在DLP中, 点云显示是将GLFWwindow进行了封装用于显示. 源码中, 用于创建窗口的函数是glfwCreateWindow(width, height, title.c_str(), NULL, NULL), 使用上述方法得不到理想的效果. 所以只能另辟蹊径. 很庆幸, Google到一个比较好的解决方案. 很感谢该博主, 成功的完成了预期的功能.

由于我原始项目错综复杂, 不利于直接呈现出该问题的解决. 所以下面我们一步一步的完成所需要的功能. 在前面的链接中下载glfw的源码, 在GitHub上面下载下来即可. 另外, 需要下载CMake, 并且假定你电脑已经安装了VS.

下载下来的glfw源码文件夹如下图所示:


glfw


下载好CMake之后, 安装. 在开始菜单能够找到 CMake(cmake-gui)的快捷方式. 打开CMake, 如下图所示:


cmake


在 “Where is the source code:” 之后选择你下载的glfw路径, 如我上图所示, 我的路径就是E:/glfw/glfw-master, 在E:/glfw目录下新建一个文件夹, 命名为glfw-build, 将该文件夹路径填入”Where to build the binaries:”, 然后点击 . 会出现下述选择窗口, 选择(当然, 我电脑安装的VS13, 所以选择该条目, 你对应选择你所安装的VS就好). 然后选择 . 然后将下图中BUILD_SHARED_LIBS勾选上, 再次点击:


configure


然后点击, 会提示 Generating done. 搞定之后, 打开E:/glfw/glfw-build后你会看到如下画面, 熟练的双击GLFW.sln就可以使用VS打开该工程了. VS打开之后, 爽快的按下F7. VS就开始工作了. 在E:\glfw\glfw-build\src\Debug路径下, 会看到生成的一些文件. 都是很熟悉的东西吧. lib文件以及dll文件. VS示图如下, 并按照图中选项找到simple示例:


VS


按照上图, 找到simple, 右击弹出下拉菜单, 依次选择<调试> –> <启动新实例>. 会得到下图展示的一个DEMO效果. 也许你会得到一个错误, 提示在E:/glfw/glfw-huild/src/Debug 里面看到的glfw3.dll文件找不到. 解决办法很简单, 将该文件复制到C:\Windows\System32中去, 或者将E:/glfw/glfw-huild/src/Debug 目录加入环境变量Path中. 第一种办法好像需要重启一次才行.


simple

 

3.2 修改代码

在simple工程中, 提供了源码, 打开simple.c可以看到其实现代码. 能够找到下述代码:

?

1

2

3

4

5

6

7

8

9

<code class="language-c hljs ">glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);

glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

 

window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);

if (!window)

{

    glfwTerminate();

    exit(EXIT_FAILURE);

}</code>

其中窗口的创建, 就是使用函数glfwCreateWindow. 在VS中, 找到glfwCreateWindow函数的定义位置, 是在 glfw3.h文件中, 新加入一个函数glfwCreateWindowEx声明, 如下:


define


在原本glfwCreateWindow函数的参数列表中新加入了参数int hParent. 新加入的参数, 本应该是HWND类型, 但该类型定义于Windows.h中, 本着尽可能少的改动代码, 以int代替了HWND类型, 具体原因类似于第二节中所述.

 

现在打开win32_platform.h文件, 找到其中struct _GLFWwindowWin32定义所在的位置, 新加入HWND handleParent, 用来保存父窗口的句柄作为参数传递给创建窗口的函数. 如下图所示:


新加入参数


修改好参数结构体之后, 现在定位glfwCreateWindow函数的定义, 定义于文件window.c中. 复制glfwCreateWindow函数的定义, 粘贴在glfwCreateWindow函数的定义的下方, 更改函数名为glfwCreateWindowEx并加入参数int hParent. 在该函数的实现中找到_glfwPlatformCreateWindow函数的调用地方, 在其前方加入下述代码:

 

?

1

<code class="language-c hljs ">window->win32.handleParent = hParent;</code>

效果如下:


传参


现在, 沿着_glfwPlatformCreateWindow函数的函数调用一直找到API CreateWindowExW函数的调用地方, 位于win32_window.c文件定义的static int createWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig)函数中被调用. 在 CreateWindowExW函数前加入下述代码, 并将CreateWindowExW函数的倒数第四个参数改成window->win32.handleParent.

 

?

1

2

3

4

<code class="language-cpp hljs ">if (NULL != window->win32.handleParent) {

    exStyle = 0;

    style = WS_CHILDWINDOW | (wndconfig->visible ? WS_VISIBLE : 0);

}</code>

截图如下:


code1


修改好了之后, 对代码进行编译, 还是运行simple示例进行验证. 仍然可以得到前面原始代码所展示的效果. 说明我们代码的修改没有对原本性能产生破坏.

 

3.3 效果验证

本来, 预想是如同前一个例子, 在MFC按键响应函数中通过CreateProcess调用已经编译好的simple.exe可执行程序, 完成界面的显示. 可是一直无法成功. 通过CreateProcess调用sample文件夹中任意示例均无法成功调用. 一直没有找到具体是为什么. 由于该步骤只是验证功能. 所以就通过另一个办法来完成验证.

首先, 在原始MFC界面中加入三个Edit Control, 重新编写<开始>按键响应函数, 代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<code class="language-cpp hljs ">void CaboutMFCDlg::OnBnClickedButton1()

{

    // TODO:  在此添加控件通知处理程序代码

 

    HWND viewWnd = GetDlgItem(IDC_STATIC)->m_hWnd;

    CRect rect;

    GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);

 

    CString str;

    str.Format("%d", viewWnd);

    GetDlgItem(IDC_EDIT1)->SetWindowText(str);

 

    str.Format("%d", rect.Width());

    GetDlgItem(IDC_EDIT2)->SetWindowText(str);

 

    str.Format("%d", rect.Height());

    GetDlgItem(IDC_EDIT3)->SetWindowText(str);

}</code>

点击<开始>, 获取用于显示的Picture Control的HWND, 以及长宽. 分别显示在三个Edit Control中. 然后在simple的代码中写死代码完成该功能(恕小弟无能, 当前只能用这样无奈的方式完成功能验证了). simple.c中的代码片段截取如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<code class="language-cpp hljs ">glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);

glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

 

// 将MFC中获取到的三个值分别替代这三个变量.

int viewWnd = 5114720; // Picture Control的HWND

int width = 536; // Picture Control的宽

int height = 294; // Picture Control的高

window = glfwCreateWindowEx(width, height, "Simple example", NULL, NULL, viewWnd);

// 注释掉原本创建窗口所调用的函数, 换作我们新增的创建窗口函数glfwCreateWindowEx

// window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);

if (!window)

{

    glfwTerminate();

    exit(EXIT_FAILURE);

}</code>

编译simple工程生成可执行文件, 然后按照上面图示的方式运行simple.可以看到如下结果.


这里写图片描述

 

4. 其他程序嵌入MFC

该话题, 也是无意间在网站上浏览到相关的资料, 感觉很有趣, 就试着做了一下, 现在也整理出来和大家分享一下. 原作者 在他的博客中描述了一些, 但是不够具体. 我在这儿更具体的描述一下. 另外, 在原作者描述的实现方式中, 主要是考虑所有功能均在一端实现, 被调用的exe完全不知道自己是被嵌入到MFC中在运行.

其中, 主要思想是, 调用CreateProcess后是可以得到被创建进程的信息, 其中就包括进程ID. 则可以通过枚举进程ID进而得到被创建进程的窗口句柄. 然后就可以对该进程的窗口进行上述内容中的SetParent操作了. 在示例代码中, 我是直接调用Windows自带的记事本进行演示.

为了方便展示, 此处直接使用全局变量. 定义了两个变量, 分别保存进程句柄以及进程窗口句柄. 定义如下:

?

1

2

<code class="language-cpp hljs ">HWND apphwnd;

HANDLE handle;</code>

然后, 定义创建进程的函数, 创建成功后利用进程ID枚举获取窗口句柄, 代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

<code class="language-cpp hljs ">// 回调函数, 枚举获取窗口句柄

int CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param)

{

    DWORD pID;

    DWORD TpID = GetWindowThreadProcessId(hwnd, &pID);

    if (TpID == (DWORD)param)

    {

        apphwnd = hwnd;

        return false;

    }

    return true;

}

// 第一个参数是被调用进程的路径, 第二格参数是需传入的参数列表

HANDLE StartNewProcess(LPCTSTR program, LPCTSTR args)

{

    HANDLE hProcess = NULL;

    PROCESS_INFORMATION processInfo;

    STARTUPINFO startupInfo;

    ::ZeroMemory(&startupInfo, sizeof(startupInfo));

    startupInfo.cb = sizeof(startupInfo);

    startupInfo.dwFlags = STARTF_USESHOWWINDOW;

    startupInfo.wShowWindow = SW_HIDE;

    if (::CreateProcess(program, (LPTSTR)args,

        NULL,  // process security

        NULL,  // thread security

        FALSE, // no inheritance

        0,     // no startup flags

        NULL,  // no special environment

        NULL,  // default startup directory

        &startupInfo,

        &processInfo))

    { /* success */

        Sleep(50);//wait for the window of exe application created

        ::EnumWindows(&EnumWindowsProc, processInfo.dwThreadId);

        hProcess = processInfo.hProcess;

    } /* success */

    return hProcess;//Return HANDLE of process.

}</code>

当然, 该进程的关闭也是需要定义相关的函数.

?

1

2

3

<code class="language-cpp hljs ">BOOL CloseProcess() {

    return TerminateProcess(handle, 0);

}</code>

现在, 我们再次重新编写<开始>按键响应函数, 代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

<code class="language-cpp hljs ">void CaboutMFCDlg::OnBnClickedButton1()

{

    // TODO:  在此添加控件通知处理程序代码

    handle = StartNewProcess("C:\\Windows\\notepad.exe", NULL);

 

    // CRect rect;

    // GetDlgItem(IDC_STATIC)->GetWindowRect(&rect);

    // ::MoveWindow(apphwnd, rect.left, rect.top, rect.Width(), rect.Height(), false);

    ::SetWindowLong(apphwnd, GWL_STYLE, WS_VISIBLE);

 

    HWND viewWnd = GetDlgItem(IDC_STATIC)->GetSafeHwnd();

    ::SetParent(apphwnd, viewWnd);

}</code>

代码很简单, 我就没有写注释了. 其中被注释掉的三行代码, 本来应该是完成将新建的进程移动到指定位置, 但是移动之后, 会出现错误. 也没有找到原因. 希望哪位朋友知道原因并成功解决了的话, 告诉我一声. 谢谢.

该内容几乎和上面给出原作者的示例一样. 感兴趣的朋友, 可以查看原作者的表述.

最后, 结果示例如下:


这里写图片描述

 

OK, 打完收工.

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

xrdeng

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值