前面我们已经学习了如何在Ubuntu Touch上面制作一个Scope应用。Scope也是Ubuntu上面一个非常重要的,又和其他平台区分的一种应用。它能很好地把web services整合到手机平台中,就像是系统的一部分。Scope也对手机制造商来说也是非常重要的。它可以让他们深度定制自己的服务到系统中。
值得指出的是:由于一些原因,目前所有的Scope的开发必须是在Ubuntu OS Utopic (14.10)版本之上的。在Ubuntu OS 14.04上是不可以的。
1)创建一个最基本的Scope应用
首先打开我们的Ubuntu SDK。选择“Unity Scope"模版。
然后选择好项目的路径,并同时选好自己的项目名称"dianping"。我们选择“Empty scope” template。
接下来,我们就完成剩下的步骤来完成一个最基本的Scope应用。我们可以直接在电脑上运行。当然我们也可以把它运行到手机中。
如果你能运行到这里,说明你的安装环境是没有问题的。如果有问题的话,请参阅我的Ubuntu SDK安装文章。这个最基本的应用其实没有什么内容。在下面的章节中我们来向这里添加一些东西以实现我们所需要的一些东西。
2)加入对Qt的支持
我们首先打开在“src”中的CMakeLists.txt文件,并加入如下的句子:
find_package(Qt5Network REQUIRED)
find_package(Qt5Core REQUIRED)
include_directories(${Qt5Core_INCLUDE_DIRS})
include_directories(${Qt5Network_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 Network)
# Link against the object library and our external library dependencies
target_link_libraries(
scope
${SCOPE_LDFLAGS}
${Boost_LIBRARIES}
)
# 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 Network)
# Register the test with CTest
add_test(
scope-unit-tests
scope-unit-tests
)
3)代码讲解
src/dianping-scope.cpp
这个文件定义了一个unity::scopes::ScopeBase的类。它提供了客户端用来和Scope交互的起始接口。
- 这个类定义了“start", "stop"及"run"来运行scope。绝大多数开发者并不需要修改这个类的大部分实现。在我们的例程中,由于需要,我们将做简单的修改
- 它也同时实现了另外的两个方法:search 和 preview。我们一般来说不需要修改这俩个方法的实现。但是他们所调用的函数在具体的文件中必须实现
注意:我们可以通过研究Scope API的头文件来对API有更多的认识。更多的详细描述,开发者可以在http://developer.ubuntu.com/api/scopes/sdk-14.10/查看。
我们再重新编译我们的应用,如果我们没有错误的话,我们的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
};
#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);
}
src/dianping-query.cpp
这个文件定义了一个unity::scopes::SearchQueryBase类。
这个类用来产生由用户提供的查询字符串而生产的查询结果。这个结果可能是基于json或是xml的。这个类可以用来进行对返回的结果处理并显示。
- 得到由用户输入的查询字符串
- 向web services发送请求
- 生成搜索的结果(根据每个scope不同而不同)
- 创建搜索结果category(比如不同的layout-- grid/carousel)
- 根据不同的搜寻结果来绑定不同的category以显示我们所需要的UI
- 推送不同的category来显示给最终用户
基本上所有的代码集中在"run"方法中。这里我们加入了一个”QCoreApplication”变量。这主要是为了我们能够使用signal/slot机制。
当我们使用dianping发生请求时,我们需要对我们的请求发生请求进行签名。具体的细节可以参照链接点评网上的“API请求签名生成文档”。为此,我们设计了如下的helper方法getQueryString。这个类的输入参数是一个搜索的关键词,最终的输出是一个可以访问的URL。这里的appkey及secret可以在下面的代码中找到。通过这个方法,我们可以得到一个URL并在浏览器中打开链接看到我们需要解析的json数据。
QString Query::getQueryString(QString query) {
QMap<QString, QString> map;
map["category"] = "美食";
map["city"] = query;
map["sort"] = "2";
map["limit"] = "20";
map["platform"] = "2";
QCryptographicHash generator(QCryptographicHash::Sha1);
QString temp;
temp.append(appkey);
QMapIterator<QString, QString> i(map);
while (i.hasNext()) {
i.next();
qDebug() << i.key() << ": " << i.value();
temp.append(i.key()).append(i.value());
}
temp.append(secret);
qDebug() << temp;
qDebug() << "UTF-8: " << temp.toUtf8();
generator.addData(temp.toUtf8());
QString sign = generator.result().toHex().toUpper();
// qDebug() << sign;
QString url;
url.append(BASE_URI);
url.append("appkey=");
url.append(appkey);
url.append("&");
url.append("sign=");
url.append(sign);
i.toFront();
while (i.hasNext()) {
i.next();
qDebug() << i.key() << ": " << i.value();
url.append("&").append(i.key()).append("=").append(i.value());
}
qDebug() << "Final url: " << url;
return url;
}
整个Query类的设计如下:
#include <boost/algorithm/string/trim.hpp>
#include <scope/query.h>
#include <unity/scopes/Annotation.h>
#include <unity/scopes/CategorisedResult.h>
#include <unity/scopes/CategoryRenderer.h>
#include <unity/scopes/QueryBase.h>
#include <unity/scopes/SearchReply.h>
#include <iomanip>
#include <sstream>
// The following headers are added
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QCoreApplication>
namespace sc = unity::scopes;
namespace alg = boost::algorithm;
using namespace std;
using namespace api;
using namespace scope;
using namespace unity::scopes; // added
const QString appkey = "3562917596";
const QString secret = "091bf584e9d24edbbf48971d65307be3";
const QString BASE_URI = "http://api.dianping.com/v1/business/find_businesses?";
//Create a JSON string to be used tro create a category renderer - uses grid layout
std::string CR_GRID = R"(
{
"schema-version" : 1,
"template" : {
"category-layout" : "grid",
"card-size": "small"
},
"components" : {
"title" : "title",
"art" : {
"field": "art",
"aspect-ratio": 1.6,
"fill-mode": "fit"
}
}
}
)";
//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": "small",
"overlay" : true
},
"components" : {
"title" : "title",
"art" : {
"field": "art",
"aspect-ratio": 1.6,
"fill-mode": "fit"
}
}
}
)";
Query::Query(const sc::CannedQuery &query, const sc::SearchMetadata &metadata,
Config::Ptr config) :
sc::SearchQueryBase(query, metadata), client_(config) {
}
void Query::cancelled() {
client_.cancel();
}
void Query::run(sc::SearchReplyProxy const& reply) {
// Firstly, we get the query string here.
CannedQuery query = SearchQueryBase::query();
QString queryString = QString::fromStdString(query.query_string());
QString uri;
if ( queryString.isEmpty() ) {
queryString = QString("北京");
uri = getQueryString(queryString);
} else {
uri = getQueryString(queryString);
}
qDebug() << "queryString: " << queryString;
CategoryRenderer rdrGrid(CR_GRID);
CategoryRenderer rdrCarousel(CR_CAROUSEL);
QString title = queryString + "美味";
auto carousel = reply->register_category("dianpingcarousel", title.toStdString(), "", rdrCarousel);
auto grid = reply->register_category("dianpinggrid", "", "", rdrGrid);
QEventLoop loop;
QNetworkAccessManager manager;
QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect(&manager, &QNetworkAccessManager::finished,
[reply, carousel, grid, this](QNetworkReply *msg){
QByteArray data = msg->readAll();
QString json = data;
// qDebug() << "Data:" << json;
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8(), &err);
if (err.error != QJsonParseError::NoError) {
qCritical() << "Failed to parse server data: " << err.errorString();
} else {
// Find the "payload" array of results
QJsonObject obj = doc.object();
QJsonArray results = obj["businesses"].toArray();
// for each result
const std::shared_ptr<const Category> * top;
bool isgrid = false;
//loop through results of our web query with each result called 'result'
for(const auto &result : results) {
if ( isgrid ) {
top = &grid;
isgrid = false;
} else {
isgrid = true;
top = &carousel;
}
//create our categorized result using our pointer, which is either to out
//grid or our carousel Category
CategorisedResult catres((*top));
//treat result as Q JSON
QJsonObject resJ = result.toObject();
// load up vars with from result
auto name = resJ["name"].toString();
// qDebug() << "name: " << name;
name = removeTestInfo( name );
auto business_uri = resJ["business_url"].toString();
// qDebug() << "business_uri: " << business_uri;
auto s_photo_uri = resJ["s_photo_url"].toString();
// qDebug() << "s_photo_url: " << s_photo_uri;
auto photo_uri = resJ["photo_url"].toString();
// qDebug() << "photo_url: " << photo_uri;
auto rating_s_img_uri = resJ["rating_s_img_url"].toString();
// qDebug() << "rating_s_img_uri: " << rating_s_img_uri;
auto address = resJ["address"].toString();
auto telephone = resJ["telephone"].toString();
//set our CateogroisedResult object with out searchresults values
catres.set_uri(business_uri.toStdString());
catres.set_dnd_uri(business_uri.toStdString());
catres.set_title(name.toStdString());
catres.set_art(photo_uri.toStdString());
catres["address"] = Variant(address.toStdString());
catres["telephone"] = Variant(telephone.toStdString());
//push the categorized result to the client
if (!reply->push(catres)) {
break; // false from push() means search waas cancelled
}
}
}
}
);
qDebug() << "Uri:" << uri ;
manager.get(QNetworkRequest(QUrl(uri)));
loop.exec();
}
QString Query::getQueryString(QString query) {
QMap<QString, QString> map;
map["category"] = "美食";
map["city"] = query;
map["sort"] = "2";
map["limit"] = "20";
map["platform"] = "2";
QCryptographicHash generator(QCryptographicHash::Sha1);
QString temp;
temp.append(appkey);
QMapIterator<QString, QString> i(map);
while (i.hasNext()) {
i.next();
qDebug() << i.key() << ": " << i.value();
temp.append(i.key()).append(i.value());
}
temp.append(secret);
qDebug() << temp;
qDebug() << "UTF-8: " << temp.toUtf8();
generator.addData(temp.toUtf8());
QString sign = generator.result().toHex().toUpper();
// qDebug() << sign;
QString url;
url.append(BASE_URI);
url.append("appkey=");
url.append(appkey);
url.append("&");
url.append("sign=");
url.append(sign);
i.toFront();
while (i.hasNext()) {
i.next();
qDebug() << i.key() << ": " << i.value();
url.append("&").append(i.key()).append("=").append(i.value());
}
qDebug() << "Final url: " << url;
return url;
}
// The following method is used to remove the
// "这是一条测试商户数据,仅用于测试开发,开发完成后请申请正式数据..." string
const QString TEST_STRING = "(这是一条测试商户数据,仅用于测试开发,开发完成后请申请正式数据...)";
QString Query::removeTestInfo(QString name)
{
if ( name.contains(TEST_STRING) ) {
int index = name.indexOf(TEST_STRING);
QString newName = name.left(index);
// qDebug() << "newName: " << newName;
return newName;
} else {
qDebug() << "it does not contain the string";
return name;
}
}
我们可以参阅 http://developer.dianping.com/来注册成为dianping网站的开发者。在网址 http://developer.dianping.com/app/tutorial可以找到开发指南。可以通过getQueryString()方法来得到所需要请求的uri。
创建并注册CategoryRenderers
在本例中,我们创建了两个JSON objects. 它们是最原始的字符串,如下所示,它有两个field:template及components。template是用来定义是用什么layout来显示我们所搜索到的结果。这里我们选择的是”grid"及小的card-size。components项可以用来让我们选择预先定义好的field来显示我们所需要的结果。这里我们添加了"title"及“art"。
std::string CR_GRID = R"(
{
"schema-version" : 1,
"template" : {
"category-layout" : "grid",
"card-size": "small"
},
"components" : {
"title" : "title",
"art" : {
"field": "art",
"aspect-ratio": 1.6,
"fill-mode": "fit"
}
}
}
)";
更多关于 CategoryRenderer 类的介绍可以在 docs找到。
我们为每个JSON Object创建了一个CategoryRenderer,并同时向reply object注册:
CategoryRenderer rdrGrid(CR_GRID);
CategoryRenderer rdrCarousel(CR_CAROUSEL);
QString title = queryString + "美味";
auto carousel = reply->register_category("dianpingcarousel", title.toStdString(), "", rdrCarousel);
auto grid = reply->register_category("dianpinggrid", "", "", rdrGrid);
我们可以运行我们所得到的程序,看看我们的结果。我们可以在“Search query”中输入其它城市,比如“上海”或“广州”等,我们就会看到不同的显示结果。
src/dianping-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"。
PreviewWidget w_header("headerId", "header");
最终的程序如下:
#include <scope/preview.h>
#include <unity/scopes/ColumnLayout.h>
#include <unity/scopes/PreviewWidget.h>
#include <unity/scopes/PreviewReply.h>
#include <unity/scopes/Result.h>
#include <unity/scopes/VariantBuilder.h>
#include <iostream>
#include <QString>
namespace sc = unity::scopes;
using namespace std;
using namespace scope;
using namespace unity::scopes;
Preview::Preview(const sc::Result &result, const sc::ActionMetadata &metadata) :
sc::PreviewQueryBase(result, metadata) {
}
void Preview::cancelled() {
}
void Preview::run(sc::PreviewReplyProxy const& reply) {
// Support three different column layouts
// 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", "infoId", "telId", "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", "telId", "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");
// Standard subtitle field here gets our 'artist' key value
// w_header.add_attribute_mapping("subtitle", "artist");
PreviewWidget w_art("artId", "image");
w_art.add_attribute_mapping("source", "art");
PreviewWidget w_info("infoId", "text");
w_info.add_attribute_mapping("text", "address");
PreviewWidget w_tel("telId", "text");
w_tel.add_attribute_mapping("text", "telephone");
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_tel, w_actions});
// And push them to the PreviewReplyProxy as needed for use in the preview
reply->push(widgets);
}
运行的效果图如下:
在手机上的运行情况如下:
整个完整的代码在如下的网址可以看到:
bzr branch lp:~liu-xiao-guo/debiantrial/dianpingqtjson
一个比这个更加完整的department scope可以在网址“在Ubuntu OS上创建一个department 点评Scope (Qt XML)”找到。