QML

Итоговый мини-проект: полноценное QML-приложение с C++ backend

Введение

В заключительной статье цикла QML (и всего плана статей в его текущем объёме) собираем итоговый проект, демонстрирующий совместное применение разобранных тем: приложение для управления задачами (todo-менеджер) с C++ моделью данных (статья 489), навигацией между экранами (статья 492), адаптивным макетом (статья 497) и локализацией (статья 495).

Концепция

Архитектура проекта: TaskListModel на C++ (наследник QAbstractListModel, статья 489), хранящий список задач и предоставляющий методы добавления/удаления/изменения статуса задачи, регистрируется как QML-тип (статья 490) и используется в ListView для отображения списка задач (статья 488). Навигация между списком задач и экраном детального просмотра/редактирования конкретной задачи реализована через StackView (статья 492). Макет адаптируется между однокол��ночным (телефон) и двухколоночным (список + детали одновременно видны на широком экране) вариантом в зависимости от ширины окна (статья 497).

Пример кода: Итоговый мини-проект

// TaskListModel — C++ модель задач (упрощённая версия принципа статьи 489)
class TaskListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    enum Roles { TitleRole = Qt::UserRole + 1, CompletedRole };

    int rowCount(const QModelIndex & = QModelIndex()) const override { return m_tasks.size(); }

    QVariant data(const QModelIndex &index, int role) const override
    {
        if (!index.isValid()) return {};
        const Task &task = m_tasks.at(index.row());
        if (role == TitleRole) return task.title;
        if (role == CompletedRole) return task.completed;
        return {};
    }

    QHash<int, QByteArray> roleNames() const override
    {
        return { {TitleRole, "title"}, {CompletedRole, "completed"} };
    }

    Q_INVOKABLE void addTask(const QString &title)
    {
        beginInsertRows(QModelIndex(), m_tasks.size(), m_tasks.size());
        m_tasks.append({title, false});
        endInsertRows();
    }

    Q_INVOKABLE void toggleCompleted(int row)
    {
        if (row < 0 || row >= m_tasks.size()) return;
        m_tasks[row].completed = !m_tasks[row].completed;
        emit dataChanged(index(row), index(row), {CompletedRole}); // уведомление об изменении ОДНОГО поля

    }

private:
    struct Task { QString title; bool completed; };
    QList<Task> m_tasks;
};
// Главный экран приложения с адаптивным макетом и навигацией
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    visible: true
    width: 800; height: 600
    title: qsTr("Менеджер задач")

    property bool isWideLayout: width > 700

    RowLayout {
        anchors.fill: parent
        visible: isWideLayout

        ListView {
            Layout.preferredWidth: parent.width * 0.4
            Layout.fillHeight: true
            model: taskListModel
            delegate: ItemDelegate {
                width: ListView.view.width
                text: title
                onClicked: detailsPanel.selectedTitle = title
            }
        }

        Pane {
            id: detailsPanel
            Layout.fillWidth: true
            Layout.fillHeight: true
            property string selectedTitle: ""
            Label { text: qsTr("Детали: ") + detailsPanel.selectedTitle }
        }
    }

    StackView {
        id: narrowStackView
        anchors.fill: parent
        visible: !isWideLayout
        initialItem: listPageComponent
    }

    Component {
        id: listPageComponent
        Page {
            ListView {
                anchors.fill: parent
                model: taskListModel
                delegate: ItemDelegate {
                    width: ListView.view.width
                    text: title
                    onClicked: narrowStackView.push(detailsPageComponent, {taskTitle: title})
                }
            }
        }
    }

    Component {
        id: detailsPageComponent
        Page {
            property string taskTitle: ""
            Label { anchors.centerIn: parent; text: qsTr("Детали: ") + taskTitle }
        }
    }
}

Пояснения к коду

TaskListModel объединяет паттерны статьи 489 (обязательные rowCount/data/roleNames) с Q_INVOKABLE-методами addTask/toggleCompleted, которые можно вызывать прямо из QML-кода (аналогично статье 490) — это даёт QML-стороне возможность изменять данные модели через простой вызов метода, не требуя реализации этой логики в самом QML. Главный QML-файл показывает практическое сочетание двух разных навигационных подходов — на широком экране (isWideLayout) используется постоянно видимый RowLayout с одновременно отображаемым списком и деталями, а на узком экране (StackView, статья 492) применяется традиционная навигация «список → детали» с переходом между отдельными страницами, и переключение между этими двумя принципиально разными подходами происходит автоматически через visible, привязанный к ширине окна (статья 497).

Подводные камни

  • Дублирование логики отображения деталей задачи между широким (detailsPanel) и узким (detailsPageComponent) вариантами макета без выделения общей части в переиспользуемый компонент (статья 484) — для реального проекта, где детали задачи значительно сложнее единственного Label, стоит выделить содержимое панели/страницы деталей в отдельный, общий компонент, используемый в обоих вариантах макета, а не дублировать одинаковую разметку в двух разных местах кода.
  • Недостаточное тестирование перехода между широким и узким вариантом макета во время выполнения (изменение размера окна десктопа в реальном времени, а не только при первом запуске с заранее фиксированным размером) — оба варианта макета (RowLayout+Pane и StackView) должны корректно вести себя при динамическом пересечении порога isWideLayout уже во время работы приложения, и это поведение стоит явно проверить, а не предполагать его корректность только на основании раздельного тестирования каждого варианта при фиксированном размере окна.
  • Использование emit dataChanged() без явного указания списка изменившихся ролей (третий параметр {CompletedRole} в примере toggleCompleted) — без этого уточнения подключённые виды могут перерисовывать весь делегат целиком при любом изменении любого поля элемента модели, тогда как явное указание конкретно изменившейся роли позволяет более оптимальным реализациям ограничиться обновлением только реально изменившейся части визуального представления делегата.
  • Отсутствие сохранения состояния задач между запусками приложения (модель, показанная в примере, хранит данные только в памяти, статья только демонстрирует архитектурный паттерн UI и модели, не персистентность) — для реального, готового к практическому использованию приложения необходима интеграция с persistent-хранилищем (SQL-цикл основного плана, статьи 393-442, или более простое решение через QSettings/JSON-файл для небольшого объёма данных), что является естественным следующим шагом развития этого учебного, но не полностью законченного итогового проекта.

Углублённый цикл QML (20 статей, 479-498) полностью завершён — от философии декларативного UI и базового синтаксиса через привязки, состояния, анимации, модели/виды, интеграцию с C++, частицы и шейдеры, до производительности, локализации, тестирования, адаптивного дизайна и итогового, полноценного приложения. Общий план статей теперь включает 498 тем, охватывая весь спектр Qt-разработки от языковых основ через embedded/IoT, мультимедиа, СУБД, DevOps, алгоритмы и теперь — глубокое владение современным декларативным UI Qt Quick/QML.