用QT制作聆心云数据可视化平台
因為博客園越來越不好用,所以就此轉戰csdn。
之所以要制作這個可視化平臺呢,是因為了完成【數據可視化】課程的大作業,一篇綜述和聆心云平臺的數據展示。
好吧,現在先來看作業要求:
各位同學好,就本課程的期末期末考試說明如下:?
本課程的期末考試不采用閉卷考試的形式,而是采用平時大作業+文獻綜述的形式。具體要求如下:?
1. 寫一篇最新的可視化理論研究、技術研究、產品開發等方面的綜述文章(成績占比50%)?
。?
要求(1)閱讀10篇左右相關中外文論文,(2)論文篇幅在5000字的(圖文并茂)
2. 開發一個可視化系統(成績占比50%)。?
利用自己比較熟悉的可視化編程工具(推薦Python,Java+ECharts或Pyecharts)等,開發一個基于“聆心云3D在線數字化沙盤游戲”的自我沙盤游戲數據的可視化系統。
要求(1)注冊聆心云賬號;
(2)做30個沙盤游戲,保存在個人賬號里;
(3)數據的獲取、整理、存儲;
(4)可視化展示:要求展示不少于3種可視化形式(在所有作品使用沙具名稱的詞云;使用頻率最高的10個沙具的使用次數的柱狀圖;10個最多的操作行為的占比餅圖;等)
(5)撰寫系統設計文檔要求:a. 需求分析;b.設計規劃:數據來源和獲取方法,數據存取和處理形式;c.可視化作品的編碼實現過程,采用什么工具,如何編碼實現;d. 心得體會。?
3. 最后提交作業的形式:作業1為電子文檔,作業2為可軟件系統代碼+設計文檔和系統運行結果。?
4. 每個作業都獨立完成,提交截止日期2023.2.20。
綜述,前段時間放了兩周疫情假,趁假期搞定了已經,而這個可視化系統,花了我將近一周的時間。
首先看最終效果:
視頻效果
接下來我們來看下怎么制作的。
需求分析
根據作業需求,我們需要從聆心云數據接口爬取數據、數據持久化與讀取、數據展示等幾大步驟。
在數據爬取上,根據接口要求,我們需要才用http get的方式爬取數據
在數據持久化和讀取上,起初我考慮的是mysql\mongo\redis等數據庫,但考慮到這些方式都需要客戶機上特別安裝數據庫并配置,不利于程序的傳播,所以我決定使用文件的方式進行數據持久化。
進行數據展示的時候,我們可以使用多種語言工具進行編程,比如Python\Java+ECharts或Pyecharts等,但考慮到我個人的代碼熟悉情況,我決定使用Qt進行編程。
設計規劃
數據來源和獲取方法
1、用戶需要注冊聆心云賬號,并進行一定數量的沙盤游戲
2、可通過
https://lingxinyun.cn/sp/getIdByAuth?mobile=用戶名&passwd=密碼
獲取用戶所有游戲局數的步數和對應游戲ID,結果如下
? ??[{"id":25930,"cnt":47},{"id":25933,"cnt":349},{"id":25934,"cnt":473},{"id":26308,"cnt":131},{"id":26311,"cnt":276},{"id":26318,"cnt":91},{"id":26324,"cnt":371},{"id":26909,"cnt":443},{"id":26915,"cnt":279},{"id":26940,"cnt":166},{"id":26944,"cnt":326},{"id":26945,"cnt":224},{"id":26946,"cnt":619},{"id":26972,"cnt":747},{"id":26973,"cnt":1196},{"id":27524,"cnt":1156},{"id":27525,"cnt":555},{"id":27545,"cnt":621},{"id":27569,"cnt":807},{"id":27570,"cnt":1622},{"id":27571,"cnt":737},{"id":27572,"cnt":844},{"id":27573,"cnt":907},{"id":27574,"cnt":795},{"id":27645,"cnt":398}]??3、可通過
https://lingxinyun.cn/sp/getSandPlay?id=游戲ID
獲取對應游戲局數的詳細操作以及道具,結果如下
["創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","刪除沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","移動沙具(草坪)","創建沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","縮放沙具(草坪)","移動沙具(草坪)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(草叢)","移動沙具(草叢)","創建沙具(蘭)","移動沙具(蘭)","創建沙具(菊)","移動沙具(菊)","創建沙具(蜜蜂)","移動沙具(蜜蜂)","縮放沙具(蜜蜂)","移動沙具(蜜蜂)","移動沙具(蜜蜂)","旋轉沙具(蜜蜂)","旋轉沙具(蜜蜂)","移動沙具(草坪)","移動沙具(草坪)","創建沙具(草房)","移動沙具(草房)","縮放沙具(草房)","創建沙具(女學生)","移動沙具(女學生)","移動沙具(女學生)","移動沙具(女學生)","創建沙具(母雞)","移動沙具(母雞)","縮放沙具(母雞)","創建沙具(母雞)","移動沙具(母雞)","縮放沙具(母雞)","旋轉沙具(母雞)","移動沙具(母雞)","創建沙具(兔)","移動沙具(兔)","縮放沙具(兔)","旋轉沙具(兔)","移動沙具(兔)","移動沙具(兔)","旋轉沙具(兔)","移動沙具(兔)","移動沙具(兔)","創建沙具(蘋果樹)","移動沙具(蘋果樹)","縮放沙具(蘋果樹)","縮放沙具(草坪)","縮放沙具(草坪)","縮放沙具(蘋果樹)"]4、進行json解析、字符串和統計操作,即可拆出所需的所有數據
數據存儲與處理形式
起初我考慮的是mysql\mongo\redis等數據庫,但考慮到這些方式都需要客戶機上特別安裝數據庫并配置,不利于程序的傳播,所以我決定使用文件的方式進行數據持久化。
單個文件大概如下
序列化存儲和反序列化讀取規則如下
序列化的時候單局數據與單局數據隔斷,使用|符號,單局內ID與json隔斷,使用&符號,這樣反序列化的時候,可以直接使用split函數,方便的進行數據拆解
可視化作品的編程實現過程
1、總體思路:
①通過接口爬取沙盤數據,并用文件進行存儲;
②使用C++對沙盤數據進行分析;
③分析結果用QT制作應用程序進行展示。
? ? ? 2、作業結構:
????
? ? ? 3、爬取并持久化沙盤數據
①獲取所有沙盤Id并根據沙盤ID獲取具體數據
void UserInfoMgr::doLogin(QString userName,QString userCryptPass) {QString targetUrl = QString("https://lingxinyun.cn/sp/getIdByAuth?mobile=%1&passwd=%2").arg(userName, userCryptPass);m_userName = userName;m_userPass = userCryptPass;QHttpMgr::me()->httpGet(targetUrl); }void UserInfoMgr::onLoginSuccess(QString url, QByteArray data) {// 解析參數auto parseRes = parseUri(url.toStdString().c_str());std::string absPath = std::get<0>(parseRes);auto paramMap = std::get<1>(parseRes);// 獲取roleID和typestd::string mobileNo = getParamValue(paramMap,"mobile");if (mobileNo != ""){if (data.length() == 0){qInfo("數據獲取失敗,url:%s", url.toStdString().c_str());emit onAllDataLoaded(1);return;}QJsonDocument jsonDoc = QJsonDocument::fromJson(QString(data).toUtf8());if(!jsonDoc.isArray()){qInfo("數據解析失敗,data:%s", QString(data).toStdString().c_str());emit onAllDataLoaded(2);return;}m_jsonIDArray = jsonDoc.array();for (int i=0;i < m_jsonIDArray.size();i ++){QJsonObject jsonTmp = m_jsonIDArray[i].toObject();QString targetUrl = QString("https://lingxinyun.cn/sp/getSandPlay?id=%1").arg(jsonTmp["id"].toInt());QHttpMgr::me()->httpGet(targetUrl);}} else {std::string szId = getParamValue(paramMap,"id");if (szId != ""){if (data.length() == 0){qInfo("數據獲取失敗,url:%s", url.toStdString().c_str());emit onAllDataLoaded(3);return;}QJsonDocument jsonDoc = QJsonDocument::fromJson(QString(data).toUtf8());if(!jsonDoc.isArray()){qInfo("數據解析失敗,data:%s", QString(data).toStdString().c_str());emit onAllDataLoaded(4);return;}QJsonArray tmpArray;tmpArray = jsonDoc.array();uint32_t uID = std::stoi(szId);m_detailMap[uID] = tmpArray;size_t currentMapNum = m_detailMap.size();if (currentMapNum >= (uint32_t)m_jsonIDArray.size()){QString szFullFilePath = QCoreApplication::applicationDirPath() + "/" + m_userName + ".lxcloud";this->saveFile(szFullFilePath);this->doAnalyze();this->createWordCloudPng();}} else {emit onAllDataLoaded(5);return;}} }②存儲沙盤數據
void UserInfoMgr::saveFile(QString filename) {//用IODevice方式保存文本文件QFile aFile(filename);//aFile.setFileName(aFileName);if (!aFile.open(QIODevice::WriteOnly | QIODevice::Text)){QString strInfo = "寫文件失敗,請檢查磁盤空間大小或聯系開發人員。";qWarning(strInfo.toStdString().c_str());return;}for (auto it = m_detailMap.begin(); it != m_detailMap.end(); ++it){uint32_t uID = it->first;QJsonArray jsonTmp = it->second;QJsonDocument document;document.setArray(jsonTmp);QByteArray byteArray = document.toJson(QJsonDocument::Compact);QString strJson = QString(byteArray);QString strWrite = QString("%1&%2|").arg(uID).arg(strJson);QByteArray strBytes=strWrite.toUtf8();aFile.write(strBytes, strBytes.length()); //寫入文件}aFile.close(); }4、數據展示
這里的沙盤操作是經過了拆分的,比如"移動沙具(草叢)"會被拆分成"移動沙具"和"草叢"
為了方便做數據統計,這里另外存儲了另一種格式的沙盤數據,這里存儲的是完整的操作
統計沙盤數據
void UserInfoMgr::doAnalyze() {for (auto it = m_detailMap.begin(); it != m_detailMap.end(); ++it){QJsonArray jsonTmp = it->second;for (int i = 0; i < jsonTmp.size(); i++){QString szTmp = jsonTmp[i].toString();int32_t leftBracketPos = szTmp.indexOf('(');QRegExp rx("[0-9\)]");int32_t rightBracketPos = szTmp.indexOf(rx);QString szOperate = szTmp.mid(0,leftBracketPos);QString szItem = szTmp.mid(leftBracketPos + 1,rightBracketPos - leftBracketPos - 1);if (leftBracketPos == -1){szOperate = szTmp;szItem = "";}auto itOperate = m_operateMap.find(szOperate);if (itOperate != m_operateMap.end()){uint32_t currNum = itOperate->second;itOperate->second = currNum + 1;} else {m_operateMap[szOperate] = 1;m_strOperateWC = m_strOperateWC + " " + szOperate;}auto itItem = m_itemMap.find(szItem);if (itItem != m_itemMap.end() && szItem!=""){uint32_t currNum = itItem->second;itItem->second = currNum + 1;} else {m_itemMap[szItem] = 1;m_strItemWC = m_strItemWC + " " + szItem;}}}this->doItemMapSort(); }沙盤數據可視化
餅圖:
這里使用的技術,主要是Qt自帶的QtCharts,可以很方便的制作餅圖,并且根據需求,我只展示了前10種最多的操作和道具,其余的,都被我歸并為了“其它”類
以下是代碼:
#ifndef PIECHARTFORM_H #define PIECHARTFORM_H#include <QWidget>namespace Ui { class PieChartForm; }class PieChartForm : public QWidget {Q_OBJECTpublic:explicit PieChartForm(QWidget *parent = nullptr);~PieChartForm();private:Ui::PieChartForm *ui; private:void createItemPie();void createOperatePie(); };#endif // PIECHARTFORM_H #include "PieChartform.h" #include "ui_PieChartform.h"#include "userInfoMgr.h"#include "drilldownchart.h" #include "drilldownslice.h" #include <QtCore/QRandomGenerator> #include <QtCharts/QChartView> #include <QtCharts/QLegend> #include <QtCharts/QPieSeries>PieChartForm::PieChartForm(QWidget *parent) :QWidget(parent),ui(new Ui::PieChartForm) {ui->setupUi(this);createItemPie();createOperatePie(); }PieChartForm::~PieChartForm() {delete ui; }void PieChartForm::createItemPie() {auto& itemVec = UserInfoMgr::me()->getSortedItemVec();QStringList labels;for (auto it = itemVec.begin();it != itemVec.end();it++){labels.push_back(it->name);}DrilldownChart *chart = new DrilldownChart();chart->setTheme(QChart::ChartThemeDark);chart->setAnimationOptions(QChart::AllAnimations);chart->legend()->setVisible(true);chart->legend()->setAlignment(Qt::AlignTop);QPieSeries *yearSeries = new QPieSeries();yearSeries->setName("道具分布圖");yearSeries->setHoleSize(0.3);for (int i=0;i < labels.size();i++) {QString name = labels[i];uint32_t nNum = itemVec[i].num;QPieSeries *series = new QPieSeries();*yearSeries << new DrilldownSlice(nNum, name, series);}chart->changeSeries(yearSeries);QChartView *chartView = new QChartView(chart);chartView->setRenderHint(QPainter::Antialiasing);ui->verticalLayout_4->addWidget(chartView); }void PieChartForm::createOperatePie() {auto& operateMap = UserInfoMgr::me()->getOperateMap();QStringList labels;for (auto it = operateMap.begin();it != operateMap.end();it++){labels.push_back(it->first);}DrilldownChart *chart = new DrilldownChart();chart->setTheme(QChart::ChartThemeLight);chart->setAnimationOptions(QChart::AllAnimations);chart->legend()->setVisible(true);chart->legend()->setAlignment(Qt::AlignTop);QPieSeries *yearSeries = new QPieSeries();yearSeries->setName("操作分布圖");for (const QString &name : labels) {QPieSeries *series = new QPieSeries();*yearSeries << new DrilldownSlice(operateMap[name], name, series);}chart->changeSeries(yearSeries);QChartView *chartView = new QChartView(chart);chartView->setRenderHint(QPainter::Antialiasing);ui->verticalLayout_3->addWidget(chartView); }以下是實際效果:
?
柱狀圖
考慮到Qt自帶的QtCharts在柱狀圖方面,表現不如第三方插件好用,所以我才用了第三方插件QCustomPlot來制作。
以下是代碼:
#ifndef BARCHARTFORM_H #define BARCHARTFORM_H#include <QWidget>namespace Ui { class BarChartForm; }class BarChartForm : public QWidget {Q_OBJECTpublic:explicit BarChartForm(QWidget *parent = nullptr);~BarChartForm();private:Ui::BarChartForm *ui; private:void generateOpChart();void generateItemChart(); };#endif // BARCHARTFORM_H #include "BarChartForm.h" #include "ui_BarChartForm.h"#include "userInfoMgr.h"#include "qcustomplot.h"BarChartForm::BarChartForm(QWidget *parent) :QWidget(parent),ui(new Ui::BarChartForm) {ui->setupUi(this);generateOpChart();generateItemChart(); }BarChartForm::~BarChartForm() {delete ui; }void BarChartForm::generateOpChart() {uint32_t maxOpNum = 0;auto& operateMap = UserInfoMgr::me()->getOperateMap();QCPAxis *keyAxis = ui->widgetOperate->xAxis;QCPAxis *valueAxis = ui->widgetOperate->yAxis;QCPBars *fossil = new QCPBars(keyAxis, valueAxis); // 使用xAxis作為柱狀圖的key軸,yAxis作為value軸fossil->setAntialiased(false); // 為了更好的邊框效果,關閉抗齒鋸fossil->setName("Fossil fuels"); // 設置柱狀圖的名字,可在圖例中顯示fossil->setPen(QPen(QColor(0, 168, 140).lighter(130))); // 設置柱狀圖的邊框顏色fossil->setBrush(QColor(0, 168, 140)); // 設置柱狀圖的畫刷顏色// 為柱狀圖設置一個文字類型的key軸,ticks決定了軸的范圍,而labels決定了軸的刻度文字的顯示QVector<double> ticks;QVector<QString> labels;uint32_t index = 1;for (auto it = operateMap.begin();it != operateMap.end();it++){labels.push_back(it->first);maxOpNum = it->second > maxOpNum ? it->second : maxOpNum;ticks << index;index++;}maxOpNum *= 1.1;QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);textTicker->addTicks(ticks, labels);keyAxis->setTicker(textTicker); // 設置為文字軸keyAxis->setTickLabelRotation(60); // 軸刻度文字旋轉60度keyAxis->setSubTicks(false); // 不顯示子刻度keyAxis->setTickLength(0, 4); // 軸內外刻度的長度分別是0,4,也就是軸內的刻度線不顯示keyAxis->setRange(0, ticks.size()+1); // 設置范圍keyAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);valueAxis->setRange(0, maxOpNum);valueAxis->setPadding(35); // 軸的內邊距,可以到QCustomPlot之開始(一)看圖解valueAxis->setLabel("操作柱狀圖");valueAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);QVector<double> fossilData;for (int i=0; i<labels.size(); i++) {QString name = labels[i];fossilData << operateMap[name];}fossil->setData(ticks, fossilData); }void BarChartForm::generateItemChart() {uint32_t maxOpNum = 0;auto& itemVec = UserInfoMgr::me()->getSortedItemVec();QCPAxis *keyAxis = ui->widgetItem->yAxis;QCPAxis *valueAxis = ui->widgetItem->xAxis;QCPBars *fossil = new QCPBars(keyAxis, valueAxis); // 使用xAxis作為柱狀圖的key軸,yAxis作為value軸fossil->setAntialiased(false); // 為了更好的邊框效果,關閉抗齒鋸fossil->setName("Fossil fuels"); // 設置柱狀圖的名字,可在圖例中顯示fossil->setPen(QPen(QColor(0, 168, 140).lighter(130))); // 設置柱狀圖的邊框顏色fossil->setBrush(QColor(0, 168, 140)); // 設置柱狀圖的畫刷顏色// 為柱狀圖設置一個文字類型的key軸,ticks決定了軸的范圍,而labels決定了軸的刻度文字的顯示QVector<double> ticks;QVector<QString> labels;uint32_t index = 1;for (auto it = itemVec.begin();it != itemVec.end();it++){labels.push_back(it->name);maxOpNum = it->num > maxOpNum ? it->num : maxOpNum;ticks << index;index++;}maxOpNum *= 1.1;QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);textTicker->addTicks(ticks, labels);keyAxis->setTicker(textTicker); // 設置為文字軸keyAxis->setTickLabelRotation(60); // 軸刻度文字旋轉60度keyAxis->setSubTicks(false); // 不顯示子刻度keyAxis->setTickLength(0, 4); // 軸內外刻度的長度分別是0,4,也就是軸內的刻度線不顯示keyAxis->setRange(0, ticks.size()+1); // 設置范圍keyAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);valueAxis->setRange(0, maxOpNum);valueAxis->setPadding(35); // 軸的內邊距,可以到QCustomPlot之開始(一)看圖解valueAxis->setLabel("操作柱狀圖");valueAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);QVector<double> fossilData;for (int i=0; i<labels.size(); i++) {QString name = labels[i];fossilData << itemVec[i].num;}fossil->setData(ticks, fossilData); }以下是效果:
?
詞云
詞云這里最開始我曾考慮過使用opencv上手硬寫,但是幾經嘗試,雖然我做出來了,但是效果在顏色和形狀上有點不盡人意,于是決定直接使用python自帶的wordcloud庫。
QT本身是支持C++和python的混合編程的,所以將python文件導入qt,直接代碼中調用即可。
以下是對應的Python文件
# This Python file uses the following encoding: utf-8# if __name__ == "__main__": # pass import imageio import wordcloud as wcdef generateWordcloud(words_list, back_img_path, file_path): #根據文本文件生成詞云background_image=imageio.imread(back_img_path) #讀取背景圖片w=wc.WordCloud(background_color='white',# 設置背景顏色font_path = './fonts/simfang.ttf',# 設置字體mask=background_image # 設置背景圖片)w.generate(words_list) #生成詞云w.to_file(file_path) # 生成圖片print("生成詞云成功!")return back_img_path# generateWordcloud("挖沙子 創建沙具 移動沙具 縮放沙具 移動沙具 縮放沙具 移動沙具 創建沙具 移動沙具 縮放沙具 刪除沙具 挖沙子 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 刪除沙具 刪除沙具 刪除沙具 刪除沙具 刪除沙具 創建沙具 移動沙具 縮放沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具 刪除沙具 縮放沙具 縮放沙具 旋轉沙具 移動沙具 創建沙具 移動沙具 縮放沙具 創建沙具 移動沙具 創建沙具 移動沙具 創建沙具 移動沙具", "./images/shoucang.png", "D:/work/build-LingXinCloud-Desktop_Qt_5_15_2_MinGW_64_bit-Debug/debug/wc_operate.png")以下是效果:
?
其它功能?
登陸
這里將用戶輸入的用戶名密碼記錄,用于填入接口取數據。
至于記錄用戶名這個小功能,則是通過注冊表存取實現的。
void LoginDialog::readLocalData() {QSettings settings(BMI_GROUP_NAME,BMI_PROJECT_NAME);m_isSaved = settings.value(LOCAL_DATA_IS_SAVED,0).toInt();m_user = settings.value(LOCAL_DATA_USER_NAME,"").toString();m_password = settings.value(LOCAL_DATA_USER_PASS,"").toString();if (m_isSaved > 0) {ui->lineEditUserName->setText(m_user);}if (m_isSaved > 0) {ui->checkBoxRemberPass->setChecked(true);} }void LoginDialog::writeLocalData() {QSettings settings(BMI_GROUP_NAME,BMI_PROJECT_NAME);settings.setValue(LOCAL_DATA_USER_NAME,m_user);// password need to cryptsettings.setValue(LOCAL_DATA_USER_PASS,m_password);settings.setValue(LOCAL_DATA_IS_SAVED,m_isSaved); }?關于聆心云
一個QMessageBox搞定
void MainWindow::on_action_BMI_triggered() {QMessageBox::about(this, tr("關于聆心云"),tr("<b>聆心云心理健康平臺</b>是由山東大學、山東中醫藥大學、齊魯師范學院等多家高校聯合開發的一款沙盤游戲,""我們致力于通過構建沙游世界,映射內心鏡像,釋放青少年無限潛能,共同創作美好未來。")); }?
最后放出程序的下載鏈接
下載鏈接
總結
以上是生活随笔為你收集整理的用QT制作聆心云数据可视化平台的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【uniapp、微信小程序】解决引入外部
- 下一篇: 和芯星通2019秋招面试