Пишем свою навигацию в Qt

от автора

Всем привет. Я android разработчик с небольшим стажем. И сегодня я бы хотел поделиться опытом разработки учебного проекта на C++ и Qt с простой навигацией между экранами. Буду рад услышать критику или дополнения к моему решению и надеюсь, что оно сможет кому-то помочь и упростить жизнь.

Задача

Прежде чем начать, определимся с двумя терминами, чтобы в итоге не возникало путаницы.
Окно — самое обычное окно в windows или любой другой os.
Экран — какой-то контент, который может сменяться другим внутри окна.

У меня была идея проекта, что-то вроде упрощенной версии Trello, но для ее реализации нужно было определиться с тем, как будет осуществляться смена контента в окне. Какие вообще есть варианты? Мы можем создавать разные окна на каждую задачу, но такой вариант плохо сказывается на пользовательском опыте и не нравится мне.

Чтобы отказаться от главного окна, нужно написать навигатор, который будет уметь заменять экраны внутри QStackedWidget и предоставит различные сценарии переходов от одного экрана к другому. Также необходимо знать о предыдущих экранах для возможности вернуться к ним и как-то передавать данные.

На изображении ниже представлена схема приложения и переходы. Когда приложение запускается, мы видим заставку, потом выполняется замена экрана на стартовый или на главный. При этом вернуться с главного экрана на загрузку мы не можем.

В итоге, посмотрев на схему, мы можем составить небольшой список подзадач, которые хотим реализовать:

  1. Общий родитель у экранов для реализации взаимодействий.

  2. Фабрика экранов.

  3. Определение команд для навигации у экранов.

  4. Хранение стека экранов.

Общая модель экрана

Очевидный этап проработки навигатора — вынесение связанных с ним методов в отдельный класс BaseFragment. Это базовый экран, от которого мы унаследуем все остальные. Пока это просто абстракция, но мы к нему еще вернемся и допишем.

class BaseFragment: public QFrame {     Q_OBJECT  signals:     //тут мы потом определим сигналы для навигатора   public:     BaseFragment();     ~BaseFragment();      //тут реализуем что-то из жизненного цикла };

Далее реализуем несколько экранов опираясь на BaseFragment. Каждый из фрагментов является виджетом, который мы можем поместить в QStackedWidget и выбрать отображаемый.

class LoginFragment: public BaseFragment  class StartFragment: public BaseFragment  class RegistrationFragment : public BaseFragment

Фабрика фрагментов

После того, как мы сделали несколько фрагментов, нам нужно создать фабрику экранов, чтобы сделать навигацию независимой от конкретно этого проекта. Реализация навигации из проекта в проект не будет меняться, достаточно определить правило, по которому мы будем создавать экраны.

class BaseScreensFactory { public:     BaseScreensFactory();     ~BaseScreensFactory();      virtual BaseFragment* create(QString tag);     virtual QString createStart(); }; 

Фабрика имеет всего два виртуальных метода:

  1. create(QString tag) — создает экран по его идентификатору (идентификатор мы указываем в конкретной реализации фабрики);

  2. createStart() — возвращает идентификатор стартового экрана.

Ниже реализация фабрики в моем проекте:

// screensfacrory.h заголовочный файл namespace screens {     static const QString SPLASH_TAG = "splash";     static const QString START_TAG = "start";     static const QString LOGIN_TAG = "login";     static const QString REGISTRATION_TAG = "registration";   	// и так далее..... };  class ScreensFactory: public BaseScreensFactory { public:     ScreensFactory();     ~ScreensFactory();      BaseFragment* create(QString tag) override;     QString createStart() override; };  // screensfacrory.cpp исходники BaseFragment* ScreensFactory::create(QString tag) {     qDebug("ScreensFactory create");     if (tag == SPLASH_TAG) {         return new SplashFragment;     } else if (tag == START_TAG) {         return new StartFragment;     } else if (tag == LOGIN_TAG) {         return new LoginFragment;     } else if (tag == REGISTRATION_TAG) {        // и так далее.....     } }  QString ScreensFactory::createStart() {     return SPLASH_TAG; // идентификатор стартового экрана. }

Наконец сама навигация

Когда у нас есть модель экрана и фабрика, создающая эти экраны по названию, мы можем написать класс, который будет получать от текущего экрана название следующего и создавать его. Далее новый экран будет добавляться в QStackedWidget контейнер и показываться пользователю.

Нужные нам методы навигации:

  • navigateTo(tag) — переход к новому экрану с добавлением этого экрана в цепочку открытых живущих.

  • back() — переход к предыдущему экрану с в цепочке с удалением текущего из нее.

  • replace(tag) — замена текущего экрана в цепочке на новый.

  • newRootScreen(tag) — удаление текущей цепочки и создание нового экрана.

  • navigateToWhithData(tag, data) — то же самое, что и navigateTo(tag), только вместе с именем экрана передается ссылка на какой-то объект.

Например, когда мы открываем экран с регистрацией, находясь на экране приветствия, выполняется метод navigateTo(REGISTRATION_TAG), после регистрации нужно открыть главный экран, но запретить переход назад. Для этого выполняем newRootScreen(MAIN_TAG).

Чтобы навигатор понимал, что нужно текущему фрагменту, в класс BaseFragment нужно дописать сигналы для методов навигации, описанных выше.

class BaseFragment: public QFrame {     Q_OBJECT  signals:   	//дописанные сигналы     void back();     void navigateTo(QString tag);     void newRootScreen(QString tag);     void replace(QString tag);          void navigateWhithData(QString tag, BaseModel* model);   public:     BaseFragment();     ~BaseFragment();    	//дописанные методы для жизненного цикла     virtual void onPause();     virtual void onResume();     virtual void setData(BaseModel* model); };

Поскольку фрагменты не все время находятся на экране, мы можем ставить на паузу процессы, выполняющиеся в нем, вызывая onPause(), и возобновлять, вызывая onResume() из навигатора, который мы скоро напишем. И метод setData() мы вызываем при передаче данных в фрагмент.

После долгих подготовок переходим к написанию навигации. Навигатор будет получать QStackedWidget как контейнер для фрагментов и BaseScreensFactory для создания фрагментов.

navigator.h:

class Navigator: public QObject {     Q_OBJECT private:     QStackedWidget *currentContainer;     BaseScreensFactory *screensFactory;     QLinkedList<BaseFragment*> stack;      /**      * @brief createAndConnect      * @param tag тэг создаваемого фрагмента.      *      * Создание фрагмента по тегу и его      * прикрепление к навигатору.      *      * @return фрагмент присоединенный к слотам навигатора.      */     BaseFragment* createAndConnect(QString tag);      /**      * @brief connectFragment      * @param fragment фрагмент который переходит      *        в активное состояние.      *      * Прикрепление текущего фрагмента      * к слотам навигатора для быстрого      * и удобного перехода между экранами.      *      */     void connectFragment(BaseFragment *fragment);      /**      * @brief disconnectFragment      * @param fragment      *      * Отключение сигналов от фрагмента.      */     void disconnectFragment(BaseFragment *fragment); public:     Navigator(             QStackedWidget *container,             BaseScreensFactory *screensFactory     );     ~Navigator();     BaseFragment* getStartScreen();  public slots:     /**      * @brief navigateTo      * @param tag имя следующего экрана.      *      * Переход к следующему экрану.      */     void navigateTo(QString tag);      /**      * @brief back      *      * Переход назад по цепочке.      */     void back();      /**      * @brief replace      * @param tag имя экрана на который      *        произойдет замена.      *      * Замена текущего экрана с сохранением      * предыдущей цепочки.      */     void replace(QString tag);      /**      * @brief newRootScreen      * @param tag имя экрана на который      *        произойдет замена.      *      * Замена текущего экрана на новый и сброс      * всей цепочки экранов.      */     void newRootScreen(QString tag);      /**      * @brief navigateWhithData      * @param model      *      * Тот же navigateTo но с данными.      */     void navigateWhithData(QString tag, BaseModel* model); };

Во время смены экранов происходит немного магии с их заменой. При создании фрагмента его сигналы нужно прикрепить к слотам навигатора. При смене фрагмента текущий нужно открепить от слотов и прикрепить новый. connectFragment присоединяет все слоты к сигналам, после этого навигатор управляется этим фрагментом. disconnectFragment открепляет все сигналы. createAndConnect создает фрагмент по его имени через фабрику и сразу прикрепляет его к навигатору. getStartScreen создает стартовый экран по имени, указанному в фабрике, и прикрепляет его к навигатору.

BaseFragment* Navigator::getStartScreen() {     return createAndConnect(this->screensFactory->createStart()); }  void Navigator::connectFragment(BaseFragment *fragment) {     connect(fragment, &BaseFragment::back, this, &Navigator::back);     connect(fragment, &BaseFragment::replace, this, &Navigator::replace);     connect(fragment, &BaseFragment::navigateTo, this, &Navigator::navigateTo);     connect(fragment, &BaseFragment::newRootScreen, this, &Navigator::newRootScreen);     connect(fragment, &BaseFragment::navigateWhithData, this, &Navigator::navigateWhithData); }  void Navigator::disconnectFragment(BaseFragment *fragment) {     disconnect(fragment, &BaseFragment::back, this, &Navigator::back);     disconnect(fragment, &BaseFragment::replace, this, &Navigator::replace);     disconnect(fragment, &BaseFragment::navigateTo, this, &Navigator::navigateTo);     disconnect(fragment, &BaseFragment::newRootScreen, this, &Navigator::newRootScreen);     disconnect(fragment, &BaseFragment::navigateWhithData, this, &Navigator::navigateWhithData); }  BaseFragment* Navigator::createAndConnect(QString tag) {     BaseFragment *fragment = this->screensFactory->create(tag);     connectFragment(fragment);     return fragment; }

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

Navigator::Navigator(         QStackedWidget *container,         BaseScreensFactory *screensFactory ) {     this->screensFactory = screensFactory;     this->currentContainer = container;     BaseFragment* startFragment = getStartScreen();     this->stack.append(startFragment);      currentContainer->addWidget(stack.last());     currentContainer->setCurrentIndex(0); }

Остается только прописать каждый слот. В слотах нет ничего сложного: чтобы не плодить плашки с кодом я приведу только реализацию navigateTo. В нем мы создаем новый фрагмент по идентификатору, затем тормозим текущий фрагмент и открепляем от навигатора. Далее прикрепляем новый фрагмент и добавляем его в контейнер.

void Navigator::navigateTo(QString tag) {     BaseFragment *newFragment = this->screensFactory->create(tag);     stack.last()->onPause();     disconnectFragment(stack.last());     connectFragment(newFragment);     stack.append(newFragment);     currentContainer->addWidget(newFragment);     currentContainer->setCurrentWidget(newFragment); }

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

MainWindow::MainWindow(QWidget *parent)     : QMainWindow(parent) {        try {         container = new QStackedWidget;          this->factory = new ScreensFactory;         this->navigator = new Navigator(                     this->container,                     this->factory         );          this->setCentralWidget(container);     } catch (std::exception& e) {         qDebug("%s", e.what());     } }

Взгляд со стороны

Сейчас мы посмотрели на большое количество кусочков кода, сперва может показаться, что они мало чем связаны. После долгих попыток разобраться это ощущение не пропадает, и это на самом деле хорошо. В итоге мы получили возможность разрабатывать различные экраны приложения абсолютно независимо от остальных частей. Мы пишем реализацию BaseFragment и добавляем ее в фабрику.

Мне мое решение очень помогло в реализации проекта. Я смог ускорить разработку и разделить код.

Всем дочитавшим спасибо, надеюсь кому-то это поможет.

Вот ссылка GitHub проекта

ссылка на оригинал статьи https://habr.com/ru/post/550440/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *