您的位置:首页 > 编程语言 > Qt开发

使用Ubuntu SDK创建中国天气Scope应用(Qt XML)

2014-10-09 17:48 513 查看
在这篇文章里,我们将使用Ubuntu SDK从零开始来创建一个“中国天气”的Scope应用。通过这个过程,让开发者了解Scope在Ubuntu上的开发流程,以及对Scope有更深的认识。该应用完全使用Qt C++及std C++来完成的。更多关于Scope的知识,可以在网址:http://developer.ubuntu.com/scopes/。我们开发应用的最终显示图为:





上一篇文章中,我们已经使用std C++完成了一个同样的Scope应用。这里我们来使用Qt C++ APIs来完成这个同样的应用。

1)启动Ubuntu SDK来创建一个基本的Scope应用

首先,我们来打开我们的Ubuntu SDK来创建一个最基本的应用。我们选择菜单“New file or Project”或使用热键“Ctrl +N”。我们选择“Unity Scope”模版。



我们给我们的应用一个名字“ChinaWeather”。我们同事也选择template的类型为“Empty scope”





接下来,我们也同时选择不同的Kit,这样我们都可以在他们上面编译并部署我们的应用。



我们直接在电脑的Desktop上运行我们的scope。就像我们先前选择的template所说的一样,这个Scope没有做任何实质性的东西。为了确保我们能够在desktop上运行我们的scope并看到界面,我们可以点击“Projects”,并在Desktop中的“Run Configuration”中进行设置。确保选中“chinaweather”。



你可以点击一下显示的结果。



如果你能运行到这里,说明你的安装环境是没有问题的。如果有问题的话,请参阅我的Ubuntu
SDK安装文章。这个最基本的应用其实没有什么内容。在下面的章节中我们来向这里添加一些东西以实现我们所需要的一些东西。

2)加入对Qt的支持

我们可以看到在项目的“src”目录下有两个目录:apiscope。api目录下的代码主要是为了来访问我们的web service来得到一个json或是xml的数据。在这个项目中,我们并不刺昂采用这个目录中的client类。有兴趣的开发者可以尝试把自己的client和scope的代码分开。

首先我们需要在百度的开发者网站来申请我们的开发者账号。大家可以放问网站来申请账号。我们首先来做一个测试以确保我们的账号是可以工作的。按照文中所提到的,我们可以在浏览器中输入如下的地址:http://api.map.baidu.com/telematics/v3/weather?location=%E5%8C%97%E4%BA%AC&output=xml&ak=DdzwVcsGMoYpeg5xQlAFrXQt。我们可以得到如下的内容:



首先,我们可以看到API是工作的。没有任何问题。显示的架构是xml格式的。由于我们要使用Qt及Qt中的xml库来帮助我们解析我们得到的xml格式的数据,我们在项目中加入对Qt的支持。我们首先打开在“src”中的CMakeLists.txt文件,并加入如下的句子:

add_definitions(-DQT_NO_KEYWORDS)
find_package(Qt5Network REQUIRED)
find_package(Qt5Core REQUIRED)
find_package(Qt5Xml REQUIRED)

include_directories(${Qt5Core_INCLUDE_DIRS})
include_directories(${Qt5Network_INCLUDE_DIRS})
include_directories(${Qt5Xml_INCLUDE_DIRS})

....

# Build a shared library containing our scope code.
# This will be the actual plugin that is loaded.
add_library(
scope SHARED
$<TARGET_OBJECTS:scope-static>
)

qt5_use_modules(scope Core Xml Network) 

# Link against the object library and our external library dependencies
target_link_libraries(
scope
${SCOPE_LDFLAGS}
${Boost_LIBRARIES}
)


我们可以看到,我们加入了对Qt Core,XML及Network库的调用。同时,我们也打开"tests/unit/CMakeLists.txt"文件,并加入“qt5_use_modules(scope-unit-tests Core Xml Network)":

