在前面的一些文章中,我们已经介绍了一些怎么利用Qt和C++ API来创建一个Scope。它们都是一些基本的Scope。在这篇文章中,我们将介绍department Scope,并掌握开发它的方法。Department Scope将会在许多的Scope中进行分类搜寻。更多关于Scope的介绍可以在网址http://developer.ubuntu.com/scopes/找到。我们最终的Scope的界面如下:
1)什么是department Scope
http://api.dianping.com/v1/business/find_businesses?appkey=3562917596&sign=16B7FAB0AE9C04F356C9B1BE3BB3B77829F83EDA&category=美食&city=上海&latitude=31.18268013000488&longitude=121.42769622802734&sort=1&limit=20&offset_type=1&out_offset_type=1&platform=2
进行分析,我们可以把“category”设置为我们的部门,这样我们就可以对每个领域进行分别的搜寻。我们也可以通API接口来得到所有点评的category:
2)创建一个基本的Scope
2)加入对Qt的支持
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}
)
# 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
)
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);
}
3)代码讲解
src/scope/scope.cpp
- 这个类定义了“start", "stop"及"run"来运行scope。绝大多数开发者并不需要修改这个类的大部分实现。在我们的例程中,我们将不做任何的修改
- 它也同时实现了另外的两个方法:search 和 preview。我们一般来说不需要修改这俩个方法的实现。但是他们所调用的函数在具体的文件中必须实现
sc::SearchQueryBase::UPtr Scope::search(const sc::CannedQuery &query,
const sc::SearchMetadata &metadata) {
const QString scopePath = QString::fromStdString(scope_directory());
const QString cachePath =QString::fromStdString(cache_directory());
// Boilerplate construction of Query
return sc::SearchQueryBase::UPtr(new Query(query, metadata, scopePath,cachePath, config_));
}
同时我们也要对Query类中的构造函数进行修改,以便能够进行编译:
Query::Query(const sc::CannedQuery &query, const sc::SearchMetadata &metadata, QString const& scopeDir,
QString const& cacheDir, Config::Ptr config) :
sc::SearchQueryBase( query, metadata ),
m_scopeDir( scopeDir ),
m_cacheDir( cacheDir ),
client_(config)
{
qDebug() << "CacheDir: " << m_cacheDir;
qDebug() << "ScopeDir " << m_scopeDir;
qDebug() << m_urlRSS;
}
class Query: public unity::scopes::SearchQueryBase {
....
private:
QString m_scopeDir;
QString m_cacheDir;
....
}
重新编译我们的Scope。如果大家此时还有任何的问题的话,可以下载我的源码
src/scope/query.cpp
这个类用来产生由用户提供的查询字符串而生产的查询结果。这个结果可能是基于json或是xml的。这个类可以用来进行对返回的结果处理并显示。
- 得到由用户输入的查询字符串
- 向web services发送请求
- 生成搜索的结果(根据每个scope不同而不同)
- 创建搜索结果category(比如不同的layout-- grid/carousel)
- 根据不同的搜寻结果来绑定不同的category以显示我们所需要的UI
- 推送不同的category来显示给最终用户
- 基本上所有的代码集中在"run"方法中。这里我们加入了一个”QCoreApplication”变量。这主要是为了我们能够使用signal/slot机制。
QString Query::getUrl(QString addr, QMap<QString, QString> map) {
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();
QString url;
url.append(addr);
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;
}
这里用到的“ appKey”及“ secret”是两个定义的QString常量。开发者需要到点评的 网站进行申请。这里的addr就是请求的url的前面部分,比如http://api.dianping.com/v1/metadata/get_categories_with_businesses。这里的map实际上是像如下的一组数据,用来存储请求的参数的。我们利用这个方法来得到我们的department的url。如下:
Query::Query(const sc::CannedQuery &query, const sc::SearchMetadata &metadata, QString const& scopeDir,
QString const& cacheDir, Config::Ptr config) :
sc::SearchQueryBase( query, metadata ),
m_scopeDir( scopeDir ),
m_cacheDir( cacheDir ),
// m_limit( 0 ),
client_(config)
{
qDebug() << "CacheDir: " << m_cacheDir;
qDebug() << "ScopeDir " << m_scopeDir;
QMap<QString,QString> map;
map["format"] = "xml";
m_urlRSS = getUrl(DEPARTMENTS, map);
qDebug() << "m_urlRSS: " << m_urlRSS;
}
const QString DEPARTMENTS = "http://api.dianping.com/v1/metadata/get_categories_with_businesses?";
m_urlRSS: "http://api.dianping.com/v1/metadata/get_categories_with_businesses?appkey=3562917596&sign=4BAF8DD42A36538E17207A1C10F819571B00BF6E&format=xml"
void Query::run(sc::SearchReplyProxy const& reply) {
qDebug() << "Run is started .............................!";
// Create an instance of disk cache and set cache directory
m_diskCache = new QNetworkDiskCache();
m_diskCache->setCacheDirectory(m_cacheDir);
QEventLoop loop;
QNetworkAccessManager managerDepts;
QObject::connect(&managerDepts, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect(&managerDepts, &QNetworkAccessManager::finished,
[reply,this](QNetworkReply *msg){
if( msg->error()!= QNetworkReply::NoError ){
qWarning() << "failed to retrieve raw data, error:" << msg->error();
rssError(reply,ERROR_Connection);
return;
}
QByteArray data = msg->readAll();
// qDebug() << "XML data is: " << data.data();
QString deptUrl = rssDepartments( data, reply );
CannedQuery cannedQuery = query();
QString deptId = qstr(cannedQuery.department_id());
qDebug() << "department id: " << deptId;
if (!query().department_id().empty()){ // needs departments support
qDebug() << "it is not empty xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx!";
deptUrl = m_depts[deptId];
qDebug() << "depatUrl: " << deptUrl;
} else {
qDebug() << "It is empty ===================================!";
}
if ( deptUrl.isEmpty() )
return;
});
managerDepts.setCache(m_diskCache);
managerDepts.get(QNetworkRequest(QUrl(m_urlRSS)));
loop.exec();
}
这里其实很简单,我们通过对m_urlRSS的请求,并把得到的结果传给rssDepartments来解释所得到的xml格式的数据。每一个department都有一个叫做department_id来识别。它是一个独有的区别其他的String。rss_Departments的实现如下:
QString Query::rssDepartments( QByteArray &data, unity::scopes::SearchReplyProxy const& reply ) {
QDomElement docElem;
QDomDocument xmldoc;
DepartmentList rss_depts;
QString firstname = "";
CannedQuery myquery( SCOPE_NAME );
myquery.set_department_id( TOP_DEPT_NAME );
Department::SPtr topDept;
if ( !xmldoc.setContent(data) ) {
qWarning()<<"Error importing data";
return firstname;
}
docElem = xmldoc.firstChildElement("results");
if (docElem.isNull()) {
qWarning() << "Error in data," << "results" << " not found";
return firstname;
}
docElem = docElem.firstChildElement("categories");
if ( docElem.isNull() ) {
qWarning() << "Error in data," << "categories" << " not found";
return firstname;
}
docElem = docElem.firstChildElement("category");
// Clear the previous departments since the URL may change according to settings
m_depts.clear();
int index = 0;
while ( !docElem.isNull() ) {
QString category = docElem.attribute("name","");
qDebug() << "category: " << category;
if ( !category.isEmpty() ) {
QString url = getDeptUrl(category);
QString deptId = QString::number(index);
if (firstname.isEmpty()) {
//Create the url here
firstname = url;
topDept = move(unity::scopes::Department::create( "",
myquery, category.toStdString()));
} else {
Department::SPtr aDept = move( unity::scopes::Department::create( deptId.toStdString(),
myquery, category.toStdString() ) );
rss_depts.insert( rss_depts.end(), aDept );
}
m_depts.insert( QString::number(index), url );
index++;
}
docElem = docElem.nextSiblingElement("category");
}
// Dump the deparmemts
QMapIterator<QString, QString> i(m_depts);
while (i.hasNext()) {
i.next();
qDebug() << i.key() << ": " << i.value();
}
topDept->set_subdepartments( rss_depts );
try {
reply->register_departments( topDept );
} catch (std::exception const& e) {
qWarning() << "Error happened: " << e.what();
}
return firstname;
}
bzr branch lp:~liu-xiao-guo/debiantrial/dianpingdept2
void Query::run(sc::SearchReplyProxy const& reply) {
qDebug() << "Run is started .............................!";
// Initialize the scopes
initScope();
// Get the current location of the search
auto metadata = search_metadata();
if ( metadata.has_location() ) {
qDebug() << "Location is supported!";
auto location = metadata.location();
if ( location.has_altitude()) {
cerr << "altitude: " << location.altitude() << endl;
cerr << "longitude: " << location.longitude() << endl;
cerr << "latitude: " << location.latitude() << endl;
auto latitude = std::to_string(location.latitude());
auto longitude = std::to_string(location.longitude());
m_longitude = QString::fromStdString(longitude);
m_latitude = QString::fromStdString(latitude);
}
if ( m_longitude.isEmpty() ) {
m_longitude = DEFAULT_LONGITUDE;
}
if ( m_latitude.isEmpty() ) {
m_latitude = DEFAULT_LATITUDE;
}
qDebug() << "m_longitude1: " << m_longitude;
qDebug() << "m_latitude1: " << m_latitude;
} else {
qDebug() << "Location is not supported!";
m_longitude = DEFAULT_LONGITUDE;
m_latitude = DEFAULT_LATITUDE;
}
// Create an instance of disk cache and set cache directory
m_diskCache = new QNetworkDiskCache();
m_diskCache->setCacheDirectory(m_cacheDir);
QEventLoop loop;
QNetworkAccessManager managerDepts;
QObject::connect(&managerDepts, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect(&managerDepts, &QNetworkAccessManager::finished,
[reply,this](QNetworkReply *msg){
if( msg->error()!= QNetworkReply::NoError ){
qWarning() << "failed to retrieve raw data, error:" << msg->error();
rssError(reply,ERROR_Connection);
return;
}
QByteArray data = msg->readAll();
// qDebug() << "XML data is: " << data.data();
QString deptUrl = rssDepartments( data, reply );
CannedQuery cannedQuery = query();
QString deptId = qstr(cannedQuery.department_id());
qDebug() << "department id: " << deptId;
if (!query().department_id().empty()){ // needs departments support
qDebug() << "it is not empty xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx!";
deptUrl = m_depts[deptId];
qDebug() << "depatUrl: " << deptUrl;
} else {
qDebug() << "It is empty ===================================!";
}
if ( deptUrl.isEmpty() )
return;
QEventLoop loop;
QNetworkAccessManager managerRSS;
QObject::connect( &managerRSS, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QObject::connect( &managerRSS, &QNetworkAccessManager::finished,
[reply,this](QNetworkReply *msg ){
if( msg->error() != QNetworkReply::NoError ){
qWarning() << "failed to retrieve specific dept raw data, error:" <<msg->error();
rssError( reply, ERROR_Connection );
return;
}
QByteArray data = msg->readAll();
if( query().query_string().empty() ){
rssImporter( data, reply, CATEGORY_HEADER );
} else {
rssImporter( data, reply, CATEGORY_SEARCH );
}
});
managerRSS.setCache( m_diskCache );
managerRSS.get( QNetworkRequest( QUrl(deptUrl)) );
loop.exec();
});
managerDepts.setCache(m_diskCache);
managerDepts.get(QNetworkRequest(QUrl(m_urlRSS)));
loop.exec();
}
上面我们可以看到我们定义了另外一个QEventLoop。在这里,我们通过对刚才所得到的deptUrl做一个新的请求,并把得到的数据传到rssImporter函数中进行解析。
void Query::rssImporter(QByteArray &data, unity::scopes::SearchReplyProxy const& reply, QString title) {
QDomElement docElem;
QDomDocument xmldoc;
CannedQuery cannedQuery = query();
QString query = qstr( cannedQuery.query_string() );
if ( !xmldoc.setContent( data ) ) {
qWarning()<<"Error importing data";
return;
}
docElem = xmldoc.documentElement();
//find result
docElem = docElem.firstChildElement("businesses");
if (docElem.isNull()) {
qWarning()<<"Error in data,"<< "result" <<" not found";
return;
}
CategoryRenderer rdrGrid(CR_GRID);
CategoryRenderer rdrCarousel(CR_CAROUSEL);
auto carousel = reply->register_category("dianpingcarousel", title.toStdString(), "", rdrCarousel);
auto grid = reply->register_category("dianpinggrid", "", "", rdrGrid);
bool isgrid = false;
docElem = docElem.firstChildElement("business");
while (!docElem.isNull()) {
QString business_id = docElem.firstChildElement("business_id").text();
// qDebug() << "business_id: " << business_id;
QString name = docElem.firstChildElement("name").text();
// qDebug() << "name: " << name;
// Let's get rid of the test info in the string
name = removeTestInfo(name);
QString branch_name = docElem.firstChildElement("branch_name").text();
// qDebug() << "branch_name: " << branch_name;
QString address = docElem.firstChildElement("address").text();
// qDebug() << "address: " << address;
QString telephone = docElem.firstChildElement("telephone").text();
// qDebug() << "telephone: " << telephone;
QString city = docElem.firstChildElement("city").text();
// qDebug() << "city: " << city;
QString photo_url = docElem.firstChildElement("photo_url").text();
// qDebug() << "photo_url: " << photo_url;
QString s_photo_url = docElem.firstChildElement("s_photo_url").text();
// qDebug() << "s_photo_url: " << s_photo_url;
QString rating_s_img_uri = docElem.firstChildElement("rating_s_img_uri").text();
// qDebug() << "rating_s_img_uri: " << rating_s_img_uri;
QString business_url = docElem.firstChildElement("business_url").text();
// qDebug() << "business_url: " << business_url;
QDomElement deals = docElem.firstChildElement("deals");
QDomElement deal = deals.firstChildElement("deal");
QString summary = deal.firstChildElement("description").text();
// qDebug() << "Summary: " << summary;
if ( !query.isEmpty() ) {
if ( !name.contains( query, Qt::CaseInsensitive ) &&
!summary.contains( query, Qt::CaseInsensitive ) &&
!address.contains( query, Qt::CaseInsensitive ) ) {
qDebug() << "it is going to be skipped";
docElem = docElem.nextSiblingElement("business");
continue;
} else {
qDebug() << "it is going to be listed!";
}
}
docElem = docElem.nextSiblingElement("business");
// for each result
const std::shared_ptr<const Category> * top;
if ( isgrid ) {
top = &grid;
isgrid = false;
} else {
isgrid = true;
top = &carousel;
}
CategorisedResult catres((*top));
catres.set_uri(business_url.toStdString());
catres.set_dnd_uri(business_url.toStdString());
catres.set_title(name.toStdString());
catres["subtitle"] = address.toStdString();
catres["summary"] = summary.toStdString();
catres["fulldesc"] = summary.toStdString();
catres.set_art(photo_url.toStdString());
catres["art2"] = s_photo_url.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()<<"parsing ended";
}
if ( !query.isEmpty() ) {
if ( !name.contains( query, Qt::CaseInsensitive ) &&
!summary.contains( query, Qt::CaseInsensitive ) &&
!address.contains( query, Qt::CaseInsensitive ) ) {
qDebug() << "it is going to be skipped";
docElem = docElem.nextSiblingElement("business");
continue;
} else {
qDebug() << "it is going to be listed!";
}
}
创建并注册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);
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");
4)加入设置
// The followoing function is used to retrieve the settings for the scope
void Query::initScope()
{
qDebug() << "Going to retrieve the settings!";
unity::scopes::VariantMap config = settings(); // The settings method is provided by the base class
if (config.empty())
qDebug() << "CONFIG EMPTY!";
m_limit = config["limit"].get_double();
cerr << "limit: " << m_limit << endl;
}
并在“run”的开始部分调用它:
void Query::run(sc::SearchReplyProxy const& reply) {
qDebug() << "Run is started .............................!";
// Initialize the scopes
initScope();
....
}
[limit]
type = number
defaultValue = 20
displayName = 搜寻条数
configure_file(
"com.ubuntu.developer.liu-xiao-guo.dianping_dianping-settings.ini"
"${CMAKE_BINARY_DIR}/src/com.ubuntu.developer.liu-xiao-guo.dianping_dianping-settings.ini"
)
INSTALL(
FILES "${CMAKE_BINARY_DIR}/src/com.ubuntu.developer.liu-xiao-guo.dianping_dianping-settings.ini"
DESTINATION "${SCOPE_INSTALL_DIR}"
)
我们可以运行一下“Run CMake”,这样,我们在Project中可以看到新添加的.ini文件。重新运行我们的Scope,并在Scope的右上角的设置图标(像有锯齿的 )去尝试改变limit的值,看看效果是什么样的。