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或者流程模拟软件开发感兴趣的同学一点帮助,
最后,祝好。
本文中的所有代码: