首页 Qt 学习之路 2 Qt 学习之路 2(67):访问网络(3)

Qt 学习之路 2(67):访问网络(3)

17 2.6K

上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作。本章我们将继续完善前面介绍的天气程序。

注意到我们在WeatherDetail类中有一个icon属性。到现在为止我们还没有用到这个属性。下面我们考虑如何修改我们的程序。

通过查看 OpenWeatherMap 的相关 API 我们可以发现,当我们查询天气时会附带这么一个 icon 属性。这个属性其实是网站上的一个天气的图片。还是以上一章我们见到的 JSON 返回值为例:

{"coord":{"lon":116.397232,"lat":39.907501},"sys":{"country":"CN","sunrise":1381530122,"sunset":1381570774},"weather":[{"id":800,"main":"Clear","description":"晴","icon":"01d"}],"base":"gdps stations","main":{"temp":20,"pressure":1016,"humidity":34,"temp_min":20,"temp_max":20},"wind":{"speed":2,"deg":50},"clouds":{"all":0},"dt":1381566600,"id":1816670,"name":"Beijing","cod":200}

注意到其中的 icon:01d 这个键值对。通过文档我们知道,01d 实际对应于网站上的一张图片:http://openweathermap.org/img/w/01d.png。这就是我们的思路:当我们获取到实际天气数据时,我们根据这个返回值从网站获取到图片,然后显示到我们的程序中。

回忆下我们的NetWorker类的实现。我们将其finished()信号与我们自己实现的槽函数连接起来,其代码大致相当于:

connect(d->netWorker, &NetWorker::finished, [=] (QNetworkReply *reply) {
    ...
});

我们将finished()信号与一个 Lambda 表达式连接起来,其参数就是服务器的响应值。这样一来就会有一个问题:我们实际是有两次网络请求,第一次是向服务器请求当前的天气情况,第二次是根据第一次响应值去请求一张图片。每次网络请求完成时都会发出finished()信号,这就要求我们在槽函数中区分当前到底是哪一个请求的返回。所以,我们需要修改下有关网络请求的代码:

class NetWorker : public QObject
{
    ...
    QNetworkReply *get(const QString &url);
    ...
};

...

QNetworkReply * NetWorker::get(const QString &url)
{
    return d->manager->get(QNetworkRequest(QUrl(url)));
}

首先要修改的是NetWorker类的get()函数。我们要让这个函数返回一个QNetworkReply *变量。这个对象其实是QNetworkAccessManager::get()函数的返回值,我们简单地将其返回出来。接下来要修改的是MainWindow::Private的代码:

class MainWindow::Private
{
public:
    Private(MainWindow *q) :
        mainWindow(q)
    {
        netWorker = NetWorker::instance();
    }

    void fetchWeather(const QString &cityName)
    {
        QNetworkReply *reply = netWorker->get(QString("http://api.openweathermap.org/data/2.5/weather?q=%1&mode=json&units=metric&lang=zh_cn").arg(cityName));
        replyMap.insert(reply, FetchWeatherInfo);
    }

    void fetchIcon(const QString &iconName)
    {
        QNetworkReply *reply = netWorker->get(QString("http://openweathermap.org/img/w/%1.png").arg(iconName));
        replyMap.insert(reply, FetchWeatherIcon);
    }

    NetWorker *netWorker;
    MainWindow *mainWindow;
    QMap<QNetworkReply *, RemoteRequest> replyMap;
};

我们的请求是在MainWindow::Private私有类中完成的,为此添加了一个QMap属性。注意我们在原有的fetchWeather()和新增的fetchIcon()函数中都将NetWorker::get()函数的返回值保存下来。RemoteRequest只是一个枚举,定义如下:

enum RemoteRequest {
    FetchWeatherInfo,
    FetchWeatherIcon
};

显然,我们的代码能够清晰地描述出我们的网络请求的返回结果对应于哪一种操作:fetchWeather()NetWorker::get()函数的返回值对应于FetchWeatherInfo操作,而fetchIcon()NetWorker::get()函数的返回值则对应于FetchWeatherIcon操作。我们不需要区分每种操作的具体 URL 地址,因为我们的响应依照操作的不同而不同,与 URL 无关。

下面我们只看槽函数的改变:

connect(d->netWorker, &NetWorker::finished, [=] (QNetworkReply *reply) {
    RemoteRequest request = d->replyMap.value(reply);
    switch (request) {
    case FetchWeatherInfo:
    {
        QJsonParseError error;
        QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll(), &error);
        if (error.error == QJsonParseError::NoError) {
            if (!(jsonDocument.isNull() || jsonDocument.isEmpty()) && jsonDocument.isObject()) {
                QVariantMap data = jsonDocument.toVariant().toMap();
                WeatherInfo weather;
                weather.setCityName(data[QLatin1String("name")].toString());
                QDateTime dateTime;
                dateTime.setTime_t(data[QLatin1String("dt")].toLongLong());
                weather.setDateTime(dateTime);
                QVariantMap main = data[QLatin1String("main")].toMap();
                weather.setTemperature(main[QLatin1String("temp")].toFloat());
                weather.setPressure(main[QLatin1String("pressure")].toFloat());
                weather.setHumidity(main[QLatin1String("humidity")].toFloat());
                QVariantList detailList = data[QLatin1String("weather")].toList();
                QList details;
                foreach (QVariant w, detailList) {
                    QVariantMap wm = w.toMap();
                    WeatherDetail *detail = new WeatherDetail;
                    detail->setDesc(wm[QLatin1String("description")].toString());
                    detail->setIcon(wm[QLatin1String("icon")].toString());
                    details.append(detail);

                    QHBoxLayout *weatherDetailLayout = new QHBoxLayout;
                    weatherDetailLayout->setDirection(QBoxLayout::LeftToRight);
                    weatherDetailLayout->addWidget(new QLabel(detail->desc(), this));
                    weatherDetailLayout->addWidget(new QLabel(this));
                    weatherLayout->addLayout(weatherDetailLayout);

                    d->fetchIcon(detail->icon());
                }
                weather.setDetails(details);

                cityNameLabel->setText(weather.cityName());
                dateTimeLabel->setText(weather.dateTime().toString(Qt::DefaultLocaleLongDate));
            }
        } else {
            QMessageBox::critical(this, tr("Error"), error.errorString());
        }
        break;
    }
    case FetchWeatherIcon:
    {
        QHBoxLayout *weatherDetailLayout = (QHBoxLayout *)weatherLayout->itemAt(2)->layout();
        QLabel *iconLabel = (QLabel *)weatherDetailLayout->itemAt(1)->widget();
        QPixmap pixmap;
        pixmap.loadFromData(reply->readAll());
        iconLabel->setPixmap(pixmap);
        break;
    }
    }

    reply->deleteLater();
});

槽函数最大的变化是,我们依照MainWindow::Private中保存的对应值,找到这个reply对应的操作类型,然后使用一个switch语句进行区分。注意我们在FetchWeatherInfo操作的foreach循环中增加了对WeatherDetail数据的显示。在末尾使用一个d->fetchIcon(detail->icon())语句从网络获取对应的图片。在FetchWeatherIcon操作中,我们根据QHBoxLayoutitemAt()函数找到先前添加的用于显示图片的 label,然后读取 reply 的数据值,以二进制的形式加载图片。虽然代码很长,有些函数我们也是第一次见到,但是整体思路很简单。下面来看最终的运行结果:

weather 图片加载示例

我们今天介绍了这种技术,用于区分一个程序中的多次网络请求(这在一个应用中是经常遇到的)。当然这只是其中一种解决方案,如果你有更好的解决方案,也请留言告诉豆子~

17 评论

kosl90 2014年3月26日 - 23:59

可不可以考虑对每个请求的函数返回的reply连接finished信号,而不是networkmanager的finished信号。

回复
豆子 2014年3月27日 - 08:59

可以的,这只是一种实现方式。你也可以连接 reply 的 readReady 信号。

回复
动感超人 2014年5月12日 - 21:46

请问QMap类型变量的声明为什么不是模板形式,能编译通过吗?我自己测试的时候replyMap.insert(reply, FetchWeatherInfo);这句话编译不过去:
error: C2663: “QMap::insert”: 2 个重载没有“this”指针的合法转换
with
[
Key=QNetworkReply,
T=RemoteRequest
]

回复
豆子 2014年5月13日 - 14:13

应该是模板形式的,因为 HTML 页面的关系把模板给丢掉了。抱歉

回复
动感超人 2014年5月13日 - 21:31

你好,我主要是想问第二个问题,我的QMap的定义是:QMap replyMap;
但是replyMap.insert(reply, FetchWeatherInfo);这句话编译不过去,错误提示见上面的留言,也不清楚是为什么

回复
豆子 2014年5月14日 - 10:17

加上模板信息也是错误的吗(原文已经修改过了)?你用的什么编译器?

回复
动感超人 2014年5月14日 - 21:50

是我的问题,void fetchWeather(const QString &cityName) const的声明按照上一节了,没把后面那个const去掉,所以编译不过去。。。用Qt5.2.1的版本,编译器报的错误不太准确,我又换成Qt4.8.5版本试了一下,才发现这个问题。。。

datde 2014年6月4日 - 00:13

运行出现以下错误,探出指令引用0x00000000,对话框。并提示:
QWaitCondition: Destroyed while threads are still waiting
ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread 14107a98. Receiver '' (of type 'QNetworkAccessManager') was created in thread 32a48", file kernel\qcoreapplication.cpp, line 505
程序异常结束。

回复
datde 2014年6月4日 - 00:14

请问豆子这是什么原因造成的? Cannot send events to objects owned by a different thread. ,不太懂奥

回复
datde 2014年6月4日 - 06:34

解决了,是我修改了豆子的上述代码段,修改错误造成的 🙂

回复
datde 2014年6月4日 - 06:44

请问豆子,每点击一次refresh按钮,虽然只会显示1个、天气图片,但是会显示n次 比如





按4次就有4个

回复
豆子 2014年6月5日 - 09:43

有可能是因为发出了多个信号,所以有多次调用。因为代码只是演示性质,很多方面都不完善,还需要进一步改进。

回复
SD 2016年2月20日 - 12:01

豆子老师您好,想请教您两个问题:
1. 我把 NetWorker 类的定义 和函数分别 放在 networker.h 和 networker.cpp里, class NetWorker::Private 的具体代码放在 networker.cpp 里(NetWorker类的其他函数,比如构造函数,也都在里面 ),但这样编译会报错:Invalid use of incomplete type ' class NetWorker::Private' 和 forward declaration of ' class NetWorker::Private' ,但如果把 Private类的具体代码直接写在 声明的地方(networker.h里),就可以运行;类似的 class MainWindow::Private 也会出现这种问题,不知道为什么。。

2. WeatherDetail 和 WeatherInfo 类 各自的 class Private 在这里有什么用处吗?
非常感谢您!

回复
豆子 2016年2月22日 - 22:18

1. 这是标准 C++ 的问题,C++ 在编译时需要找到这个类,因此,当 MainWindow::Private 在 .cpp 文件中时,需要在 .h 中做前置声明。
2. Private 类只是一种设计方式,目的是实现代码与声明代码的分离。详情查阅 C++ Private 设计模式。

回复
SD 2016年2月23日 - 22:34

刚看到您的回复,非常感谢~
我在 .h 文件里加了前置声明(class Private;),编译还是会出问题。我再查一下私有类 和 前置声明的用法吧。多谢豆子老师!

回复
sarrow 2016年4月7日 - 12:25

class MainWindow::Private
{
public:
Private()
{
netWorker = NetWorker::instance();
}

QNetworkReply * fetchWeather(const QString& cityName) const
{
return netWorker->get(QString("http://api.openweathermap.org/data/2.5/weather?"
"q=%1&mode=json&units=metric&lang=zh_cn"
"&APPID=6b55db98c0b1a112f1f98bd93e4726ac").arg(cityName));
}

QNetworkReply * fetchIcon(const QString& iconName) const
{
return netWorker->get(QString("http://openweathermap.org/img/w/%1"
".png").arg(iconName));
}

NetWorker * netWorker;
};

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
d(new MainWindow::Private)
{
qDebug() <addItem(tr("Beijing"), QLatin1String("Beijin,cn"));
cityList->addItem(tr("Shanghai"), QLatin1String("Shanghai,cn"));
cityList->addItem(tr("Nanjing"), QLatin1String("Nanjing,cn"));

QLabel * cityLabel = new QLabel(tr("City: "), this);
QPushButton * refreshButton = new QPushButton(tr("Refresh"), this);
QHBoxLayout * cityListLayout = new QHBoxLayout;

cityListLayout->setDirection(QBoxLayout::LeftToRight);
cityListLayout->addWidget(cityLabel);
cityListLayout->addWidget(cityList);
cityListLayout->addWidget(refreshButton);

QVBoxLayout * weatherLayout = new QVBoxLayout;
weatherLayout->setDirection(QBoxLayout::TopToBottom);
QLabel * cityNameLabel = new QLabel(this);
weatherLayout->addWidget(cityNameLabel);
QLabel * dateTimeLabel = new QLabel(this);
weatherLayout->addWidget(dateTimeLabel);

QWidget * mainWidget = new QWidget(this);
QVBoxLayout * mainLayout = new QVBoxLayout(mainWidget);
mainLayout->addLayout(cityListLayout);
mainLayout->addLayout(weatherLayout);

this->setCentralWidget(mainWidget);
this->resize(320, 120);
this->setWindowTitle(tr("Weather"));

connect(refreshButton, &QPushButton::clicked, [=] () {
QNetworkReply * replyWheather = d->fetchWeather(cityList->itemData(cityList->currentIndex()).toString());
connect(replyWheather, &QNetworkReply::finished, [=]() {
QJsonParseError error;
QJsonDocument jsonDocument = QJsonDocument::fromJson(replyWheather->readAll(), &error);
if (error.error == QJsonParseError::NoError) {
if (!(jsonDocument.isNull() || jsonDocument.isEmpty()) && jsonDocument.isObject()) {
QVariantMap data = jsonDocument.toVariant().toMap();
WeatherInfo weather;
weather.setCityName(data[QLatin1String("name")].toString());
QDateTime dateTime;
dateTime.setTime_t(data[QLatin1String("dt")].toLongLong());
weather.setDateTime(dateTime);
QVariantMap main = data[QLatin1String("main")].toMap();
weather.setTemperature(main[QLatin1String("temp")].toFloat());
weather.setPressure(main[QLatin1String("pressure")].toFloat());
weather.setHumidity(main[QLatin1String("humidity")].toFloat());
QVariantList detailList = data[QLatin1String("weather")].toList();
QList details;
foreach (QVariant w, detailList) {
QVariantMap wm = w.toMap();
WeatherDetail *detail = new WeatherDetail;
detail->setDesc(wm[QLatin1String("description")].toString());
detail->setIcon(wm[QLatin1String("icon")].toString());
details.append(detail);

QHBoxLayout *weatherDetailLayout = new QHBoxLayout;
weatherDetailLayout->setDirection(QBoxLayout::LeftToRight);
weatherDetailLayout->addWidget(new QLabel(detail->desc(), this));
weatherDetailLayout->addWidget(new QLabel(this));
weatherLayout->addLayout(weatherDetailLayout);

QNetworkReply * replyIcon = d->fetchIcon(detail->icon());
connect(replyIcon, &QNetworkReply::finished, [=]() {
QHBoxLayout *weatherDetailLayout = (QHBoxLayout *)weatherLayout->itemAt(2)->layout();
QLabel *iconLabel = (QLabel *)weatherDetailLayout->itemAt(1)->widget();
QPixmap pixmap;
pixmap.loadFromData(replyIcon->readAll());
iconLabel->setPixmap(pixmap);
});
}
weather.setDetails(details);

cityNameLabel->setText(weather.cityName());
dateTimeLabel->setText(weather.dateTime().toString(Qt::DefaultLocaleLongDate));
}
} else {
QMessageBox::critical(this, tr("Error"), error.errorString());
}
});
});
}

回复
Black8Mamba 2020年5月15日 - 16:43

connect函数中
QList details 应该改成 QList details吧?

回复

回复 动感超人 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2