FreeCAD中数据显示流程
济南友泉软件有限公司
目录
Open Inventor(以下简称OIV)是SGI公司使用C++编写的基于OpenGL的面向对象三维图形软件开发包。使用OIV开发包,程序员可以快速、简洁地开发出各种类型的交互式三维图形软件。OIV具有平台无关性,它可以在Microsoft Windows、Unix、Linux等多种操作系统中使用。OIV允许使用C、C++、Java、DotNet多种编程语言进行程序开发。经过多年的发展,OIV已经基本上成为面向对象的3D图形开发“事实上”的工业标准。广泛地应用于机械工程设计与仿真、医学和科学图像、地理科学、石油钻探、虚拟现实、科学数据可视化等领域。
OIV目前主要由三家公司负责维护,相应的有三个主要的版本。SGI最早提出并开发OIV的UNIX版本;TGS公司最早将OIV由Unix系统移植到Microsoft Windows下的公司;SIM公司开发的Coin3D OIV可以同时在UNIX和Microsoft Windows下使用,Coin3D OIV免费版本的使用协议采用的是GPL协议,但如果要在商业软件使用Coin3D,商业用户需要每年支付一笔很少的开发费用。
目前,FreeCAD使用的是Coin3D OIV这个版本。
由于OIV采用C++面向对象的编程思想进行设计,因此相对于直接调用OpenGL C API来说,使用OIV开发图形图像软件将会更加简单而且开发效率更高。
使用OIV编写应用程序比较简单,其要点是根据需要把景物节点组装成景物(Scene),并设置景物节点的事件及其响应。
- 景物与节点
OIV使用层次化的树状结构来存储模型数据,其中的节点称之为景物节点(SoNode及其子类);而景物(Scene)则是由若干景物节点组成的;景物图(Scene)则指场景中景物的集合。
SoCube |
立方体 |
|
|
|
|
- 行为
行为(SoAction及其子类)主要用来渲染景物。
SoGLRenderAction |
应用GL图形库对景物图进行渲染 |
SoGetBoundingBoxAction |
在景物途中计算一个对象的三维包围盒 |
SoWriteAction |
将景物图写入文件 |
SoHandleEventAction |
处理事件 |
SoRayPickAction |
沿直线选择对象 |
SoCallbackAction |
调用回调函数 |
- 事件
事件(SoEvent及其子类)用来表示键盘、鼠标等外部设备对景物的操作。
SoButtonEvent |
|
SoLocation2Event |
|
SoMotion3Event |
|
- 传感器
传感器(SoSensor及其子类)用于检测各类事件,当有事件发生时便会调用注册的对应回调函数。
- 引擎
引擎(SoEngine及其子类)用于景物运动模拟,适合于物体运动、机械结构关节点等应用场景。
虚基类SoBase不仅提供了类型管理功能。而且实现了基于引用计数的内存管理。每个对象都有一个引用计数的变量,只要它被使用过一次,引用计数就加1,不再使用时,引用计数就减1,如果引用计数变成0,那么这个对象就自动被删除。
通过下面的代码可以直观地了解OIV程序开发的流程,
#include <Inventor/Qt/SoQt.h>
#include <Inventor/Qt/viewers/SoQtExaminerViewer.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoMaterial.h>
int main(int, char **argv)
{
//初始化Inventor
QWidget *myWindow = SoQt::init(argv[0]);
//创建观察器
SoQtExaminerViewer *myViewer = new SoQtExaminerViewer(myWindow);
//创建场景
SoSeparator *root = new SoSeparator;
SoMaterial *myMaterial = new SoMaterial;
myMaterial->diffuseColor.setValue(1.0, 0.0, 0.0);//红色
root->addChild(myMaterial);
root->addChild(new SoCube);//增加上一个立方体
//观察器和场景相关联
myViewer->setSceneGraph(root);
//显示主窗口
myViewer->show();
SoQt::show(myWindow);
// 主循环
SoQt::mainLoop();
return 0;
}
程序运行结果如下:
App::Document采用层次化的树状结构存储文档数据,每个文档节点称之为文档对象(App::DocumentObjectObject)。
App::DocumentObjectObject包含描述对象的数据,提供了对象数据持久化的功能,同时采用Boost库中信号-槽机制用于管理数据更新操作的相应。
ViewProvider定义了各种数据在View3Dinventor(派生于MDIView)、TreeView中显示的接口。对于View3Dinventor,主要完成模型数据转换成OIV渲染数据结构的功能;对于TreeView,主要定义了节点图标、双击响应等方面的接口。
View3DInventor通过其内部View3DInventorViewer来完成在Qt窗口内使用OIV渲染几何文档数据。
- 设置景物图
为了显示模型,仅需要调用setSceneGraph()指定需要显示的所有景物的根节点。
void View3DInventorViewer::setSceneGraph(SoNode *root);
本节以Part模块中Part::Box类型文档对象来分析文档对象的创建、显示过程。
当在点击菜单[Part/Primitives/Cube]或者直接在工具栏上点击Cube图标,便会调用CmdPartBox::activated()函数,
void CmdPartBox::activated(int iMsg)
{
Q_UNUSED(iMsg);
QString cmd;
cmd = qApp->translate("CmdPartBox","Cube");
openCommand((const char*)cmd.toUtf8());
runCommand(Doc,"App.ActiveDocument.addObject(\\"Part::Box\\",\\"Box\\")");
cmd = QString::fromLatin1("App.ActiveDocument.ActiveObject.Label = \\"%1\\"")
.arg(qApp->translate("CmdPartBox","Cube"));
runCommand(Doc,cmd.toUtf8());
commitCommand();
updateActive();
runCommand(Gui, "Gui.SendMsgToActiveView(\\"ViewFit\\")");
}
在这个函数中,会调用App::Document::addObject()来创建Part::Box类型的文档对象。
在App::Document::addObject()中,会根据传入的对象文档类型创建对象文件,然后分别触发signalNewObject、signalActivatedObject等信号。
boost::signals2::signal<void(const App::DocumentObject&)> App::Document::signalNewObject;
boost::signals2::signal<void(const App::DocumentObject&)> App::Document:: signalActivatedObject;
DocumentObject * Document::addObject(const char* sType, const char* pObjectName, bool isNew)
{
Base::BaseClass* base = static_cast<Base::BaseClass*>(Base::Type::createInstanceByName(sType,true));
string ObjectName;
if (!base)
return 0;
if (!base->getTypeId().isDerivedFrom(App::DocumentObject::getClassTypeId())) {
delete base;
std::stringstream str;
str << "'" << sType << "' is not a document object type";
throw Base::TypeError(str.str());
}
App::DocumentObject* pcObject = static_cast<App::DocumentObject*>(base);
pcObject->setDocument(this);
// do no transactions if we do a rollback!
if (!d->rollback) {
// Undo stuff
if (d->activeUndoTransaction)
d->activeUndoTransaction->addObjectDel(pcObject);
}
// get Unique name
if (pObjectName && pObjectName[0] != '\\0')
ObjectName = getUniqueObjectName(pObjectName);
else
ObjectName = getUniqueObjectName(sType);
d->activeObject = pcObject;
// insert in the name map
d->objectMap[ObjectName] = pcObject;
// cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument())
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
// insert in the vector
d->objectArray.push_back(pcObject);
// insert in the adjacence list and reference through the ConectionMap
//_DepConMap[pcObject] = add_vertex(_DepList);
// If we are restoring, don't set the Label object now; it will be restored later. This is to avoid potential duplicate
// label conflicts later.
if (!d->StatusBits.test(Restoring))
pcObject->Label.setValue( ObjectName );
// Call the object-specific initialization
if (!d->undoing && !d->rollback && isNew) {
pcObject->setupObject ();
}
// mark the object as new (i.e. set status bit 2) and send the signal
pcObject->setStatus(ObjectStatus::New, true);
signalNewObject(*pcObject);
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
}
signalActivatedObject(*pcObject);
// return the Object
return pcObject;
}
在Gui::Document中,可以看到信号signalNewObject关联到了Gui::Document::slotNewObject,即
Document::Document(App::Document* pcDocument,Application * app)
{
d = new DocumentP;
d->_iWinCount = 1;
// new instance
d->_iDocId = (++_iDocCount);
d->_isClosing = false;
d->_isModified = false;
d->_pcAppWnd = app;
d->_pcDocument = pcDocument;
d->_editViewProvider = 0;
// Setup the connections
d->connectNewObject = pcDocument->signalNewObject.connect
(boost::bind(&Gui::Document::slotNewObject, this, _1));
d->connectDelObject = pcDocument->signalDeletedObject.connect
(boost::bind(&Gui::Document::slotDeletedObject, this, _1));
d->connectCngObject = pcDocument->signalChangedObject.connect
(boost::bind(&Gui::Document::slotChangedObject, this, _1, _2));
d->connectRenObject = pcDocument->signalRelabelObject.connect
(boost::bind(&Gui::Document::slotRelabelObject, this, _1));
d->connectActObject = pcDocument->signalActivatedObject.connect
(boost::bind(&Gui::Document::slotActivatedObject, this, _1));
d->connectActObjectBlocker = boost::signals2::shared_connection_block
(d->connectActObject, false);
d->connectSaveDocument = pcDocument->signalSaveDocument.connect
(boost::bind(&Gui::Document::Save, this, _1));
d->connectRestDocument = pcDocument->signalRestoreDocument.connect
(boost::bind(&Gui::Document::Restore, this, _1));
d->connectStartLoadDocument = App::GetApplication().signalStartRestoreDocument.connect
(boost::bind(&Gui::Document::slotStartRestoreDocument, this, _1));
d->connectFinishLoadDocument = App::GetApplication().signalFinishRestoreDocument.connect
(boost::bind(&Gui::Document::slotFinishRestoreDocument, this, _1));
d->connectExportObjects = pcDocument->signalExportViewObjects.connect
(boost::bind(&Gui::Document::exportObjects, this, _1, _2));
d->connectImportObjects = pcDocument->signalImportViewObjects.connect
(boost::bind(&Gui::Document::importObjects, this, _1, _2, _3));
d->connectUndoDocument = pcDocument->signalUndo.connect
(boost::bind(&Gui::Document::slotUndoDocument, this, _1));
d->connectRedoDocument = pcDocument->signalRedo.connect
(boost::bind(&Gui::Document::slotRedoDocument, this, _1));
d->connectTransactionAppend = pcDocument->signalTransactionAppend.connect
(boost::bind(&Gui::Document::slotTransactionAppend, this, _1, _2));
d->connectTransactionRemove = pcDocument->signalTransactionRemove.connect
(boost::bind(&Gui::Document::slotTransactionRemove, this, _1, _2));
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
_pcDocPy = new Gui::DocumentPy(this);
if (App::GetApplication().GetParameterGroupByPath
("User parameter:BaseApp/Preferences/Document")->GetBool("UsingUndo",true)){
d->_pcDocument->setUndoMode(1);
// set the maximum stack size
d->_pcDocument->setMaxUndoStackSize(App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")->GetInt("MaxUndoSize",20));
}
}
在Document::slotNewObject()函数中,主要完成以下工作:
- 根绝文档对象obj关联的ViewProvider类型,创建派生于ViewProviderDocumentObject的pcProvider,
- pcProvider调用attach()关联文档对象obj
- 将pcProvider添加到所有ViewInventor3D视图中进行显示
void Document::slotNewObject(const App::DocumentObject& Obj)
{
ViewProviderDocumentObject* pcProvider = static_cast<ViewProviderDocumentObject*>(getViewProvider(&Obj));
if (!pcProvider) {
//Base::Console().Log("Document::slotNewObject() called\\n");
std::string cName = Obj.getViewProviderName();
if (cName.empty()) {
// handle document object with no view provider specified
Base::Console().Log("%s has no view provider specified\\n", Obj.getTypeId().getName());
return;
}
setModified(true);
Base::BaseClass* base = static_cast<Base::BaseClass*>(Base::Type::createInstanceByName(cName.c_str(),true));
if (base) {
// type not derived from ViewProviderDocumentObject!!!
assert(base->getTypeId().isDerivedFrom(Gui::ViewProviderDocumentObject::getClassTypeId()));
pcProvider = static_cast<ViewProviderDocumentObject*>(base);
d->_ViewProviderMap[&Obj] = pcProvider;
try {
// if successfully created set the right name and calculate the view
//FIXME: Consider to change argument of attach() to const pointer
pcProvider->attach(const_cast<App::DocumentObject*>(&Obj));
pcProvider->updateView();
pcProvider->setActiveMode();
}
catch(const Base::MemoryException& e){
Base::Console().Error("Memory exception in '%s' thrown: %s\\n",Obj.getNameInDocument(),e.what());
}
catch(Base::Exception &e){
e.ReportException();
}
#ifndef FC_DEBUG
catch(...){
Base::Console().Error("App::Document::_RecomputeFeature(): Unknown exception in Feature \\"%s\\" thrown\\n",Obj.getNameInDocument());
}
#endif
}
else {
Base::Console().Warning("Gui::Document::slotNewObject() no view provider for the object %s found\\n",cName.c_str());
}
}
if (pcProvider) {
std::list<Gui::BaseView*>::iterator vIt;
// cycling to all views of the document
for (vIt = d->baseViews.begin();vIt != d->baseViews.end();++vIt) {
View3DInventor *activeView = dynamic_cast<View3DInventor *>(*vIt);
if (activeView)
activeView->getViewer()->addViewProvider(pcProvider);
}
// adding to the tree
signalNewObject(*pcProvider);
// it is possible that a new viewprovider already claims children
handleChildren3D(pcProvider);
}
}
运行结果:
TreeDockWidget通过内嵌的TreeWidget类型的对象来层次化地显示文档对象。
在TreeWidget的构造函数中,关联了新建文档等信号,可以看到App::Application::signalNewDocument信号关联到了Gui::TreeWidget::slotNewDocument。
TreeWidget::TreeWidget(QWidget* parent)
: QTreeWidget(parent), contextItem(0), fromOutside(false)
{
this->setDragEnabled(true);
this->setAcceptDrops(true);
this->setDropIndicatorShown(false);
this->setRootIsDecorated(false);
this->createGroupAction = new QAction(this);
this->createGroupAction->setText(tr("Create group..."));
this->createGroupAction->setStatusTip(tr("Create a group"));
connect(this->createGroupAction, SIGNAL(triggered()),
this, SLOT(onCreateGroup()));
this->relabelObjectAction = new QAction(this);
this->relabelObjectAction->setText(tr("Rename"));
this->relabelObjectAction->setStatusTip(tr("Rename object"));
#ifndef Q_OS_MAC
this->relabelObjectAction->setShortcut(Qt::Key_F2);
#endif
connect(this->relabelObjectAction, SIGNAL(triggered()),
this, SLOT(onRelabelObject()));
this->finishEditingAction = new QAction(this);
this->finishEditingAction->setText(tr("Finish editing"));
this->finishEditingAction->setStatusTip(tr("Finish editing object"));
connect(this->finishEditingAction, SIGNAL(triggered()),
this, SLOT(onFinishEditing()));
this->skipRecomputeAction = new QAction(this);
this->skipRecomputeAction->setCheckable(true);
this->skipRecomputeAction->setText(tr("Skip recomputes"));
this->skipRecomputeAction->setStatusTip(tr("Enable or disable recomputations of document"));
connect(this->skipRecomputeAction, SIGNAL(toggled(bool)),
this, SLOT(onSkipRecompute(bool)));
this->markRecomputeAction = new QAction(this);
this->markRecomputeAction->setText(tr("Mark to recompute"));
this->markRecomputeAction->setStatusTip(tr("Mark this object to be recomputed"));
connect(this->markRecomputeAction, SIGNAL(triggered()),
this, SLOT(onMarkRecompute()));
this->searchObjectsAction = new QAction(this);
this->searchObjectsAction->setText(tr("Search..."));
this->searchObjectsAction->setStatusTip(tr("Search for objects"));
connect(this->searchObjectsAction, SIGNAL(triggered()),
this, SLOT(onSearchObjects()));
// Setup connections
connectNewDocument = Application::Instance->signalNewDocument.connect(boost::bind(&TreeWidget::slotNewDocument, this, _1));
connectDelDocument = Application::Instance->signalDeleteDocument.connect(boost::bind(&TreeWidget::slotDeleteDocument, this, _1));
connectRenDocument = Application::Instance->signalRenameDocument.connect(boost::bind(&TreeWidget::slotRenameDocument, this, _1));
connectActDocument = Application::Instance->signalActiveDocument.connect(boost::bind(&TreeWidget::slotActiveDocument, this, _1));
connectRelDocument = Application::Instance->signalRelabelDocument.connect(boost::bind(&TreeWidget::slotRelabelDocument, this, _1));
QStringList labels;
labels << tr("Labels & Attributes");
this->setHeaderLabels(labels);
// make sure to show a horizontal scrollbar if needed
#if QT_VERSION >= 0x050000
this->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
#else
this->header()->setResizeMode(0, QHeaderView::ResizeToContents);
#endif
this->header()->setStretchLastSection(false);
// Add the first main label
this->rootItem = new QTreeWidgetItem(this);
this->rootItem->setText(0, tr("Application"));
this->rootItem->setFlags(Qt::ItemIsEnabled);
this->expandItem(this->rootItem);
this->setSelectionMode(QAbstractItemView::ExtendedSelection);
#if QT_VERSION >= 0x040200
// causes unexpected drop events (possibly only with Qt4.1.x)
this->setMouseTracking(true); // needed for itemEntered() to work
#endif
this->statusTimer = new QTimer(this);
connect(this->statusTimer, SIGNAL(timeout()),
this, SLOT(onTestStatus()));
connect(this, SIGNAL(itemEntered(QTreeWidgetItem*, int)),
this, SLOT(onItemEntered(QTreeWidgetItem*)));
connect(this, SIGNAL(itemCollapsed(QTreeWidgetItem*)),
this, SLOT(onItemCollapsed(QTreeWidgetItem*)));
connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)),
this, SLOT(onItemExpanded(QTreeWidgetItem*)));
connect(this, SIGNAL(itemSelectionChanged()),
this, SLOT(onItemSelectionChanged()));
this->statusTimer->setSingleShot(true);
this->statusTimer->start(300);
documentPixmap = new QPixmap(Gui::BitmapFactory().pixmap("Document"));
}
当新建文档时,调用TreeWidget:: slotNewDocument()完成了DocumentItem类型文档项的创建,对应文档模型的根节点。
void TreeWidget::slotNewDocument(const Gui::Document& Doc)
{
DocumentItem* item = new DocumentItem(&Doc, this->rootItem);
this->expandItem(item);
item->setIcon(0, *documentPixmap);
item->setText(0, QString::fromUtf8(Doc.getDocument()->Label.getValue()));
DocumentMap[ &Doc ] = item;
}
而在DocumentItem的构造函数中,完成了新建对象文档事件的关联。可以看到,Gui::Document::signalNewObject关联到了Gui::DocumentItem::slotNewObject
DocumentItem::DocumentItem(const Gui::Document* doc, QTreeWidgetItem * parent)
: QTreeWidgetItem(parent, TreeWidget::DocumentType), pDocument(doc)
{
// Setup connections
connectNewObject = doc->signalNewObject.connect(boost::bind(&DocumentItem::slotNewObject, this, _1));
connectDelObject = doc->signalDeletedObject.connect(boost::bind(&DocumentItem::slotDeleteObject, this, _1));
connectChgObject = doc->signalChangedObject.connect(boost::bind(&DocumentItem::slotChangeObject, this, _1));
connectRenObject = doc->signalRelabelObject.connect(boost::bind(&DocumentItem::slotRenameObject, this, _1));
connectActObject = doc->signalActivatedObject.connect(boost::bind(&DocumentItem::slotActiveObject, this, _1));
connectEdtObject = doc->signalInEdit.connect(boost::bind(&DocumentItem::slotInEdit, this, _1));
connectResObject = doc->signalResetEdit.connect(boost::bind(&DocumentItem::slotResetEdit, this, _1));
connectHltObject = doc->signalHighlightObject.connect(boost::bind(&DocumentItem::slotHighlightObject, this, _1,_2,_3));
connectExpObject = doc->signalExpandObject.connect(boost::bind(&DocumentItem::slotExpandObject, this, _1,_2));
connectScrObject = doc->signalScrollToObject.connect(boost::bind(&DocumentItem::slotScrollToObject, this, _1));
setFlags(Qt::ItemIsEnabled/*|Qt::ItemIsEditable*/);
}
在DocumentItem::slotNewObject中通过调用createNewItem创建树节点。
void DocumentItem::slotNewObject(const Gui::ViewProviderDocumentObject& obj) {
createNewItem(obj);
}
结合前述文档对象的创建过程可以看出,当App::Document::addObject()创建完成文档对象之后,触发App::Document::signalNewObject事件,由于此事件关联到了Gui::DocumentItem::slotNewObject槽函数,会调用createNewItem函数在TreeWidget创建对应的树节点。
boost::signals2::signal<void(const App::DocumentObject&)> App::Document::signalNewObject;
bool DocumentItem::createNewItem(const Gui::ViewProviderDocumentObject& obj,
QTreeWidgetItem *parent, int index, DocumentObjectItemsPtr ptrs)
{
const char *name;
if (!obj.showInTree() || !(name=obj.getObject()->getNameInDocument()))
return false;
if (!ptrs) {
auto &items = ObjectMap[name];
if (!items) {
items.reset(new DocumentObjectItems);
}
else if(items->size() && parent==NULL) {
Base::Console().Warning("DocumentItem::slotNewObject: Cannot add view provider twice.\\n");
return false;
}
ptrs = items;
}
std::string displayName = obj.getObject()->Label.getValue();
DocumentObjectItem* item = new DocumentObjectItem(
const_cast<Gui::ViewProviderDocumentObject*>(&obj), ptrs);
if (!parent)
parent = this;
if (index<0)
parent->addChild(item);
else
parent->insertChild(index,item);
// Couldn't be added and thus don't continue populating it
// and delete it again
if (!item->parent()) {
delete item;
}
else {
item->setIcon(0, obj.getIcon());
item->setText(0, QString::fromUtf8(displayName.c_str()));
populateItem(item);
}
return true;
}
附录A: OIV测试CMakeLists
#add_defintions(-D_FC_GUI_ENABLED_)
#add_defintions(-DFREECADMAINPY)
######################## OIV ########################
SET(OIV_SRCS
Test_OIV.cpp
)
include_directories(
${COIN3D_INCLUDE_DIRS}
)
SET(OIV_LIBS
${Boost_LIBRARIES}
${COIN3D_LIBRARIES}
${SOQT_LIBRARIE}
${OPENGL_gl_LIBRARY}
FreeCADGui
)
if (BUILD_QT5)
include_directories(
${Qt5Core_INCLUDE_DIRS}
${Qt5Widgets_INCLUDE_DIRS}
${Qt5OpenGL_INCLUDE_DIRS}
${Qt5PrintSupport_INCLUDE_DIRS}
${Qt5Svg_INCLUDE_DIRS}
${Qt5Network_INCLUDE_DIRS}
${Qt5UiTools_INCLUDE_DIRS}
)
list(APPEND OIV_LIBS
${Qt5Core_LIBRARIES}
${Qt5Widgets_LIBRARIES}
${Qt5OpenGL_LIBRARIES}
${Qt5PrintSupport_LIBRARIES}
${Qt5Svg_LIBRARIES}
${Qt5Network_LIBRARIES}
${Qt5UiTools_LIBRARIES}
)
else()
include_directories(
${QT_INCLUDE_DIR}
)
list(APPEND OIV_LIBS
${QT_LIBRARIES}
${QT_QTUITOOLS_LIBRARY}
)
endif()
add_definitions(-D SOQT_DLL)
add_executable(Test_OIV WIN32 ${OIV_SRCS})
target_link_libraries(Test_OIV ${OIV_LIBS})
SET_BIN_DIR(Test_OIV Test_OIV)
if(WIN32)
INSTALL(TARGETS Test_OIV
RUNTIME DESTINATION bin
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
elseif(APPLE AND NOT BUILD_WITH_CONDA)
INSTALL(TARGETS Test_OIV
RUNTIME DESTINATION MacOS
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
else()
INSTALL(TARGETS Test_OIV
RUNTIME DESTINATION bin
)
endif()
暂无评论内容