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

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

31 4.2K

上一章我们了解了NetWorker类的简单实现。不仅如此,我们还提到了几个 C++ 开发时常用的设计模式。这些在接下来得代码中依然会用到。

现在我们先来研究下 OpenWeatherMap 的相关 API。之所以选择 OpenWeatherMap,主要是因为这个网站提供了简洁的 API 接口,非常适合示例程序,并且其开发也不需要额外申请 App ID。OpenWeatherMap 的 API 可以选择返回 JSON 或者 XML,这里我们选择使用 JSON 格式。在进行查询时,OpenWeatherMap 支持使用城市名、地理经纬度以及城市 ID,为简单起见,我们选择使用城市名。我们先来看一个例子:http://api.openweathermap.org/data/2.5/weather?q=Beijing,cn&mode=json&units=metric&lang=zh_cn。下面是这个链接的参数分析:

参数名字传入值说明
qBeijing,cn查询中国北京的天气
modejson返回格式为 JSON
unitsmetric返回单位为公制
langzh_cn返回语言为中文

点击链接,服务器返回一个 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}

我们从这里找到 JSON 各个字段的含义。现在我们关心的是:时间(dt);气温(temp);气压(pressure);湿度(humidity)和天气状况(weather)。基于此,我们设计了WeatherInfo类,用于封装服务器返回的信息:

class WeatherDetail
{
public:
    WeatherDetail();
    ~WeatherDetail();

    QString desc() const;
    void setDesc(const QString &desc);

    QString icon() const;
    void setIcon(const QString &icon);

private:
    class Private;
    friend class Private;
    Private *d;
};

class WeatherInfo
{
public:
    WeatherInfo();
    ~WeatherInfo();

    QString cityName() const;
    void setCityName(const QString &cityName);

    quint32 id() const;
    void setId(quint32 id);

    QDateTime dateTime() const;
    void setDateTime(const QDateTime &dateTime);

    float temperature() const;
    void setTemperature(float temperature);

    float humidity() const;
    void setHumidity(float humidity);

    float pressure() const;
    void setPressure(float pressure);

    QList<WeatherDetail *> details() const;
    void setDetails(const QList<WeatherDetail *> details);

private:
    class Private;
    friend class Private;
    Private *d;
};

QDebug operator <<(QDebug dbg, const WeatherDetail &w);
QDebug operator <<(QDebug dbg, const WeatherInfo &w);

WeatherInfoWeatherDetail两个类相互合作存储我们所需要的数据。由于是数据类,所以只有单纯的 setter 和 getter 函数,这里不再把源代码写出来。值得说明的是最后两个全局函数:

QDebug operator <<(QDebug dbg, const WeatherDetail &w);
QDebug operator <<(QDebug dbg, const WeatherInfo &w);

我们重写了<<运算符,以便能够使用类似qDebug() << weatherInfo;这样的语句进行调试。它的实现是这样的:

QDebug operator <<(QDebug dbg, const WeatherDetail &w)
{
    dbg.nospace() << "("
                  << "Description: " << w.desc() << "; "
                  << "Icon: " << w.icon()
                  << ")";
    return dbg.space();
}

QDebug operator <<(QDebug dbg, const WeatherInfo &w)
{
    dbg.nospace() << "("
                  << "id: " << w.id() << "; "
                  << "City name: " << w.cityName() << "; "
                  << "Date time: " << w.dateTime().toString(Qt::DefaultLocaleLongDate) << ": " << endl
                  << "Temperature: " << w.temperature() << ", "
                  << "Pressure: " << w.pressure() << ", "
                  << "Humidity: " << w.humidity() << endl
                  << "Details: [";
    foreach (WeatherDetail *detail, w.details()) {
        dbg.nospace() << "( Description: " << detail->desc() << ", "
                      << "Icon: " << detail->icon() << "), ";
    }
    dbg.nospace() << "] )";
    return dbg.space();
}

这两个函数虽然比较长,但是很简单,这里不再赘述。

下面我们来看主窗口:

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    class Private;
    friend class Private;
    Private *d;
};

正如前面所说的,这里依然使用了 d 指针模式。头文件没有什么可说的。MainWindow::Private的实现依旧简单:

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

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

    NetWorker *netWorker;
};

我们将MainWindow所需要的NetWorker作为MainWindow::Private的一个成员变量。MainWindow::Private提供了一个fetchWeather()函数。由于NetWorker提供的函数都是相当底层的,为了提供业务级别的处理,我们将这样的函数封装在MainWindow::Private中。当然,你也可以在NetWorker中直接提供类似的函数,这取决于你的系统分层设计。

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      d(new MainWindow::Private)
{
    QComboBox *cityList = new QComboBox(this);
    cityList->addItem(tr("Beijing"), QLatin1String("Beijing,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);
    setCentralWidget(mainWidget);
    resize(320, 120);
    setWindowTitle(tr("Weather"));

    connect(d->netWorker, &NetWorker::finished, [=] (QNetworkReply *reply) {
        qDebug() << reply;
        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<WeatherDetail *> 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);
                }
                weather.setDetails(details);

                cityNameLabel->setText(weather.cityName());
                dateTimeLabel->setText(weather.dateTime().toString(Qt::DefaultLocaleLongDate));
            }
        } else {
            QMessageBox::critical(this, tr("Error"), error.errorString());
        }
        reply->deleteLater();
    });
    connect(refreshButton, &QPushButton::clicked, [=] () {
        d->fetchWeather(cityList->itemData(cityList->currentIndex()).toString());
    });
}

MainWindow::~MainWindow()
{
    delete d;
    d = 0;
}

接下来我们来看MainWindow的构造函数和析构函数。构造函数虽然很长但是并不复杂,主要是对界面的构建。我们这里略过这些界面的代码,直接看两个信号槽的连接。

    connect(d->netWorker, &NetWorker::finished, [=] (QNetworkReply *reply) {
        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<WeatherDetail *> 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);
                }
                weather.setDetails(details);

                cityNameLabel->setText(weather.cityName());
                dateTimeLabel->setText(weather.dateTime().toString(Qt::DefaultLocaleLongDate));
            }
        } else {
            QMessageBox::critical(this, tr("Error"), error.errorString());
        }
        reply->deleteLater();
    });
    connect(refreshButton, &QPushButton::clicked, [=] () {
        d->fetchWeather(cityList->itemData(cityList->currentIndex()).toString());
    });

由于使用了 Qt5,我们选择新的连接语法。第一个connect()函数中,我们按照 API 文档中描述的那样对服务器返回的 JSON 字符串进行解析,然后将数据填充到一个WeatherInfo的对象。然后操作界面的两个控件显示数据。值得注意的是函数的最后一行,reply->deleteLater();。当网络请求结束时,delete 服务器返回的QNetworkReply对象是用户的责任。用户需要选择一个恰当的时机进行 delete 操作。但是,我们不能直接在finiahed()信号对应的槽函数中调用delete运算符。相反,我们需要使用deleteLater()函数,正如前面代码中显示的那样。第二个槽函数则相对简单,仅仅是重新获取新的数据。

选择我们可以运行下程序了:

weather 示例

31 评论

Sanhu Li 李三乎 2014年3月20日 - 00:00

博主,倒数第二个那段代码是不是有问题
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
d(new MainWindow::Private(this))
MainWindow的Private类没有这个构造函数啊,而且这个指针传进去也没什么用

还有lambda表达式,因为要用这个得在pro里加上
CONFIG += c++11
然后会出现一个错误
qlist.h:53: error: 'initializer_list' file not found
这好像是一个已知的bug不知道怎么解决

再一个是博主用的那些QList不加的话会报错

回复
Sanhu Li 李三乎 2014年3月20日 - 01:09

呃,lambda表达式问题解决了,是在Mac上的已知错误
参见
http://stackoverflow.com/questions/15446207/compile-error-when-trying-to-compile-a-qt-project

回复
豆子 2014年3月20日 - 08:50

是的,应该将 Private 类的参数移除。关于 Mac 上面 C++ 11 的问题之前也遇到过,不过后来升级到 10.9.1 并将 XCode 升级到最新版后,这个错误就没有了。

回复
datde 2014年6月3日 - 23:13

在windows下编译时候,在2个connnect连接函数中出错,

D:\Qt\network\mainwindow.cpp:85: error: no matching function for call to 'MainWindow::connect(QPushButton*&, void (QAbstractButton::*)(bool), MainWindow::MainWindow(QWidget*)::__lambda0)'
});
^
请问是什么原因啊,急死了

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

是不是在 pro 文件中没有添加 CONFIG += c++11?或者是编译器不支持?

回复
newbie 2014年3月26日 - 17:56

额~我这个咋是美国时间(CST)呢?

回复
kosl90 2014年3月28日 - 23:17

QList而不是QList吧

回复
mouse 2014年5月31日 - 20:02

你好,豆子,问下代码 QList details() const;
void setDetails(const QList details);
中 QList 的数据类型是什么?

回复
豆子 2014年6月3日 - 09:49

QList<WeatherDetail *>,;-P

回复
delifue 2014年7月25日 - 09:19

qt中有没有能处理html文档的内容?

回复
豆子 2014年7月25日 - 23:22

不知道你说的处理HTML文件是什么意思?如果要显示的话,使用WebKit就可以了

回复
星火零光 2015年3月7日 - 18:24

博主,我想把网络请求返回的结果直接放入qml用js来解析json,可是qml要怎么才能调用到网络请求返回的结果呢?

回复
豆子 2015年3月9日 - 14:15

个人意见,QML 定位应该是数据展示,所以有关数据分析之类的应该交给 C++ 去做。C++ 分析网络数据,然后将其准备成 QML 需要的格式,QML 只做展示。

回复
seikareikou 2015年10月1日 - 11:12

豆子先生你好!
我有个问题想请教。
我用QNetworkAccessManager类来管理所有的get和post请求,然后在自定义的finish槽函数里用一个信号urlRequestChanged(QString r),把返回的数据传递到qml里,再用简单的js函数把数据放入listview中。但是如果我在同一个qml界面里有多个listview要同时放入不同数据,我要怎么区分哪次请求对应哪个listview对象

回复
豆子 2015年10月9日 - 10:35

这个没有办法区分,可行的一个方法是,用一个 QHash 对象记录 request 和 view 的对应关系。

回复
seikareikou 2015年10月11日 - 17:04

谢谢你的回答

回复
2016年3月2日 - 16:47

你好,请问什么现在打开该链接
http://api.openweathermap.org/data/2.5/weather?q=Beijing,cn&mode=json&units=metric&lang=zh_cn
会返回错误的信息
{"cod":401, "message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."}

另外,假如直接
打开这个
http://api.openweathermap.org/data/2.5/weather?q=beijing,cn&appid=44db6a862fba0b067b1930da0d769e98
可以倒是可以,但是不是中文。

是网站更新了吗?

回复
豆子 2016年3月3日 - 09:36

网站做过更新了,你可以先用浏览器访问 http://api.openweathermap.org/data/2.5/weather?q=Beijing,cn&lang=zh_cn&units=metric,地址栏会自动添加一个 appid,然后再使用这个 appid 就可以了

回复
2016年3月3日 - 09:42

真的是这样,的确后面要加一个appid才可以。

http://api.openweathermap.org/data/2.5/weather?q=Beijing,cn&mode=json&units=metric&lang=zh_cn&appid=44db6a862fba0b067b1930da0d769e98

返回
{"coord":{"lon":116.4,"lat":39.91},"weather":[{"id":721,"main":"Haze","description":"薄雾","icon":"50d"}],"base":"stations","main":{"temp":6,"pressure":1013,"humidity":50,"temp_min":6,"temp_max":6},"visibility":2800,"wind":{"speed":1},"clouds":{"all":20},"dt":1456966800,"sys":{"type":1,"id":7405,"message":0.0116,"country":"CN","sunrise":1456958675,"sunset":1456999714},"id":1816670,"name":"Beijing","cod":200}

谢谢。

回复
LinWM 2016年3月21日 - 21:43

http://api.openweathermap.org/data/2.5/weather?q=Beijing,cn&mode=json&units=metric&lang=zh_cn&APPID=6b55db98c0b1a112f1f98bd93e4726ac
(APPID要大写,到OpenWeatherMap上注册个账号,才会给分配一个APPID)

回复
piaodonkey 2016年6月12日 - 20:24

connect(d->netWorker, &NetWorker::finished, [=] (QNetworkReply *reply) 老大请问您这个*reply是怎么传进来的啊

回复
piaodonkey 2016年6月12日 - 20:54

我意思是没看到这个参数被赋值啊但是怎么直接就可以用了

回复
piaodonkey 2016年6月14日 - 17:51

明白了,是我信号槽和lambda理解不透彻

回复
我是新手 2016年6月20日 - 15:22

感觉很厉害,不过看不懂,好惆怅

回复
Leon 2016年9月28日 - 22:13

QSslSocket: cannot resolve SSLv2_client_method
QSslSocket: cannot resolve SSLv2_server_method

这是什么情况???

回复
ccppaa 2016年11月1日 - 16:24

大神,要上传的数据转换成json类型后,会产生精度丢失的问题,有影响吗,有没有什么解决的方法,网上搜了一下,说是计算机进制转换产生的问题,也没有找到什么解决的方法,

回复
豆子 2016年11月19日 - 10:42

浮点数的精度一般都会受影响,这个是保存方式的问题,没有办法解决。你可以试着把浮点数按照自己需要的精度转换成字符串再传输。

回复
Wakfu 2018年3月23日 - 11:03

豆子老师您好,我在做这一章的程序的时候,最终编译出现了
Runtime Error
......
This application has requested the Runtime to terminated it in an unusual way,
.....,
请问这是什么情况,前面都没有出现过这个状况。

回复
豆子 2018年3月25日 - 09:40

运行期错误一般是内存之类的错误,可能是指针释放或者其它问题,需要一步一步慢慢检查了。这个不大清楚实际原因

回复
Wakfu 2018年3月25日 - 10:26

嗯 好的 谢谢豆子老师
因为以前没有遇到相关问题 怀疑是程序有问题 但是百度后都是说少了什么什么运行库
现在有了豆子老师的指导,就比较确定了,再次感谢豆子老师

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

QDebug operator <<(QDebug dbg, const WeatherDetail &w)
QDebug operator <<(QDebug dbg, const WeatherInfo &w)
应该改成
QDebug& operator <<(QDebug dbg, const WeatherDetail &w)
QDebug& operator <<(QDebug dbg, const WeatherInfo &w)
少了&

回复

回复 豆子 取消回复

关于我

devbean

devbean

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

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