Qt大型工程開發技術選型Part3:Qt呼叫C#編寫的COM元件例項

軒先生。發表於2022-12-22

Qt大型工程開發技術選型Part3:Qt呼叫C#編寫的COM元件例項以及錯誤總結

ok,前面鋪墊了那麼多,現在來寫一個開發例項,我會把其中隱藏的坑和陷阱簡單談談,並在文章最後總結。

不願意看長篇大論的可以直接看例項:CS_COM_Build

廢話不多說直接起步。

先說場景,我這邊是一個C#的DLL,然後讓一個COM元件去載入這個DLL,然後再讓Qt去呼叫這個C#的COM元件,也就是說有三個工程,如圖所示:

image

其中FrameWork是一個窗體的DLL,如下圖左邊所示,右邊是QtController的窗體,MiddleCOM則是充當了一箇中介軟體,用於為普通的C#DLL提供COM服務。

image

如圖所示,兩個窗體之間可以進行互動,其中Qt應用程式是主程式,而C#的窗體程式則是以COM元件形式釋出的。

一、寫一個窗體

ok話不多說,先來寫一個窗體:

using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace FrameWork
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load(object sender, EventArgs e)
		{

		}
		//宣告一個委託,可以向外部傳送訊息
		public delegate void SendMessageOutEventHandler(System.String strValue);
		public event SendMessageOutEventHandler SendMessageOut;
		
		private void button1_Click(object sender, EventArgs e)
		{
			this.SendMessageOut(this.textBox1.Text);
		}
		//接收訊息,展示到窗體上
		public void getMessage(System.String strValue)
		{
			this.richTextBox1.AppendText(strValue);
		}
	}
}

image

二、寫一個COM的中介軟體

現在我們來做套殼的COM元件。先新增一個C#的類庫(這不會也要教吧)

image
AssemblyInfo.cs中,講這個[assembly:ComVisuble(false)] 改為true

image

右鍵這個COM工程,點選屬性,找到為COM互操作註冊

如果沒有這一步,可能會導致Qt在呼叫的時候彈出報錯提示CoCreateInstance failure(系統在找不到指定檔案。),原因是如果沒有互操作註冊,在登錄檔中你去找到你的這個類,你會發現少了幾行,比如BaseCode,就會導致COM在註冊的時候找不到實際的程式碼檔案,無法找到DLL檔案進行載入。

image

