前言

练手项目

代码参考b站https://www.bilibili.com/video/BV1GW42197ff视频

软件内图标来自https://www.iconfont.cn/阿里巴巴阿里巴巴矢量图标库

主要包含以下几个核心功能:

  1. 网络请求:通过网络接口获取天气数据。

  2. 数据解析:将获取到的 JSON 格式的天气数据进行解析。

  3. 界面更新:把解析后的数据展示在应用界面上。

  4. 图表绘制:绘制最高温度和最低温度的折线图,并在图上显示具体的温度数值。

正文

UI

UI绘制没什么好说的,就是拉控件,然后用网上的res修改图标样式,改排版,大部分都是label,一个push button(搜索按钮),一个lineEdit(搜索栏)

我在代码调试中换了图标,原因是下载的图片有正方形的有长方形的,不统一不好适应UI,虽然图标样式很好看但是还是换过了一套。

主要代码

1. 网络请求与数据获取

weatherRead 函数中,我们使用 QNetworkAccessManager 来发起网络请求,向天气 API 发送请求并获取数据。以下是代码:

void Widget::weatherRead()
{
    manager = new QNetworkAccessManager(this);
    // 天气 API 的 URL
    strUrl = "http://gfeljm.tianqiapi.com/api?unescape=1&version=v9&appid=83941685&appsecret=Py2be4hk";
    QNetworkRequest res(strUrl);
    manager->get(res);
    reply = manager->get(res);

    // 连接信号与槽,当请求完成时调用 readHttpReply 函数
    connect(manager,&QNetworkAccessManager::finished,this,&Widget::readHttpReply);

    // 初始化一些界面元素列表,用于后续更新界面
    mDateList << ui->labelday0 << ui->labelday1 << ui->labelday2 << ui->labelday3 << ui->labelday4 << ui->labelday5;
    mWeekList << ui->labeldate0 << ui->labeldate1 << ui->labeldate2 << ui->labeldate3 << ui->labeldate4 << ui->labeldate5;
    mIconList << ui->labelweather0 << ui->labelweather1 << ui->labelweather2 << ui->labelweather3 << ui->labelweather4 << ui->labelweather5;
    mWeaTypeList << ui->labelWeather0 << ui->labelWeather1 << ui->labelWeather2 << ui->labelWeather3 << ui->labelWeather4 << ui->labelWeather5;
    mAirqList << ui->labelairq0 << ui->labelairq1 << ui->labelairq2 << ui->labelairq3 << ui->labelairq4 << ui->labelairq5;
    mFxList << ui->labelFX0 << ui->labelFX1 << ui->labelFX2 << ui->labelFX3 << ui->labelFX4 << ui->labelFX5;

    // 建立天气类型与对应图标资源的映射
    mTypeMap.insert("晴", ":/res/晴.png");
    mTypeMap.insert("多云", ":/res/多云.png");
    mTypeMap.insert("大雪", ":/res/大雪.png");
    mTypeMap.insert("大雨", ":/res/大雨.png");
    mTypeMap.insert("雷暴", ":/res/雷暴.png");
    mTypeMap.insert("雷雨", ":/res/雷雨.png");
    mTypeMap.insert("小雪", ":/res/小雪.png");
    mTypeMap.insert("小雨", ":/res/小雨.png");
    mTypeMap.insert("阴", ":/res/阴.png");
    mTypeMap.insert("雨夹雪", ":/res/雨夹雪.png");
    mTypeMap.insert("中雪", ":/res/中雪.png");
    mTypeMap.insert("中雨", ":/res/中雨.png");
    mTypeMap.insert("雾", ":/res/雾.png");

    // 为两个用于绘制折线图的 widget 安装事件过滤器
    ui->widget0404->installEventFilter(this);
    ui->widget0405->installEventFilter(this);
}

上述代码可以看出其实我没有用多少图标资源,有些天气的图标没有适配,没办法,阿里巴巴上面天气套图火的我都看过了,总是缺少一些图片,不得已只能将就着用了。

weatherRead() 函数中, ui->widget0404ui->widget0405 安装了事件过滤器,Widget 类会拦截 ui->widget0404ui->widget0405 的事件。当这两个 widget 接收到重绘事件(QPaintEvent)时,会先经过 Widget 类的 eventFilter() 方法处理。

在这个函数中,我们创建了一个 QNetworkAccessManager 对象来管理网络请求。通过 QNetworkRequest 设置请求的 URL,然后使用 manager->get(res) 发起 GET 请求。同时,我们还将 managerfinished 信号连接到 readHttpReply 槽函数,以便在请求完成时进行处理。(AI总结)

2. 数据解析

当网络请求完成后,readHttpReply 函数会被调用,在该函数中我们会对返回的数据进行解析。以下是代码:

void Widget::readHttpReply(QNetworkReply *reply)
{
    // 获取 HTTP 响应状态码
    int resCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    if(reply->error() == QNetworkReply::NoError && resCode == 200)
    {
        // 读取响应数据
        QByteArray data = reply->readAll();
        // 调用解析函数解析数据
        parseWeatherJsonData(data);
    }
    else
    {
        // 若请求出错,弹出错误提示框
        QMessageBox mes;
        mes.setWindowTitle("错误");
        mes.setText("网络请求失败");
        mes.setStandardButtons(QMessageBox::Ok);
        mes.exec();
    }
}

void Widget::parseWeatherJsonData(QByteArray rawData)
{
    // 将原始数据转换为 JSON 文档
    QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData);
    if(!jsonDoc.isNull() && jsonDoc.isObject())
    {
        QJsonObject jsonRoot = jsonDoc.object();
        // 获取当前城市名称
        days[0].mCity = jsonRoot["city"].toString();
        // 获取当前 PM2.5 数值
        days[0].mPm25 = jsonRoot["aqi"].toObject()["pm25"].toString();
        if(jsonRoot.contains("data") && jsonRoot["data"].isArray())
        {
            QJsonArray weaArray = jsonRoot["data"].toArray();
            for(int i = 0; i < weaArray.size(); i ++)
            {
                QJsonObject obj = weaArray[i].toObject();
                // 解析每天的天气数据
                days[i].mDate = obj["date"].toString();
                days[i].mWeek = obj["week"].toString();
                days[i].mWeathType = obj["wea"].toString();
                days[i].mTemp = obj["tem"].toString();
                days[i].mTempLow = obj["tem2"].toString();
                days[i].mTempHigh = obj["tem1"].toString();
                days[i].mFx = obj["win"].toArray()[0].toString();
                days[i].mFl = obj["win_speed"].toString();
                days[i].mAirq = obj["air_level"].toString();
                days[i].mTips = obj["air_tips"].toString();
                days[i].mHu = obj["humidity"].toString();
            }
            // 解析完成后更新界面
            updateUI();
        }
    }
}

此处代码就是使用我设置好的api去获取json数据,然后存到我设置好的days数组中,方便更新ui。

readHttpReply 函数中,我们首先检查响应是否成功(状态码为 200 且无错误),如果成功则读取响应数据并调用 parseWeatherJsonData 函数进行解析。在 parseWeatherJsonData 函数中,我们使用 QJsonDocumentQJsonObject 来解析 JSON 数据,将解析后的数据存储在 days 数组中,最后调用 updateUI 函数更新界面。(AI总结)

3. 界面更新

updateUI 函数负责将解析后的数据显示在界面上。以下是代码:

void Widget::updateUI()
{
    // 更新当前日期和星期
    ui->labelCurrentDate->setText(days[0].mDate+" "+days[0].mWeek);
    // 更新城市名称
    ui->labelCity->setText(days[0].mCity+"市");
    // 更新当前温度
    ui->labelTemp->setText(days[0].mTemp+"℃");
    // 更新最低最高温度范围
    ui->labelTempRange->setText(days[0].mTempLow+"~"+days[0].mTempHigh+"℃");
    // 更新天气类型和图标
    ui->labelWeathertype->setText(days[0].mWeathType);
    ui->labelWeathericon->setPixmap(mTypeMap[days[0].mWeathType]);
    // 更新感冒指数
    ui->labelGanmao->setText(days[0].mTips);
    // 更新风向
    ui->labelFXType01->setText(days[0].mFx);
    // 更新风力
    ui->labelFXType02->setText(days[0].mFl);
    // 更新 PM2.5 数值
    ui->labelPMType02->setText(days[0].mPm25);
    // 更新湿度
    ui->labelSDType02->setText(days[0].mHu);
    // 更新空气质量
    ui->labelZLType02->setText(days[0].mAirq);

    for(int i = 0; i < 6; i++)
    {
        // 更新未来几天的星期
        mWeekList[i]->setText(days[i].mWeek);

        // 处理日期格式并更新
        QStringList daylist = days[i].mDate.split("-");
        mDateList[i]->setText(daylist.at(1)+"-"+daylist.at(2));

        // 处理天气类型并更新图标
        QStringList iconList = days[i].mWeathType.split("转");
        mIconList[i]->setPixmap(mTypeMap[iconList.at(0)]);

        // 更新天气类型文字描述
        mWeaTypeList[i]->setText(days[i].mWeathType);

        // 更新空气质量
        QString airQ = days[i].mAirq;
        mAirqList[i]->setText(airQ);
        if(airQ == "优")
        {
            // 若空气质量为优,设置绿色背景
            mAirqList[i]->setStyleSheet("background-color: rgb(116, 168, 0); border-radius: 5px;");
        }
        else
        {
            // 否则设置橙色背景
            mAirqList[i]->setStyleSheet("background-color: rgb(252, 183, 16); border-radius: 5px;");
        }

        // 更新风向和风力
        mFxList[i]->setText(days[i].mFx+"\n"+days[i].mFl);
    }
    // 强制界面更新
    update();
}

这一段主要是更新了UI上面的界面,然后调用update()触发重绘,继而更新下面的UI。

