Приложение РОСА Заметки

Начало
Предустановленное приложение "Заметки" представляет собой редактор заметок, написанный с использованием Qt, QML и C++. На примере данного приложения можно изучить принципы работы с данными и взаимодействие между QML и C++ в контексте мобильных приложений для Rosa Mobile.

Структура приложения
Приложение состоит из части на C++ (логики, моделей и вспомогательных классов) и части на QML.
- main.cpp – Точка входа в приложение. Здесь инициализируются настройки, регистрируются типы C++ для использования в QML, загружается основной QML-файл.
- NotesModel – Модель данных (QAbstractListModel) для хранения и управления заметками.
- DocumentHandler – Вспомогательный класс, связанный с редактированием и отображением содержимого заметки (работа с QTextDocument, загрузка и сохранение).
- SelectionProxyModel – Прокси-модель над NotesModel, позволяющая выбирать несколько заметок, выделять их и обрабатывать операции с выделенными элементами.
- QML-файлы (main.qml, EditPage.qml, NotesPage.qml, TabletNotesPage.qml и др.) –Определяют вид и логику пользовательского интерфейса.
Точка входа в приложение
main.cpp
Файл main.cpp содержит функцию main(), с которой начинается выполнение программы:
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QApplication app(argc, argv);
// Установка стиля
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
}
// Подключение перевода, установка домена локализации
KLocalizedString::setApplicationDomain("rosanotes");
// Установка иконки
QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.rosanotes")));
// Инициализация движка QML
QQmlApplicationEngine engine;
// Регистрация C++ классов в пространстве имён QML
qmlRegisterType("org.kde.rosanotes", 1, 0, "DocumentHandler");
qmlRegisterType("org.kde.rosanotes", 1, 0, "NotesModel");
qmlRegisterType("org.kde.rosanotes", 1, 0, "SelectionProxyModel");
qmlRegisterAnonymousType("org.kde.rosanotes", 1);
qmlRegisterType("org.kde.rosanotes", 1, 0, "SortFilterModel");
// Регистрация KAboutData как синглтона для получения информации о приложении
qmlRegisterSingletonType("org.kde.rosanotes", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(KAboutData::applicationData());
});
// Локализация
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
// Путь к QML-ресурсам
engine.addImportPath(QStringLiteral("qrc:/contents/ui"));
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty()) {
return -1;
}
return app.exec();
}
Обратите внимание, что qmlRegisterType здесь используется чтобы "отдать" классы DocumentHandler, NotesModel, SelectionProxyModel в QML, чтобы там можно было использовать , и т. п.
Модель заметок
NotesModel
NotesModel наследуется от QAbstractListModel, хранит список заметок в локальной директории и предоставляет методы для добавления, удаления, переименования и проверки существования заметок. Основные методы модели включают в себя:
- rowCount – возвращает количество существующих заметок
- data – возвращает выбранные данные (путь, дату или имя) по индексу заметки
- addNote, deleteNote, renameNote – управление заметками
- path – возвращает путь к директории, где хранятся заметки
Пример объявления (фрагмент notesmodel.h):
class NotesModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
enum Role { Path = Qt::UserRole + 1, Date, Name };
Q_ENUM(Role)
...
};
Здесь, Q_PROPERTY даёт возможность связывать свойство path с QML (чтение/запись, сигнал pathChanged), enum Role определяет роли, которые будут возвращаться в методе data (Name, Path, Date).
Реализация
Рассмотрим фрагмент notesmodel.cpp:
int NotesModel::rowCount(const QModelIndex &index) const
{
return m_path.isEmpty() ? 0 : directory.entryList(QDir::Files).count();
}
QVariant NotesModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case Role::Path:
return QUrl::fromLocalFile(directory.entryInfoList(QDir::Files).at(index.row()).filePath());
case Role::Date:
return directory.entryInfoList(QDir::Files).at(index.row()).birthTime();
case Role::Name:
return directory.entryInfoList(QDir::Files).at(index.row()).fileName().replace(".md", "");
}
...
}
rowCount возвращает количество файлов .md в директории с заметками, data возвращает либо путь к файлу, либо дата, либо имя файла (без расширения .md) по заданному индексу.
Методы манипуляции заметками:
void NotesModel::addNote(const QString &name)
{
beginResetModel();
QFile file(m_path + QDir::separator() + name + ".md");
if (file.open(QFile::WriteOnly)) {
file.write("");
}
endResetModel();
}
void NotesModel::deleteNote(const QUrl &path)
{
beginResetModel();
QFile::remove(path.toLocalFile());
endResetModel();
}
void NotesModel::renameNote(const QUrl &path, const QString &name)
{
QString newPath = QFileInfo(path.toLocalFile()).absolutePath() + QDir::separator() + name + ".md";
beginResetModel();
QFile::rename(path.toLocalFile(), newPath);
endResetModel();
}
Методы newNotePath и newNoteName создают корректный путь/имя файла, если есть повторяющиеся названия. isNameExist проверяет, есть ли уже заметка с таким именем.В этих методах вызывается beginResetModel() и endResetModel(), чтобы QML мог обновить представления, связанные с моделью.
Обработка содержимого заметок
DocumentHandler
Класс DocumentHandler предназначен для работы с QTextDocument, который лежит в основе TextArea (RichText-редактор в QML). Он управляет:
- Загрузкой и сохранением текста в/из файла
- Форматированием (bold, italic, underline и т. д.)
- Отслеживает изменения (свойство modified)
Объявление в documenthandler.h:
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
// ...
Вся логика форматирования (установка жирного/курсива/подчёркивания) сводится к методам, которые меняют QTextCharFormat текущего выделения:
void DocumentHandler::setBold(bool bold)
{
QTextCharFormat format;
format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
mergeFormatOnWordOrSelection(format);
Q_EMIT boldChanged();
}
Загрузка/сохранение:
void DocumentHandler::load(const QUrl &fileUrl)
{
// Получаем путь
const QString fileName = QQmlFile::urlToLocalFileOrQrc(fileUrl);
// Считываем данные и устанавливаем в QTextDocument
...
}
void DocumentHandler::saveAs(const QUrl &fileUrl)
{
// Записываем данные из QTextDocument в HTML (toHtml().toUtf8())
...
}
В QML этот класс подключается как DocumentHandler { id: document ... }, а затем свойства, такие как, document.bold связываются с кнопками, которые меняют форматирование текста.
Выделение в списке
SelectionProxyModel
SelectionProxyModel – это QSortFilterProxyModel, который поверх NotesModel даёт возможность выбора нескольких элементов. Он хранит набор выбранных индексов (m_selectedIndexes) и предоставляет соответствующие методы:
- select(int sourceIndex, bool on)
- toggleSelected(int sourceIndex)
- selectAll(), deselectAll()
- data(..., SelectedRole)
В QML мы получаем доступ к роли "selected", которая показывает, отмечен ли элемент. Пример:
QVariant SelectionProxyModel::data(const QModelIndex &index, int role) const
{
if (role == SelectedRole) {
return m_selectedIndexes.contains(mapToSource(index).row());
}
return QSortFilterProxyModel::data(index, role);
}
Когда пользователь "долго жмёт" на заметку или иным способом отмечает элемент, вызывается toggleSelected – индекс попадает/исчезает из m_selectedIndexes.
QML-интерфейс
Файл main.qml
main.qml определяет главное окно Kirigami:
Kirigami.ApplicationWindow {
id: root
pageStack.initialPage: isTablet ? "qrc:/TabletNotesPage.qml" : "qrc:/NotesPage.qml"
...
}
Здесь создается приложение на основе Kirigami.ApplicationWindow. В зависимости от того, запущено ли приложение на планшете или нет, загружается различный начальный QML-файл (TabletNotesPage.qml или NotesPage.qml).
Файл NotesPage.qml

