【CAPE-OPEN】单元模块Heater开发简捷流程之一(不完全版)

前言

因为距离上一次开发已经过去了好几个月,中间因为重建博客和论坛的原因,同样也是因为在 《单元模块开发入门实例Heater(一)》 篇中最后遗留的bug因素,所以打算重新详细的讲解一下一个单元模块的构建。

第一次开发这个模块的时候都是24年2月份的时候了,是在同年10月份重新开始CAPE-OPEN(以下简称CO)的学习和开发,在11月份解决了无法计算和闪退的bug,发布了第一版BlockTest01,这一版其实已经和刚开始2月份的myBlockTest那一版有了很多的不同,担心很多人再开始看今天的这个Heater模块开发实例(二)的时候看不懂,所以本文开始直接从零详细的一步步讲解如何构建一个CO单元模块。

虽然说本文中代码是重新演示的,其中的原理或者接口用法,可以去回顾博客中的《热力学物性包开发篇》和《单元模块开发入门篇》,哪一个步骤有疑惑也可以去翻看这两篇讲解。

同时,如果有哪一步没跟上,或者不理解,也可以参考上面这三篇中的源码,地址如下:

备注:BlockTest01是最新的,没有bug的版本;myBlockTest是完全根据《单元模块开发入门篇》教程写的,有一点点小bug;myThermoTest是《热力学物性包开发篇》教程中的源码;

如果有需要,建议可以对比一下BlockTest01和myBlockTest就知道闪退和无法计算的那个bug如何解决得了,哦对了,值得一提的是,BlockTest01目前只支持AspenV11(我用这个版本测试的,你也可以试试v12/v14),不支持COFE,应该是使用的CO版本或者获取流股的时候使用了智能指针的关系;

哦对了,还有一件事,本文是根据B站蔡老师(ID:bcbooo)的讲解来写的,所以中间可能会夹杂着一些bug和解决bug的步骤,会显得很繁琐,但是对于新手来说跟着写一遍、改一遍收获还是有的,最起码就知道哪里应该怎么写。

创建项目

环境:VS2022,Win11LTSC

打开VS2022,点击创建新项目,创建ATL项目

命名为HeaterExample,点击创建:

名称可以自己定义。

这里也可以不选择那个将解决方案和项目放在同一目录中;

默认动态链接库即可,点击确定:

HeaterExampleOperation

为了防止idl文件路径报错问题,先点击一下全部保存:

添加第一个ATL简单对象,右键项目解决方案资源管理器中的项目名称,点击添加,新建项:

选择 ATLATL简单对象,命名为 HeaterExampleOperation,点击添加:

ATL简单对象的 属性默认 即可,点击完成:

如果这里报错idl文件路径问题,建议直接删除项目,然后重新建立,创建项目之后先点击全部保存按钮,然后再进行添加,这是一个Visual Studio的bug,如果不想重建项目,可以参考:VS2022社区版新建ATL简单对象报错idl文件路径问题热力学物性包开发入门2 | OrdisBlog 解决。

或者(暂时没想到解决办法)

添加完成后如图所示:

不要做任何编辑,默认即可,点击一下全部保存,点击最上方的工具栏的生成,生成解决方案:

如果有如下报错:

Microsoft.CppCommon.targets(2412,5): error MSB8011: 未能注册输出。请尝试启用“逐用户重定向”,或者使用提升的权限从命令提示符处注册该组件。

点击顶部工具栏项目,HeaterExample 项目属性:

将“ 逐用户重定向 ”选项设置为“ ”,点击应用:

然后点击顶部工具栏,生成,重新生成解决方案,编译成功如下提示:

ICapeUnit

在CAPE-OPEN官网下载混合器的一个代码示例,解压后备用,链接如下:

https://colan.repositoryhosting.com/trac/colan_examples/downloads

在解压之后的源码文件夹 CPPSource 中,找到 CAPE-OPENv1-1-0.tlb​ 文件,将其复制到 本项目根目录 中备用,

点击一下 全部保存,切换到 类视图

右键 CHeaterExampleOperation​ ,点击添加,实现接口:

选择文件类型,选择 刚才复制到项目根目录 下的 CAPE-OPENv1-1-0.tlb​ 文件,找到 ICapeUnit​ 接口,点击添加:

这里如果报错了 pch.cpp​ 文件问题,参考: 热力学物性包开发入门2 | OrdisBlog 解决。

或者(暂时没有想到更好的解决办法)


分割线分割线分割线

这里插入一个题外话,如果你下载了我的源码,或者是你自己的项目在另一台电脑上打开,发现编译失败的问题,那么大概率是 pch.h​ 中的CAPE-OPEN接口预编译文件路径有问题,需要修改为新电脑上的 绝对路径

分割线分割线分割线


如何判断导入是否成功,切换回解决资源管理器视图,选择对应类的源文件(假设我们刚才对类 CHeaterExampleOperation​ 进行了添加,那么对应的源文件就是 HeaterExampleOperation.h​ ),查看是否添加进来了对应的接口以及接口实现方法:

点击一下 全部保存,点击重新生成解决方案,然后会发现报错了,编译不通过,这并不是说导入接口的方式是错误的,看过物性包开发入门篇的,应该都知道,这是CO标准自己的定义问题,所以还是需要从CO标准提供的示例中(上文下载的示例代码),找和现在导入的接口方法有什么不同,就是错误之处,

在解压之后的源码文件夹 CPPSource 中,找到 CPPMixerSplitterUnitOperation.h​ 这个文件,往下翻,找到 ICapeUnit Methods​ 部分,对比有什么区别:

第一处:

第二处:

修改为与示例一致即可,但是我们可以看到,实际上这几个实现函数,参数的命名都是不规范的,所以我们略微修改一下,最终这部分的代码如下:

// ICapeUnit Methods
public:
	STDMETHOD(get_ports)(LPDISPATCH *ports)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(get_ValStatus)(CapeValidationStatus *pValStatus)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Calculate)()
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Validate)(BSTR *message, VARIANT_BOOL *pValidateStatus)
	{
		 return E_NOTIMPL;
	}

};

组件注册

要让流程模拟软件访问到我们写的模块,就得通过注册表注册生成的dll文件,打开资源文件中的 HeaterExampleOperation.rgs​ 文件,写入下列代码:

'Implemented Categories'
{
    {678C09A5-7D66-11D2-A67D-00105A42887F} 
    {678C09A1-7D66-11D2-A67D-00105A42887F}
    {4150C28A-EE06-403f-A871-87AFEC38A249}
    {0D562DC8-EA8E-4210-AB39-B66513C0CD09}
    {4667023A-5A8E-4CCA-AB6D-9D78C5112FED}
}
CapeDescription 
{
    val Name =  s 'LaughHeater'
		val Description = s 'Written By laugh'
		val CapeVersion = s '1.1'
		val ComponentVersion = s '24.11.29.1'
		val VendorURL = s 'https://imbhj.com/'
		val About = s 'Heater Example Test Operation Block'
}

键值是固定的,不需要改,下面的 Description 部分可以自己根据实际情况来定义

插入位置如图所示:

点击全部保存,点击生成->重新生成解决方案,编译完成之后打开Aspen,添加CAPE-OPEN模块就可以看到我们注册好的组件了:

在COFE中:

到这里注册就结束了,下面正式开始代码部分。

PortsArray

单元模块的 port 端口,是一个数组类型,为了描述这个 port,需要新建一个 ATL简单对象 来保存和描述这个端口;

先点击生成,重新生成解决方案,然后等待编译完成,点击全部保存,右键项目名称进行新建:

名称为 PortsArray

点击添加之后的ATL简单对象的 属性页面默认 即可,直接点击完成;

如果点击完成之后报错,翻看上文中的处理办法。

新建好的 PortsArray 如图所示:

点击全部保存,点击生成-重新生成解决方案,编译无错误,进行下一步。

ICapeCollection

点击 视图 - 类视图,给刚才新建的 PortsArray 添加实现接口:

类视图中的 CPortsArray​ 前面的C是class的意思,也就是C++中的类。

PortArray.h​ 文件中查看添加完成的接口:

点击全部保存,点击生成-重新生成解决方案,编译无错误,但是为了防止后续出现问题,还是尽量不要让传入的参数和返回的参数名称一致,修改如下:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Count)(long *pCount)
	{
		 return E_NOTIMPL;
	}

首先来定义端口数量,一般来说,一个单元模块的端口数量最少为2,也就是一进一出,如下:

PortsArray.h​ 文件中,ICapeCollection Methods​ 部分:

STDMETHOD(Count)(long *pCount)
{
	// 定义端口数
	*pCount = 2;

	return S_OK;
}

返回到 HeaterExampleOperation​ 部分,首先添加对 PortsArray​ 的引用:

HeaterExampleOperation.h​ 文件中,头部引用部分:

// HeaterExampleOperation.h: CHeaterExampleOperation 的声明

#pragma once
#include "resource.h"       // 主符号

#include "PortsArray.h"		// 添加对 PortsArray 的引用

#include "HeaterExample_i.h"

然后创建 PortsArray​ 实例:

HeaterExampleOperation.h​ 文件中,ICapeUnit Methods​ 部分:

STDMETHOD(get_ports)(LPDISPATCH *ports)
{
	// 创建端口数组
	CComObject<CPortsArray> *pPortArray;
	// 实例化创建的端口数组
	CComObject<CPortsArray>::CreateInstance(&pPortArray);
	// 返回获取的 ports 结果
	pPortArray->QueryInterface(IID_IDispatch, (LPVOID*)ports);

	return S_OK;
}

点击生成-重新生成解决方案,编译无报错,但是测试发现在Aspen中却无法读取到端口信息,继续往下写。

MaterialPort

端口是有一个专门的接口来实现的,其中包含一些端口的信息,比如名字、类型、描述等。

先点击生成,重新生成解决方案,然后等待编译完成,点击全部保存,右键项目名称进行新建:

命名为 MaterialPort

点击添加之后的ATL简单对象的属性页面默认即可,直接点击完成;

添加好的如图所示:

ICapeUnitPort

点击 视图 - 类视图,给刚才新建的 MaterialPort​ 添加实现接口:

返回 MaterialPort.h​ 文件,查看添加好的接口:

点击全部保存,重新生成解决方案,发现编译出现错误,那么还是和前文的解决方案一致,在解压之后的源码文件夹 CPPSource 中,找到 MaterialPort.h​ 这个文件,往下翻到 ICapeUnitPort Methods​ 部分,看一下有什么区别:

修改类型和名称后的代码:

// ICapeUnitPort Methods
public:
	STDMETHOD(get_portType)(CapePortType *portType)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(get_direction)(CapePortDirection *portDirection)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(get_connectedObject)(LPDISPATCH *connectedObject)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Connect)(LPDISPATCH objectToConnect)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Disconnect)()
	{
		 return E_NOTIMPL;
	}

全部保存,重新编译,无报错;

返回 PortsArray.h​ 文件,将端口数修改为 1​ (肯定是不对的,后面会改),假设只有一个端口:

PortsArray.h​ 文件,ICapeCollection Methods​ 部分:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(Count)(long *pCount)
	{
		// 定义端口数
		*pCount = 1;
        //*pCount = 2;

		return S_OK;
	}

PortsArray.h​ 文件,顶部引入部分:

// PortsArray.h: CPortsArray 的声明

#pragma once
#include "resource.h"       // 主符号

#include "MaterialPort.h"	// 添加对 MaterialPort 的引用

#include "HeaterExample_i.h"

创建一个端口,在 PortsArray.h​ 文件下图所示部分:

private:
	// 创建一个端口实例
	CComObject<CMaterialPort> *port1;

将端口 port1​ 实例化,在 PortsArray.h​ 文件中 CPortsArray()​ 部分:

public:
	CPortsArray()
	{
		// 实例化创建的端口
		CComObject<CMaterialPort>::CreateInstance(&port1);
	}

实例化之后就是对其进行赋值:

PortsArray.h​ 文件,ICapeCollection Methods​ 部分:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		// 给实例化好的端口进行赋值
		port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);

		return S_OK;
	}

	STDMETHOD(Count)(long *pCount)
	{
		// 定义端口数
		//*pCount = 2;
		*pCount = 1;

		return S_OK;
	}

全部保存,重新编译一下,无报错;发现在Aspen中还是不能获取到端口,继续往下写,

返回到 HeaterExampleOperation.h​ 文件,ICapeUnit Methods​ 部分,注释掉下面一行:

然后和端口实例类似,在顶部私有成员进行创建:

HeaterExampleOperation.h​ 文件,顶部部分:

private:
	// 创建端口数组
	CComObject<CPortsArray> *pPortArray;

然后来到 MaterialPort.h​ 文件中,设置端口的一些参数:

MaterialPort.h​ 文件,ICapeUnitPort Methods​ 部分:

// ICapeUnitPort Methods
public:
	STDMETHOD(get_portType)(CapePortType *portType)
	{
		// 设置端口类型为流股类型
		*portType = CapePortType::CAPE_MATERIAL;
	
		return S_OK;
	}
	STDMETHOD(get_direction)(CapePortDirection *portDirection)
	{
		// 设置端口流股方向为进口
		*portDirection = CapePortDirection::CAPE_INLET;
	
		return S_OK;
	}
	STDMETHOD(get_connectedObject)(LPDISPATCH *connectedObject)
	{
		// 设置端口流股连接状态为未连接
		*connectedObject = NULL;
	
		return S_OK;
	}
	STDMETHOD(Connect)(LPDISPATCH objectToConnect)
	{
		 return E_NOTIMPL;
	}
	STDMETHOD(Disconnect)()
	{
		 return E_NOTIMPL;
	}

为了更好的描述端口和流股的连接状态,也就是 objectToConnect​ 部分,需要定义一个变量来存放,在 MaterialPort.h​ 文件,顶部部分:

private:
	// 创建一个物流对象连接实例
	LPDISPATCH pMaterialObject;

接着完善 MaterialPort.h​ 文件,ICapeUnitPort Methods​ 部分:

// ICapeUnitPort Methods
public:
	STDMETHOD(get_portType)(CapePortType *portType)
	{
		// 设置端口类型为流股类型
		*portType = CapePortType::CAPE_MATERIAL;
		return S_OK;
	}
	STDMETHOD(get_direction)(CapePortDirection *portDirection)
	{
		// 设置端口流股方向为进口
		*portDirection = CapePortDirection::CAPE_INLET;
		return S_OK;
	}
	STDMETHOD(get_connectedObject)(LPDISPATCH *connectedObject)
	{
		// 设置端口流股连接状态为未连接
		//*connectedObject = NULL;
		// 设置端口流股连接状态为连接状态变量中存放的
		*connectedObject = pMaterialObject;
		return S_OK;
	}
	STDMETHOD(Connect)(LPDISPATCH objectToConnect)
	{
		// 连接时的状态,强行连接到手动创建的物流对象
		pMaterialObject = objectToConnect;
		return S_OK;
	}
	STDMETHOD(Disconnect)()
	{
		// 断开时的状态,强行赋值
		pMaterialObject = NULL;
		return S_OK;
	}

全部保存,重新编译之后发现还是不行,那么应该是设置的 LPDISPATCH​ 类型不对,更改一下 MaterialPort.h​ 文件,头部部分的定义并给它一个初始值:

private:
	// 创建一个物流对象连接实例
	//LPDISPATCH pMaterialObject;
	IDispatch *pMaterialObject;

public:
	CMaterialPort()
	{
		// 给物流对象链接状态实例赋一个初始值
		pMaterialObject = NULL;
	}

更改一下 MaterialPort.h​ 文件,ICapeUnitPort Methods​ 部分的 objectToConnect​ 方式:

STDMETHOD(Connect)(LPDISPATCH objectToConnect)
{
	// 连接时的状态,强行连接到手动创建的物流对象
	//pMaterialObject = objectToConnect;
	objectToConnect->QueryInterface(IID_IDispatch, (LPVOID*)&pMaterialObject);

	return S_OK;
}

全部保存,重新编译,无报错;打开COFE,测试已经可以连接一个流股:

Aspen还是不可以,下面继续修改和完善。

回到 HeaterExampleOperation.h​ 文件中,继续完善 ICapeUnit Methods​ 部分,首先还是从我们之前下载的CPPSource文件夹里,找到 BSTR.h​ 和 Variant.h​ 这两个文件,将其复制到项目根目录下,右键点击-添加-现有项

选择刚才复制进来的两个文件,点击确定:

HeaterExampleOperation.h​ 文件中的头部,添加对 Variant.h​ 文件的引用:

// HeaterExampleOperation.h: CHeaterExampleOperation 的声明

#pragma once
#include "resource.h"       // 主符号

#include "PortsArray.h"		// 添加对 PortsArray 的引用
#include "Variant.h"		// 添加对 Variant 的引用

#include "HeaterExample_i.h"

在刚引入的 Variant.h​ 文件中的头部,添加对 std::wstring​ 的引用:

#pragma once

#include "BSTR.h"
// 添加对 wstring 的引用
#include <string>
using namespace std;

如图:

返回 HeaterExampleOperation.h​ 文件中,继续完善 ICapeUnit Methods​ 部分:

// ICapeUnit Methods
public:
	STDMETHOD(get_ports)(LPDISPATCH *ports)
	{
		// 创建端口数组
		//CComObject<CPortsArray> *pPortArray;
		// 实例化创建的端口数组
		CComObject<CPortsArray>::CreateInstance(&pPortArray);
		// 返回获取的 ports 结果
		pPortArray->QueryInterface(IID_IDispatch, (LPVOID*)ports);

		return S_OK;
	}

	STDMETHOD(get_ValStatus)(CapeValidationStatus *pValStatus)
	{
		// 默认端口状态可用
		*pValStatus = CapeValidationStatus::CAPE_VALID;

		return S_OK;
	}

	STDMETHOD(Calculate)()
	{
        // 计算部分先空着
		return S_OK;
	}

	STDMETHOD(Validate)(BSTR *message, VARIANT_BOOL *pValidateStatus)
	{
		// 带有检查功能的状态获取
		CBSTR msg(L"NO ERROR");
		*message = msg;
		// 状态:成功
		*pValidateStatus = TRUE;

		return S_OK;
	}

全部保存,编译一下,没问题;

ICapeUtilities

点击视图-类试图,右键 CHeaterExampleOperation​ -添加-实现接口:

返回 HeaterExampleOperation.h​ 文件中,检查接口是否添加:

按照惯例,修改一下返回的参数名称,并且填充一下相关的内容:

HeaterExampleOperation.h​ 文件中,ICapeUtilities Methods​ 部分:

// ICapeUtilities Methods
public:
	STDMETHOD(get_parameters)(LPDISPATCH *pParameters)
	{
		// 暂时忽略这个接口,赋值为空(与工况分析、灵敏度分析等有关)
		*pParameters = NULL;

		return S_OK;
	}

	STDMETHOD(put_simulationContext)(LPDISPATCH pSimulationContext)
	{
		// 该接口是当单元模块状态异常(如计算陷入死循环)时,单元模块与模拟软件通信,告诉模拟软件单元模块状态异常,需要强制结束
		// 这里暂时不做实现

		return S_OK;
	}

	STDMETHOD(Initialize)()
	{
		// 端口数组已在前文的构造函数 CHeaterExampleOperation() 中初始化完成,这里直接返回 OK 即可

		return S_OK;
	}

	STDMETHOD(Terminate)()
	{
		// 单元模块卸载,这里暂时不做实现,返回空结果

		return S_OK;
	}

	STDMETHOD(Edit)()
	{
		// 双击单元模块的逻辑,显示一个弹窗
		MessageBox(NULL, L"Hello World", L"by laugh", MB_OK);

		return S_OK;
	}

在上一段中,因为实例初始化只需要一次即可,也就是 ICapeUnit Methods中​ 的 pPortArray​ 变量,所以之前写的位置不对,需要将初始化挪到前文中的 CHeaterExampleOperation()​ 部分,如图:

复制并注释上图这一行,粘贴到下图中的位置:

全部保存,编译一下,没有报错;

ParametersArray

PortsArray​ 类似,Parameter​ 也是一个数组,所以需要一个ATL简单对象来承载它,右键项目-添加-新建项:

选择 ATL简单对象,命名为 ParametersArray​ :

属性页面默认即可,直接点确定;

添加好的简单对象如图:

ICapeCollection

PortsArray​ 一样,ParametersArray​ 简单对象也是只需要一个 ICapeCollection​ 接口即可,点击视图-类试图切换到类试图,右键 CParametersArray​ ,添加-实现接口:

添加好的接口如图:

这里的 ParametersArray​ 数组实际上并不需要真的传值过去,只需要具有这个功能即可,所以实现很简单,当然和之前的类似,记得修改相关的参数名称:

ParametersArray.h​ 文件中,ICapeCollection Methods​ 部分:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		// 返回一个空数组,暂不做实现

		return S_OK;
	}

	STDMETHOD(Count)(long *pCount)
	{
		// 返回一个空数组,暂不做实现
		*pCount = NULL;

		return S_OK;
	}

