Тема: Control Panel applet на QML
Краткая аннотация:
Система MeeGo Harmattan имеет удобный единый интерфейс для конфигурирования программ - Панель Управления (Настройки). Каждый программист может там разместить настройки своей программы, но лишь немногие делают это. Может быть не хватает понятного описания из разряда "how-to"? Или процесс написания модуля для панели управления получается слишком запутанным и отнимающим много времени? Захотелось разобраться с этими вопросамм и как-то упростить процесс.
Данная статья описывает тернистый путь к достижению поставленной цели: написать апплет, интерфейс которого будет построен на элементах QML. При этом пришлось столкнуться и с трудностями и с разочарованиями. Возможно, мои пробы и ошибки окажутся полезными не только при создании апплета с настройками, но и в других случаях, где требуется объединить QML и QGraphicsWidget'ы.
Введение:
Почти каждая программа должна где-то хранить свои настройки. И в большинстве случаев программы предлагают нам свой интерфейс и складывают всё в свое собственное хранилище.
В Harmattan, как и в большинстве уважающих себя современных ОС, существует централизованное представление настроек (как системных, так и для отдельных программ) - Панель Управления. Особенно хорошо в эту панель вписывается конфигурирование программ-демонов, не нуждающихся в собственном GUI.
Документация (libduicontrolpanel) предлагает нам три способа встроить свои настройки в Панель Управления.
1. Создать Xml-файл определенного формата, который потом будет преобразован в нужные виджеты (т.е. система сама создаст апплет по нашему файлу);
2. Написание полностью своего апплета на C++, где мы можем делать все что захотим (в определенных рамках, разумеется);
3. Перенаправление пользователя во внешнее приложение (не очень хороший вариант, так как заставляет пользователя ждать, пока приложение загрузится).
Первый вариант хорош, когда настроек немного и вполне хватает тех вариантов виджетов, что могут быть описаны через XML. Хороший пример такого подхода можно глянуть там (control-panel-applet).
Второй вариант позволяет получить полностью нативный интерфейс, выполняющийся в пределах Панели Управления, но накладывает определенные ограничения по структуре модуля, требует использования фреймворка MeegoTouch и соответствующих виджетов. Кому доводилось делать GUI на QWidget'ах без GUI-дизайнера - тот поймет, с чем придется столкнуться (да, еще следует учесть почти полное отсутствие примеров и единственный источник качественной информации - Harmattan API Documentation).
Третий вариант дает полную свободу действий и бо'льшую простоту реализации (по сравнению со вторым вариантом) - ибо можно использовать любой подход к построению GUI, какой будет больше по душе (например, тот же QML). Но за это мы расплачиваемся большим временем запуска. В общем, неинтересный вариант, хотя и имеющий право на существование.
Поразмыслив над предложенными вариантами, появилось желание скрестить ужа с ежом - сделать нативный апплет, но реализовав при этом весь GUI на QML. Получить возможность быстро проектировать GUI, простую и наглядную подгонку и отладку этого GUI, простоту добавления/изменения настроек, не расплачиваясь за это минусами использования внешнего приложения.
Базовая подготовка:
Перво-наперво сделаем чуть упрощенный вариант реализации апплета из Harmattan API Documentation, ибо при написании бинарного апплета от MeegoTouch мы никуда не денемся.
Создадим 2 класса: SampleQmlApplet (интерфейс для интеграции в Панель Управления) и SampleMainWidget (виджет, представляющий GUI).
sampleapplet.h
#ifndef SAMPLEAPPLET_H
#define SAMPLEAPPLET_H
#include <DcpAppletIf>
#include <QObject>
class DcpStylableWidget;
class MAction;
class SampleMainWidget;
class SampleQmlApplet : public QObject, public DcpAppletIf
{
Q_OBJECT
Q_INTERFACES(DcpAppletIf)
public:
virtual void init();
virtual DcpStylableWidget* constructStylableWidget(int widgetId);
virtual SampleMainWidget* pageMain();
virtual QVector<MAction *> viewMenuItems();
private slots:
void showAboutDialog();
};
#endif // SAMPLEAPPLET_H
sampleapplet.cpp
#include <MLibrary>
#include <MMessageBox>
#include <DcpStylableWidget>
#include "sampleapplet.h"
#include "samplemainwidget.h"
#include "dcpapplet.h"
M_LIBRARY
Q_EXPORT_PLUGIN2(sampleqmlapplet, SampleQmlApplet)
void SampleQmlApplet::init()
{//}
DcpStylableWidget* SampleQmlApplet::constructStylableWidget(int widgetId)
{
switch (widgetId) {
case DcpApplet::Main:
return pageMain();
break;
default:
break;
};
return 0;
}
SampleMainWidget* SampleQmlApplet::pageMain()
{
return new SampleMainWidget();
}
QVector<MAction*> SampleQmlApplet::viewMenuItems()
{
QVector<MAction*> vector(1);
vector[0] = new MAction("About", this);
vector[0]->setLocation(MAction::ApplicationMenuLocation);
connect(vector[0],SIGNAL(triggered()),this,SLOT(showAboutDialog()));
return vector;
}
void SampleQmlApplet::showAboutDialog()
{
MMessageBox* dialog = new MMessageBox ("About Sample Applet", "<b>It's working!</b>", M::OkButton);
dialog->appear();
}
samplemainwidget.h
#ifndef SAMPLEMAINWIDGET_H
#define SAMPLEMAINWIDGET_H
#include <DcpStylableWidget>
#include <QGraphicsWidget>
class MLabel;
class SampleMainWidget : public DcpStylableWidget
{
Q_OBJECT
public:
SampleMainWidget(QGraphicsWidget *parent = 0);
virtual ~SampleMainWidget();
protected:
void initWidget();
private:
MLabel *m_aboutLabel;
};
#endif // SAMPLEMAINWIDGET_H
samplemainwidget.cpp
#include "samplemainwidget.h"
#include "dcpapplet.h"
#include <mlayout.h>
#include <mlinearlayoutpolicy.h>
#include <mwidgetcreator.h>
#include <MLabel>
M_REGISTER_WIDGET_NO_CREATE (SampleMainWidget)
SampleMainWidget::SampleMainWidget(QGraphicsWidget *parent)
:DcpStylableWidget(parent)
{
initWidget();
setReferer(DcpApplet::NoReferer);
}
SampleMainWidget::~SampleMainWidget()
{ }
void SampleMainWidget::initWidget()
{
MLayout *mainLayout = new MLayout(this);
MLinearLayoutPolicy *mainLayoutPolicy =
new MLinearLayoutPolicy(mainLayout, Qt::Horizontal);
mainLayout->setPolicy(mainLayoutPolicy);
m_aboutLabel = new MLabel("Label", this);
m_aboutLabel->setStyleName("LabelAbout");
mainLayoutPolicy->addItem(m_aboutLabel, Qt::AlignLeft);
setLayout(mainLayout);
}
Вспомогательный header с описанием констант: dcpapplet.h
#ifndef DCPAPPLET_H
#define DCPAPPLET_H
namespace DcpApplet
{
enum
{
Main = 0
};
const int NoReferer = -1;
}
#endif // DCPAPPLET_H
Файл проекта: src.pro
TEMPLATE = lib
CONFIG += plugin gui meegotouch duicontrolpanel silent
HEADERS = \
sampleapplet.h \
samplemainwidget.h \
dcpapplet.h
SOURCES = \
sampleapplet.cpp \
samplemainwidget.cpp
DESTDIR = ./lib
TARGET = $$qtLibraryTarget(dcpsampleqmlapplet)
desktop.files += *.desktop
desktop.path = /usr/share/duicontrolpanel/desktops
target.path += /usr/lib/duicontrolpanel/applets
INSTALLS += target \
desktop
Desktop-файл, по которому система будет знать, что делать с нашим апплетом: sampleqmlapplet.desktop
[Desktop Entry]
Type=ControlPanelApplet
Name=Sample Applet
X-logical-id=Sample Applet
X-translation-catalog=Sample Applet
X-Maemo-Service=com.nokia.DuiControlPanel
X-Maemo-Method=com.nokia.DuiControlPanelIf.appletPage
X-Maemo-Object-Path=/
X-Maemo-Fixed-Args=Sample Applet
[DUI]
X-DUIApplet-Applet=libdcpsampleqmlapplet.so
[DCP]
Category=Applications
Order=4
WidgetType= Label
Собираем, устанавливаем. Находим в разделе "Параметры"->"Приложения" пункт "Sample Applet". Запускаем и любуемся пустым экраном с серым лейблом, двумя кнопками в тулбаре и окошком "About"
Апплет работает. Осталось только навесить на него наши QML-ные компоненты.
Попытка номер Раз:
Попробуем в нашем виджете вместо MLabel вставить QML-объект. Казалось бы, ничего сложного, есть даже подходящий пример, показывающий как расположить QDeclarativeComponent на QGraphicsScene (а именно это нам и нужно в случае расположения своего QML-элемента на MLayout): QDeclarativeComponent на QGraphicsScene
Создадим для тестов максимально простой QML-файл, с белым квадратом "внутри": FirstPage.qml
import QtQuick 1.1
Rectangle {
id: mainPage
width: 480
height: 300
color: "white"
}
Перенесем этот файл в ресурсы программы, дабы не захламлять файловую систему на нашем смарте.
И модифицируем наш класс SampleMainWidget следующим образом:
#include <QDeclarativeEngine>
#include <QDeclarativeComponent>
...
// m_aboutLabel = new MLabel("Label", this);
// m_aboutLabel->setStyleName("LabelAbout");
QDeclarativeEngine engine;
QDeclarativeComponent c(&engine, QUrl("qrc:qml/FirstPage.qml"));
QGraphicsLayoutItem* obj = qobject_cast<QGraphicsLayoutItem*>(c.create());
mainLayoutPolicy->addItem(obj);
// mainLayoutPolicy->addItem(m_aboutLabel, Qt::AlignLeft);
И добавим в PRO-файл строчку:
QT += declarative
Собираем, устанавливаем, запускаем и... ничего. Т.е. апплет работает, но нашего квадрата там нет. Fail.
Если заглянуть в отладку, то окажется, что строка
QGraphicsLayoutItem* obj = qobject_cast<QGraphicsLayoutItem*>(c.create());
просто возвращает пустой объект.
Не работает это преобразование, а так хотелось [s]верить в чудо[/s], чтобы быстро и просто...
Попытка номер Два:
Посмотрим, что еще нам предлагает документация: Интеграция QML
Попробуем получить QWidget из QDeclarativeView и вставить его через QGraphicsProxyWidget.
Объявим в samplemainwidget.h член класса
#include <QDeclarativeView>
...
private:
QDeclarativeView *mView;
И подредактируем в samplemainwidget.cpp создание нашего виджета следующим образом:
// QDeclarativeEngine engine;
// QDeclarativeComponent c(&engine, QUrl("qrc:qml/FirstPage.qml"));
// QGraphicsLayoutItem* obj = qobject_cast<QGraphicsLayoutItem*>(c.create());
// mainLayoutPolicy->addItem(obj);
mView = new QDeclarativeView();
mView->setSource(QUrl("qrc:qml/FirstPage.qml"));
QGraphicsProxyWidget *proxy_graphicsitem_1 = new QGraphicsProxyWidget(this);
proxy_graphicsitem_1->setWidget(mView);
mainLayoutPolicy->addItem(proxy_graphicsitem_1);
Ура! Наш белый прямоугольник появился на экране.
Теперь попробуем QML-посложнее.
Обновленный FirstPage.qml
import QtQuick 1.1
import com.nokia.meego 1.0
PageStackWindow {
id: appWindow
initialPage: mainPage
Page {
width: 200
height: 200
id: mainPage
Label {
id: lbl
text: "TextLabel"
}
Button {
anchors.top: lbl.bottom
text: "Button"
}
}
}
Смотрим рисунок. Забавно. Но не совсем то, что надо. От PageStackWindow придется отказаться.
При дальнейших экспериментах выясняетя, что вдобавок к повернутому экрану есть еще и проблемы с фокусом (очень ощутимые на TextInput: и сам фокус появляется далеко не сразу, и виртуальная клавиатура появляться не желает...). Различные игры со StrongFocus и подобными вещами пользы не принесли. Fail.
Этого, собственно, и следовало ожидать. У нас же получился бутерброд: QGraphicsScene, на котором лежит виджет с лэйаутом, в котором снова виджет, являющийся еще одним QGraphicsScene, со своими лэйаутом и виджетами...
Попытка номер Три:
А что если вернуться к попытке номер Раз, выкинув лишние прослойки, и... просто уложить наш QDeclarativeComponent прямо на основной виджет. Осталось только придумать как.
А чего тут думать-то? Если хорошенько поискать, то найдется функция setParentItem, которая и придет нам на помощь! В результате получаем вот такой новый вид функци initWidget:
void SampleMainWidget::initWidget()
{
QDeclarativeEngine engine;
QDeclarativeComponent c(&engine, QUrl("qrc:qml/FirstPage.qml"));
QGraphicsObject *mWdg = qobject_cast<QGraphicsObject*>(c.create());
mWdg->setParentItem( this );
}
Не нужен бутерброд с лэйаутами, не нужны лишние преобразования, просто наш основной QGraphicsWidget (а именно от него в конченом счете наследуется DcpStylableWidget) становится родителем для нового QDeclarativeComponent'а.
Правда, работать в таком эта конструкция не будет. А все потому, что по невнимательности мы не учли того, что QDeclarativeEngine за пределами функции инициализации перестанет сущестовать. Нужно сделать его членом класса. Добавим в samplemainwidget.h:
private:
QDeclarativeEngine mEngine;
и поменяем первые две строчки в initWidget на
// QDeclarativeEngine engine;
QDeclarativeComponent c(&mEngine, QUrl("qrc:qml/FirstPage.qml"));
В общем, на этом можно было даже поставить точку. Концепт доказал свою работоспособность.
Расширяем функционал
Разобъем наши настройки на несколько страниц. Так как от PageStackWindow мы уже отказались, то решить вопрос смены страниц обычным QML-тулбаром не получится. Вместо него мы можем попробовать TabBarLayout-элемент. Да, этот вариант даже работоспособен, но выглядит уж очень притянутым за уши (2 тулбара в одном окне - совсем некрасиво).
Переключение страниц лучше реализовать через родной тулбар апплета (тем более, что этот тулбар уже есть и никуда от него не деться). Значит, еще немножко копнем в MeegoTouch фреймворк.
Основной виджет модифицируем так, чтобы он мог отображать разные QML-файлы: добавим передачу строки с именем файла в наш виджет, передадим эту строку в функцию инициалиции контролов (initWidget),
SampleMainWidget::SampleMainWidget([color=blue]QString qmlFile,[/color] QGraphicsWidget *parent)
:DcpStylableWidget(parent)
{
setReferer(DcpApplet::NoReferer);
initWidget([color=blue]qmlFile[/color]);
}
и там модифицируем строку описания комопента
QDeclarativeComponent c(&mEngine, QUrl("qrc:qml/" + qmlFile));
Да, надо еще не забыть поправить в заголовочном файле изменившиеся параметры вызовов соответствующих функций.
В файле dcpapplet.h добавим в наше перечисление еще один элемент (вообще, нумерация виджетов апплета может быть любая, лишь бы первый виджет имел ID=0)
Page2 = 1
Основные изменения будут в файле sampleapplet.cpp.
В функцию constructStylableWidget добавим новый вариант case:
...
case DcpApplet::Main:
return pageMain();
break;
case DcpApplet::Page2:
return pageSecond();
break;
Функция pageMain() будет выглядеть так:
SampleMainWidget* SampleQmlApplet::pageMain()
{
SampleMainWidget *wdg = new SampleMainWidget(QString("FirstPage.qml"));
connect (this,SIGNAL(changeWidget(int)),wdg,SIGNAL(changeWidget(int)));
return wdg;
}
Здесь стоит обратить внимание на используемый сигнал. Он потребуется нам для переключения между окнами нашего апплета.
В апплете переключение окон должно производится по сигналу changeWidget. Но поступает этот сигнал от виджета. При этом у нас нет прямых рычагов управленя виджетами в пределах апплета - этим занимается сама система (создает виджеты, уничтожает, переключает по команде...) Поэтому по нажатию кнопок в тулбаре нашего апплета мы будем посылать соответствующий сигнал с нужным номером виджета, а активный виджет уже будет перенаправлять этот сигнал дальше.
Создадим функцию генерации второго QML-окна
SampleMainWidget* SampleQmlApplet::pageSecond()
{
SampleMainWidget *wdg = new SampleMainWidget(QString("SecondPage.qml"));
connect (this,SIGNAL(changeWidget(int)),wdg,SIGNAL(changeWidget(int)));
return wdg;
}
Добавим кнопки в тулбар. Кнопки в тулбаре представляют собой набор элементов MAction, которые могут иметь текст, иконку и пр. (в общем, небольшое расширение над QAction), тут же указывается местополжение в тулбаре. Для разнообразия сделаем одну кнопку с текстовым заголовком, другую - с иконкой:
QVector<MAction*> SampleQmlApplet::viewMenuItems()
{
QVector<MAction*> vector(3);
vector[0] = new MAction("About", this);
vector[0]->setLocation(MAction::ApplicationMenuLocation);
connect(vector[0],SIGNAL(triggered()),this,SLOT(showAboutDialog()));
vector[1] = new MAction("Main",this);
vector[1]->setLocation(MAction::ToolBarLocation);
connect(vector[1],SIGNAL(triggered()),this,SLOT(mainPageTriggered()));
vector[2] = new MAction(this);
vector[2]->setIconID ("icon-m-image-edit-red-eyes-remove");
vector[2]->setLocation(MAction::ToolBarLocation);
connect(vector[2],SIGNAL(triggered()),this,SLOT(secondPageTriggered()));
return vector;
}
Создадим слоты, которые будут генерировать сигналы для переключения окон:
void SampleQmlApplet::mainPageTriggered()
{
emit changeWidget(DcpApplet::Main);
}
void SampleQmlApplet::secondPageTriggered()
{
emit changeWidget(DcpApplet::Page2);
}
Добавим необходимые изменения в заголовочный файл sampleapplet.h:
public:
virtual void init();
virtual DcpStylableWidget* constructStylableWidget(int widgetId);
virtual SampleMainWidget* pageMain();
virtual SampleMainWidget* pageSecond();
virtual QVector<MAction *> viewMenuItems();
signals:
void changeWidget(int widgetId);
public slots:
void mainPageTriggered();
void secondPageTriggered();
Ну и сделаем 2 почти одинаковых QML-фйла FirstPage.qml и SecondPage.qml:
import QtQuick 1.1
import com.nokia.meego 1.0
Rectangle {
id: mainPage
width: 480
height: 800
color: "black"
property int textSize: 28
Rectangle {
id: mainPage
width: 480
height: 800
color: "black"
property int textSize: 28
Column {
spacing: 20
Row {
id: boolInput
Label {
text: "First Page";
width: mainPage.width - switchComponent.width
font.family: "Nokia pure"
font.pixelSize: textSize
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
Switch {
id: switchComponent;
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: stringInput
height: boolInput.height
Label {
id: lbInput
text: "string value";
width: mainPage.width/2
font.pixelSize: textSize
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: textInput
placeholderText: "Enter text here"
width: mainPage.width/2
}
}
}
Component.onCompleted: {
theme.inverted = true;
}
}
Во втором поменяем пару строк (и не забудем добавить сам файл в ресурсы программы).
id: firstLabel
text: "First Page";
на
id: firstLabel
text: "Second Page";
Собираем, устанавливаем, запускаем. Радуемся полученному результату.
Тут нас поджидает ще один момент. Желательно отключить прокручивание родительского виджета - иначе оно будет мешать прокручиванию внутри наших контролов (например, при использовании элемента DatePickerDialog). Сделать это не просто, а очень просто. Достаточно переопределить в samplemainwidget.cpp функцию pagePans():
bool SampleMainWidget::pagePans() const
{
return false;
}
В заголовочном файле samplemainwidget.h, соответственно, в раздел public добавим:
bool pagePans() const;
А для прокрутки будем использовать QML-элемент Flickable:
Flickable {
contentHeight: 1000
anchors.fill: parent
interactive: true
Column {
...
}
Еще один момент, касательно сборки проекта. Поначалу упорно вылезала ошибка "Undefined interface", хотя был подключен нужный хидер #include <DcpAppletIf> и он определенно находился в нужном месте. Пришлось, в итоге, указать в sampleapplet.h полный путь к файлу DcpAppletIf.h - после чего ошибки исчезли и сборка пошла нормально.
В качестве заключения:
За рамками данной статьи осталось проектирование самого GUI (вариант с различными типами данных есть в прилагаемом проекте), а также собственно загрузка и сохранение настроек. Я остановился на использовании GConf. Это и удобный переход от 1-го варианта апплета (в виде XML-конфигуратора) к бинарному апплету без смены системы хранения настроек; и возможность бэкапа настроек из GConf на уровне стандартной резервной копии; и сигналы, оповещающие об изменениях в настройках, которые очень удобно отлавливать в программе-демоне, работающей 24 часа в сутки. Использование GConf также есть в прилагаемом проекте.
p.s. Готовый проект (работоспособный шаблон апплета настроек) - в приложении.