Тема: Доступ к SQLite из приложения на QML, для MeeGo Harmattan устройств.
[size=5]Аннотация[/size]
Эта статья рассказывает о разработке приложения, использующего механизм доступа к базе данных SQLite, для MeeGo Harmattan устройств на QML и Quick Components.
Часто сдавая экзамен на категорию, сертификат, etc Вы решаете тесты, которые представляют собой вопросы и список ответов на них, Вам всего лишь надо указать правильный. Вот если бы у Вас была программка, содержащая вопросы и ответы, то возможно она могла бы дать Вам небольшое преимущество на экзамене, нужно только незаметно воспользоваться смартфоном..., но это уже другая история.
Итак база данных представляет собой сборник вопросов (тестов) и вариантов ответов на них с указанием правильного. Доступ к базе будет осуществляться с использованием Offline Storage API.
[size=5]Логика[/size]
У пользователя есть возможность набрать в строке поиска часть вопроса (searchStr). Нажав на кнопку поиска отправляем searchStr в предложение Like SQL запроса:
Select * from quiestions where SomeField like %searchStr%
где quiestions наша таблица с вопросами, на выходе получаем набор записей состоящих из вопросов, содержащих searchStrig. Поместим набор записей в ListView, тем самым позволив пользователю уточнить свой выбор.
Выбирая нужный вопрос пользователь инициализирует отправку SQL запроса:
Select * from answers where id = idQuestion
где answers наша таблица с ответами, а idQuestion идентификатор вопроса. Получив ответы выводим их в ListView, выделив правильный каким нибудь интерфейсным элементом.
Интерфейс состоит из двух окон Pages в терминологии QML(MainPage{}, AnswersPage{}).
Основное окно (MainPage{}) содержит:
заголовок (Header рекомендуется создавать у каждого окна, соглассно UI Guidelines);
поле для ввода текста (TextField{}) и кнопка поиска (разместим её прямо в поле ввода);
список вопросов (ListView{});
ToolBar{} обязательный элемент, позволяющий нам перемещаться между окнами, вызывать меню, etc.
Окно ответов (AnswersPage{}) содержит:
заголовок (Header{});
выбранный вопрос (Text{});
список ответов с индикатором (ListView{});
ToolBar{}.
[size=5]Программная часть[/size]
Логику мы обсудили, интерфейс спроектировали, пора заняться программированием.
Запустим Qt Creator, на первом шаге выберем шаблон Qt Quick Project->Qt Quick Application, на втором зададим имя проекта, я думаю qaTools (Question Answer Tools) будет в самый раз, на третьем шаге укажем тип приложения Qt Quick Components for MeeGo/Harmattan, следующие шаги оставим без изменения.
Давайте посмотрим, что для нас сделал Qt Creator.
Нативная часть кода содержится в main.cpp, где создается экземпляр класса QmlApplicationViewer viewer, который загружает декларативную часть,
viewer.setMainQmlFile(QLatin1String("qml/qaTools/main.qml"))
.
Каталог qaTools/qml/qaToosl/ как раз и содержит декларативную часть кода. На данный момент у нас там два файла main.qml и MainPage.qml. main.qml это основной файл нашего приложения:
import QtQuick 1.1
import com.nokia.meego 1.0
PageStackWindow {
id: appWindow
initialPage: mainPage
MainPage {
id: mainPage
}
ToolBarLayout {
id: commonTools
visible: true
ToolIcon {
platformIconId: "toolbar-view-menu"
anchors.right: (parent === undefined) ? undefined : parent.right
onClicked: (myMenu.status === DialogStatus.Closed) ? myMenu.open() : myMenu.close()
}
}
Menu {
id: myMenu
visualParent: pageStack
MenuLayout {
MenuItem { text: qsTr("Sample menu item") }
}
}
}
Центральное понятие в QML - элемент. Элементы представляют собой базовые строительные блоки, из которых формируется программа на QML. Большинство элементов могут быть контейнерами для других элементов. У разработчика есть возможность создавать (описывать) собственные QML типы.
Основная идея мобильных приложений на QML это переключение экранов в стеке. В терминах Qt компонентов экраны называются страницами (Pages{}), а главный контейнер окном (Window{}). В нашем приложении основной элемент это PageStackWindow{}, который является наследником Window{} и содержит в себе компонент pageStack. У pageStack есть методы которые позволяют перемещать страницы в стеке (pop(), push()).
initialPage - это свойство устанавливает начальную страницу в стеке (pageStack), в нашем случае это mainPage.
PageStackWindow{} является контейнером для элементов MainPage{}, ToolBarLayout{}, Menu{}.
ToolBarLayout{} предоставляет макет для элементов размещенных в контейнере ToolBar{}. Сам ToolBar{} явно не объявлен, а является свойством pageStack, который в свою очередь является свойством PageStackWindow{}.
Давайте сразу следовать UI Guidelines от Nokia, согласно которому навигация между страницами может осуществляться тремя способами: Tab Bar Navigation, Drill Down Navigation, Tab Bar + Back Navigation. Мы будем использовать Drill Down Navigation, в этом случае для каждой страницы, кроме основной у ToolBar{} должна быть кнопка “назад”. На основной она тоже может присутствовать, но должна быть неактивна. Что ж одна кнопка (ToolIcon{}) у нас уже есть, она прижата вправо и отвечает за вызов меню, добавим кнопку back и прижмем её влево:
...
ToolIcon {
enabled: (pageStack.currentPage === mainPage) ? false : true
platformIconId: (pageStack.currentPage === mainPage) ? "toolbar-back-dimmed" : "toolbar-back"
anchors.left: (parent === undefined) ? undefined : parent.left
onClicked: pageStack.pop()
}
...
Код достаточно прост, если активное окно mainPage, тогда кнопка неактивна. Событие onClicked кнопки вытягивает из стека предыдущее окно. Это и есть Drill Down Navigation смысл которого состоит в том, чтобы возвращать пользователя на предыдущее окно, до тех пор пока не появится основное.
Еще один элемент в файле main.qml это Menu{}, что тут можно сказать меню есть меню, давайте просто заменим текст “Sample menu item” на “About”.
Последний элемент это MainPage{}, который как раз является типом, описываемым разработчиком самостоятельно. Для описания собственных типов достаточно создать файл с именем типа с Большой буквы с расширением qml. В нашем случае Qt Creator это сделал за нас. Файл MainPage.qml:
import QtQuick 1.1
import com.nokia.meego 1.0
Page {
tools: commonTools
Label {
id: label
anchors.centerIn: parent
text: qsTr("Hello world!")
visible: false
}
Button{
anchors {
horizontalCenter: parent.horizontalCenter
top: label.bottom
topMargin: 10
}
text: qsTr("Click here!")
onClicked: label.visible = true
}
}
Основным элементом - контейнером здесь является Page{} (страница). У страницы есть свойство tools, благодаря которому мы можем использовать на странице элемент ToolBarLayout{} определенный в main.qml. Дочерние элементы это Label{} - позволяет разместить тестовую информацию и Button{} (кнопка), позволяет выполнить какое либо действие при нажатии на неё. Элемент Label{} нам здесь не нужен, просто удалим его определение из файла, так же удалим свойство top у кнопки, вместо него добавим
verticalCenter: parent.verticalCenter
, а на обработчик onClicked повесим следующий код:
onClicked: pageStack.push(answersPage)
Обработчик проталкивает в стек и одновременно выводит на экран страницу с идентификатором answersPage. Страницы с таким идентификатором у нас нет, давайте создадим её.
В Qt Creatore выберете New File or Project...->QML->QML File. Ввведите имя AnswersPage.qml, не забудте указать путь qaTools/qml/qaTools. Давайте добавим в файл описание страницы:
import QtQuick 1.1
import com.nokia.meego 1.0
Page {
tools: commonTools
}
Пока у нас только голая страница с тулбаром. Для того чтобы pageStack знал о существовании нашей страницы добавим её объявление в main.qml:
...
MainPage {
id: mainPage
}
AnswersPage {
id: answersPage
}
...
Таким образом приложение сразу загрузит наши страницы в память, что немного не корректно, т.к. рекомендуется загружать страницы динамически по мере необходимости, но так как в нашем приложении всего две страницы то можно сделать и так.
В соответствии с нашим макетом, каждое окошко должно иметь заголовок. Стандартного элемента QML для этих целей не существует, поэтому опишем его сами. Создадим в Qt Creator новый файл Header.qml:
import QtQuick 1.1
import com.nokia.meego 1.0
Rectangle {
id: header
property alias text: title.text
color: "yellow"
height: 72
width: parent.width
anchors.top: parent.top
Label {
id: title
font.weight: Font.Light
font.pixelSize: 32
anchors {
left: parent.left; leftMargin: 20
verticalCenter: parent.verticalCenter
}
}
}
Тут все достаточно просто, рисуем прямоугольник, внутри которого размещаем Label{}. Обратите внимание на следующую строчку:
property alias text: header.Text
Этим определением мы создаем у элемента свойство text и указываем, что оно является алиасом для свойства text элемента Label.
Ну что ж, давайте добавим на наши странички заголовок:
MainPage.qml
...
tools: commonTool
Header {
id: header
text: "Вопросы"
}
...
AnswersPage.qml
...
tools: commonTool
Header {
id: header
text: "Ответы"
}
...
Теперь мы можем запустить наше приложение в симуляторе и посмотреть как все выглядит, и как переключаются странички.
Добавим в MainPage.qml недостающие элементы (определение Button{} удалите, больше он нам не нужен).
Строка поиска:
...
TextField {
id: searchField
anchors {top: header.bottom; left: parent.left; right: parent.right; margins: 7}
placeholderText: "Введите текст"
width: parent.width
platformStyle: TextFieldStyle { paddingRight: searchIcon.width }
ToolIcon {
id: searchIcon
platformIconId: "toolbar-search"
anchors { top: parent.top; right: parent.right }
height: parent.height; width: parent.height
}
}
...
TextField{} элемент специально предназначен для ввода текста. Элементы в QML крепятся друг к другу с помощью якорей (anchors), к нижней части нашего заголовка мы и прикрепим его (TextField{}).
placeholderText это подсказка для пользователя.
platformStyle: TextFieldStyle { paddingRight: searchIcon.width } позволяет нам создать отступ для текста, ведь справа прямо в строке поиска мы разместим кнопку поиска (ToolIcon{}).
Список ListView с найденными вопросами.
Элемент ListView предназначен для отображения списков. То какие это данные и как они будут выглядеть определяют два свойства model и delegate. model это структура и хранилище данных, а delegate определяет как будет выглядеть отдельный элемент ListView. В составе пакета com.nokia.extras есть готовый элемент ListDelegate{}, однако для наших целей он не подходит. Давайте создадим собственный. Добавьте в проект файл с именем QuestionsDelegate.qml:
import QtQuick 1.1
import com.nokia.meego 1.0
Item {
id: q
property int idQ: 0
property alias text: textField.text
signal clicked
height: textField.height + 7
width: parent.width
anchors {left: parent.left; right: parent.right; margins: 10}
Text {
id: textField
width: parent.width
font.pointSize: 6
wrapMode: TextEdit.Wrap
}
Rectangle {
id: devider
anchors.top: textField.bottom
anchors.topMargin: 5
height: 2; width: parent.width
color: "gray"
opacity: 0.7
}
Rectangle {
anchors.top: devider.bottom
height: 1; width: parent.width
color: "white"
}
MouseArea {
anchors.fill: parent
onClicked: {
q.clicked();
}
}
}
Контейнером для всех элементов здесь является Item, в QML Item самый простой визуальный элемент, все остальные визуальные элементы наследуются от него. Хотя Item не имеет визуального представления, у него есть такие свойства как x, y, width, height. Идеология его использования состоит в группировке дочерних элементов.
Item содержит Text{} - элемент в который мы поместим текст вопроса, вопросы могут быть длинными, так что установим свойство
wrapMode: TextEdit.Wrap
, чтобы текст переносился на другую строчку, свойство не будет работать без width, что логично, не зная ширину текста невозможно разбить строки. Это кстати один из важных моментов в QML, многие свойства зависят от других, еще один пример margins установив его значение вы получите отступы только у тех свойств, который “заякарили” (anchors {left: parent.left; right: parent.right})
Для визуального разделения теста добавим две тоненьких линии серого и белого цвета (Rectangle{}).
Последний элемент это MouseArea{} он занимает всю площадь Item{}, добавили мы его для того чтобы можно было обрабатывать события касания к экрану у нашего Item{}.
Свойству height элемента Item{} присвоим значение textField.height + 7. Что такое 7? Это отступ, т.к. мы не можем “заякорить” наш элемент снизу, то просто увеличим высоту.
Мы определили у Item{} два свойства IdQ и text и один сигнал clicked. С text все понятно, у него такая же роль как и у ранее созданного нами Header{}. Разница в определении состоит в том, что для свойств без ключевого слова alias система резервирует память. IdQ будет хранить id вопроса и понадобится нам в дальнейшем для обращения к базе данных. У MouseArea{} есть событие onClicked, в котором мы и вызываем сигнал clicked, описанный нами у Item{}.
Теперь мы можем описать ListView{}, после определения TextField{} в MainPage.qml добавьте следующий код:
...
ListModel {
id: itemModel
}
ListView {
id: view
anchors {
left: parent.left; right: parent.right;
top: searchField.bottom; bottom: parent.bottom;
topMargin: 7;
}
model: itemModel
clip: true
delegate: QuestionsDelegate{
text: model.text
idQ: model.idQ
onClicked: {
pageStack.push(answersPage, {idQ: model.idQ});
}
}
}
...
Здесь мы описываем простую модель ListModel{} для нашего ListView{}, которую в дальнейшем будем заполнять из базы данных, соединяем вместе модель и делегат. Свойства IdQ и text делегата заполняем из модели, а на событие onClicked делегата повесим вызов answersPage и передадим ей (странице) параметр idQ.
Свойство clip у ListView{} отвечает за красивую обрезку элементов, если они не помещаются на странице, без этого свойства все элементы сбились бы в некрасивую кучку .
Добавим к ListView{} скролбар.
ScrollDecorator { flickableItem: view }
На этом пока закончим с QuestionPage и перейдем к AnswersPage. Пока что у нас там только Header{}. Раз уж мы передаем параметр idQ из MainPage{}, то нам его надо где то хранить. Для этих целей опишем у AnswersPage{} свойство:
Page {
id: answers
property int idQ: 0
tools: commonTools
...
И добавим элементы из нашего макета:
...
Text {
id: q
width: parent.width
anchors {top: header.bottom; left: parent.left;
right: parent.right; margins: 10}
font.pointSize: 6
font.italic: true
wrapMode: TextEdit.Wrap;
}
ListModel {
id: itemModel
}
ListView {
id: view
anchors {
left: parent.left; right: parent.right;
top: q.bottom; bottom: parent.bottom;
topMargin: 7;
}
model: itemModel
clip: true
delegate: AnswersDelegate{
text: model.text
color: model.color
}
}
ScrollDecorator { flickableItem: view }
...
Во первых это элемент Text{} который содержит текст вопроса выбранного пользователем. А во вторых ListView{} с ответами. Тут мы тоже используем собственный делегат - AnswersDelegate. Давайте опишем его в файле AnswersDelegate.qml:
import QtQuick 1.1
import com.nokia.meego 1.0
Item {
id: a
property alias text: textField.text
property alias color: indicator.color
height: row.height + 12
width: parent.width
anchors {left: parent.left; right: parent.right; margins: 10}
Row{
id: row
width: parent.width
spacing: 7
Rectangle {
id: indicator
width: 4
height: textField.height
}
Text {
id: textField
font.pointSize: 6
width: parent.width - indicator.width - row.spacing
wrapMode: TextEdit.Wrap;
}
}
Rectangle {
id: devider
anchors.top: row.bottom
anchors.topMargin: 5
height: 2; width: parent.width
color: "gray"
opacity: 0.7
}
Rectangle {
anchors.top: devider.bottom
height: 1; width: parent.width
color: "white"
}
}
Отличие от QuestionsDelegate в том, что здесь мы размещаем прямоугольник (indicator) шириной 4 пикселя перед текстом ответа. Свойство color прямоугольника выносим в property, если ответ правильный то будем закрашивать его зеленым, иначе красным цветом. Прямоугольник с ответом помещаем в элемент row{}, чтобы сгруппировать их относительно других элементов.
[size=5]Доступ к данным[/size]
Для доступа к данным из QML приложений необходимо использовать Offline Storage API, который предоставляет нам возможность взаимодействовать с локальной базой данных SQLite. Сама база по умолчанию храниться в подкаталоге Databases, каталога, который нам возвратит вызов функции
QDeclarativeEngine::offlineStoragePath()
Вызов API осуществляется из JavaScript функций в QML файле.
db = openDatabaseSync(identifier, version, description, estimated_size, callback(db))
Эта функция вернет нам идентификатор открытой базы данных.
Давайте подробно поговорим о параметре identifier. В документации о нем упоминается вскользь в одной фразе: Returns the database identified by identifier. Т.е подразумевается что identifier это имя базы данных. На самом деле физически файл базы данных имеет следующее имя Qt.md5(identifier).sqlite. Т.е модель использования Offline Storage API подразумеваемая разработчиками следующая: программист в коде создает базу (openDatabaseSync() создаст файл если его не существует), заполняет её данными и потом использует. О том с каким именем сохраняется файл ему знать необязательно . Очевидно, что нам это не подходит, т.к. у нас уже есть готовая база и все что нам нужно это разместить эту базу в нужном месте и с нужным именем.
О том как деплоить базу вместе с приложением поговорим позже, а сейчас давайте разместим наш файл в том месте файловой системы , где его сможет найти симулятор.
Если Вы работаете под Windows, то это: %APPDATA%\Nokia\QtSimulator\data\QML\OfflineStorage\.
Если под Linux, то Вам необходимо добавить в main.cpp следующие строки:
...
#include <QtGui>
#include <QDeclarativeEngine>
...
qDebug() << viewer.engine()->offlineStoragePath();
...
что позволит вам узнать, где по мнению симулятора находиться offlineStoragePath.
В файл MainPage.qml добавим следующую функцию (должна находится в границах определения Page{}):
Component.onCompleted: {
var db = openDatabaseSync("Answers", "1.0", "The Answers SQL database", 1000000);
}
Метод Component.onCompleted выполнится когда страница будет полностью создана. Первый параметр мы уже обсудили, второй version - версия нашей базы (у нас всегда 1.0), третий и четвертый это описание и размер, в текущей версии API не используется. Запустим в симуляторе наше приложение, в результате в каталоге %APPDATA%\Nokia\QtSimulator\data\QML\OfflineStorage\Database появятся два файла:
7d5a6969802bb5e1d931b510a8fdb3ba.sqlite,
7d5a6969802bb5e1d931b510a8fdb3ba.ini.
Как я уже говорил openDatabaseSync() создает файл базы данных, если он не существует. Второй файл 7d5a6969802bb5e1d931b510a8fdb3ba.ini создается один раз в момент создания базы данных и содержит её характеристики.
Вот таким хитрым путем мы и получили имя файла, в котором должна лежать наша база. Теперь мы можем переместить нашу базу данных на место 7d5a6969802bb5e1d931b510a8fdb3ba.sqlite. В дальнейшем openDatabaseSync() будет использовать именно его. Кстати имя фала мы могли бы получить выполнив следующий код console.log(Qt.md5(identifier)), как я уже упоминалось ранее имя файла это md5 хэш от identifier. Обработчик события Component.onCompleted удалите, оно больше нам не нужно.
Итак, в соответствии с логикой работы нашего приложения пользователь должен отправить запрос к базе данных, нажав на кнопку поиска. Добавим обработчик onClicked в файле MainPage.qml для ToolIcon{id: searchIcon}:
...
height: parent.height; width: parent.height
onClicked: {
findQ(itemModel, searchField.text)
searchField.platformCloseSoftwareInputPanel()
}
...
В обработчике мы вызываем функцию findQ() и передаем ей два параметра модель нашего ListView{} и текст из поля searchField. Также мы закрываем SIP (при получении фокуса поле ввода открывает SoftwareInputPanel, позволяя пользователю ввести текст).
Функция findQ() у нас не определена, давайте напишем её:
function findQ(model, str) {
model.clear()
var db = openDatabaseSync("Answers", "1.0", "The Answers SQL database", 1000000);
db.readTransaction(
function(tx) {
var rs = tx.executeSql('Select id, q from questions where q like ?', "%" + str + "%");
for (var i=0; i< rs.rows.length; i++) {
model.append({“idQ”: rs.rows.item(i).id, “text”: rs.rows.item(i).q});
}
}
)
}
В первую очередь очистим наш ListView{}, путем удаления данных из модели - model.clear(). C openDataBaseSync() мы уже знакомы. Метод readTransaction(callback(tx)) создает транзакцию для чтения и передает ее в callback(tx), в которой мы и обращаемся к нашей базе данных, отправляя ей SQL запрос через вызов метода executeSql(). Далее в цикле мы добавляем в модель полученные данные, используя метод ListModel.append().
На этом этапе можно запустить приложение в симуляторе, набрать запрос в строке поиска и увидеть результат.
Согласно разработанной логике пользователь уточняет вопрос, путем выбора его из ListView{}. Событие onClicked{} у нас описано и в нем мы вызываем AnswersPage{}, передавая ему id вопроса. В текущем варианте уточняя вопрос мы переходим на пустую страницу с ответами, давайте исправим это.
Добавим в описание AnswersPage{} следующее событие:
...
onStatusChanged: {
if(status === PageStatus.Activating) {
getAnswers(itemModel, answers.idQ);
}
}
...
Событие возникает при смене статуса страницы, всего их бывает четыре (Inactive, Active, Activating, Deactivating), нас интересует Activating, происходит перед тем как страница станет активна. Это как раз тот момент, когда мы хотим получить данные и разместить их на странице. За это у нас отвечает функция getAnswers(). Ей мы передаем два параметра, модель и id вопроса, который мы определили как свойство страницы.
function getAnswers(model, idq){
var db = openDatabaseSync("Answers", "1.0", "Answers Database!", 1000000);
db.readTransaction(
function(tx) {
var rs = tx.executeSql('select q, a from Questions q where id = ?', idq);
q.text = rs.rows.item(0).q;
var answerNum = rs.rows.item(0).a
model.clear()
rs = tx.executeSql('select a, num from answers a where id_questions = ?', idq);
for (var i=0; i< rs.rows.length; i++) {
model.append({"text": rs.rows.item(i).a,
"color": (rs.rows.item(i).num === answerNum) ? "green" : "red"});
}
}
)
}
Здесь мы выполняем два запроса к базе данных. В первом мы получаем вопрос и правильный ответ. Вопрос выводим в элемент Text{id:q}, а номер правильного ответа записываем в переменную answerNum. Во втором запросе мы получаем список ответов, зная правильный ответ, мы можем выделить его зеленым цветом, в момент добавления в модель.
Ну вот теперь можно запустить приложение в эмуляторе и проверить его функциональность. Вроде бы все хорошо, но если пользователь наберет в строке поиска что нибудь, чего нет у нас в базе данных, то... ничего не происходит, давайте покажем ему сообщение, в котором укажем ему на это. Для этих целей существует элемент InfoBanner{}. Элемент содержится в пакете com.nokia.extras 1.1. Добавим его описание в MainPage.qml:
...
import com.nokia.extras 1.1
...
InfoBanner {
id: banner
}
...
Добавим функцию для показа банера:
function showError(str){
banner.text = str;
banner.show();
}
Функцию showError() будем вызывать из findQ() следующим образом:
...
var rs = tx.executeSql('Select id, q from questions where q like ?', "%" + str + "%");
if (rs.rows.length === 0){
showError("Не найденно вопросов содержащих '" + str + "'");
}
for (var i=0; i< rs.rows.length; i++) {
...
[size=5]“Развертывание” на устройстве[/size]
Мы должны загрузить на устройство нашу базу данных. Сделать это можно, поместив файлы в deb пакет нашего приложения. Откройте в Qt Creator файл qaTools.pro, добавьте в конец следующие строчки:
data.path = /opt/qaTools/data/Databases/
data.files += data/*
INSTALLS += data
Создайте в каталоге проекта папку data и поместите туда файлы:
7d5a6969802bb5e1d931b510a8fdb3ba.sqlite,
7d5a6969802bb5e1d931b510a8fdb3ba.ini, и после установки пакета на устройстве, база окажется в каталоге /opt/qaTools/data/Databases/
Куда бы мы не поместили, базу а openDataBaseSync() будет её искать в подкаталоге Databases, каталога , который нам вернет метод offlineStoragePath(). Благо у QDeclarativeEngine, есть метод setOfflineStoragePath(), которому мы и передадим путь к каталогу в котором у нас будет лежать база данных. Добавим в main.cpp следующую строчку:
...
#include <QDeclarativeEngine>
...
viewer.engine()->setOfflineStoragePath("/opt/qaTools/data/");
...
Итак компилируем запускаем на реальном устройстве, или эмуляторе и видим, что размер шрифта вопросов и ответов очень маленький (по сравнению с симулятором). Выставьте размер шрифта у элементов Text{} в QuestionsDelegate.qml, AnswersDelegate.qml, AnswersPage.qml, какой вам больше нравится, я поставил 16.
Осталось нарисовать иконку, в стиле MeeGo Harmattan, скачайте отсюда http://harmattan-dev.nokia.com/docs/ux/ … pment.html NokiaN9_Icon_Templates.zip в нем есть шаблоны и инструкция, как сделать иконку, у меня получилось так:
Готовое приложение выглядит так:
[size=5]Заключение[/size]
Если вы дочитали до этого момента, значит вы написали приложение (может первое, может n-ное, но надеюсь не последнее). Конечно мы сделали только основные вещи, возможно где то есть ошибки, например если нажать на кнопку поиска без ввода текста, то в Select передастся простая строка, и он вернет, все строки из таблицы. Хотя может это не баг, а фича . Можно вынести обращения к базе данных в отдельный модуль. Необходимо как то реагировать если пользователь меняет темы (thems), в конце концов добавить окно About . В общем есть место для творчества.
Надеюсь наше приложение поможет кому нибудь получить сертификат, а статья написать много разных программ!
P.S. Приложение достаточно легко портировать под платформу Symbian^3