# Our test executable.
# It includes the object code from the scope
add_executable(
scope-unit-tests
scope/test-scope.cpp
$<TARGET_OBJECTS:scope-static>
)

# Link against the scope, and all of our test lib dependencies
target_link_libraries(
scope-unit-tests
${GTEST_BOTH_LIBRARIES}
${GMOCK_LIBRARIES}
${SCOPE_LDFLAGS}
${TEST_LDFLAGS}
${Boost_LIBRARIES}
)

qt5_use_modules(scope-unit-tests Core Xml Network)

# Register the test with CTest
add_test(
scope-unit-tests
scope-unit-tests
)


我们再重新编译我们的应用,如果我们没有错误的话,我们的Scope可以直接在desktop下直接运行。这里我们加入了一个”QCoreApplication”变量。这主要是为了我们能够使用signal/slot机制及生成一个Qt应用。我们来修改scope.h文件,并加QoreApplication的变量app及forward申明。我们也必须同时加入一个方法"run"。

class QCoreApplication; // added

namespace scope {
class Scope: public unity::scopes::ScopeBase {
public:
void start(std::string const&) override;
void stop() override;
void run(); // added
unity::scopes::PreviewQueryBase::UPtr preview(const unity::scopes::Result&,
const unity::scopes::ActionMetadata&) override;
unity::scopes::SearchQueryBase::UPtr search(
unity::scopes::CannedQuery const& q,
unity::scopes::SearchMetadata const&) override;

protected:
api::Config::Ptr config_;
QCoreApplication *app; //added
};


我们同时打开scope.cpp,并做如下的修改:

#include <QCoreApplication> // added

...

void Scope::stop() {
/* The stop method should release any resources, such as network connections where applicable */
delete app;
}

void Scope::run()
{
int zero = 0;
app = new QCoreApplication(zero, nullptr);
}


重新编译应用,如果有错误及时查出问题。这样我们就完成了对Qt的支持。

3)代码讲解


src/scope/scope.cpp

这个文件定义了一个unity::scopes::ScopeBase的类。它提供了客户端用来和Scope交互的起始接口。

这个类定义了“start", "stop"及"run"来运行scope。绝大多数开发者并不需要修改这个类的大部分实现。在我们的例程中,我们将不做任何的修改
它也同时实现了另外的两个方法:search 和 preview。我们一般来说不需要修改这俩个方法的实现。但是他们所调用的函数在具体的文件中必须实现

注意:我们可以通过研究Scope API的头文件来对API有更多的认识。更多的详细描述,开发者可以在http://developer.ubuntu.com/api/scopes/sdk-14.10/查看。


src/scope/query.cpp

这个文件定义了一个unity::scopes::SearchQueryBase类。

这个类用来产生由用户提供的查询字符串而生产的查询结果。这个结果可能是基于json或是xml的。这个类可以用来进行对返回的结果处理并显示。

得到由用户输入的查询字符串
向web services发送请求
生成搜索的结果(根据每个不同而不同)
创建搜索结果category(比如不同的layout-- grid/carousel)
根据不同的搜寻结果来绑定不同的category以显示我们所需要的UI
推送不同的category来显示给最终用户


创建并注册CategoryRenderers

在本例中,我们创建了两个JSON objects. 它们是最原始的字符串,如下所示,它有两个field:template及components。template是用来定义是用什么layout来显示我们所搜索到的结果。这里我们选择的是”vertical-journal"和carousel的layout。components项可以用来让我们选择预先定义好的field来显示我们所需要的结果。这里我们添加了"title"及“art"。

std::string CAT_GRID = R"(
{
"schema-version" : 1,
"template" : {
"category-layout" : "vertical-journal",
"card-layout": "horizontal",
"card-size": "small",
"collapsed-rows": 0
},
"components" : {
"title" : "title",
"subtitle":"subtitle",
"summary":"summary",
"art":{
"field": "art2",
"aspect-ratio": 1
}
}
} )";

//Create a JSON string to be used tro create a category renderer - uses carousel layout
std::string CR_CAROUSEL = R"(
{
"schema-version" : 1,
"template" : {
"category-layout" : "carousel",
"card-size": "large",
"overlay" : true
},
"components" : {
"title" : "title",
"art" : {
"field": "art",
"aspect-ratio": 1.6,
"fill-mode": "fit"
}
}
}
)";


这里每个layout都有自己需要显示的component(title, art,subtitle等)。这些我们可以通过在scope解析我们的xml时填上去。我们在文件的开始部分加入如上的的template的定义。

void Query::run(sc::SearchReplyProxy const& reply) {
/* This is where the actual processing of the current search query takes place.
* It's where you may want to query a local or remote data source for results
* matching the query.*/

// Trim the query string of whitespace
const CannedQuery &query(sc::SearchQueryBase::query());
string query_string = alg::trim_copy(query.query_string());

if ( query_string.empty() ) {
query_string = "北京";
}

QString queryUri = BASE_URI.arg(query_string.c_str());
qDebug() << "queryUrl: " << queryUri;

// Generate a network request to the OpenClipArt server and parse the result
QEventLoop loop;

QNetworkAccessManager manager;
QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect(&manager, &QNetworkAccessManager::finished,
[reply, query_string, this](QNetworkReply *msg){
QByteArray data = msg->readAll();

qDebug() << "XML data is: " << data.data();

Query::rssImporter(data,reply, QString::fromStdString(query_string));

});

// The query is the search string and was passed to this Query object's constructor by the client
// Empty search string yields no results with openclipart API.
manager.get(QNetworkRequest(QUrl(queryUri)));
loop.exec();
}

void Query::rssImporter(QByteArray &data, unity::scopes::SearchReplyProxy const& reply, QString title) {
QDomElement docElem;
QDomElement rootElem;
QDomDocument xmldoc;

QString query = title;
qDebug() << "query string: " << query;

if ( !xmldoc.setContent(data) ) {
qWarning()<<"Error importing data";
return;
}

rootElem = xmldoc.documentElement();

// Shows the CityWeatherResponse
qDebug() << "TagName: " << rootElem.tagName();

// Find CityWeatherResponse
docElem = rootElem.firstChildElement("date");
if (docElem.isNull()) {
qWarning()<< "Error in data," << "CityWeatherResponse" << " not found";
return;
}

QString date = docElem.text();
qDebug() << "date: " << date;

int indexYear = date.indexOf("-");
QString year = date.left(indexYear);

// Get the month
int indexMonth = date.indexOf("-", indexYear + 1);
QString month = date.mid(indexYear + 1, indexMonth - indexYear - 1);
// Get the day
QString day = date.right(date.length() - indexMonth - 1);
QDate qDate( year.toInt(), month.toInt(), day.toInt());
qDebug() << "Date: " << qDate.toString();

docElem = rootElem.firstChildElement("results");

QDomElement sum = docElem.firstChildElement("index");
QString summary = getSummary(sum);
qDebug() << "summary: " << summary;

QString pmiIndex = docElem.firstChildElement("pm25").text();
qDebug() << "PMI index: "  << pmiIndex;

QDomElement cityElem = docElem.firstChildElement("currentCity");
QString city = cityElem.text();
qDebug() << "city: " << city;

docElem = docElem.firstChildElement("weather_data");

QDomNodeList dateList = docElem.elementsByTagName("date");

// Below is also a way to get the list of the dates
int count = dateList.count();
for ( int i = 0; i < count; i ++ ) {
QDomNode node = dateList.at(i);
qDebug() << "date: " << node.toElement().text();
}

/* We're now registering (creating) two new categoryies, one with grid layout, the other wiht carousel.
* Categories can be created at any point
* during query processing inside the run method, but it's recommended
* to create them as soon as possible (ideally as soon as they are known to the scope) */
CategoryRenderer rdrGrid(CAT_GRID);
CategoryRenderer rdrCarousel(CR_CAROUSEL);

auto catCar = reply->register_category("openclipartcarousel", city.toStdString(), "", rdrCarousel);
auto catGrid = reply->register_category("Chineweather", "", "", rdrGrid);

QDomElement result = docElem.firstChildElement("date");
int index = 0;

bool done = false;

while (!result.isNull()) {
QString date = result.text();
qDebug() << "date: " << date;

QString dayPictureUrl = result.nextSiblingElement("dayPictureUrl").text();
qDebug() << "dayPictureUrl: " << dayPictureUrl;

QString nightPictureUrl = result.nextSiblingElement("nightPictureUrl").text();
qDebug() << "nightPictureUrl: " << nightPictureUrl;

QString weather = result.nextSiblingElement("weather").text();
qDebug() << "weather: " << weather;

QString wind = result.nextSiblingElement("wind").text();
qDebug() << "wind: " << wind;

QString temperature = result.nextSiblingElement("temperature").text();
qDebug() << "temperature: " << temperature;

result = result.nextSiblingElement("date");

QString daytime;
daytime.append("白天: ");
daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));

CategorisedResult catres(catCar);

// Set the picture for the day
catres.set_uri(URI.toStdString());
catres.set_dnd_uri(URI.toStdString());

catres.set_title(daytime.toStdString());
catres.set_art(dayPictureUrl.toStdString());

// Add some extra data, and they will be shown in the preview
catres["weather"] = Variant(weather.toStdString());
catres["temperature"] = Variant(temperature.toStdString());
catres["wind"] = Variant(wind.toStdString());

//push the categorized result to the client
if (!reply->push(catres)) {
break; // false from push() means search waas cancelled
}

// Set the picture for the night
catres.set_uri(URI.toStdString());
catres.set_dnd_uri(URI.toStdString());

QString nighttime;
nighttime.append("晚上: ");
// nighttime.append(date1);
nighttime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));

catres.set_title(nighttime.toStdString());
catres.set_art(nightPictureUrl.toStdString());

//push the categorized result to the client
if (!reply->push(catres)) {
break; // false from push() means search waas cancelled
}

if ( index == 0 && !done ) {
CategorisedResult catres(catGrid);

// we handle it specially for today
catres.set_uri(URI.toStdString());
catres.set_art(dayPictureUrl.toStdString());

QString sub = weather + " " + " " + temperature + " " + wind + "  PMI: " + pmiIndex;
catres["subtitle"] = sub.toStdString();
catres["weather"] = Variant(sub.toStdString());
catres["summary"]= summary.toStdString();
catres["wind"] = Variant(summary.toStdString());

QDateTime current = QDateTime::currentDateTime();\
QTime time = current.time();

QString daytime;
if ( time.hour() > 6 && time.hour() < 18 ) {
catres["art2"] = dayPictureUrl.toStdString();
daytime.append("白天: ");
daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));
} else {
catres["art2"] = nightPictureUrl.toStdString();
daytime.append("晚上: ");
daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));
}

catres.set_title(daytime.toStdString());

if (!reply->push(catres)) {
break; // false from push() means search waas cancelled
}

done = true;
continue;
}

index ++;

qDebug() <<  "============================================";
}

qDebug()<<"parsing ended";
}

// This function is used to get the summary of the day
QString Query::getSummary(QDomElement &docElem) {
QDomElement result = docElem.firstChildElement("title");

QString summary;
while (!result.isNull()) {
summary += result.text() + ": ";
summary += result.nextSiblingElement("zs").text() + ", ";
summary += result.nextSiblingElement("tipt").text() + ", ";
summary += result.nextSiblingElement("des").text() + "\n";

result = result.nextSiblingElement("title");
}

return summary;
}


