C# WPF调用 QT窗口

WPF 程序内嵌 QT 窗体

1、目标:将QT控件(Qwiget)(或则基于QWiget的控件)(或则任何第三方C++控件)封装为WPF可调用的用户控件。简单来说就是WPF程序调用QT窗体控件。

本人需要使用3D控件显示一些3D点云等功能。但是又找不到好的兼容WPF的控件(大点云和效率原因)。最后选用CloudConpare作为点云显示控件(该控件基于QT的QWiget)。
目前网络上这方面的内容并不是太多,而且多数还都是雷同。
个人感觉 C# 跨平台并不是太友好(或则我水平不够),特别是3D方面。

本人对QT不是很熟悉,但是CloudCompare是基于QT写的界面,所以没办法只能慢慢摸索,好在以前有MFC编程经验,对C++还有点积累,过程中关于配置方面的问题我尽量详细说明(对于C#编程者来说还是挺恶心的,QT的配置和CloudCompare的配置挺麻烦的)。

实现过程如下(可能需要稍微有点基础才能看懂,我认为有意思的会写多点 ,写的不是太系统),可能有点乱,有点啰嗦,想到哪里写到哪,见谅。

心路:
一开始查询资料想通过中间库 CLR链接C#和C++,简单demo也测试OK,后期碰到控件句柄传递问题调试也比较麻烦,遂放弃(现在想来应该也是可以实现的)。
最终直接使用 C形式函数导出 的形式来创建DLL,也挺方便的。专业点叫P/Invoke(推荐大家一本书《精通.NET互操作:P/Invoke,C++Interop和COM Interop》)。

大概逻辑:WPF控件句柄–>传递给C++生成的QT库–>动态库根据传递过来的句柄创建一个QWiget–>根据这个QWiget生成一个GLWindow(真实的3D显示窗体)–>C#保存这个指针方便下次访问。
数据传逻辑:主要是C#这边发给C++ Dll数据,主要涉及到托管和非托管资源的问题、字节对齐等。

实现过程中可能有几个主要问题:
1、C#和C++混合编程;
2、QT程序(QT事件循环QApplication.exec())如何封装为Dll;
3、QT的UI线程和WPF的UI线程冲突问题(目前本人也没完全弄懂,欢迎沟通);

工具:VS2019(Qt Visual Studio Tool 2.7.0)、QT5.14.2、CloudCompare2.6.12源码。。。。

上代码:
1、创建一个VS-QT的动态库。
C# WPF调用 QT窗口_第1张图片选择Qtwidgets application,然后修改配置生成为dll。网上资料很多,不详细描述(大概流程:修改输出类型为dll,将没必要的文件删除即可(Main)(所有文件都可以删除,然后新增一个QtWidgets class也行))。如下:
C# WPF调用 QT窗口_第2张图片

我们这里界面就不需要设置(因为我们不是直接用QT界面),只需要一个空白的Qwidget。(同志们只需要用Qwiget就可以在这里绘制了)
右键 编译这个.ui文件。会生成一个ui_xxxx.h文件。添加这个文件到项目中,(可能在生成目录的uic文件夹下,如有必要还需要将这个文件路径加到包含目录中。若是不添加可能会生成失败)。
首次生成失败很正常,各种QT环境问题。不要心急。
C# WPF调用 QT窗口_第3张图片
这里我们就获得了一个带QT的界面的C++库。
2、WPF中引用它:
若是你们用的是Winform,那就比较方便了,直接拿到控件句柄(我这里使用的panel控件)传递给动态库就行了。
若是你用的是WPF,会麻烦一点(因为WPF控件获取不到“控件”句柄),这里在下试了很多,获得的都是窗体句柄。所以只能使用WindowsFormsHost。上代码
C# WPF调用 QT窗口_第4张图片
这里是本人在WPF创建了一个用户控件的xaml文件。上代码

public partial class Window3DControl : UserControl
    {
        public Window3DControl()
        {
            InitializeComponent();
        }

        /// 
        /// 3D显示 窗口
        /// 
        public Window3DControl_Net Control3D = new Window3DControl_Net();


        /// 
        /// 创建真实的3D窗体,GlWindow
        /// 
        /// 
        public bool Create3DWindow()
        {
            int res = Control3D.Creat3DWindow_Net((long)Panel3D.Handle, Panel3D.Width, Panel3D.Height);

            return res == 0;
        }

        /// 
        /// 控件发生变化
        /// 
        /// 
        /// 
        private void Window3DControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if(Control3D.IsInitWindow)
            {
                int width = (int)(WindowForm3D.ActualWidth * 1.25);
                int Heigth = (int)(WindowForm3D.ActualHeight * 1.25);
                int res = Control3D.WindowSizeChanged_Net(width, Heigth);
            }
        }

        /// 
        /// 3D窗体切换
        /// 
        public void ChangeWindowModel()
        {
            if(WindowForm3D.Visibility == Visibility.Visible)
            {
                WindowForm3D.Visibility = Visibility.Collapsed;
                Button2D.Visibility = Visibility.Visible;
            }
            else
            {
                WindowForm3D.Visibility = Visibility.Visible;
                Button2D.Visibility = Visibility.Collapsed;
            }
        }

    }

上面是用户控件的代码部分。也很简单只有几个简单功能。其中Control3D对象是我封装的一个调用动态库的接口类(只是封装了一层),功能上直接调用C形式的函数接口也是可以的。上代码
C# WPF调用 QT窗口_第5张图片
这里是封装的3D对象的部分接口,C#这边就很简单了,都封装成用户控件了,那不是想怎么玩怎么玩。(关于C#定义函数接口就不描述了,C++那边导出函数接口也不描述了)。上代码
C# WPF调用 QT窗口_第6张图片
还有一点很重要(我感觉)如何显示QT窗口正好覆盖WPF中句柄对应的控件(可能会碰到直接显示QT窗口在主界面的左上角)。有两步很重要,通过如上图,可以看到我们将控件句柄和对应的尺寸传递过去,并且将生成的QT窗口的实例对应的指针传回来,为了下次能够直接拿到这个QT窗口。可以看下我的创建窗口函数是如何实现的,上代码
C# WPF调用 QT窗口_第7张图片
图中标记部分,很重要 。(忘记从哪个大神哪里偷学来的)
另外为了C# 那边能拿到C++这边的对象指针。使用了二级指针概念(这个不太好解释,要自己细细理解,)。
还有一个问题QT事件循环,(若是你调用过QT的dll,可能就会发现这个问题),我这边解决办法就是,在程序开始(你认为的合适的地方)调用InitQTEnvironment接口,上代码。

#include "qmfcapp.h"
int InitQtEnvironment()
{
	try
	{
		//if (QApplication::instance() == NULL)
		//{
		//	int argc = 0;
		//	g_app = new QApplication(argc, NULL);
		//	//QWidget* GroudWidget = new QWidget(NULL);
		//	//GroudWidget->setGeometry(0, 0,0, 1);//设置widget的大小
		//	//GroudWidget->showMinimized();
		//	//g_app->exec();
		//}
		if (QApplication::instance() == NULL)
		{
			return  QMfcApp::pluginInstance();
		}
		return 0;
	}
	catch(QException ex )
	{
		printf("Init Error");

		printf(ex.what());
		return -1;
	}
	
}

两种方式都行,目的是创建出来QApplication这个静态对象即可。(其中QMfcApp是偷学某个大佬)。
OK,其实到这里整个流程已经基本打通了,路走通了,其余的都好办了。
关于数据传递问题,这里也说一下。C#和C++的基础数据类型在内存中占位不尽相同,有兴趣可以看下对照表(网上一大堆)。
若是使用CLR包装C++dll,可以解决类型不一致问题(个人感觉CLR就是一次形参类型转换而已,可能是我太浅薄了)。
若是我们这种直接调用C形式的函数接口。需要传递参数需要注意几点。
1、基础类型也需要注意内存占位是否一致。
2、结构体类型,需要考虑字节对齐和托管资源问题。思路:两边定义相同类型的结构体。直接传递结构体指针(首地址)本质上还是内存要一样,不能乱。先上代码(C++)
C# WPF调用 QT窗口_第8张图片
C#中数据结构
C# WPF调用 QT窗口_第9张图片
仔细对比(字节对齐,还要考虑编译器可能优化)。
数组必须确定传递长度(内存分配才能连续),并且C#这边申请的内存必须是非托管内存(使用Marsh申请)。关于C++传递数据到C#,上文也提到了二级指针,基础类型或确定内存长度的可以直接传递,不确定的对象使用二级指针。关于内存释放问题。大家可以网上找下,有几种常用方法,此处不赘述。

按照惯例,上效果图。
C# WPF调用 QT窗口_第10张图片
遗留问题:
玩过WPF的都知道,WPF有一个隐藏的UI线程,我们的属性绑定控件和刷线界面都是
这个线程来更新的。QT也有一个类似的界面线程。因为我这边主程序是WPF,QWiget只是作为一个控件来用的。
目前发现他们两个界面线程会出现抢资源情况(我也是猜的,因为出现WPF的源已经改变了但是目标控件上没有刷新(及时刷新)的现象)。各位大佬若有这方面的见解,请一定联系我。抱拳!!!

写的不妥的地方,请大佬们不吝赐教,再次抱拳!!!!

你可能感兴趣的:(WPF,QT,混合编程,wpf,qt,c#,c++)