单元模块开发入门实例Heater(二)
前言
因为距离上一次开发已经过去了好几个月,中间因为重建博客和论坛的原因,同样也是因为在《单元模块开发入门实例Heater(一)》篇中最后遗留的bug因素,所以打算重新详细的讲解一下一个单元模块的构建。
第一次开发这个模块的时候都是24年2月份的时候了,是在同年10月份重新开始CAPE-OPEN(以下简称CO)的学习和开发,在11月份解决了无法计算和闪退的bug,发布了第一版BlockTest01,这一版其实已经和刚开始2月份的myBlockTest那一版有了很多的不同,担心很多人再开始看今天的这个Heater模块开发实例(二)的时候看不懂,所以本文开始直接从零详细的一步步讲解如何构建一个CO单元模块。
虽然说本文中代码是重新演示的,其中的原理或者接口用法,可以去回顾《热力学物性包开发篇》和《单元模块开发入门篇》,哪一个步骤有疑惑也可以去翻看这两篇讲解。
同时,如果有哪一步没跟上,或者不理解,也可以参考上面这三篇中的源码,地址如下:
- https://github.com/laugh0608/BlockTest01
- https://github.com/laugh0608/myBlockTest
- https://github.com/laugh0608/myThermoTest
备注: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/98 和 https://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.h
和 Variant.h
这两个文件,将其复制到项目根目录下,右键点击-添加-现有项:
选择刚才复制进来的两个文件,点击确定:
在 HeaterExampleOperation.h
文件中的头部,添加对 Variant.h
文件的引用:
// HeaterExampleOperation.h: CHeaterExampleOperation 的声明
#pragma once
#include "resource.h" // 主符号
#include "PortsArray.h" // 添加对 PortsArray 的引用
#include "Variant.h" // 添加对 Variant 的引用
#include "HeaterExample_i.h"
在刚引入的 Variant.h
文件中的头部,添加对 std::wstring
的引用:
#pragma once
#include "BSTR.h"
// 添加对 wstring 的引用
#include <string>
using namespace std;
如图:
返回 HeaterExampleOperation.h
文件中,继续完善 ICapeUnit Methods
部分:
// ICapeUnit Methods
public:
STDMETHOD(get_ports)(LPDISPATCH *ports)
{
// 创建端口数组
//CComObject<CPortsArray> *pPortArray;
// 实例化创建的端口数组
CComObject<CPortsArray>::CreateInstance(&pPortArray);
// 返回获取的 ports 结果
pPortArray->QueryInterface(IID_IDispatch, (LPVOID*)ports);
return S_OK;
}
STDMETHOD(get_ValStatus)(CapeValidationStatus *pValStatus)
{
// 默认端口状态可用
*pValStatus = CapeValidationStatus::CAPE_VALID;
return S_OK;
}
STDMETHOD(Calculate)()
{
// 计算部分先空着
return S_OK;
}
STDMETHOD(Validate)(BSTR *message, VARIANT_BOOL *pValidateStatus)
{
// 带有检查功能的状态获取
CBSTR msg(L"NO ERROR");
*message = msg;
// 状态:成功
*pValidateStatus = TRUE;
return S_OK;
}
全部保存,编译一下,没问题;
ICapeUtilities
点击视图-类试图,右键 CHeaterExampleOperation
-添加-实现接口:
返回 HeaterExampleOperation.h
文件中,检查接口是否添加:
按照惯例,修改一下返回的参数名称,并且填充一下相关的内容:
在
HeaterExampleOperation.h
文件中,ICapeUtilities Methods
部分:
// ICapeUtilities Methods
public:
STDMETHOD(get_parameters)(LPDISPATCH *pParameters)
{
// 暂时忽略这个接口,赋值为空(与工况分析、灵敏度分析等有关)
*pParameters = NULL;
return S_OK;
}
STDMETHOD(put_simulationContext)(LPDISPATCH pSimulationContext)
{
// 该接口是当单元模块状态异常(如计算陷入死循环)时,单元模块与模拟软件通信,告诉模拟软件单元模块状态异常,需要强制结束
// 这里暂时不做实现
return S_OK;
}
STDMETHOD(Initialize)()
{
// 端口数组已在前文的构造函数 CHeaterExampleOperation() 中初始化完成,这里直接返回 OK 即可
return S_OK;
}
STDMETHOD(Terminate)()
{
// 单元模块卸载,这里暂时不做实现,返回空结果
return S_OK;
}
STDMETHOD(Edit)()
{
// 双击单元模块的逻辑,显示一个弹窗
MessageBox(NULL, L"Hello World", L"by laugh", MB_OK);
return S_OK;
}
在上一段中,因为实例初始化只需要一次即可,也就是 ICapeUnit Methods
中的 pPortArray
变量,所以之前写的位置不对,需要将初始化挪到前文中的 CHeaterExampleOperation()
部分,如图:
复制并注释上图这一行,粘贴到下图中的位置:
全部保存,编译一下,没有报错;
ParametersArray
和 PortsArray
类似,Parameter
也是一个数组,所以需要一个ATL简单对象来承载它,右键项目-添加-新建项:
选择ATL简单对象,命名为 ParametersArray
:
属性页面默认即可,直接点确定;
添加好的简单对象如图:
ICapeCollection
和 PortsArray
一样,ParametersArray
简单对象也是只需要一个 ICapeCollection
接口即可,点击视图-类试图切换到类试图,右键 CParametersArray
,添加-实现接口:
添加好的接口如图:
这里的 ParametersArray
数组实际上并不需要真的传值过去,只需要具有这个功能即可,所以实现很简单,当然和之前的类似,记得修改相关的参数名称:
在
ParametersArray.h
文件中,ICapeCollection Methods
部分:
// ICapeCollection Methods
public:
STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
{
// 返回一个空数组,暂不做实现
return S_OK;
}
STDMETHOD(Count)(long *pCount)
{
// 返回一个空数组,暂不做实现
*pCount = NULL;
return S_OK;
}
返回 HeaterExampleOperation.h
文件中,在头部加入对 ParametersArray.h
文件的引用:
在
HeaterExampleOperation.h
文件中,头部部分:
// HeaterExampleOperation.h: CHeaterExampleOperation 的声明
#pragma once
#include "resource.h" // 主符号
#include "PortsArray.h" // 添加对 PortsArray 的引用
#include "Variant.h" // 添加对 Variant 的引用
#include "ParametersArray.h" // 添加对 ParametersArray 的引用
#include "HeaterExample_i.h"
创建 Parameters
数组实例并进行初始化:
在
HeaterExampleOperation.h
文件中,CHeaterExampleOperation
部分:
private:
// 创建端口数组
CComObject<CPortsArray> *pPortArray;
// 创建 Parameter 参数集数组
CComObject<CParametersArray>* pParametersArray;
public:
CHeaterExampleOperation()
{
// 实例化创建的端口数组
CComObject<CPortsArray>::CreateInstance(&pPortArray);
// 实例化创建的 Parameters 参数集数组
CComObject<CParametersArray>::CreateInstance(&pParametersArray);
}
更改上文中的 get_parameters
函数中获取 parameters
的方式:
在
HeaterExampleOperation.h
文件中,ICapeUtilities Methods
部分:
STDMETHOD(get_parameters)(LPDISPATCH *pParameters)
{
// 暂时忽略这个接口,赋值为空(与工况分析、灵敏度分析等有关)
//*pParameters = NULL;
// 返回获取的 parameters 结果
pParametersArray->QueryInterface(IID_IDispatch, (LPVOID*)pParameters);
return S_OK;
}
全部保存,编译一下,没有报错;
但是发现还是有问题,经过断点测试,应该是在 get_port
的过程中出现了问题,对获取端口的方式进行修正,回到 MaterialPort.h
文件中,补充一下端口的参数:
在
MaterialPort.h
文件中,CMaterialPort
部分:
private:
// 创建一个物流对象连接实例
//LPDISPATCH pMaterialObject;
IDispatch *pMaterialObject;
// 传入参数,为端口流股方向
CapePortDirection pDirection;
public:
//CMaterialPort()
CMaterialPort(CapePortDirection pDirection)
{
// 给物流对象链接状态实例赋一个初始值
pMaterialObject = NULL;
// 将端口方向参数传入公有
this->pDirection = pDirection;
}
将下面的 ICapeUnitPort Methods
部分的获取方式也更改一下:
在
MaterialPort.h
文件中,ICapeUnitPort Methods
部分:
STDMETHOD(get_direction)(CapePortDirection *portDirection)
{
// 设置端口流股方向为进口
//*portDirection = CapePortDirection::CAPE_INLET;
// 改为参数传入形式
*portDirection = this->pDirection;
return S_OK;
}
来到 PortsArray.h
文件中,修改端口数和端口实例:
private:
// 创建一个端口实例
CComObject<CMaterialPort> *port1;
// 创建另一个端口实例,单元模块最少两个端口,一进一出
CComObject<CMaterialPort> *port2;
但是到这里又发现不对了,port1/2
是带参数的,明显不能使用 CComObject<CMaterialPort>::CreateInstance()
方式来分别进行实例化,所以说明刚才的设置端口方向的 CMaterialPort(CapePortDirection pDirection)
方式是有问题的;
返回到 MaterialPort.h
文件中,手动创建一个设置端口方向的函数:
在
MaterialPort.h
文件中,CMaterialPort
部分:
private:
// 创建一个物流对象连接实例
//LPDISPATCH pMaterialObject;
IDispatch *pMaterialObject;
// 传入参数,为端口流股方向
CapePortDirection pDirection;
public:
CMaterialPort()
//CMaterialPort(CapePortDirection pDirection)
{
// 给物流对象链接状态实例赋一个初始值
pMaterialObject = NULL;
// 将端口方向参数传入公有
this->pDirection = pDirection;
}
// 设置端口流股方向
void SetDirection(CapePortDirection pDirection) {
// 将端口方向参数传入共有
this->pDirection = pDirection;
}
回到 PortsArray.h
文件中,继续实例化两个端口:
在
PortsArray.h
文件中,CPortsArray
部分:
private:
// 创建一个端口实例
CComObject<CMaterialPort> *port1;
// 创建另一个端口实例,单元模块最少两个端口,一进一出
CComObject<CMaterialPort> *port2;
public:
CPortsArray()
{
// 实例化创建的端口1
CComObject<CMaterialPort>::CreateInstance(&port1);
// 设置端口1方向
port1->SetDirection(CapePortDirection::CAPE_INLET);
// 实例化创建的端口2
CComObject<CMaterialPort>::CreateInstance(&port2);
// 设置端口2方向
port2->SetDirection(CapePortDirection::CAPE_OUTLET);
}
同时修改下方的端口数量:
在
PortsArray.h
文件中,ICapeCollection Methods
部分:
// ICapeCollection Methods
public:
STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem)
{
// 给实例化好的端口进行赋值
port1->QueryInterface(IID_IDispatch, (LPVOID*)pItem);
return S_OK;
}
STDMETHOD(Count)(long *pCount)
{
// 定义端口数
*pCount = 2;
//*pCount = 1;
return S_OK;
}
这个时候问题就来了,在这里的 STDMETHOD(Item)(VARIANT id, LPDISPATCH *pItem){}
方法里,获取到的id是端口号还好说,比如 id=1
那就是端口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;
}
然后给前文的 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
文件中,来实现闪蒸算法,首先封装了三个函数,分别是获取进口流股数据、计算并进行闪蒸、赋值给出口流股,分别如下:
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或者流程模拟软件开发感兴趣的同学一点帮助,
最后,祝好。
本文中的所有代码: