用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和type
    std::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_OBJECT

public:
    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_OBJECT

public:
    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 wc

def 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 crypt
    settings.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>是由山东大学、山东中医药大学、齐鲁师范学院等多家高校联合开发的一款沙盘游戏,"
                          "我们致力于通过构建沙游世界,映射内心镜像,释放青少年无限潜能,共同创作美好未来。"
                            ));
}

 

最后放出程序的下载链接

下载链接

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值