在这个函数中,我们将 days 数组中的数据依次更新到界面的各个控件上,包括日期、温度、天气类型、空气质量等。同时,根据空气质量的不同,为空气质量显示控件设置不同的背景颜色。(AI总结)

4. 折线图绘制

我们使用 QPainter 来绘制最高温度和最低温度的折线图,并在图上显示具体的温度数值。以下是绘制最高温度折线图的代码:

void Widget::drawTempLineHigh()
{
    QPainter painter(ui->widget0404);
    // 设置橙色画笔和画刷,用于绘制最高温度折线
    QColor highColor(255, 165, 0);
    painter.setBrush(highColor);
    painter.setPen(highColor);

    int ave = 0;
    int sum = 0;
    int offSet;
    int middle = ui->widget0404->height() / 2;

    // 计算最高温度的平均值
    for (int i = 0; i < 6; i++)
    {
        bool ok;
        int tempHigh = days[i].mTempHigh.toInt(&ok);
        if (ok)
        {
            sum += tempHigh;
        }
        else
        {
            qDebug() << "Failed to convert mTempHigh to int for index" << i;
        }
    }

    if (sum > 0)
    {
        ave = sum / 6;
    }

    QPoint points[6];
    for (int i = 0; i < 6; i++)
    {
        bool ok;
        int tempHigh = days[i].mTempHigh.toInt(&ok);
        if (ok)
        {
            // 将 mAirqList[i] 的坐标转换到 ui->widget0404 的坐标系中
            QPoint mappedPoint = ui->widget0404->mapFromGlobal(mAirqList[i]->mapToGlobal(QPoint(0, 0)));
            points[i].setX(mappedPoint.x() + mAirqList[i]->width() / 2);
            offSet = (tempHigh - ave) * 3;
            points[i].setY(middle - offSet);

            int x = points[i].x();
            int y = points[i].y();
            if (x >= 0 && x < ui->widget0404->width() && y >= 0 && y < ui->widget0404->height())
            {
                // 绘制椭圆点
                painter.drawEllipse(points[i], 3, 3);

                // 设置较小的字体
                QFont smallFont = painter.font();
                smallFont.setPointSize(smallFont.pointSize() - 2);
                painter.setFont(smallFont);

                // 在点上方合适位置绘制温度度数
                QString tempText = QString::number(tempHigh) + "°C";
                QFontMetrics fm(painter.font());
                int textWidth = fm.horizontalAdvance(tempText);
                int textHeight = fm.height();
                // 调整 y 坐标偏移量,让文本位置低一点
                QPoint textPos(x - textWidth / 2, y - textHeight / 2);
                painter.drawText(textPos, tempText);

                // 恢复原来的字体
                painter.setFont(QFont());
            }
            else
            {
                qDebug() << "Ellipse position out of range for index" << i << ": (" << x << ", " << y << ")";
            }
        }
        else
        {
            qDebug() << "Failed to convert mTempHigh to int for index" << i;
        }
    }

    if (sum > 0)
    {
        // 绘制折线
        painter.drawPolyline(points, 6);
    }
}

这段代码我用AI做了辅助,主要是总是画不好伤脑筋,交给AI处理细节去了。展示的是最高温度的折线绘制代码,最低温度折线代码类似。

绘制最低温度折线图的 drawTempLineLow 函数与 drawTempLineHigh 函数逻辑类似,只是使用的颜色和数据不同。在绘制过程中,我们首先计算最高温度的平均值,然后根据每天的最高温度与平均值的差值计算每个点的坐标,绘制椭圆点和折线,并在点的上方合适位置显示具体的温度数值。(AI总结)

最终成果截图

难点解释(AI总结)

1. 网络请求与异步处理

在 Qt 中,网络请求是异步的,这意味着当我们发起一个网络请求后,程序不会等待请求完成就会继续执行后续代码。因此,我们需要使用信号与槽机制来处理请求完成的事件。在这个应用中,我们将 QNetworkAccessManagerfinished 信号连接到 readHttpReply 槽函数,当请求完成时,readHttpReply 函数会被自动调用,从而实现异步处理。

2. JSON 数据解析

JSON 是一种常用的数据交换格式,在处理网络返回的 JSON 数据时,我们需要使用 Qt 提供的 QJsonDocumentQJsonObjectQJsonArray 等类来进行解析。首先,我们将原始的 JSON 数据转换为 QJsonDocument 对象,然后通过 QJsonObjectQJsonArray 来获取具体的数据。在解析过程中,需要注意数据的类型和结构,确保能够正确获取所需的数据。

3. 坐标转换与绘制

在绘制折线图时,我们需要将界面上的控件坐标转换到绘制区域的坐标系中,以确保绘制的点和折线位置正确。在这个应用中,我们使用 mapFromGlobalmapToGlobal 方法来进行坐标转换。同时,在绘制文本时,需要考虑文本的大小和位置,确保文本能够正确显示在点的上方。