1

Тема: 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" smile
http://imageplay.net/tya22285864/01_thumb.jpg
   
Апплет работает. Осталось только навесить на него наши 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);

   
    Ура! Наш белый прямоугольник появился на экране.
http://imageplay.net/tya22285867/02_thumb.jpg
    Теперь попробуем 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"
        }
    }
}

http://imageplay.net/tya22285868/03_thumb.jpg
    Смотрим рисунок. Забавно. Но не совсем то, что надо. От 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";

    Собираем, устанавливаем, запускаем. Радуемся полученному результату.
http://imageplay.net/tya22285869/04_thumb.jpg

    Тут нас поджидает ще один момент. Желательно отключить прокручивание родительского виджета - иначе оно будет мешать прокручиванию внутри наших контролов (например, при использовании элемента 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. Готовый проект (работоспособный шаблон апплета настроек) - в приложении.

Post's attachments

QmlControlPanel.zip 12.61 kb, 14 загрузок с 2012-09-24 

У Вас недостаточно прав для загрузки файлов, прикрепленных к этому сообщению.