上一章我们了解了如何使用我们设计的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
操作中,我们根据QHBoxLayout
的itemAt()
函数找到先前添加的用于显示图片的 label,然后读取 reply 的数据值,以二进制的形式加载图片。虽然代码很长,有些函数我们也是第一次见到,但是整体思路很简单。下面来看最终的运行结果:

我们今天介绍了这种技术,用于区分一个程序中的多次网络请求(这在一个应用中是经常遇到的)。当然这只是其中一种解决方案,如果你有更好的解决方案,也请留言告诉豆子~
17 评论
可不可以考虑对每个请求的函数返回的reply连接finished信号,而不是networkmanager的finished信号。
可以的,这只是一种实现方式。你也可以连接 reply 的 readReady 信号。
请问QMap类型变量的声明为什么不是模板形式,能编译通过吗?我自己测试的时候replyMap.insert(reply, FetchWeatherInfo);这句话编译不过去:
error: C2663: “QMap::insert”: 2 个重载没有“this”指针的合法转换
with
[
Key=QNetworkReply,
T=RemoteRequest
]
应该是模板形式的,因为 HTML 页面的关系把模板给丢掉了。抱歉
你好,我主要是想问第二个问题,我的QMap的定义是:QMap replyMap;
但是replyMap.insert(reply, FetchWeatherInfo);这句话编译不过去,错误提示见上面的留言,也不清楚是为什么
加上模板信息也是错误的吗(原文已经修改过了)?你用的什么编译器?
是我的问题,void fetchWeather(const QString &cityName) const的声明按照上一节了,没把后面那个const去掉,所以编译不过去。。。用Qt5.2.1的版本,编译器报的错误不太准确,我又换成Qt4.8.5版本试了一下,才发现这个问题。。。
运行出现以下错误,探出指令引用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
程序异常结束。
请问豆子这是什么原因造成的? Cannot send events to objects owned by a different thread. ,不太懂奥
解决了,是我修改了豆子的上述代码段,修改错误造成的 🙂
请问豆子,每点击一次refresh按钮,虽然只会显示1个、天气图片,但是会显示n次 比如
晴
晴
晴
晴
按4次就有4个
有可能是因为发出了多个信号,所以有多次调用。因为代码只是演示性质,很多方面都不完善,还需要进一步改进。
豆子老师您好,想请教您两个问题:
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 在这里有什么用处吗?
非常感谢您!
1. 这是标准 C++ 的问题,C++ 在编译时需要找到这个类,因此,当 MainWindow::Private 在 .cpp 文件中时,需要在 .h 中做前置声明。
2. Private 类只是一种设计方式,目的是实现代码与声明代码的分离。详情查阅 C++ Private 设计模式。
刚看到您的回复,非常感谢~
我在 .h 文件里加了前置声明(class Private;),编译还是会出问题。我再查一下私有类 和 前置声明的用法吧。多谢豆子老师!
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());
}
});
});
}
connect函数中
QList details 应该改成 QList details吧?