在这篇文章中,我们将详细介绍如何使用我们的Ubuntu SDK来从零开始来创建一个最基本的RSS阅读器.当我们完成整个练习后,我们将熟悉Ubuntu应用的整个开发流程.
特别提醒:在模拟器中参阅文章"怎么在Ubuntu手机中打开开发者模式"打开开发者模式,这样才可以把应用部署到模拟器中。如果我们想把自己的应用部署到我们的手机中,我们也需要在我们的手机中做同样的设置.
我们整个应用的将会像上面图片显示的那样.让我们现在马上开始吧.
1)安装好自己的SDK
2)创建一个最基本的应用框架
在这里注意maintainer的格式。如果有红色的错误显示,请查看一下在“<”的左边有没有留有一个空格.
为了显示的更像一个是一个手机的界面,我们直接把“main.qml"中的尺寸设置如下:
width: units.gu(60)
height: units.gu(85)
分辨率无关:
Ubuntu的用户界面工具包的重要功能是把用户定义多个设备尺寸进行匹配。采取的方法是定义一个新的单元类型,网格单元(简写为gu)。网格单位转换为像素值取决于应用程序运行在屏幕上和设备的类型。下面是一些例子:
Device | Conversion |
Most laptops | 1 gu = 8 px |
Retina laptops | 1 gu = 16 px |
Smart phones | 1 gu = 18 px |
我们首先选择"Ubuntu SDK Desktop Kit"我们可以点击SDK屏幕左下方的绿色的运行按钮,或使用热键(Ctrl +R),运行应用。 如下图所示:
在我们的应用中,有一些代码是C++代码.我们先不要理会这些代码.让我们直接关注我们的QML文件.这些文件是用来构成我们UI的文件.最原始的应用其实没有什么。你可以按一下按钮改变方框中的文字。下面我们来开始设计我们的应用。
2)删除我们不需要的代码
由于最初的代码其实对我们来书没有多大的用处。我们现在来修改我们的代码:
1)删除在"Main.qml"中不需要的代码,以使得代码如下图所示:
2)修改page中的title使之成为"POCO 摄影".重新运行我们的应用:
3)加入一个PageStack
import QtQuick 2.4
import Ubuntu.Components 1.3
MainView {
// objectName for functional testing purposes (autopilot-qt5)
objectName: "mainView"
// Note! applicationName needs to match the "name" field of the click manifest
applicationName: "rssreader.liu-xiao-guo"
/*
This property enables the application to change orientation
when the device is rotated. The default is false.
*/
//automaticOrientation: true
width: units.gu(60)
height: units.gu(85)
PageStack {
id: pageStack
anchors.fill: parent
Component.onCompleted: {
console.log('pagestack created')
pageStack.push(listPage)
}
Page {
id: listPage
title: i18n.tr("POCO 摄影")
}
}
}
这里,我们可以看到每个component在被装载完成之后,有一个event事件onCompleted被调用。我们可以用这个方法来初始化我们的一下需要处理的事情。这里,我们把listPage压入堆栈尽管没有任何东西。
这时如果我们重新运行程序,我们会发现界面没有任何新的变化。这是因为我们的page中没有任何的数据。我们在“Application Output”窗口会发现如下的输出:
qml: pagestack created
4)加入我们自己的控件
ArticleGridView.qml
import QtQuick 2.4
import QtQuick.XmlListModel 2.0
import Ubuntu.Components 1.3
import Ubuntu.Components.ListItems 1.0
Item {
id: root
signal clicked(var instance)
anchors.fill: parent
function reload() {
console.log('reloading')
model.clear();
pocoRssModel.reload()
}
ListModel {
id: model
}
XmlListModel {
id: picoRssModel
source: "http://www.8kmm.com/rss/rss.aspx"
query: "/rss/channel/item"
onStatusChanged: {
if (status === XmlListModel.Ready) {
for (var i = 0; i < count; i++) {
// Let's extract the image
var m,
urls = [],
str = get(i).content,
rex = /<img[^>]+src\s*=\s*['"]([^'"]+)['"][^>]*>/g;
while ( m = rex.exec( str ) ) {
urls.push( m[1] );
}
var image = urls[0];
var title = get(i).title.toLowerCase();
var published = get(i).published.toLowerCase();
var content = get(i).content.toLowerCase();
var word = input.text.toLowerCase();
if ( (title !== undefined && title.indexOf( word) > -1 ) ||
(published !== undefined && published.indexOf( word ) > -1) ||
(content !== undefined && content.indexOf( word ) > -1) ) {
model.append({"title": get(i).title,
"published": get(i).published,
"content": get(i).content,
"image": image
})
}
}
}
}
XmlRole { name: "title"; query: "title/string()" }
XmlRole { name: "published"; query: "pubDate/string()" }
XmlRole { name: "content"; query: "description/string()" }
}
GridView {
id: gridview
width: parent.width
height: parent.height - inputcontainer.height
clip: true
cellWidth: parent.width/2
cellHeight: cellWidth + units.gu(1)
x: units.gu(1.2)
model: model
delegate: GridDelegate {}
Scrollbar {
flickableItem: gridview
}
}
Row {
id:inputcontainer
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: units.gu(5)
spacing:12
Icon {
width: height
height: parent.height
name: "search"
anchors.verticalCenter:parent.verticalCenter;
}
TextField {
id:input
placeholderText: "请输入关键词搜索:"
width:units.gu(25)
text:""
onTextChanged: {
console.log("text is changed");
reload();
}
}
}
}
GridDelegate.qml
import QtQuick 2.0
Item {
width: parent.width /2 * 0.9
height: width
Image {
anchors.fill: parent
anchors.centerIn: parent
source: image
fillMode: Image.PreserveAspectCrop
MouseArea {
anchors.fill: parent
onClicked: {
root.clicked(model);
}
}
Text {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: units.gu(1)
horizontalAlignment: Text.AlignHCenter
text: { return title.replace("[POCO摄影 - 人像]:", "");}
clip: true
color: "white"
font.pixelSize: units.gu(2)
}
}
}
我们可以查看一下我们的RSS feed的 地址的内容:
5)使用ArticleGridView
ArticleGridView {
id: articleList
objectName: "articleList"
anchors.fill: parent
clip: true
}
import "components"
如果这个时候我们运行我们的应用的话,我们会发现一个错误:
qrc:///Main.qml:3:1: "components": no such directory
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
view.setSource(QUrl(QStringLiteral("qrc:///Main.qml")));
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.show();
return app.exec();
}
在上面我们可以看出来,Main.qml是在一个Qt的resource文件中的,所有我们必须把我们刚才创建的QML文件也导入到我们的qrc文件中(rssreader.qrc).我们在项目左边框里,用鼠标在"rssreader.qrc"上点击右键,然后点击"Add Existing Directory".
rssreader.qrc
<RCC>
<qresource prefix="/">
<file>Main.qml</file>
<file>components/GridDelegate.qml</file>
<file>components/ArticleGridView.qml</file>
<file>components/ArticleContent.qml</file>
<file>components/ArticleListView.qml</file>
<file>components/ListDelegate.qml</file>
<file>components/images/arrow.png</file>
<file>components/images/rss.png</file>
</qresource>
</RCC>
如果我们需要的话,甚至可以使用文件编辑器对它直接进行编辑即可.更多关于Qt resource的介绍,可以参考文章"
The Qt Resource System"
6)创建一个新的Component
ArticleContent.qml
import QtQuick 2.0
import Ubuntu.Components 1.3
Item {
property alias text: content.text
Flickable {
id: flickableContent
anchors.fill: parent
Text {
id: content
textFormat: Text.RichText
wrapMode: Text.WordWrap
width: parent.width
}
contentWidth: parent.width
contentHeight: content.height
clip: true
}
Scrollbar {
flickableItem: flickableContent
}
}
7)把ArticleContent和app的其它内容连起来
首先,我们必须在ArticleGridView中每个item被点击时生成一个signal,并把这个signal连接到我们需要产生的动作。我们可以定义一个名叫"clicked"的signal。
1)打开"ArticleGridView.qml"文件,并查看如下的signal:
signal clicked(var instance)
MouseArea {
anchors.fill: parent
onClicked: {
root.clicked(model);
}
}
上面的代码表明,当我们在每个grid中点击时就会发送一个叫做clicked的信号.这个信号可以在下面的代码中被捕捉到,并连接到一个slot中.这也是Qt非常强大的信号槽的概念.
Main.qml
import QtQuick 2.4
import Ubuntu.Components 1.3
import "components"
MainView {
// objectName for functional testing purposes (autopilot-qt5)
objectName: "mainView"
// Note! applicationName needs to match the "name" field of the click manifest
applicationName: "rssreader.liu-xiao-guo"
width: units.gu(60)
height: units.gu(85)
PageStack {
id: pageStack
anchors.fill: parent
Component.onCompleted: {
console.log('pagestack created')
pageStack.push(listPage)
}
Page {
id: listPage
title: i18n.tr("POCO 摄影")
visible: false
head.actions: [
Action {
iconName: "reload"
text: "Reload"
onTriggered: articleList.reload()
}
]
ArticleGridView {
id: articleList
anchors.fill: parent
clip: true
onClicked: {
console.log('[flat] article clicked: '+instance.title)
articleContent.text = instance.content
pageStack.push(contentPage)
}
}
}
Page {
id: contentPage
title: i18n.tr("Content")
visible: false
ArticleContent {
id: articleContent
objectName: "articleContent"
anchors.fill: parent
}
}
}
Action {
id: reloadAction
text: "Reload"
iconName: "reload"
onTriggered: articleList.reload()
}
}
当我们点击grid中的每项的时候,我们通过:
onClicked: {
console.log('[flat] article clicked: '+instance.title)
articleContent.text = instance.content
pageStack.push(contentPage)
}
来捕获这个事件,并切换到另外一个叫做contentPage页面.
8)使用UbuntuListView来显示我们的内容
ArticleListView.qml
import QtQuick 2.4
import QtQuick.XmlListModel 2.0
import Ubuntu.Components 1.3
import Ubuntu.Components.ListItems 1.0
Item {
id: root
signal clicked(var instance)
anchors.fill: parent
function reload() {
console.log('reloading')
model.clear();
picoRssModel.reload()
}
ListModel {
id: model
}
XmlListModel {
id: picoRssModel
source: "http://www.8kmm.com/rss/rss.aspx"
query: "/rss/channel/item"
onStatusChanged: {
if (status === XmlListModel.Ready) {
for (var i = 0; i < count; i++) {
// Let's extract the image
var m,
urls = [],
str = get(i).content,
rex = /<img[^>]+src\s*=\s*['"]([^'"]+)['"][^>]*>/g;
while ( m = rex.exec( str ) ) {
urls.push( m[1] );
}
var image = urls[0];
var title = get(i).title.toLowerCase();
var published = get(i).published.toLowerCase();
var content = get(i).content.toLowerCase();
var word = input.text.toLowerCase();
if ( (title !== undefined && title.indexOf( word) > -1 ) ||
(published !== undefined && published.indexOf( word ) > -1) ||
(content !== undefined && content.indexOf( word ) > -1) ) {
model.append({"title": get(i).title,
"published": get(i).published,
"content": get(i).content,
"image": image
})
}
}
}
}
XmlRole { name: "title"; query: "title/string()" }
XmlRole { name: "published"; query: "pubDate/string()" }
XmlRole { name: "content"; query: "description/string()" }
}
UbuntuListView {
id: listView
width: parent.width
height: parent.height - inputcontainer.height
clip: true
visible: true
model: model
delegate: ListDelegate {}
// Define a highlight with customized movement between items.
Component {
id: highlightBar
Rectangle {
width: 200; height: 50
color: "#FFFF88"
y: listView.currentItem.y;
Behavior on y { SpringAnimation { spring: 2; damping: 0.1 } }
}
}
highlightFollowsCurrentItem: true
focus: true
// highlight: highlightBar
Scrollbar {
flickableItem: listView
}
PullToRefresh {
onRefresh: {
reload()
}
}
}
Row {
id:inputcontainer
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: units.gu(5)
spacing:12
Icon {
width: height
height: parent.height
name: "search"
anchors.verticalCenter:parent.verticalCenter;
}
TextField {
id:input
placeholderText: "请输入关键词搜索:"
width:units.gu(25)
text:""
onTextChanged: {
console.log("text is changed");
reload();
}
}
}
}
ListDelegate.qml
import QtQuick 2.0
Item {
width: ListView.view.width
height: units.gu(14)
Row {
spacing: units.gu(1)
width: parent.width
height: parent.height
x: units.gu(0.2)
Image {
id: img
property int borderLength: 2
source: image
height: parent.height *.9
width: height
anchors.verticalCenter: parent.verticalCenter
}
Column {
id: right
y: units.gu(1)
anchors.leftMargin: units.gu(0.1)
width: parent.width - img.width - parent.spacing
spacing: units.gu(0.2)
Text {
text: {
var txt = published.replace("GMT", "");
return txt;
}
font.pixelSize: units.gu(2)
font.bold: true
}
Text {
width: parent.width * .9
text: {
var tmp = title.replace("[POCO摄影 - 人像]:", "");
if ( tmp.length > 35)
return tmp.substring(0, 35) + "...";
else
return tmp;
}
// wrapMode: Text.Wrap
clip: true
font.pixelSize: units.gu(2)
}
}
}
Image {
source: "images/arrow.png"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: units.gu(0.6)
rotation: -90
}
MouseArea {
anchors.fill: parent
onClicked: {
console.log("it is clicked");
console.log("currentidex: " + listView.currentIndex);
console.log("index: " + index);
listView.currentIndex = index;
root.clicked(model);
}
}
Keys.onReturnPressed: {
console.log("Enter is pressed!");
listView.currentIndex = index;
root.clicked(model);
}
}
细心的开发者可能已经看出来了,我们的ArticleListView.qml里的文件几乎和我们的ArticleGridView.qml的内容是一样的.只是在这个文件中我们使用了UbuntuListView而不是GridView的方法来显示我们的内容.这符合我们的MVC(Model-View-Control)设计思路.最后我们千万别忘记把我们的新创建的文件加入到我们的rssreader.qrc文件中.否则,这些文件将不被认知.另外,我们需要修改在Main.qml中listPage中的部分:
Main.qml
Page {
id: listPage
title: i18n.tr("POCO 摄影")
visible: false
head.actions: [
Action {
iconName: "reload"
text: "Reload"
onTriggered: articleList.reload()
}
]
ArticleListView {
id: articleList
anchors.fill: parent
clip: true
onClicked: {
console.log('[flat] article clicked: '+instance.title)
articleContent.text = instance.content
pageStack.push(contentPage)
}
}
}
9)在应用中加入cache使UI更流畅
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <QStandardPaths>
#include <QDebug>
#include <QDir>
#include <QQmlNetworkAccessManagerFactory>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
QString getCachePath()
{
QString writablePath = QStandardPaths::
writableLocation(QStandardPaths::DataLocation);
qDebug() << "writablePath: " << writablePath;
QString absolutePath = QDir(writablePath).absolutePath();
qDebug() << "absoluePath: " << absolutePath;
absolutePath += "/cache/";
// We need to make sure we have the path for storage
QDir dir(absolutePath);
if ( dir.mkpath(absolutePath) ) {
qDebug() << "Successfully created the path!";
} else {
qDebug() << "Fails to create the path!";
}
qDebug() << "cache path: " << absolutePath;
return absolutePath;
}
class MyNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
public:
virtual QNetworkAccessManager *create(QObject *parent);
};
QNetworkAccessManager *MyNetworkAccessManagerFactory::create(QObject *parent)
{
QNetworkAccessManager *nam = new QNetworkAccessManager(parent);
QString path = getCachePath();
QNetworkDiskCache* cache = new QNetworkDiskCache(parent);
cache->setCacheDirectory(path);
nam->setCache(cache);
return nam;
}
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
qDebug() << "Original factory: " << view.engine()->networkAccessManagerFactory();
qDebug() << "Original manager: " << view.engine()->networkAccessManager();
QNetworkDiskCache* cache = (QNetworkDiskCache*)view.engine()->networkAccessManager()->cache();
qDebug() << "Original manager cache: " << cache;
view.engine()->setNetworkAccessManagerFactory(new MyNetworkAccessManagerFactory);
view.setSource(QUrl(QStringLiteral("qrc:///Main.qml")));
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.show();
return app.exec();
}
整个项目的源码在: https://github.com/liu-xiao-guo/rssreader_cache