然後我們來寫一下這個COM匯出類,注意事項都在程式碼內,可以自己看看

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace MiddleCOM
{

	//Author:Leventure
	//DateTime:2022.12.22
	//Description:一個C# COM元件例項

	//注:C#中的類,無論是方法還是事件,都需要透過介面的形式向外公佈,方法介面需要透過方法介面去提供服務,如果只是類內提供的函式或者事件,在外部可能無法正常使用。
	//注2:匯出介面可以不需要設定ComVisible(true),這樣可以使得匯出的介面更加清晰
	//注3:這個Guid是唯一的,可以使用VS提供的Guid生成工具生成

	//方法介面,向外提供方法,注意[InterfaceType(ComInterfaceType.InterfaceIsDual)]的宣告這樣的介面是雙向的
	[Guid("7EEDF2D8-836C-4294-90A0-7A144ADC93F9")]
	[InterfaceType(ComInterfaceType.InterfaceIsDual)]
	public interface IOutClass
	{
		[DispId(1)]
		void getMessage(System.String strValue);
	}


	//事件介面,注意[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]這代表這個是用作事件處理
	[Guid("7FE32A1D-F239-45ad-8188-89738C6EDB6F")]
	[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
	public interface IOutClass_Event
	{
		[DispId(11)]
		void SendMessageOut(System.String strValue);
	}


	[Guid("76BBA445-7554-4308-8487-322BAE955527")]
	[ClassInterface(ClassInterfaceType.None)] // 指示不為類生成類介面。如果未顯式實現任何介面,則該類將只能透過 IDispatch 介面提供後期繫結訪問。這是 System.Runtime.InteropServices.ClassInterfaceAttribute
											  //     的推薦設定。要透過由類顯式實現的介面來公開功能,唯一的方法是使用 ClassInterfaceType.None。
	[ComDefaultInterface(typeof(IOutClass))]        //     以指定的 System.Type 物件作為向 COM 公開的預設介面初始化 System.Runtime.InteropServices.ComDefaultInterfaceAttribute
													//     類的新例項。
	[ComSourceInterfaces(typeof(IOutClass_Event))]  //使用要用作源介面的型別初始化 System.Runtime.InteropServices.ComSourceInterfacesAttribute 類的新例項。
	[ComVisible(true)] //提供COM的可訪問性
	[ProgId("IOutClass")] //給這個匯出類一個名稱
	public class OutClass : IOutClass
	{
		private FrameWork.Form1 form1 = null;
		public OutClass()
		{
			if(form1 == null)
			{
				this.form1 = new FrameWork.Form1();
				this.form1.SendMessageOut += new FrameWork.Form1.SendMessageOutEventHandler(this.SendMessageReceived);
				this.form1.Show();
			}
		}
		//提供方法向C#DLL傳送訊息
		public void getMessage(System.String strValue)
		{
			if(this.form1 != null)
			{
				this.form1.getMessage(strValue);
			}
		}
		//這個是從方法類中繼承來的向外傳送訊息事件
		public delegate void SendMessageEventHandler(System.String strValue);
		public event SendMessageEventHandler SendMessageOut;
		//從C#DLL中傳來的訊息,轉發給COM伺服器
		private void SendMessageReceived(System.String strValue)
		{
			//向外傳送訊息
			SendMessageOut(strValue);
		}
	}
}

ok,到這裡我們的COM元件之旅幾乎就已經完成了。編譯這個DLL之後我們需要將其註冊到我們的系統中去。這裡.net的DLL和非託管的DLL的註冊方式不一樣。非託管的DLL註冊可能是透過直接在cmd中輸入regsvr32 xxx.dll進行註冊,但是.net的DLL需要透過它.net自己的工具進行的註冊,我們可以在選單欄中找到vs的開發者工具,如圖所示

image

(我這裡是用的VS2019開發,其實這裡用的只是regasm.exe這個工具,用哪個版本的無所謂)

呼叫命令如下:

image

注:如果你用的是系統提供的註冊工具regsvr32註冊的話,會提示你沒有DllServerRegister入口點,請檢查是否是dll或者ocx,這個是因為.net框架提供的COM元件是沒有給定這兩個東西的,所以需要用他們自己的工具!

OK,這個時候應該就已經註冊完成了,為了檢驗成果我們可以去Windows系統登錄檔中檢視,比如我們這裡宣告的匯出類的名稱為[ProgId("IOutClass")],就在登錄檔內查詢 IOutClass,或者查詢匯出類對應的Guid,應該就能查詢到對應的內容

比如這個路徑下
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID{76BBA445-7554-4308-8487-322BAE955527}\ProgId

image
如果出現了這一條,就說明你的註冊成功了

三、編寫Qt程式來呼叫COM元件

這部分內容比較簡單,主要是提一下報錯:

直接上介面和程式碼吧,這部分沒有什麼需要特別注意的:

image

#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_QtController.h"
#include "qaxobject.h"
#include "qfile.h"
#include "qtextstream.h"
#include "qdebug.h"
class QtController : public QMainWindow
{
	Q_OBJECT

public:
	//建構函式里呼叫了這個Init,懶得寫了,將就著看吧
	QtController(QWidget *parent = nullptr);
	~QtController();
	QAxObject ax_test;
	void Init() {
		this->ax_test.setControl("IOutClass");
		//獲取介面文件
		QString interfaces = ax_test.generateDocumentation();
		QFile docs("AX_Interfaces.html");
		docs.open(QIODevice::ReadWrite | QIODevice::Text);
		QTextStream TS(&docs);
		TS << interfaces << endl;

		qDebug() << QObject::connect(&this->ax_test, SIGNAL(SendMessageOut(QString)), this, SLOT(getMessageFromCS(QString)));
	}

private slots:
	void on_pushButton_clicked() {
		this->ax_test.dynamicCall("getMessage(QString)", this->ui.lineEdit->text());
	}
	void getMessageFromCS(QString strValue) {

			this->ui.textEdit->append(strValue);

	}
private:
	Ui::QtControllerClass ui;
};

這樣基本上就能保證呼叫了,這裡提幾個BUG,也是我們困擾了很久的地方:(憑藉記憶寫的,不一定全對哈)

四、查漏補缺、總結:

1.呼叫時提示 No Such Property、或者如圖所示:
image

答:就我們目前的經驗來看,有可能是你的COM元件中提供的屬性有問題,你依賴了其他的DLL,但是這個被依賴的DLL它的依賴可能沒被載入,也就是說缺少了部分依賴,需要你自己補全所有依賴。

2.呼叫時報錯UnKnownError,如圖:
image

答:這有可能是因為被呼叫的COM元件的位數大於呼叫方的位數導致的,比如32位的應用程式呼叫64位的程式,可能會導致UnKnown Error。

3.報錯提示CoCreateInstance failure(系統找不到指定的檔案。)

image

答:有可能是由於你在編寫COM元件的時候沒有在COM元件的屬性中勾選上COM互操作選項,導致登錄檔內BaseCode行未註冊成功。

4.註冊DLL時,顯示找不到入口點DllRegisterServer
image

答:需要用.net提供的COM註冊服務軟體regasm,具體使用方法見上方。

5.註冊的C# COM 元件,為什麼Event變成了 add_xxx(IDispatch *value) 和remove_xxx(IDispatch *value) 了?(注:xxx是事件的名稱)

答:事件需要透過繼承事件介面來繼承暴露,函式需要透過函式介面來繼承暴露,這樣會變成另外一種形式,我沒用過,我不能確保能不能用。

還有什麼問題可以提問,我看到了就會回答,如果需要私聊可以聯絡我的Github提交issue

相關文章