为了能够顺利地进行编译,我们必须修改query.h的头文件,加入我们需要的方法定义及include一些头文件:

#ifndef SCOPE_QUERY_H_
#define SCOPE_QUERY_H_

#include <api/client.h>

#include <unity/scopes/SearchQueryBase.h>
#include <unity/scopes/ReplyProxyFwd.h>

#include <QByteArray> // added
#include <QString>     // added
#include <QDomDocument> // added
#include <QDomElement>  // added

namespace scope {

/**
* Represents an individual query.
*
* A new Query object will be constructed for each query. It is
* given query information, metadata about the search, and
* some scope-specific configuration.
*/
class Query: public unity::scopes::SearchQueryBase {
public:
Query(const unity::scopes::CannedQuery &query,
const unity::scopes::SearchMetadata &metadata, api::Config::Ptr config);

~Query() = default;

void cancelled() override;

void run(const unity::scopes::SearchReplyProxy &reply) override;

private:
void rssImporter(QByteArray &data, unity::scopes::SearchReplyProxy const& reply, QString title); // added
QString getSummary(QDomElement &docElem);  // added

private:
api::Client client_;
};

}

#endif // SCOPE_QUERY_H_


我们同时打开scope.cpp文件,并在文件的开头部分加入如下的代码:

#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QCoreApplication>

const QString BASE_URI = "http://api.map.baidu.com/telematics/v3/weather?location=%1&output=xml&ak=DdzwVcsGMoYpeg5xQlAFrXQt";
const QString URI = "http://www.weather.com.cn/html/weather/101010100.shtml";

// add this one to avoid too many typing
using namespace unity::scopes;


重新编译我们的scope。如果有错误请及时修正。同时在desktop或emulator中运行我们的应用。我们可以看到如下的画面。我们也可以在Unity Scope Tool的输入框中输入"上海",我们可以看到内容会发生变化。





更多关于 CategoryRenderer 类的介绍可以在 docs找到。

我们为每个JSON Object创建了一个CategoryRenderer,并同时向reply object注册。我们修改我们的run方法来实现显示:

我们从我们的Client API中的Client::Forecast来获取我们所需要的web service的数据,把数据填入到相应的CategorisedResult中。

关于代码的部分,我们可以运行我们的Scope,并同时查看输出的结果。我已经在程序中加入了大量的输出以方便调试及查看程序的运行情况。



我们也可以尝试点击我们的画面,在另外一个画面中可以看到一个图片。到这里,我们基本上已经看到了Scope工作的了。我们下面来更进一步来在Preview中显示更多的内容。


src/scope/preview.cpp

这个文件定义了一个unity::scopes::PreviewQueryBase类。

这个类定义了一个widget及一个layout来展示我们搜索到的结果。这是一个preview结i果,就像它的名字所描述的那样。

定义在preview时所需要的widget
让widget和搜索到的数据field一一对应起来
定义不同数量的layout列(由屏幕的尺寸来定)
把不同的widget分配到layout中的不同列中
把reply实例显示到layout的widget中

大多数的代码在“run"中实现。跟多关于这个类的介绍可以在http://developer.ubuntu.com/api/scopes/sdk-14.10/previewwidgets/找到。


Preview

Preview需要来生成widget并连接它们的field到CategorisedResult所定义的数据项中。它同时也用来为不同的显示环境(比如屏幕尺寸)生成不同的layout。根据不同的显示环境来生成不同数量的column。


Preview Widgets

这是一组预先定义好的widgets。每个都有一个类型。更据这个类型我们可以生成它们。你可以在这里找到Preview Widget列表及它们提供的的field类型。

这个例子使用了如下的widgets

header:它有title及subtitle field
image:它有source field有来显示从哪里得到这个art
text:它有text field
action:用来展示一个有"Open"的按钮。当用户点击时,所包含的URI将被打开

