class NotesModel : public QAbstractListModel // открытое наследование
{
Q_OBJECT // макрос метаобъектной системы Qt
public:
explicit NotesModel(QObject *parent = nullptr); // конструктор
private:
QStringList m_notes; // поле; префикс m_ — соглашение Qt
};
void printNote(const QString &text); // ссылка на константу: без копирования,
// без возможности изменить аргумент
NotesModel *model = new NotesModel(this); // указатель на объект в куче
Правила:
QObject) создаются в куче (new) и передаются по указателю;QString, списки, структуры) передаются по константной ссылкеconst T &;delete в коде Qt почти не встречаются.У каждого QObject есть необязательный родитель (parent). При уничтожении родителя автоматически уничтожаются все его потомки. Поэтому типичный код выглядит так:
NotesModel *model = new NotesModel(&app); // приложение владеет моделью
// delete не нужен: модель будет удалена вместе с приложением
QObject — базовый класс большинства классов Qt. Макрос Q_OBJECT в объявлении класса включает метаобъектную систему: информацию о классе, доступную во время выполнения. Именно она обеспечивает сигналы/слоты, свойства и интеграцию с QML. Технически по заголовочным файлам с Q_OBJECT специальный генератор moc (meta-object compiler) создаёт дополнительный C++-код.
Сигнал — извещение «что-то произошло»; слот — обычный метод, который можно подключить к сигналу. Это основной механизм связи объектов в Qt:
// counter.h
class Counter : public QObject
{
Q_OBJECT
public:
explicit Counter(QObject *parent = nullptr);
int value() const { return m_value; }
public slots:
void increment(); // ← слот: кнопка будет "нажимать" его
signals:
void valueChanged(int newValue); // ← сигнал: Counter оповещает всех
private:
int m_value = 0;
};
// counter.cpp
void Counter::increment()
{
++m_value;
emit valueChanged(m_value); // ← сначала обновили данные, потом сообщили об этом
}
// main.cpp
Counter counter;
QPushButton *btn = new QPushButton("Добавить");
QLabel *label = new QLabel("Значение: 0");
QObject::connect(btn, &QPushButton::clicked,
&counter, &Counter::increment);
QObject::connect(&counter, &Counter::valueChanged,
label, [label](int v) {
label->setText("Значение: " + QString::number(v));
});
Важные свойства механизма:
onClicked и подобные — это те же подключения к сигналам.Чтобы данные C++-объекта были видны из QML с автоматическим обновлением привязок, их объявляют свойствами:
class Counter : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value NOTIFY valueChanged)
// READ — геттер, NOTIFY — сигнал, по которому QML обновит привязки.
// Для изменяемых из QML свойств добавляется WRITE-метод.
public:
// ...
};
В QML после этого можно писать text: counter.value — подпись обновится сама.
Методы, которые нужно вызывать из QML, помечаются Q_INVOKABLE либо объявляются как public slots:
Q_INVOKABLE void reset();
Объект создаётся в main() и публикуется в корневом контексте QML под выбранным именем:
#include <auroraapp.h>
#include <QtQuick>
#include "counter.h"
int main(int argc, char *argv[])
{
QScopedPointer<QGuiApplication> application(Aurora::Application::application(argc, argv));
application->setOrganizationName(QStringLiteral("ru.template"));
application->setApplicationName(QStringLiteral("CounterApp"));
Counter counter;
QScopedPointer<QQuickView> view(Aurora::Application::createView());
view->rootContext()->setContextProperty(QStringLiteral("counter"), &counter);
view->setSource(Aurora::Application::pathTo(QStringLiteral("qml/CounterApp.qml")));
view->show();
return application->exec();
}
QML: Label { text: counter.value }, Button { onClicked: counter.increment() }.
Плюсы: просто, один общий объект на всё приложение. Минусы: имя появляется из ниоткуда во всех QML-файлах, тип не виден инструментам IDE.
Тип регистрируется в системе QML, а объект создаётся самим QML:
qmlRegisterType<Counter>("ru.template.CounterApp", 1, 0, "Counter");
import ru.template.CounterApp 1.0
Page {
Counter { id: counter }
Label { text: counter.value }
}
Этот способ используют официальные примеры ОМП; он явный и масштабируемый.
Вызов
qmlRegisterTypeдолжен выполняться до загрузки QML-файла
(view->setSource(...)).
Список в QML (SilicaListView) отображает модель. Для динамических данных со сложной логикой стандартное решение — собственный класс на основе
QAbstractListModel. Контракт модели:
roleNames() сопоставляетrowCount() — число строк.data(index, role) — значение для строки и роли.beginInsertRows()/endInsertRows(), beginRemoveRows()/endRemoveRows() при вставке/удалении и сигнал dataChanged() при изменении существующих строк. Без них представление не узнает об изменениях (или упадёт).Скелет модели заметок:
class NotesModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
enum Roles {
TextRole = Qt::UserRole + 1,
CreatedAtRole
};
explicit NotesModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addNote(const QString &text);
Q_INVOKABLE void removeNote(int row);
signals:
void countChanged();
private:
struct Note {
QString text;
QDateTime createdAt;
};
QList<Note> m_notes;
};
Фрагмент реализации вставки (обратите внимание на парные вызовы begin/end):
void NotesModel::addNote(const QString &text)
{
const QString trimmed = text.trimmed();
if (trimmed.isEmpty())
return;
beginInsertRows(QModelIndex(), 0, 0); // вставляем в начало списка
m_notes.prepend(Note{trimmed, QDateTime::currentDateTime()});
endInsertRows();
emit countChanged();
}
В QML роли доступны в делегате по именам из roleNames():
SilicaListView {
anchors.fill: parent
model: notesModel
delegate: ListItem {
Label { text: model.text } // роль "text"
}
}
Навигация в ОС Аврора устроена как стек: новая страница кладётся поверх текущей, жест «назад» снимает её со стека. API доступен через свойство pageStack любой страницы:
pageStack.push(Qt.resolvedUrl("DetailsPage.qml"), { noteIndex: index })
pageStack.pop()
Второй аргумент push — объект со значениями свойств создаваемой страницы.
Dialog — разновидность страницы с жестами «принять» и «отменить»:
import QtQuick 2.0
import Sailfish.Silica 1.0
Dialog {
id: dialog
property string noteText: noteField.text
canAccept: noteField.text.trim().length > 0 // условие принятия
Column {
width: parent.width
DialogHeader { title: qsTr("Новая заметка") }
TextArea {
id: noteField
width: parent.width
placeholderText: qsTr("Текст заметки")
}
}
}
Вызов и получение результата:
var dialog = pageStack.push(Qt.resolvedUrl("AddNoteDialog.qml"))
dialog.accepted.connect(function() {
notesModel.addNote(dialog.noteText)
})
PullDownMenu — выдвижное меню сверху, размещается внутриv SilicaFlickable или SilicaListView; основное место для действий уровня страницы («Добавить», «Очистить»).ContextMenu — контекстное меню элемента списка, открывается долгим нажатием:SilicaListView {
anchors.fill: parent
model: notesModel
PullDownMenu {
MenuItem {
text: qsTr("Очистить всё")
onClicked: notesModel.clearAll()
}
MenuItem {
text: qsTr("Добавить заметку")
onClicked: {
var dialog = pageStack.push(Qt.resolvedUrl("NoteDialog.qml"))
dialog.accepted.connect(function() {
notesModel.addNote(dialog.noteText)
})
}
}
}
delegate: ListItem {
id: listItem
// контекстное меню — открывается долгим нажатием
menu: ContextMenu {
MenuItem {
text: qsTr("Удалить")
onClicked: listItem.remorseDelete(function() {
notesModel.removeNote(index)
})
}
}
// содержимое элемента
Column {
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
verticalCenter: parent.verticalCenter
}
Label {
width: parent.width
text: model.text
color: listItem.highlighted
? Theme.highlightColor
: Theme.primaryColor
}
Label {
width: parent.width
text: model.createdAt
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
}
}
}
Свёрнутое приложение отображается на домашнем экране «обложкой». Обложка может быть информативной (показывать состояние) и интерактивной (CoverAction):
CoverBackground {
Column {
anchors {
top: parent.top
left: parent.left
right: parent.right
topMargin: Theme.paddingLarge
leftMargin: Theme.paddingMedium
rightMargin: Theme.paddingMedium
}
spacing: Theme.paddingSmall
Label {
width: parent.width
text: qsTr("Заметки")
font.pixelSize: Theme.fontSizeMedium
color: Theme.highlightColor
}
Label {
width: parent.width
text: notesModel.count > 0
? qsTr("%1 заметок").arg(notesModel.count)
: qsTr("Нет заметок")
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
}
CoverActionList {
CoverAction {
iconSource: "image://theme/icon-cover-cancel"
onTriggered: {
if (notesModel.count > 0)
notesModel.removeNote(0)
}
}
CoverAction {
iconSource: "image://theme/icon-cover-new"
onTriggered: {
appWindow.activate()
pageStack.push(Qt.resolvedUrl("../pages/NoteDialog.qml"))
}
}
}
}
В ApplicationWindow нужен id: appWindow — без этого CoverAction не скомпилируется.
Для пустого списка Silica предлагает стандартный компонент:
SilicaListView {
// ...
ViewPlaceholder {
enabled: listView.count === 0
text: qsTr("Заметок нет")
hintText: qsTr("Потяните вниз, чтобы добавить")
}
}
Каждое приложение ОС Аврора имеет собственные каталоги конфигурации, данных и кэша; их расположение определяется OrganizationName/ApplicationName. Путь к каталогу данных запрашивается через QStandardPaths:
const QString dataDir =
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(dataDir); // каталог может не существовать
const QString filePath = dataDir + QStringLiteral("/notes.json");
Доступ к собственному каталогу данных не требует разрешений в .desktop.
Сериализация в JSON средствами Qt:
// Сохранение
QJsonArray array;
for (const Note ¬e : m_notes) {
QJsonObject object;
object.insert(QStringLiteral("text"), note.text);
object.insert(QStringLiteral("createdAt"),
note.createdAt.toString(Qt::ISODate));
array.append(object);
}
QFile file(filePath);
if (file.open(QIODevice::WriteOnly))
file.write(QJsonDocument(array).toJson());
// Загрузка: QJsonDocument::fromJson(file.readAll())
Цель: построить многостраничное приложение с моделью данных на C++: освоить QAbstractListModel, интеграцию C++ с QML, навигацию PageStack, диалоги и фирменные паттерны взаимодействия Silica.
Разработайте приложение «Заметки» для ОС Аврора:
NotesModel, унаследованный от QAbstractListModel;QString + QDateTime);text, createdAt;addNote(QString), removeNote(int) (через Q_INVOKABLEcount (Q_PROPERTY с NOTIFY) для отображения числа заметок;beginInsertRows/endInsertRows, beginRemoveRows/endRemoveRows;qmlRegisterType или контекстное свойствоSilicaListView с заметками;
Theme.secondaryColor;PullDownMenu с пунктом «Новая заметка»;ViewPlaceholder для пустого списка;Dialog): многострочное поле TextArea;canAccept запрещаетContextMenu в делегате (долгое нажатие) с пунктом «Удалить»remorseDelete.QStandardPaths::AppDataLocation);.desktop не требуются — каталог собственный.QtQuick 2.0, Sailfish.Silica 1.0;ListModel с данными;QDateTime::toString(Qt::ISODate) / QDateTime::fromString(s, Qt::ISODate) — сериализация даты;Qt.formatDateTime(model.createdAt, "dd.MM.yyyy hh:mm");accepted диалога удобно подключать сразу после pageStack.push(...);addNote — проверьте begin/end-вызовы;removeNote напрямую из onClicked пункта меню — обернитеremorseDelete, иначе элемент исчезнет без анимации и возможности отмены;CreatedAtRole должна возвращать именно QVariant(note.createdAt) как QDateTime, а не строку — иначе Qt.formatDateTime(model.createdAt, ...) в делегате вернёт пустую строку.PageStack, Dialog, меню, обложки;