Файл NotesPage.qml содержит список заметок (ListView), связанный с моделью NotesModel через прокси-модель SelectionProxyModel и SortFilterModel. Основные элементы:
- ActionButton в правом нижнем углу для добавления новой заметки.
- ListView (id: notesList), делегат которой отображает имя заметки, дату и даёт возможность при долгом нажатии перейти в режим выбора (selectionMode = true).
- SearchField (TextField), которая фильтрует список по имени (через filterModel.setFilterFixedString(text)).
Файл EditPage.qml

EditPage.qml – страница редактирования выбранной заметки. Здесь присутствует TextArea и объект DocumentHandler:
QQC.TextArea.flickable: QQC.TextArea {
id: textArea
...
DocumentHandler {
id: document
document: textArea.textDocument
cursorPosition: textArea.cursorPosition
selectionStart: textArea.selectionStart
selectionEnd: textArea.selectionEnd
Component.onCompleted: document.load(path)
onLoaded: {
textArea.text = text
}
onError: (message) => {
print(message)
}
}
}
Благодаря этому document.saveAs(path) и document.load(path) вызывают C++ код из DocumentHandler. Кроме того, на странице есть кнопки жирного, курсивного и подчёркнутого форматирования, которые меняют свойства document.bold, document.italic, document.underline. Также реализована логика переименования заметки и проверки, что имя не пустое и не занято (методы из NotesModel).
Файл TabletNotesPage.qml
В этом QML-файле предусмотрен двухпанельный интерфейс, который используется, если устройство - планшет в горизонтальной (landscape) ориентации. Справа показывается EditPage, слева - NotesPage. При этом используется States, чтобы в портретной ориентации список был на весь экран, а при выборе заметки - скрывался, и показывалась страница редактирования.
states: [
State {
when: landscapeTablet
PropertyChanges { target: notesPage; width: root.width * 0.368; visible: true }
PropertyChanges { target: editPage; anchors.leftMargin: Rosa.Theme.scale(20); visible: true }
},
State {
when: !landscapeTablet && (!notesModel || selectMode || srchMode)
PropertyChanges { target: notesPage; width: root.width; visible: true }
},
State {
when: !landscapeTablet && notesModel
PropertyChanges { target: notesPage; width: 0; visible: false }
}
]