如下是一个例子,它定义了一个叫做“headerId"的PreviewWidget。第二个参数是它的类型"header"。

[cpp] view
plaincopy

PreviewWidget w_header("headerId", "header");

最终的程序如下:

#include <QString>  // added
#include <QDebug> // added

using namespace unity::scopes; // added

....

void Preview::run(sc::PreviewReplyProxy const& reply) {
//
// This preview handler just reuses values of the original result via
// add_attribute_mapping() calls, but it could also do another network
// request for more details if needed.
//

// Client can display Previews differently depending on the context
// By creates two layouts (one with one column, one with two) and then
// adding widgets to them differently, Unity can pick the layout the
// scope developer thinks is best for the mode
ColumnLayout layout1col(1), layout2col(2);

// add columns and widgets (by id) to layouts.
// The single column layout gets one column and all widets
layout1col.add_column({"headerId", "artId", "tempId", "windId", "actionsId"});

// The two column layout gets two columns.
// The first column gets the art and header widgets (by id)
layout2col.add_column({"artId", "headerId"});
// The second column gets the info and actions widgets
layout2col.add_column({"infoId", "windId", "actionsId"});

// Push the layouts into the PreviewReplyProxy intance, thus making them
// available for use in Preview diplay
reply->register_layout({layout1col, layout2col});

//Create some widgets
// header type first. note 'headerId' used in layouts
// second field ('header) is a standard preview widget type
PreviewWidget w_header("headerId", "header");
// This maps the title field of the header widget (first param)  to the
// title field in the result to be displayed in this preview, thus providing
// the result-specific data to the preview for display
w_header.add_attribute_mapping("title", "title"); // attribute, result field name
// Standard subtitle field here gets our 'artist' key value
w_header.add_attribute_mapping("subtitle", "weather");

PreviewWidget w_art("artId", "image");
w_art.add_attribute_mapping("source", "art"); // // key, result field name

PreviewWidget w_info("tempId", "text");
w_info.add_attribute_mapping("text", "temperature");

PreviewWidget w_wind("windId", "text");
w_wind.add_attribute_mapping("text", "wind");

Result result = PreviewQueryBase::result();
QString urlString(result["uri"].get_string().c_str());
qDebug() << "[Details] GET " << urlString;
// QUrl url = QUrl(urlString);

// Create an Open button and provide the URI to open for this preview result
PreviewWidget w_actions("actionsId", "actions");
VariantBuilder builder;
builder.add_tuple({
{"id", Variant("open")},
{"label", Variant("Open")},
{"uri", Variant(urlString.toStdString())} // uri set, this action will be handled by the Dash
});
w_actions.add_attribute_value("actions", builder.end());

// Bundle out widgets as required into a PreviewWidgetList
PreviewWidgetList widgets({w_header, w_art, w_info, w_wind, w_actions});
// And push them to the PreviewReplyProxy as needed for use in the preview
reply->push(widgets);
}


我们再重新运行程序,我们可以看到如下的画面。点击左边图下面的小图标,即可看到右边的图的当天的天气的详细情况。





为了能够使得我们的应用更加像我们自己的Scope,我们可以修改在“data”目录下的icon.png及logo.png文件。我们在手机上运行:



显示的图片如下。我们可以看到我们的图片已经发生变化了。





所有的程序代码可以在如下的网址找到:

bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherqtxml

或在地址:https://github.com/liu-xiao-guo/chinaweather_scope

该应用的另外一个使用json的Qt开发范例可以在如下的网址找到:

bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherfinal

4)调试应用

当我们在开发应用时,我们可以通过上面的“cerr”在“Application Output”输出结果来查看结果。当在手机运行时,我们也可以通过查看如下的文件来看Scope的运行情况:



我们可以通过查看在手机或emulator中的文件“~/.cache/upstart/scope-registry.log”来看最新的Scope的运行情况。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: