上回书说到使用C#的winform技术实现了调用STK引擎并且展示其3D/2D控件(使用C#(winform)调用STK并展示其3D/2D控件-CSDN博客),回头想着把C++和Qt的也整一下,毕竟相对于winform而言,还是更熟悉Qt一些,于是找了些资料,重新回头看了看,同样实现了调用STK并展示3D/2D控件的功能。本文主要参考了QT调用STK12(STKX模块)_qt stk-CSDN博客一文。由于采用vscode开发并且使用cmake构建,踩了一些坑,也一并记录一下。
本文使用STK11.6,CMake>3.5,vs2022(作为CMake的生成器和编译器),Qt5.14.2(其中的msvc2017_64)。
vscode不多介绍了,这里主要介绍一下所用到的插件,主要有三个,分别是用来配置C++的一个和两个配置cmake的。配置cmake的分别是CMake和CMake Tools两个插件,前者提供基本的语法高亮与函数补全,后者是Microsoft官方出品的用于和CMake进行交互并触发构建、运行等功能的插件。
关于C++的插件这里有必要说一嘴,有两个常用的,一个是Microsoft官方出的C/C++,另一个是LLVM出的clangd,个人感觉前者在智能提示和语法解析的时候有点慢,于是一般用后者,但是在这里,由于STK是安装在Windows上的,所提供的组件也是以COM组件形式提供的,有大量特异于Windows平台的API被调用,因此CMake生成器和编译器部分必须选择vs(我这里用的vs2022),不能够是mingw或者clang之类的,而以vs作为生成器时,CMake不会生成compile_commands.json,因此clangd也无法提供语法提示,所以这里一定要选择Microsoft官方出的C/C++插件!
安装完插件后,接下来规划项目结构,我的项目结构如下所示
其中
- .vscode是vscode的配置文件夹
- build文件夹不用管,是cmake构建和生成程序的文件夹
- CppIncludes是STK官方所提供的C++二次开发库
- src是源码目录
- CMakeLists.txt是CMake配置文件
别着急,接下来我们一个个来仔细看。
.vscode文件夹中包含两个文件,settings.json和tasks.json,前者是当前工作区设置,后者是任务。在这里,我的settings.json的内容是
"cmake.configureEnvironment": {
"CMAKE_PREFIX_PATH": "xxx/5.14.2/msvc2017_64/lib/cmake"
}
其中 xxx 是你安装Qt的路径,我这里Qt的版本用的是5.14.2,5.14.2最高带的msvc的版本就到2017,用的64位(因为STK也是安装的64位,之后生成程序也要生成64位的)。这一句设置主要是告诉cmake插件,在构建时传入一个CMAKE_PREFIX_PATH变量,其值为多少。这个变量帮助cmake在find_package时优先寻找某些目录,如果你的cmake找不到qt的cmake配置文件,可以试着加上这个设置。
tasks.json的内容是
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "run",
"type": "shell",
"command": "./build/Debug/qtstk.exe",
"options": {
"env": {
"path": "$env:path;xxx/5.14.2/msvc2017_64/bin;"
}
}
}
]
}
这一段也是可有可无,他主要是为了在生成程序之后运行,由于cmake自带的运行(launch)在我写文章的此时还不支持自动加入环境变量,我又不愿意将qt的dll库路径加入全局环境变量污染,因此每次只能手动加入,写个task帮我自动加。这里我的程序的名称是qtstk.exe,其路径在build文件夹下的Debug文件夹下,这和一般用mingw生成的位置不太一样。xxx是你安装qt的路径。
CppInlcudes文件夹要到 你的STK安装路径/CodeSamples/ 这个路径下,有个压缩包,也叫CodeSamples,解压缩进入后,找到CommonFiles文件夹,里面就有这个CppInlcudes文件夹,统一一把复制到项目目录下面,这个后面有大用。
最后再讲src源码,先来讲讲CMakeLists.txt怎么写,我的CMakeLists.txt内容如下
cmake_minimum_required(VERSION 3.5)
project(qtstk CXX)
if(MSVC_IDE)
add_compile_options(/utf-8)
endif()
find_package(Qt5 COMPONENTS
Core
Widgets
AxContainer
REQUIRED
)
file(GLOB sources ${CMAKE_SOURCE_DIR}/src/*)
add_executable(${PROJECT_NAME} ${sources})
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/CppIncludes/
)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt5::Widgets
Qt5::AxContainer
)
set_target_properties(${PROJECT_NAME} PROPERTIES
AUTOMOC ON
AUTORCC ON
AUTOUIC ON
)
不得不佩服cmake的自动化性质,全篇没有一个绝对路径,而将绝对路径的配置都可以排除到文件之外,提供了极大的自动化能力和跨平台兼容性。
项目的名称叫做qtstk,也使用该名称作为生成的可执行目标的名称。
find_package找寻qt库,并且要求包含Core、Widgets和AxContainer(大小写注意)三个组件,其中AxContainer主要是容纳ActiveX控件的,因为STK的3D/2D控件是以这种形式提供的。
注意target_include_directories为生成目标添加include path,将之前STK提供的文件夹加入进去了。
后面就都是些稀松平常的内容了,有使用cmake构建编译qt应用的经验应该很容易看懂。
然后点击右边安装了cmake tools插件后提供的功能页面。
首先点击配置下面第一行配置工具包,选择vs2022,注意架构。
然后就没啥了,你也可以再点下生成目标和启动目标,不过无所谓,整个项目目前也只有一个目标,要么就是all,不影响。
选完工具包之后应该会进入第一次配置,等他下面输出结束后,看着没问题,就可以开始写代码了,不过考虑到当新加cpp文件后cmake所提供的智能感知会失效,最好是先新建要编写的文件,然后重新点一下配置(不是配置工具包)或是删除缓存并重新配置,然后再编码。
最后,我们来讲下代码。代码部分很简单,总共5个文件,main.cpp用于提供程序入口,stk.hpp和stk.cpp用于提供stk头文件include,STKWidget.hpp和STKWidget.cpp构建一个界面,最后显示的也是该界面。
首先是stk.hpp
cpp
#pragma once
#include "agstkutil.tlh"
using namespace STKUtil;
#include "agvgt.tlh"
//
#include "agstkgraphics.tlh"
#include "agstkobjects.tlh"
using namespace STKObjects;
#include "stkx.tlh"
using namespace STKXLib;
stk.cpp
cpp
#include "stk.hpp"
#include "agstkutil.tli"
//
#include "agstkgraphics.tli"
#include "agstkobjects.tli"
#include "stkx.tli"
上面两个文件一定要注意顺序!include的顺序不能乱!
然后是STKWidget.hpp
cpp
#pragma once
#include "stk.hpp"
#include <QAxWidget>
#include <QtWidgets>
class STKWidget : public QWidget {
Q_OBJECT
private:
IAgStkObjectRootPtr m_pRoot;
IAgSTKXApplicationPtr m_app;
public:
STKWidget(QWidget* parent = nullptr);
};
STKWidget.cpp
cpp
#include "STKWidget.hpp"
#include <QDebug>
STKWidget::STKWidget(QWidget* parent)
: QWidget(parent)
{
::CoInitialize(nullptr);
HRESULT ha = m_app.CreateInstance(__uuidof(AgSTKXApplication));
if (FAILED(ha)) {
qDebug() << "AgSTKXApplication Failed!";
return;
}
HRESULT hr = m_pRoot.CreateInstance(__uuidof(AgStkObjectRoot));
if (FAILED(hr)) {
qDebug() << "AgStkObjectRoot Failed!";
return;
}
setWindowTitle("STK");
auto axw3d = new QAxWidget(this);
axw3d->setControl("STKX11.VOControl");
auto axw2d = new QAxWidget(this);
axw2d->setControl("STKX11.2DControl");
auto btn = new QPushButton(this);
btn->setFont(QFont("微软雅黑", 12));
btn->setText("New Scenario");
connect(btn, &QPushButton::clicked, [&]() {
m_pRoot->NewScenario("Test");
// or
// m_app->ExecuteCommand("Unload / *");
// m_app->ExecuteCommand("New / Scenario Test");
});
auto grid = new QGridLayout(this);
grid->setColumnStretch(0, 2);
grid->setColumnStretch(1, 1);
grid->addWidget(axw3d, 0, 0, 10, 10);
grid->addWidget(axw2d, 0, 10, 5, 5);
grid->addWidget(btn, 7, 12, 1, 1);
}
最开始的 ::CoInitialize(nullptr); 这一句话不要忘记。后面是初始化app和root的代码。再后面这里注意按钮的响应函数,可以直接NewScenario,也可以通过app执行指令。
最后就是main.cpp啦
cpp
#include "STKWidget.hpp"
#include <QtWidgets>
int main(int argc, char** argv)
{
QApplication app(argc, argv);
STKWidget w;
w.show();
app.exec();
return 0;
}
很简单的内容,主要呈现STKWidget。
构建、编译、运行程序,应该有一个类似这样的界面出现
点击按钮,经过一小段时间的加载,就能够出现我们想要的场景啦!