In this article, we will use the Ubuntu SDK to create a Scope app for “Chinese Weather” from scratch. Through this process, developers can understand the Scope development process on Ubuntu and have a deeper understanding of Scope. The application is complete using Qt C++ and STD C++. More about the Scope of knowledge, can be in url: developer.ubuntu.com/scopes/. The final picture of the application we developed:
\
\
In the last article, we completed a similar Scope application using STD C++. Here we use the Qt C++ APIs to do the same.
1) Start the Ubuntu SDK to create a basic Scope application
\
First, let’s open our Ubuntu SDK to create a basic application. We choose the menu “New File or Project” or use the hot key “Ctrl + N”. We chose the “Unity Scope” template.
\
\
\
We give our app a name “ChinaWeather”. Our colleague also chose template of type “Empty Scope”
\
\
\
Next, we also selected different kits at the same time so that we could compile and deploy our application on them all.
\
\
\
\
We run our scope directly on the Desktop of the computer. As the template we chose earlier said, this Scope doesn’t do anything substantial. To ensure that we can Run our Scope on the Desktop and see the interface, we can click on “Projects” and set it up in “Run Configuration” on the Desktop. Ensure that ChinaWeather is selected.
\
\
\
\
You can click on the results that are displayed.
\
\
\
If you can get to this point, your installation environment is fine. If you have problems, please refer to my Ubuntu SDK installation article. This most basic application has no content. In the following sections we add a few things to this to implement some of the things we need.
\
2) Add support for Qt
\
We can see that under the project’s “SRC” directory there are two directories: API and Scope. The code in the API directory is mainly for accessing our Web Service to get a JSON or XML data. In this project, we don’t have to use the Client class in this directory. Interested developers can try to separate their client and Scope code.
\
\
First of all, we need to apply for our developer account on baidu’s developer website. You can ask the website to apply for an account. Let’s do a test first to make sure our account works. According to the article mentioned, we can enter the following address in your browser: api.map.baidu.com/telematics/… . We can get the following: \
\
\
\
First, we can see that the API works. No problem at all. The schema shown is in XML format. Since we are going to use Qt and the XML libraries in Qt to help us parse the data we get in XML format, we added Qt support to the project. We first open the cmakelists. TXT file in “SRC” and add the following sentence:
\
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} )Copy the code
\
As you can see, we added calls to Qt Core, XML, and Network libraries. At the same time, we are open “tests/unit/CMakeLists. TXT” file, and add “qt5_use_modules (scope – the unit – tests the 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
)
Copy the code
Let’s recompile our application, and if we have no errors, our Scope can run directly under the Desktop. Here we add a “QCoreApplication” variable. This is mainly so that we can use the signal/slot mechanism and build a Qt application. Let’s modify the scope.h file and add the QoreApplication variable app and forward declaration. We must also include a method “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 };Copy the code
\
We also open scope.cpp and make the following changes:
\
#include <QCoreApplication> // added.void Scope::stop(a) {
/* The stop method should release any resources, such as network connections where applicable */
delete app;
}
void Scope::run(a)
{
int zero = 0;
app = new QCoreApplication(zero, nullptr);
}
Copy the code
\
Recompile the application and find out if there are any errors. This completes Qt support.
\
3) Code explanation
src/scope/scope.cpp
This file defines a Unity ::scopes::ScopeBase class. It provides the starting interface that the client uses to interact with the Scope.
- This class defines “start”, “stop”, and “run” to run the scope. Most developers don’t need to change most of the implementation of this class. In our routine, we will not make any changes
- It also implements two other methods: Search and Preview. We generally do not need to change the implementation of these two methods. But the functions they call must be implemented in a concrete file
Note: We can learn more about the Scope API by examining its header file. A more detailed description, developers can developer.ubuntu.com/api/scopes/… Look at it.
src/scope/query.cpp
This document defines a unity: : scopes: : SearchQueryBase class.
This class is used to produce query results produced by user-supplied query strings. The result could be JSON or XML based. This class can be used to process and display the returned results.
- Gets the query string entered by the user
- Send the request to the Web service
- Generate search results (different for each)
- Create search result categories (such as different layouts — Grid/Carousel)
- Bind different categories based on different search results to display the UI we need
- Push different categories to display to end users
Create and register CategoryRenderers
In this example, we create two JSON objects. These are primitive strings, as shown below, with two fields: template and Components. Template is used to define what layout to use to display the search results. Here we choose the layout of “vertical-Journal” and Carousel. The components TAB allows us to select a predefined field to display the results we want. Here we add “title” and “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 ", "the aspect - thewire" : 1.6, "the fill - mode", "fit"}}})";
Copy the code
\
Each layout has its own component(title, art,subtitle, etc.) that needs to be displayed. These can be filled in by scope when it parses our XML. We add the above template definition at the beginning of the file. \
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 = "Beijing";
}
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(a);qDebug() < <"XML data is: " << data.data(a); 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(a); }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(a);// Shows the CityWeatherResponse
qDebug() < <"TagName: " << rootElem.tagName(a);// Find CityWeatherResponse
docElem = rootElem.firstChildElement("date");
if (docElem.isNull()) {
qWarning() < <"Error in data," << "CityWeatherResponse" << " not found";
return;
}
QString date = docElem.text(a);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(a); docElem = rootElem.firstChildElement("results");
QDomElement sum = docElem.firstChildElement("index");
QString summary = getSummary(sum);
qDebug() < <"summary: " << summary;
QString pmiIndex = docElem.firstChildElement("pm25").text(a);qDebug() < <"PMI index: " << pmiIndex;
QDomElement cityElem = docElem.firstChildElement("currentCity");
QString city = cityElem.text(a);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(a);for ( int i = 0; i < count; i ++ ) {
QDomNode node = dateList.at(i);
qDebug() < <"date: " << node.toElement().text(a); }/* 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(a);qDebug() < <"date: " << date;
QString dayPictureUrl = result.nextSiblingElement("dayPictureUrl").text(a);qDebug() < <"dayPictureUrl: " << dayPictureUrl;
QString nightPictureUrl = result.nextSiblingElement("nightPictureUrl").text(a);qDebug() < <"nightPictureUrl: " << nightPictureUrl;
QString weather = result.nextSiblingElement("weather").text(a);qDebug() < <"weather: " << weather;
QString wind = result.nextSiblingElement("wind").text(a);qDebug() < <"wind: " << wind;
QString temperature = result.nextSiblingElement("temperature").text(a);qDebug() < <"temperature: " << temperature;
result = result.nextSiblingElement("date");
QString daytime;
daytime.append("By day:");
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("Evening:");
// 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(a); catres["weather"] = Variant(sub.toStdString());
catres["summary"]= summary.toStdString(a); catres["wind"] = Variant(summary.toStdString());
QDateTime current = QDateTime::currentDateTime(a); \ QTime time = current.time(a); QString daytime;if ( time.hour(a) >6 && time.hour()"18 ) {
catres["art2"] = dayPictureUrl.toStdString(a); daytime.append("By day:");
daytime.append(qDate.addDays(index).toString("ddd yyyy.MM.dd"));
} else {
catres["art2"] = nightPictureUrl.toStdString(a); daytime.append("Evening:");
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;
}
Copy the code
\
To compile smoothly, we must modify the query.h header to include the method definitions we need and some headers:
\
#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(a) 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_
Copy the code
We also open the scope. CPP file and add the following code at the beginning of the file:
\
#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;
Copy the code
Recompile our scope. If there are any mistakes, please correct them in time. Also run our application on desktop or Emulator. We can see the following picture. We can also type “Shanghai” in the Unity Scope Tool input box and we can see that the content changes.
\
\
\
More information about the CategoryRenderer class can be found in Docs.
We created a CategoryRenderer for each JSON Object and registered it with the Reply Object. We modify our run method to display:
We get the data we need from the Client::Forecast in our Client API and fill it in to the corresponding CategorisedResult.
\
As for the code, we can run our Scope and see the output at the same time. I’ve added a lot of output to the program to make it easier to debug and see how it works.
\
\
\
\
We can also try clicking on our screen to see an image in another screen. At this point, we’ve basically seen Scope in action. Let’s go a step further and show more content in Preview.
\
src/scope/preview.cpp
This document defines a unity: : scopes: : PreviewQueryBase class. \
This class defines a widget and a Layout to display our search results. This is a Preview result, as its name suggests.
- Define the widgets needed during Preview
- Map the widget to the data field found in the search
- Define a different number of Layout columns (depending on the screen size)
- Assign different widgets to different columns in a layout
- Display the Reply instance in a Layout widget
Most of the code is in “run”. In the implementation. More about the introduction of this class can be in developer.ubuntu.com/api/scopes/… To find it.
Preview
Preview is needed to generate widgets and connect their fields to data items defined by CategorisedResult. It is also used to generate different layouts for different display environments, such as screen sizes. Generate a different number of columns depending on the display environment.
Preview Widgets
This is a set of predefined widgets. Each has a type. And according to this type we can generate them. You can find a list of Preview Widgets and the field types they provide here.
This example uses the following widgets
- Header: It has a title and subtitle field
- Image: It has a source field to show you where to get this art from
- Text: It has a text field
- Action: Displays a button with “Open” on it. When the user clicks, the contained URI is opened
Here is an example that defines a PreviewWidget called “headerId”. The second argument is its type “header”. \
[cpp] view plain copy
- PreviewWidget w_header(“headerId”, “header”);
\
The final procedure is as follows:
\
#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(a);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);
}
Copy the code
\
When we run the program again, we should see the following screen. Click on the small icon below the picture on the left to see the weather details for the day in the picture on the right.
\
\
\
\
To make our application more like our own Scope, we can modify the icon. PNG and logo.png files in the “data” directory. We run it on our phones:
\
\
\
The image is shown below. We can see that our image has changed.
\
\
\
\
All the code can be found at the following url:
\
bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherqtxml\
\
Or at github.com/liu-xiao-gu…
\
Another example of Qt development using JSON for this application can be found at:
\
bzr branch lp:~liu-xiao-guo/debiantrial/chinaweatherfinal\
\
4) Debug the application
When we are developing an application, we can see the results in “ApplicationOutput” by printing the results in “cerr” above. When the mobile phone is running, we can also check the operation of Scope by viewing the following files:
\
\
\
We can check the latest operation status of scope by checking the file “~/. Cache /upstart/scope-registry. Log” in the mobile phone or emulator.
\
\
\