返回 HeaterExampleOperation.h​ 文件中,在头部加入对 ParametersArray.h​ 文件的引用:

HeaterExampleOperation.h​ 文件中,头部部分:

// HeaterExampleOperation.h: CHeaterExampleOperation 的声明

#pragma once
#include "resource.h"       // 主符号

#include "PortsArray.h"		// 添加对 PortsArray 的引用
#include "Variant.h"		// 添加对 Variant 的引用
#include "ParametersArray.h"	// 添加对 ParametersArray 的引用

#include "HeaterExample_i.h"

创建 Parameters​ 数组实例并进行初始化:

HeaterExampleOperation.h​ 文件中,CHeaterExampleOperation​ 部分:

private:
	// 创建端口数组
	CComObject<CPortsArray> *pPortArray;
	// 创建 Parameter 参数集数组
	CComObject<CParametersArray>* pParametersArray;

public:
	CHeaterExampleOperation()
	{
		// 实例化创建的端口数组
		CComObject<CPortsArray>::CreateInstance(&pPortArray);
		// 实例化创建的 Parameters 参数集数组
		CComObject<CParametersArray>::CreateInstance(&pParametersArray);
	}

更改上文中的 get_parameters​ 函数中获取 parameters​ 的方式:

HeaterExampleOperation.h​ 文件中,ICapeUtilities Methods​ 部分:

STDMETHOD(get_parameters)(LPDISPATCH *pParameters)
{
	// 暂时忽略这个接口,赋值为空(与工况分析、灵敏度分析等有关)
	//*pParameters = NULL;
	// 返回获取的 parameters 结果
	pParametersArray->QueryInterface(IID_IDispatch, (LPVOID*)pParameters);

	return S_OK;
}

全部保存,编译一下,没有报错;

但是发现还是有问题,经过断点测试,应该是在 get_port​ 的过程中出现了问题,对获取端口的方式进行修正,回到 MaterialPort.h​ 文件中,补充一下端口的参数:

MaterialPort.h​ 文件中,CMaterialPort​ 部分:

private:
	// 创建一个物流对象连接实例
	//LPDISPATCH pMaterialObject;
	IDispatch *pMaterialObject;
	// 传入参数,为端口流股方向
	CapePortDirection pDirection;

public:
	//CMaterialPort()
	CMaterialPort(CapePortDirection pDirection)
	{
		// 给物流对象链接状态实例赋一个初始值
		pMaterialObject = NULL;

		// 将端口方向参数传入公有
		this->pDirection = pDirection;
	}

将下面的 ICapeUnitPort Methods​ 部分的获取方式也更改一下:

MaterialPort.h​ 文件中,ICapeUnitPort Methods​ 部分:

STDMETHOD(get_direction)(CapePortDirection *portDirection)
{
	// 设置端口流股方向为进口
	//*portDirection = CapePortDirection::CAPE_INLET;
	// 改为参数传入形式
	*portDirection = this->pDirection;

	return S_OK;
}

来到 PortsArray.h​ 文件中,修改端口数和端口实例:

private:
	// 创建一个端口实例
	CComObject<CMaterialPort> *port1;
	// 创建另一个端口实例,单元模块最少两个端口,一进一出
	CComObject<CMaterialPort> *port2;

但是到这里又发现不对了,port1/2​ 是带参数的,明显不能使用 CComObject<CMaterialPort>::CreateInstance()​ 方式来分别进行实例化,所以说明刚才的设置端口方向的 CMaterialPort(CapePortDirection pDirection)​ 方式是有问题的;

返回到 MaterialPort.h​ 文件中,手动创建一个设置端口方向的函数:

MaterialPort.h​ 文件中,CMaterialPort​ 部分:

private:
	// 创建一个物流对象连接实例
	//LPDISPATCH pMaterialObject;
	IDispatch *pMaterialObject;
	// 传入参数,为端口流股方向
	CapePortDirection pDirection;

public:
	CMaterialPort()
	//CMaterialPort(CapePortDirection pDirection)
	{
		// 给物流对象链接状态实例赋一个初始值
		pMaterialObject = NULL;

		// 将端口方向参数传入公有
		this->pDirection = pDirection;
	}

	// 设置端口流股方向
	void SetDirection(CapePortDirection pDirection) {
		// 将端口方向参数传入共有
		this->pDirection = pDirection;
	}

回到 PortsArray.h​ 文件中,继续实例化两个端口:

PortsArray.h​ 文件中,CPortsArray​ 部分:

private:
	// 创建一个端口实例
	CComObject<CMaterialPort> *port1;
	// 创建另一个端口实例,单元模块最少两个端口,一进一出
	CComObject<CMaterialPort> *port2;

public:
	CPortsArray()
	{
		// 实例化创建的端口1
		CComObject<CMaterialPort>::CreateInstance(&port1);
		// 设置端口1方向
		port1->SetDirection(CapePortDirection::CAPE_INLET);
		// 实例化创建的端口2
		CComObject<CMaterialPort>::CreateInstance(&port2);
		// 设置端口2方向
		port2->SetDirection(CapePortDirection::CAPE_OUTLET);
	}

同时修改下方的端口数量:

PortsArray.h​ 文件中,ICapeCollection Methods​ 部分:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		// 给实例化好的端口进行赋值
		port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);

		return S_OK;
	}

	STDMETHOD(Count)(long *pCount)
	{
		// 定义端口数
		*pCount = 2;
		//*pCount = 1;

		return S_OK;
	}

这个时候问题就来了,在这里的 STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem){}​ 方法里,获取到的 id​ 是端口号还好说,比如 id=1​ 那就是 port1​ ,id=2​ 那就是 port2​ ,假如不是端口号呢?获取到的 id​ 是端口的名字呢?所以说这里还是欠缺了一部分考虑,就需要准确的获取端口的名字和标识符,就需要用到接下来的这个接口。

ICapeIdentification-1

点击视图-类视图,切换到类试图,右键 CMaterialPort​ ,添加-实现接口:

添加好的接口如图:

还是老规矩,更改一下这个变量名称:

MaterialPort.h​ 文件中,ICapeIdentification Methods​ 部分:

// ICapeIdentification Methods
public:
	STDMETHOD(get_ComponentName)(BSTR *pComponentName)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(put_ComponentName)(BSTR pComponentName)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(get_ComponentDescription)(BSTR *pComponentDescription)
	{
		 return E_NOTIMPL;
	}

	STDMETHOD(put_ComponentDescription)(BSTR pComponentDescription)
	{
		 return E_NOTIMPL;
	}

然后来对端口的名字和描述做一个实现,首先来引入需要的头文件:

MaterialPort.h​ 文件中,头部部分:

// MaterialPort.h: CMaterialPort 的声明

#pragma once
#include "resource.h"       // 主符号

#include <string>			// 添加对 wstring 的引用
using namespace std;
#include "Variant.h"		// 添加对 CBSTR 的引用
#include "atlbase.h"		// 添加对 CA2W 的引用
#include "atlconv.h"		// 添加对 CA2W 的引用

#include "HeaterExample_i.h"

然后创建端口名字和描述的变量,已经对应的设置函数:

MaterialPort.h​ 文件中,CMaterialPort​ 部分:

private:
	// 创建一个物流对象连接实例
	//LPDISPATCH pMaterialObject;
	IDispatch *pMaterialObject;
	// 传入参数,为端口流股方向
	CapePortDirection pDirection;
	// 端口名称
	//string pName;
	wstring pName;
	// 端口描述
	//string pDesc;
	wstring pDesc;

public:
	CMaterialPort()
	//CMaterialPort(CapePortDirection pDirection)
	{
		// 给物流对象链接状态实例赋一个初始值
		pMaterialObject = NULL;

		// 将端口方向参数传入公有
		this->pDirection = pDirection;
	}

	// 设置端口流股方向
	void SetDirection(CapePortDirection pDirection) {
		// 将端口方向参数传入共有
		this->pDirection = pDirection;
	}

	// 设置端口名称和描述
	//void SetNameAndDesc(string pName, string pDesc) {
	void SetNameAndDesc(wstring pName, wstring pDesc) {
		this->pName = pName;
		this->pDesc = pDesc;
	}

对端口名字和描述做一个实现:

MaterialPort.h​ 文件中,ICapeIdentification Methods​ 部分:

// ICapeIdentification Methods
public:
	STDMETHOD(get_ComponentName)(BSTR *pComponentName)
	{
		// 获取端口的名字
		CBSTR n(SysAllocString(CA2W(pName.c_str())));	// string 转 const OLECHAR* 类型
		*pComponentName = n;
	
		return S_OK;
	}

	STDMETHOD(put_ComponentName)(BSTR pComponentName)
	{
		// 不做实现,返回空结果

		return S_OK;
	}

	STDMETHOD(get_ComponentDescription)(BSTR *pComponentDescription)
	{
		// 获取端口的描述
		CBSTR d(SysAllocString(CA2W(pDesc.c_str())));	// string 转 const OLECHAR* 类型
		*pComponentDescription = d;

		return S_OK;
	}

	STDMETHOD(put_ComponentDescription)(BSTR pComponentDescription)
	{
		// 不做实现,返回空结果

		return S_OK;
	}

然后在 PortsArray.h​ 文件中,给端口设置名字和描述:

PortsArray.h​ 文件中,CPortsArray​ 部分:

private:
	// 创建一个端口实例
	CComObject<CMaterialPort> *port1;
	// 创建另一个端口实例,单元模块最少两个端口,一进一出
	CComObject<CMaterialPort> *port2;

public:
	CPortsArray()
	{
		// 实例化创建的端口1
		CComObject<CMaterialPort>::CreateInstance(&port1);
		// 设置端口1方向
		port1->SetDirection(CapePortDirection::CAPE_INLET);
		// 设置端1口名字和描述
		//port1->SetNameAndDesc("INLET", "PORT1");
		port1->SetNameAndDesc(L"INLET", L"PORT1");
		// 实例化创建的端口2
		CComObject<CMaterialPort>::CreateInstance(&port2);
		// 设置端口2方向
		port2->SetDirection(CapePortDirection::CAPE_OUTLET);
		// 设置端口2名字和描述
		//port2->SetNameAndDesc("OUTLET", "PORT2");
		port2->SetNameAndDesc(L"OUTLET", L"PORT2");
	}

在下面继续完成端口ID的部分:

PortsArray.h​ 文件中,ICapeCollection Methods​ 部分:

// ICapeCollection Methods
public:
	STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
	{
		// 获取 id
		CVariant v(id, TRUE);
		wstring error;
		// 如果 id 是整数数组
		if (v.CheckArray(VT_I4, error))
		{
			// 给实例化好的端口进行赋值
			if (v.GetLongAt(0) == 0) port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
			else port2->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
		} 
		// 如果 id 是字符串数组
		else if (v.CheckArray(VT_BSTR,error))
		{
			CBSTR name = v.GetStringAt(0);
			if (CBSTR::Same(L"INLET", name)) port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
			else port2->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
		}
		// 给实例化好的端口进行赋值
		//port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);

		return S_OK;
	}

	STDMETHOD(Count)(long *pCount)
	{
		// 定义端口数
		*pCount = 2;
		//*pCount = 1;

		return S_OK;
	}

编译一下,发现有错误,应该是 MaterialPort.h​ 文件中获取端口名字的那个方法有问题,修改一下:

MaterialPort.h​ 文件中,ICapeIdentification Methods​ 部分:

// ICapeIdentification Methods
public:
	STDMETHOD(get_ComponentName)(BSTR *pComponentName)
	{
		// 获取端口的名字
		//CBSTR n(SysAllocString(CA2W(pName.c_str())));	// string 转 const OLECHAR* 类型
		//*pComponentName = n;
		*pComponentName = SysAllocString(pName.c_str());
	
		return S_OK;
	}

	STDMETHOD(put_ComponentName)(BSTR pComponentName)
	{
		// 不做实现,返回空结果
		return S_OK;
	}

	STDMETHOD(get_ComponentDescription)(BSTR *pComponentDescription)
	{
		// 获取端口的描述
		//CBSTR d(SysAllocString(CA2W(pDesc.c_str())));	// string 转 const OLECHAR* 类型
		//*pComponentDescription = d;
		*pComponentDescription = SysAllocString(pDesc.c_str());

		return S_OK;
	}

	STDMETHOD(put_ComponentDescription)(BSTR pComponentDescription)
	{
		// 不做实现,返回空结果
		return S_OK;
	}

全部保存,编译一下, 没什么问题,但是测试还是发现无法连接流股,看来哪里还是有问题在,继续完善;