前言

因为距离上一次开发已经过去了好几个月,中间因为重建博客和论坛的原因,同样也是因为在《单元模块开发入门实例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简单对象,右键项目解决方案资源管理器中的项目名称,点击添加,新建项:

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

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

如果这里报错idl文件路径问题,建议直接删除项目,然后重新建立,创建项目之后先点击全部保存按钮,然后再进行添加,这是一个Visual Studio的bug,如果不想重建项目,可以参考:https://bbs.imbhj.com/t/topic/98https://blog.imbhj.com/archives/W5ZqHtU7 解决。

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

添加完成后如图所示:

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

如果有如下报错:

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

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

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

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

ICapeUnit

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

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

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

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

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

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

这里如果报错了 pch.cpp 文件问题,参考: https://blog.imbhj.com/archives/W5ZqHtU7 解决。

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


分割线分割线分割线

这里插入一个题外话,如果你下载了我的源码,或者是你自己的项目在另一台电脑上打开,发现编译失败的问题,那么大概率是 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模块就可以看到我们注册好的组件了:

V11版本实测也可以

在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.hVariant.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 那就是端口1,id=2 那就是端口2,假如不是端口号呢?获取到的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;
	}

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

ICapeIdentification-2

第一个 ICapeIdentification 是给 CMaterialPort 添加的,现在给 CPortsArray 也添加上这个标识符接口,点击视图-类视图,右键 CPortsArray,添加-实现接口:

检查添加好的接口:

然后对其进行实现:

PortsArray.h 文件中,ICapeIdentification Methods 部分:

记得按照老传统,更改参数的名字:

// ICapeIdentification Methods
public:
	STDMETHOD(get_ComponentName)(BSTR *pComponentName)
	{
		// 获取端口数组的名字
		//CBSTR n(SysAllocString(CA2W("Ports Array Name")));	// string 转 const OLECHAR* 类型
		CBSTR n(SysAllocString(L"Ports Array Name"));	// 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("Ports Array Desc")));	// string 转 const OLECHAR* 类型
		CBSTR d(SysAllocString(L"Ports Array Desc"));	// string 转 const OLECHAR* 类型
		*pComponentDescription = d;

		return S_OK;
	}

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

回到 HeaterExampleOperation.h 文件中,修改一下 get_ports 的方法:

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

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

	return S_OK;
}

全部保存,编译,无错误;但是用COFE测试还是会崩溃,通过断点调试发现是 get_ports 第一次返回了一个空值,那么需要对这个空值进行拦截一下:

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

STDMETHOD(get_ports)(LPDISPATCH *ports)
{
	// 获取端口为空时进行拦截
	if (ports == NULL) return E_FAIL;
	//if (*ports == NULL) return E_FAIL;
	
	// 创建端口数组
	//CComObject<CPortsArray> *pPortArray;
	// 实例化创建的端口数组
	//CComObject<CPortsArray>::CreateInstance(&pPortArray);
	// 返回获取的 ports 结果
	//pPortArray->QueryInterface(IID_IDispatch, (LPVOID*)ports);
	*ports = (ICapeCollection*)pPortArray;
	pPortArray->AddRef();

	return S_OK;
}

全部保存,编译一下,无报错;在COFE里测试一下,哎,发现不崩溃了,但是新的问题又来了,端口又读取不到且流股无法连接了,在前文中已经实现过了流股连接,这里明显是又出现了新的bug,那么下面继续来完善;

类似于上面的 get_ports,将下面的 get_parameters 也更换为指针的方式:

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

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

	return S_OK;
}

然后给前文的 PortsArrayParametersArray 数组实例化都加上计数函数:

HeaterExampleOperation.h 文件中,CHeaterExampleOperation 部分:

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

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

然后通过断点调试,发现了 PortsArray.h 文件中的获取端口ID方式有点问题,这个ID传过来之后是一个整数类型,没有考虑到,修改一下:

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

STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
{
	// 给实例化好的端口进行赋值
	//port1->QueryInterface(IID_IDispatch, (LPVOID*)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);
	//}
	
	// 判断ID是个整数类型
	if (id.vt == VT_I4) {
		if (id.lVal == 1) {
			// 端口1赋值
			port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
			port1->AddRef();
		} else {
			// 端口2赋值
			port2->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
			port2->AddRef();
		}
	}

	return S_OK;
}

同样的,给上面的端口实例化也加上计数函数:

PortsArray.h 文件中,CPortsArray 部分:

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

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

全部保存,编译一下,无错误;使用COFE测试一下,发现已经可以连接两个端口和流股了:

但是在某些旧版本中,发现并没有识别到单元模块的名称,显示的是unknown,如图:

所以还需要进行完善一下。

ICapeIdentification-3

那么也直接给单元模块增加一个标识符接口,点击视图-类视图,右键 CHeaterExampleOperation,添加-实现接口:

检查一下添加好的接口:

然后对其进行实现:

HeaterExampleOperation.h 文件中,ICapeIdentification Methods 部分:

记得按照老传统,更改参数的名字:

// ICapeIdentification Methods
public:
	STDMETHOD(get_ComponentName)(BSTR *pComponentName)
	{
		// 获取单元模块名字
		CBSTR n(SysAllocString(L"LAUGH Heater"));	// string 转 const OLECHAR* 类型
		*pComponentName = n;

		return S_OK;
	}

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

	STDMETHOD(get_ComponentDescription)(BSTR *pComponentDescription)
	{
		// 获取单元模块描述
		CBSTR d(SysAllocString(L"LAUGH Heater Desc"));	// string 转 const OLECHAR* 类型
		*pComponentDescription = d;

		return S_OK;
	}

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

全部保存,编译一下,无错误;测试COFE也可以正确识别到端口和连接两个流股,实测AspenV14也是可以的:

接下来就可以去实现计算部分的内容了,回到 PortsArray.h 文件中,先来获取到入口流股和出口流股的热力学对象:

PortsArray.h 文件中,CPortsArray 部分:

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

public:
	// 获取入口流股的热力学对象
	ICapeThermoMaterial* getInlet() {
		return (ICapeThermoMaterial*)port1;
	}
	// 获取出口流股的热力学对象
	ICapeThermoMaterial* getOutlet() {
		return (ICapeThermoMaterial*)port2;
	}

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

然后来到前文引入的 Variant.h 文件中,新建一个返回值:

Variant.h 文件中,public 部分:

public:
    // 新建一个返回值,方便 Calculate 接口调用
    VARIANT& Pvalue()
    {
        return value;
    }

位置如图所示:

来到 HeaterExampleOperation.h 文件中,实现计算部分,首先添加一个头文件支持 wstring 类型:

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

// HeaterExampleOperation.h: CHeaterExampleOperation 的声明

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

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

#include "HeaterExample_i.h"

然后写计算部分:

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

STDMETHOD(Calculate)()
{
	// 实现计算,通过 PortsArray 中的热力学接口转化而来
	// 定义一个临时变量 v
	CVariant v;
	// 从 pPortArray 获取到入口流股的热力学对象,并从其中获取到温度值,赋值给临时变量 v
	pPortArray->getInlet()->GetOverallProp(L"temperature", L"empty", &v.Pvalue());
	// 定义一个临时变量 error 用来返回错误信息
	wstring error;
	// 检查临时变量 v 是否为数组类型
	v.CheckArray(VT_R8, error);
	// 读取临时变量 v 数组中的第一个值并赋值给临时变量 T,类型为双精度浮点
	double T = v.GetDoubleAt(0);
	// 将临时变量 T 中的数值转换为长字符串并赋值给临时变量 sw
	string s = to_string(T);
	wstring stamp = wstring(s.begin(), s.end());
	LPCWSTR sw = stamp.c_str();
	// 跳出一个弹窗,显示临时变量 sw 的值,也就是温度
	MessageBox(NULL, sw, L"", MB_OK);

	return S_OK;
}

全部保存,重新编译,无错误;打开aspen或者cofe测试,发现无法读取,看来还是有bug,继续完善,

来到 MaterialPort.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;
	}

	// 返回流股对象给 PortsArray 中的 getInlet 函数
	IDispatch*& getMaterial() {
    	return pMaterialObject;
	}

	// 设置端口流股方向
	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;
	}

然后返回 PortsArray.h 文件中,处理一下获取到的 pMaterialObject 对象:

PortsArray.h 文件中,CPortsArray 部分:

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

public:
	// 获取入口流股的热力学对象
	ICapeThermoMaterial* getInlet() {
		return (ICapeThermoMaterial*)port1->getMaterial();
	}
	// 获取出口流股的热力学对象
	ICapeThermoMaterial* getOutlet() {
		return (ICapeThermoMaterial*)port2->getMaterial();
	}

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

编译测试之后发现还是有问题,继续修改bug,

来到 MaterialPort.h 文件中,更改一下 pMaterialObject 的获取方式:

MaterialPort.h 文件中,CMaterialPort 部分:

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

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

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

	// 返回流股对象给 PortsArray 中的 getInlet 函数
	ICapeThermoMaterial*& getMaterial() {
		return pMaterialObject;
	}
	/*IDispatch*& getMaterial() {
		return pMaterialObject;
	}*/

	// 设置端口流股方向
	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 文件中,ICapeUnitPort Methods 部分:

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

回到 HeaterExampleOperation.h 文件中,修改一下计算的实现方式:

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

STDMETHOD(Calculate)()
{
	// 实现计算,通过 PortsArray 中的热力学接口转化而来
	// 定义一个临时变量 v
	//CVariant v;
	// 从 pPortArray 获取到入口流股的热力学对象,并从其中获取到温度值,赋值给临时变量 v
	//pPortArray->getInlet()->GetOverallProp(L"temperature", L"empty", &v.Pvalue());
	// 重新定义一个临时变量
	VARIANT v2;
	v2.vt = VT_EMPTY;
	// 获取进口流股摩尔流量,赋值给 v2
	HRESULT hr = pPortArray->getInlet()->GetOverallProp(L"totalFlow", L"mole", &v2);
	// 从 v2 中取值赋值给 v
	CVariant v(v2, TRUE);
	// 定义一个临时变量 error 用来返回错误信息
	wstring error;
	// 检查临时变量 v 是否为数组类型
	v.CheckArray(VT_R8, error);
	// 读取临时变量 v 数组中的第一个值并赋值给临时变量 T,类型为双精度浮点
	double T = v.GetDoubleAt(0);
	// 将临时变量 T 中的数值转换为长字符串并赋值给临时变量 sw
	string s = to_string(T);
	wstring stamp = wstring(s.begin(), s.end());
	LPCWSTR sw = stamp.c_str();
	// 跳出一个弹窗,显示临时变量 sw 的值,也就是温度
	MessageBox(NULL, sw, L"", MB_OK);

	return S_OK;
}

全部保存,重新编译,无错误;打开COFE进行测试,可以正确的弹窗显示正确的流量:

这样就简单完成了一个单元模块的一整个流程,但是发现点击确定之后还是会有报错,这是因为还没有实现给流股赋值,只实现了给读取入口流股的功能,下一个章节来继续完善这个模块。

实现闪蒸计算

首先呢,经过测试之后发现自从AspenPlusV11版本之后,一直到现在的V14版本,都采用了CAPE-OPENv1.1的标准,我们上文中写的一些方法、函数、接口是不太对的,并且部分地方还有点小bug,所以需要修改一下,那么下面一起来进行修改吧。

首先来到 MaterialPort.h 文件中(本文件中一共有三处变化,请务必注意),修改获取物流对象的方式,ICapeThermoMaterial (兼容COFE软件)更换为 CComPtr<ICapeThermoMaterial> ,更换一个智能指针方式,也是CAPE-OPENv1.1的接口,但是兼容AspenPlusV11-14软件;

MaterialPort.h 文件中,CMaterialPort 部分:

private:
	// 创建一个物流对象连接实例
	//LPDISPATCH pMaterialObject;
	//IDispatch *pMaterialObject;
	// 下面这个热力学指针的方法也是CAPE-OPENv1.1的接口,兼容COFE软件
	//ICapeThermoMaterial* pMaterialObject;
	// 变化1:更换一个智能指针方式,也是CAPE-OPENv1.1的接口,但是兼容AspenPlusV11-14软件
	CComPtr<ICapeThermoMaterial> pMaterialObject;
	// 传入参数,为端口流股方向
	CapePortDirection pDirection;
	// 端口名称
	//string pName;
	wstring pName;
	// 端口描述
	//string pDesc;
	wstring pDesc;

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

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

	// 返回流股对象给 PortsArray 中的 getInlet 函数
    // 变化2:同样也更换为了智能指针,兼容AspenPlus
    CComPtr<ICapeThermoMaterial>& getMaterial() {
    	return pMaterialObject;
    }
    // 兼容COFE软件
    /*ICapeThermoMaterial*& getMaterial() {
        return pMaterialObject;
    }*/
    // 已弃用
    /*IDispatch*& getMaterial() {
    	return pMaterialObject;
    }*/

	// 设置端口流股方向
	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 文件中,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;
		// 改为参数传入形式
		*portDirection = this->pDirection;
		
		return S_OK;
	}

	STDMETHOD(get_connectedObject)(LPDISPATCH *connectedObject)
	{
		// 设置端口流股连接状态为未连接
		//*connectedObject = NULL;
		// 设置端口流股连接状态为连接状态变量中存放的
		*connectedObject = pMaterialObject;
		// 变化3:增加计数函数
		(*connectedObject)->AddRef();
		
		return S_OK;
	}

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

	STDMETHOD(Disconnect)()
	{
		// 断开时的状态,强行赋值
		pMaterialObject = NULL;
		
		return S_OK;
	}

回到 HeaterExampleOperation.h 文件中,来实现闪蒸算法,首先封装了三个函数,分别是获取进口流股数据、计算并进行闪蒸、赋值给出口流股,分别如下:

1:

// 获取进口流股物流对象中的参数,主要为温度、压力、摩尔流量、摩尔组成
BOOL GetOverallTPFlowComposition(double& temperature, double& pressure, double& totalMoleFlow, CVariant& moleComposition)
{
	// 定义临时变量
	HRESULT hr;
	std::wstring error;
	CVariant myValue;
	// PValue() 函数在 Variant.h 文件中定义返回 value 值
	// 获取温度
	hr = pPortArray->getInlet()->GetOverallProp(CBSTR(_T("temperature")), NULL, &myValue.Pvalue());
	myValue.CheckArray(VT_R8, error);
	temperature = myValue.GetDoubleAt(0);
	// 获取压力
	hr = pPortArray->getInlet()->GetOverallProp(CBSTR(_T("pressure")), NULL, &myValue.Pvalue());
	!myValue.CheckArray(VT_R8, error);
	pressure = myValue.GetDoubleAt(0);
	// 获取总摩尔流量
	hr = pPortArray->getInlet()->GetOverallProp(CBSTR(_T("totalFlow")), CBSTR(_T("mole")), &myValue.Pvalue());
	!myValue.CheckArray(VT_R8, error);
	totalMoleFlow = myValue.GetDoubleAt(0);
	// 获取组分的摩尔分率
	VARIANT pv;
	pv.vt = VT_EMPTY;
	hr = pPortArray->getInlet()->GetOverallProp(CBSTR(_T("fraction")), CBSTR(_T("mole")), &pv);
	myValue.CheckArray(VT_R8, error);
	moleComposition.Set(pv, TRUE);

	return 1;
}

2:

// 将计算完毕的参数赋值给流股并执行一次闪蒸
BOOL SetOverallTPFlowCompositionAndFlash(double temperature, double pressure, double totalMoleFlow, CVariant& moleComposition)
{
	// 定义临时变量
	HRESULT hr;
	CVariant myValue;
	// 设置温度
	myValue.MakeArray(1, VT_R8);
	myValue.SetDoubleAt(0, temperature);
	hr = pPortArray->getOutlet()->SetOverallProp(CBSTR(L"temperature"), NULL, myValue);
	// 设置压力
	myValue.MakeArray(1, VT_R8);
	myValue.SetDoubleAt(0, pressure);
	hr = pPortArray->getOutlet()->SetOverallProp(CBSTR(L"pressure"), NULL, myValue);
	// 设置总摩尔流量
	myValue.MakeArray(1, VT_R8);
	myValue.SetDoubleAt(0, totalMoleFlow);
	hr = pPortArray->getOutlet()->SetOverallProp(CBSTR(L"totalFlow"), CBSTR(L"mole"), myValue);
	// 设置组分摩尔分率
	hr = pPortArray->getOutlet()->SetOverallProp(CBSTR(L"fraction"), CBSTR(L"mole"), moleComposition);
	// 执行一次闪蒸,确定出口流股的相态
	CalcEquilibriumByTemperatureAndPressure();

	return 1;
}

3:

// 闪蒸函数
BOOL CalcEquilibriumByTemperatureAndPressure()
{
	// 定义临时变量
	CVariant flashSpec1, flashSpec2;
	CBSTR overall(L"overall");
	// 温度闪蒸
	flashSpec1.MakeArray(3, VT_BSTR);
	flashSpec1.AllocStringAt(0, L"temperature");
	flashSpec1.SetStringAt(1, NULL);
	flashSpec1.SetStringAt(2, overall);
	// 压力闪蒸
	flashSpec2.MakeArray(3, VT_BSTR);
	flashSpec2.AllocStringAt(0, L"pressure");
	flashSpec2.SetStringAt(1, NULL);
	flashSpec2.SetStringAt(2, overall);
	// 创建一个闪蒸计算的实例
	CComPtr<ICapeThermoEquilibriumRoutine> capeThermoEquilibriumRoutine;
	// 获取赋值完毕的出口流股信息
	pPortArray->getOutlet()->QueryInterface(IID_ICapeThermoEquilibriumRoutine, (LPVOID*)&capeThermoEquilibriumRoutine);
	// 执行闪蒸
	HRESULT hr = capeThermoEquilibriumRoutine->CalcEquilibrium(flashSpec1, flashSpec2, CBSTR(_T("unspecified")));

	return 1;
}

这三个函数放的位置如图所示:

这里我为了方便表示函数所在位置,所以把函数折叠起来了,并不是只写了一行。

然后是计算部分:

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

STDMETHOD(Calculate)()
{
	// 实现计算,通过 PortsArray 中的热力学接口转化而来
	// 定义一个临时变量 v
	//CVariant v;
	// 从 pPortArray 获取到入口流股的热力学对象,并从其中获取到温度值,赋值给临时变量 v
	//pPortArray->getInlet()->GetOverallProp(L"temperature", L"empty", &v.Pvalue());
	// 重新定义一个临时变量
	//VARIANT v2;
	//v2.vt = VT_EMPTY;
	// 获取进口流股摩尔流量,赋值给 v2
	//HRESULT hr = pPortArray->getInlet()->GetOverallProp(L"totalFlow", L"mole", &v2);
	// 从 v2 中取值赋值给 v
	//CVariant v(v2, TRUE);
	// 定义一个临时变量 error 用来返回错误信息
	//wstring error;
	// 检查临时变量 v 是否为数组类型
	//v.CheckArray(VT_R8, error);
	// 读取临时变量 v 数组中的第一个值并赋值给临时变量 T,类型为双精度浮点
	//double T = v.GetDoubleAt(0);
	// 将临时变量 T 中的数值转换为长字符串并赋值给临时变量 sw
	//string s = to_string(T);
	//wstring stamp = wstring(s.begin(), s.end());
	//LPCWSTR sw = stamp.c_str();
	// 跳出一个弹窗,显示临时变量 sw 的值,也就是温度
	//MessageBox(NULL, sw, L"", MB_OK);

	// 实现闪蒸计算
	// 定义需要传入的参数
	double temperature, pressure, totalMoleFlow;
	CVariant moleComposition;
	// 调用获取入口流股物流对象参数
	GetOverallTPFlowComposition(temperature, pressure, totalMoleFlow, moleComposition);

	// 临时定义参数部分
	temperature = 400;	// 默认单位为 K
	pressure = 301325;	// 默认单位为 Pa

	// 设置出口流股物流对象参数
	SetOverallTPFlowCompositionAndFlash(temperature, pressure, totalMoleFlow, moleComposition);

	return S_OK;
}

全部保存,重新编译,无错误;打开Aspen测试发现居然闪退,然后找了一圈发现是粗心大意把一处代码修改后忘了注释掉了:

MaterialPort.h 文件中,CMaterialPort 部分:

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

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

注释掉这里的 this->pDirection = pDirection; 这句即可。

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

但是测试发现AspenPlusV14版本会直接闪退,COFE也会直接闪退,我对照了我之前写的代码,发现没有任何问题,一模一样,但是之前明明在AspenPlusV11上测试成功过,但是现在V14又不行了,很奇怪很奇怪,我只能怀疑可能是环境导致的。

我这里猜测的原因有以下几点:一就是Aspen版本问题,可能V14就是不兼容部分CAPE-OPEN的接口了;二是环境的问题,之前在V11版本成功是引用了CAPE-OPEN的一个集成环境,而不是本文中的tlb文件;三是Cpp编译器版本的问题,V11版本成功可能是使用了旧的编译器,而本文我使用的是最新版的VS2022,Cpp的编译器甚至还是测试版本的(因为我尝试将编译好的dll文件注册到虚拟机里的时候报错了Cpp的依赖库问题)。

但是为了能把这篇单元模块开发完结掉,我们接下来会忽略掉这个问题,让其在兼容性更好的COFE上运行,只需要修改其中一行代码即可:

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

注释掉这个计数函数,然后全部保存,编译,无问题;打开COFE进行测试:

入口流股:

计算完成:

闪蒸计算后的出口流股:

符合设置参数:

实现界面参数输入

一个完整的单元模块肯定是可以在流程模拟界面进行参数输入的,而不是现在这样改变参数还需要重新编译,所以接下来就来实现单元模块的界面。

经常做模拟的时候可以发现,AspenPlus在计算的时候会产生很多碎文件,命名很奇怪的那种小文件(如下图),还很多,但是计算完毕关闭Aspen的时候这些文件又都会自动被删除,这其实就是Aspen采用的传输参数的一种方法,临时文件法(下文会用到)。

单元模块的界面实现方式有以下两种:

界面底层一体化:

很好理解,本文中的底层就是上文中写的代码,也就是Cpp实现的,那么一体化也就是将界面的实现也封装在dll文件中,使用Cpp来实现,这种方式比较简单,但是Cpp写界面稳定性较差,多线程实现很难;

界面底层分离:

也就是说,界面和底层不封装在一起,而是通过某种手段来进行互相调用。有以下几种方式:

  • 界面与底层通过DLL接口相互调用:更简单直接,但限制了编写语言,且在功能较为复杂的时候限制了代码版本迭代,存在接口混乱的问题;
  • 界面与底层通过碎文件(临时文件)进行中转:方便,简单,快捷,但容易产生大量碎文件,可能存在越权问题等;碎文件也可以同时结合socket/HTTP方式,更高效,方便部署;
  • 界面与底层通过socket/HTTP通信:方便模块与模块的结合,模块非常独立,方便打包出售,可以利用服务器计算快速完成计算,但是模拟软件计算量较大,模块之间衔接比较紧密,并不太适合这种模拟,数据传输有延迟、丢包等风险;

接下来就开始实现,通过JavaSwing给上文中的单元模块写一个输入参数的UI界面,首先来设计一下这个UI,对于一个heater模块而言,需要设置出口温度和压力,也就是上文代码中规定的两个参数,那么这个UI就应该如图所示:

打开IDEA,新建一个Java项目,命名为 HeaterGUI:(名字随意)

新建一个Java类,命名为 HeaterGUI,构造一个窗体:

import javax.swing.*;
import java.awt.*;

public class HeaterGUI extends JFrame
{
    private JTextField textField_T;
    private JComboBox comboBox_T;

    public HeaterGUI(){
        // 窗体大小
        setSize(800,400);
        // 窗体标题
        setTitle("Laugh Heater");

        // 创建一个UI表格,2行1列
        setLayout(new GridLayout(2,1));
        // 创建输入温度面板
        JPanel panel_T = new JPanel();
        // 添加显示文本
        panel_T.add(new JLabel("请输入出口温度:"));
        // 添加一个文本框
        textField_T = new JTextField();
        panel_T.add(textField_T);
        // 添加一个下拉列表用来放温度单位
        String[] temperatureUnits = {"K", "C", "F"};
        comboBox_T = new JComboBox<>(temperatureUnits);
        panel_T.add(comboBox_T);
        // 将输入温度面板放在第一行第一个位置
        add(panel_T,0,0);

        // 关闭按钮
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 在创建窗口后,调用 setVisible(true) 来显示窗口
        setVisible(true);
    }

    public static void main(String[] args) {
        new HeaterGUI();
    }
}

运行测试一下:

输入框太小了,得改改:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.PrintWriter;

public class HeaterGUI extends JFrame implements ActionListener
{
    // 创建两个文本显示示例
    private JTextField textField_T, textField_P;
    // 创建两个文本输入框实例
    private JComboBox comboBox_T, comboBox_P;
    // 创建两个按钮实例
    private JButton button_Submit, button_Cancel;

    public HeaterGUI(){
        // 窗体大小
        setSize(400,200);
        // 窗体标题
        setTitle("Laugh Heater");

        // 创建一个UI表格,3行1列
        setLayout(new GridLayout(3,1));
        // 创建输入温度面板
        JPanel panel_T = new JPanel();
        // 添加显示文本
        panel_T.add(new JLabel("请输入出口温度:"));
        // 添加一个文本框
        textField_T = new JTextField();
        // 设置文本框的宽度
        textField_T.setColumns(10);
        // 将文本框添加到温度面板中
        panel_T.add(textField_T);
        // 添加一个下拉列表用来放温度单位
        String[] temperatureUnits = {"K", "C", "F"};
        comboBox_T = new JComboBox<>(temperatureUnits);
        // 将下拉列表添加到温度面板中
        panel_T.add(comboBox_T);
        // 将输入温度面板按次序添加在第一行第列
        add(panel_T);

        // 创建输入压力面板
        JPanel panel_P = new JPanel();
        // 添加显示文本
        panel_P.add(new JLabel("请输入出口压力:"));
        // 添加一个文本框
        textField_P = new JTextField();
        // 设置文本框的宽度
        textField_P.setColumns(10);
        // 将文本框添加到压力面板中
        panel_P.add(textField_P);
        // 添加一个下拉列表用来放压力单位
        String[] pressureUnits = {"Pa", "bar", "atm"};
        comboBox_P = new JComboBox<>(pressureUnits);
        // 将下拉列表添加到压力面板中
        panel_P.add(comboBox_P);
        // 将输入温度面板按次序添加在第二行第一列
        add(panel_P);

        // 创建一个按钮面板
        JPanel panel_SubmitAndCancel = new JPanel();
        // 创建确认按钮
        button_Submit = new JButton("确定");
        // 给按钮绑定事件监听
        button_Submit.addActionListener(this);
        // 将确认按钮添加到按钮面板中
        panel_SubmitAndCancel.add(button_Submit);
        // 创建取消按钮
        button_Cancel = new JButton("取消");
        // 给按钮绑定事件监听
        button_Cancel.addActionListener(this);
        // 将取消按钮添加到按钮面板中
        panel_SubmitAndCancel.add(button_Cancel);
        // 将按钮面板按次序添加到表格中第三行第一列
        add(panel_SubmitAndCancel);

        // 关闭按钮
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 在创建窗口后,调用 setVisible(true) 来显示窗口
        setVisible(true);
    }

    public static void main(String[] args) {
        new HeaterGUI();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // 提交按钮
        if(e.getSource() == button_Submit){
            // 获取输入框中的温度值,统一转换单位为K
            double temperature_Out = Double.parseDouble(textField_T.getText());
            // 单位转换C->K
            if(comboBox_T.getSelectedIndex() == 1) temperature_Out = temperature_Out + 273.15;
            // 单位转换F->K
            else if (comboBox_T.getSelectedIndex() == 2) temperature_Out = (temperature_Out-32)*5/9+273.15;

            // 获取输入框中的压力值,统一转换单位为Pa
            double pressure_Out = Double.parseDouble(textField_P.getText());
            // 单位转换bar->Pa
            if (comboBox_P.getSelectedIndex() == 1) pressure_Out = pressure_Out*100000;
            // 单位转换atm->Pa
            else if (comboBox_P.getSelectedIndex()== 2) pressure_Out = pressure_Out*101325;

            // 将输入结果输出到指定路径下的data.txt文件中暂存
            try {
                // 创建一个txt文件,注意这里的路径当前执行的用户要有权限进行访问
                PrintWriter pw = new PrintWriter(new File("C:/Users/laugh/Downloads/laughHeater_data.txt"));
                // 保存温度值
                pw.println(temperature_Out);
                // 保存压力值
                pw.println(pressure_Out);
                pw.close();
            } catch (FileNotFoundException ex) {
                throw new RuntimeException(ex);
            }
        }
        // 取消按钮
        else if (e.getSource() == button_Cancel){
            // 如果点击取消按钮,则直接返回空,并触发exit关闭窗口
        }
        System.exit(0);
    }
}

然后执行,测试一下:

欧克,没什么问题,在CMD或者power shell中执行测试一下:

D:/SDK/Java/bin/java.exe -classpath D:/Code/Java-Vue/HeaterGUI/out/production/HeaterGUI/ HeaterGUI

注意JDK的路径和源文件所在的路径,以及输出的class文件、包名,大小写也要注意。

没什么问题,接着下一步。

单元模块耦合界面UI

打开VS,回到 HeaterExampleOperation.h 文件中,ICapeUtilities Methods 部分:

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

	// 调用写好的UI界面程序生成数据中转文件
	system("D:/SDK/Java/bin/java.exe -classpath D:/Code/Java-Vue/HeaterGUI/out/production/HeaterGUI/ HeaterGUI");

	return S_OK;
}

注意JDK的路径和源文件所在的路径。

还是在 HeaterExampleOperation.h 文件中,ICapeUnit Methods 部分:

STDMETHOD(Calculate)()
{
	// 实现闪蒸计算
	// 定义需要传入的参数
	double temperature, pressure, totalMoleFlow;
	CVariant moleComposition;
	// 调用获取入口流股物流对象参数
	GetOverallTPFlowComposition(temperature, pressure, totalMoleFlow, moleComposition);

	// 临时定义参数部分
	//temperature = 400;	// 默认单位为 K
	//pressure = 301325;	// 默认单位为 Pa

	// 读取UI界面程序输出的数据中转文件中的温度和压力
	ifstream file("C:/Users/laugh/Downloads/laughHeater_data.txt");
	file >> temperature >> pressure;
	file.close();

	// 设置出口流股物流对象参数
	SetOverallTPFlowCompositionAndFlash(temperature, pressure, totalMoleFlow, moleComposition);

	return S_OK;
}

注意要读取的文件路径和上文中Java界面UI程序中的临时文件输出路径一致。

还是在 HeaterExampleOperation.h 文件中,顶部部分:

// HeaterExampleOperation.h: CHeaterExampleOperation 的声明

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

#include "PortsArray.h"		// 添加对 PortsArray 的引用
#include "Variant.h"		// 添加对 Variant 的引用
#include "ParametersArray.h"	// 添加对 ParametersArray 的引用
#include <string>		// 添加对 wstring 的引用
#include <fstream>		// 添加对 ifstream 的引用
#include <cstdlib>		// 添加对 cstdlib 的引用
using namespace std;

#include "HeaterExample_i.h"

全部保存,重新编译,无错误;打开COFE进行测试:

入口流股:

双击模块,点击Edit:

还是有bug,点击确定之后关闭这个页面就会闪退,唉,放弃了。

结束语

自从了解到CAPE-OPEN以来,已经过去了一年多了,每天也就只有下班了回到宿舍那么一两个小时可以学习,期间遇到了很多问题,但一直磕磕绊绊都过来了,

现在,我确实是决定放弃了,这一年里我感觉自己就是无头苍蝇,这里一榔头,那里一棒子,没有明确的目标,也没有引路人,虽然说B站的蔡老师(ID:bcbooo)确实教会了我很多,但同时也带来了更多疑惑,我当然知道自己这是底子不扎实导致的,

但究其根本的原因,我在摸索的这一年里真的深深的感受到了CAPE-OPEN的满满的恶意,接口不规范,方法用法混乱,文档虽然非常详细但压根不适合新手入门,对于一个不是计算机科班出身的人来说真的太难了,整个项目我就重构了三次之多,蔡老师的几个视频我更是翻看了一遍又一遍,甚至我2024年的年度up主就是蔡老师,

但最终我还是失败了,深深的有一种挫败感,

写下这段结束语的时候,我只想对自己说释怀吧,是时候转移方向了,别这么死磕了,可能就是自己不适合,

关于CAPE-OPEN的我自己的写代码的过程就记录了接近三四万字,虽然我在这条路上是个失败者,但也能希望我这些过程能给同样对CAPE-OPEN或者流程模拟软件开发感兴趣的同学一点帮助,

最后,祝好。

本文中的所有代码:

https://github.com/laugh0608/LaughHeaterTest