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

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;
}

然后给前文的 PortsArray​ 和 ParametersArray​ 数组实例化都加上计数函数:

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​ 文件中,来实现闪蒸算法,首先封装了三个函数,分别是获取进口流股数据、计算并进行闪蒸、赋值给出口流股,分别如下:

第一个获取参数函数:

// 获取进口流股物流对象中的参数,主要为温度、压力、摩尔流量、摩尔组成
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;
}

第二个赋值函数:

// 将计算完毕的参数赋值给流股并执行一次闪蒸
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;
}

第三个闪蒸函数:

// 闪蒸函数
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或者流程模拟软件开发感兴趣的同学一点帮助,

最后,祝好。

本文中的所有代码: