前面我们已经学习了如何在Ubuntu Touch上面制作一个Scope应用。Scope也是Ubuntu上面一个非常重要的,又和其他平台区分的一种应用。它能很好地把web services整合到手机平台中,就像是系统的一部分。
值得指出的是:由于一些原因,目前所有的Scope的开发必须是在Ubuntu OS Utopic (14.10)版本之上的。在Ubuntu OS 14.04上是不可以的。
1)创建一个最基本的Scope应用
首先打开我们的Ubuntu SDK。选择“Unity Scope"模版。
然后选择好项目的路径,并同时选好自己的项目名称"dianping"。
接下来,我们就完成剩下的步骤来完成一个最基本的Scope应用。我们可以直接在电脑上运行。当然我们也可以把它运行到手机中。
如果你能运行到这里,说明你的安装环境是没有问题的。如果有问题的话,请参阅我的Ubuntu SDK安装文章。这个最基本的应用其实没有什么内容。在下面的章节中我们来向这里添加一些东西以实现我们所需要的一些东西。
2)代码讲解
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/查看。
src/dianping-query.cpp
这个文件定义了一个unity::scopes::SearchQueryBase类。
这个类用来产生由用户提供的查询字符串而生产的查询结果。这个结果可能是基于json或是xml的。这个类可以用来进行对返回的结果处理并显示。
- 得到由用户输入的查询字符串
- 向web services发送请求
- 生成搜索的结果(根据每个应用不能而不同)
- 创建搜索结果category(比如不同的layout-- grid/carousel)
- 根据不同的搜寻结果来绑定不同的category以显示我们所需要的UI
- 推送不同的category来显示给最终用户
基本上所有的代码集中在"run"方法中。这里我们加入了一个”QCoreApplication”变量。这主要是为了我们能够使用signal/slot机制。
#ifndef DEMOSCOPE_H
#define DEMOSCOPE_H
#include <unity/scopes/ScopeBase.h>
#include <unity/scopes/QueryBase.h>
#include <unity/scopes/ReplyProxyFwd.h>
#include <unity/scopes/QueryBase.h>
#include <unity/scopes/PreviewQueryBase.h>
#include <QCoreApplication>
class DianpingScope : public unity::scopes::ScopeBase
{
public:
virtual void start(std::string const&) override;
virtual void stop() override;
unity::scopes::PreviewQueryBase::UPtr preview(const unity::scopes::Result& result,
unity::scopes::ActionMetadata const& metadata) override;
virtual unity::scopes::SearchQueryBase::UPtr search(unity::scopes::CannedQuery const& q,
unity::scopes::SearchMetadata const& metadata) override;
void run() override;
private:
QCoreApplication *app;
};
#endif
#include "dianping-scope.h"
#include "dianping-query.h"
#include "dianping-preview.h"
#include <unity-scopes.h>
using namespace unity::scopes;
void DianpingScope::start(std::string const&)
{
}
void DianpingScope::stop()
{
delete app;
}
void DianpingScope::run()
{
// an instance of QApplication is needed to make Qt happy
int zero = 0;
app = new QCoreApplication(zero, nullptr);
}
SearchQueryBase::UPtr DianpingScope::search(CannedQuery const &q, SearchMetadata const& metadata)
{
SearchQueryBase::UPtr query(new DianpingQuery(q, metadata));
return query;
}
PreviewQueryBase::UPtr DianpingScope::preview(Result const& result, ActionMetadata const& metadata) {
PreviewQueryBase::UPtr preview(new DianpingPreview(result, metadata));
return preview;
}
#define EXPORT __attribute__ ((visibility ("default")))
extern "C"
{
EXPORT
unity::scopes::ScopeBase*
// cppcheck-suppress unusedFunction
UNITY_SCOPE_CREATE_FUNCTION()
{
return new DianpingScope();
}
EXPORT
void
// cppcheck-suppress unusedFunction
UNITY_SCOPE_DESTROY_FUNCTION(unity::scopes::ScopeBase* scope_base)
{
delete scope_base;
}
}
如果这时我们来试图来编译应用,可能会出现如下的错误。问题在于我们并没有加载相应的Qt库。
In file included from /home/liuxg/Examples/dianping/src/dianping-scope.cpp:1:0:
/home/liuxg/Examples/dianping/src/dianping-scope.h:10:28: fatal error: QCoreApplication: No such file or directory
#include <QCoreApplication>
由于这个是一个需要网路连接的应用,我们可以把如下的Qt5Network加入到我们的CMake (src/CMakeLists.txt)文件中:
add_definitions(-DQT_NO_KEYWORDS)
find_package(Qt5Network REQUIRED)
add_library(
${SCOPE_LIB_TARGET_NAME} SHARED
dianping-preview.cpp
dianping-query.cpp
dianping-scope.cpp
)
qt5_use_modules(${SCOPE_LIB_TARGET_NAME} Network)
target_link_libraries(${SCOPE_LIB_TARGET_NAME} ${UNITY_SCOPES_LDFLAGS})
set_property(TARGET ${SCOPE_LIB_TARGET_NAME} PROPERTY COMPILE_FLAGS ${UNITY_SCOPES_CFLAGS})
install(TARGETS ${SCOPE_LIB_TARGET_NAME}
LIBRARY DESTINATION "${SCOPE_INSTALLDIR}"
)
这里我们必须注意的是也要同时加上add_definitions(-DQT_NO_KEYWORDS)。否则我们接下来当我们使用signal/slot时会出现一些错误。我们再编译我们的应用。确保这个时候没有任何的编译错误。在CMakeLists.txt中加入这些行后,可以在项目中点击右键再重运行“Run CMake"以确保新的变化已经起作用。
#include "dianping-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 <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QCoreApplication>
using namespace unity::scopes;
//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"
},
"components" : {
"title" : "title",
"art" : {
"field": "art",
"aspect-ratio": 1.6,
"fill-mode": "fit"
}
}
}
)";
DianpingQuery::DianpingQuery(CannedQuery const& query, SearchMetadata const& metadata) :
SearchQueryBase(query, metadata)
{
}
DianpingQuery::~DianpingQuery()
{
}
void DianpingQuery::cancelled()
{
}
const QString appkey = "3562917596";
const QString secret = "xxxxxxxxxxxx"; // PLEASE supply your secret here
const QString BASE_URI = "http://api.dianping.com/v1/business/find_businesses?";
QString DianpingQuery::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;
}
void DianpingQuery::run(unity::scopes::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 topCar = reply->register_category("dianpingcarousel", title.toStdString(), "", rdrCarousel);
auto topGrid = reply->register_category("dianpinggrid", "", "", rdrGrid);
QEventLoop loop;
QNetworkAccessManager manager;
QObject::connect(&manager, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect(&manager, &QNetworkAccessManager::finished,
[reply, topCar, topGrid, 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 grid = false;
//loop through results of our web query with each result called 'result'
for(const auto &result : results) {
if ( grid ) {
top = &topGrid;
grid = false;
} else {
grid = true;
top = &topCar;
}
//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();
auto business_uri = resJ["business_url"].toString();
qDebug() << "business_uri: " << business_uri;
auto s_photo_uri = resJ["s_photo_url"].toString();
auto photo_uri = resJ["photo_url"].toString();
auto rating_s_img_uri = resJ["rating_s_img_url"].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());
//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();
}
我们可以参阅 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 topCar = reply->register_category("dianpingcarousel", title.toStdString(), "", rdrCarousel);
auto topGrid = reply->register_category("dianpinggrid", "", "", rdrGrid);
我们可以运行我们所得到的程序,看看我们的结果。
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"dianping-preview.h"
#include <QString>
#include <QDebug>
#include<unity/scopes/PreviewWidget.h>
#include<unity/scopes/ColumnLayout.h>
#include<unity/scopes/PreviewReply.h>
#include <unity/scopes/VariantBuilder.h>
using namespace unity::scopes;
DianpingPreview::DianpingPreview(Result const& result, ActionMetadata const& metadata) :
PreviewQueryBase(result, metadata)
{
}
DianpingPreview::~DianpingPreview()
{
}
void DianpingPreview::cancelled()
{
}
void DianpingPreview::run(unity::scopes::PreviewReplyProxy const& reply)
{
// 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", "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", "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", "description");
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_actions});
// And push them to the PreviewReplyProxy as needed for use in the preview
reply->push(widgets);
}
运行的效果图如下:
整个完整的代码在如下的网址可以看到:
bzr branch lp:~liu-xiao-guo/debiantrial/dianpingtraining