[C++, Qt] Пишем свою навигацию в Qt
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет. Я android разработчик с небольшим стажем. И сегодня я бы хотел поделиться опытом разработки учебного проекта на C++ и Qt с простой навигацией между экранами. Буду рад услышать критику или дополнения к моему решению и надеюсь, что оно сможет кому-то помочь и упростить жизнь.ЗадачаПрежде чем начать, определимся с двумя терминами, чтобы в итоге не возникало путаницы.
Окно — самое обычное окно в windows или любой другой os.
Экран — какой-то контент, который может сменяться другим внутри окна.
У меня была идея проекта, что-то вроде упрощенной версии Trello, но для ее реализации нужно было определиться с тем, как будет осуществляться смена контента в окне. Какие вообще есть варианты? Мы можем создавать разные окна на каждую задачу, но такой вариант плохо сказывается на пользовательском опыте и не нравится мне. Чтобы отказаться от главного окна, нужно написать навигатор, который будет уметь заменять экраны внутри QStackedWidget и предоставит различные сценарии переходов от одного экрана к другому. Также необходимо знать о предыдущих экранах для возможности вернуться к ним и как-то передавать данные.На изображении ниже представлена схема приложения и переходы. Когда приложение запускается, мы видим заставку, потом выполняется замена экрана на стартовый или на главный. При этом вернуться с главного экрана на загрузку мы не можем.
В итоге, посмотрев на схему, мы можем составить небольшой список подзадач, которые хотим реализовать:
- Общий родитель у экранов для реализации взаимодействий.
- Фабрика экранов.
- Определение команд для навигации у экранов.
- Хранение стека экранов.
Общая модель экранаОчевидный этап проработки навигатора — вынесение связанных с ним методов в отдельный класс 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();
};
Фабрика имеет всего два виртуальных метода:
- create(QString tag) — создает экран по его идентификатору (идентификатор мы указываем в конкретной реализации фабрики);
- 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 проекта
===========
Источник:
habr.com
===========
Похожие новости:
- [Алгоритмы] OpenCV ищет студентов для Google Summer of Code-2021
- [Qt] Одноплатный компьютер для embedded программиста
- [Программирование, C++] C++ 14 с точки зрения олимпиадного программирования
- [Программирование, C++] Multithreading
- [C++, Автомобильные гаджеты] Automotive, трансформируемся
- [Программирование, C++, Системы сборки] Кроссплатформенный C++ с vcpkg и зависимостями
- [Программирование, C++, Git, Qt] QGit, улучшения
- [Информационная безопасность, Криптография, Python, C++, ООП] Поддержка токенов PKCS#11 с ГОСТ-криптографией в Python. Часть II — Обёртка PyKCS11
- [Java, C++, Go] Внимание: все сервисы Bintray будут прекращены, а ваши учетные записи будут отключены 1 мая 2021 года. — JFrog
- [Программирование, C++, C, Разработка под Linux] Приёмы неблокирующего программирования: полные барьеры памяти (перевод)
Теги для поиска: #_c++, #_qt, #_c++, #_qt, #_navigatsija (Навигация), #_arhitektura (Архитектура), #_prilozhenie (Приложение), #_c++, #_qt
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:02
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет. Я android разработчик с небольшим стажем. И сегодня я бы хотел поделиться опытом разработки учебного проекта на C++ и Qt с простой навигацией между экранами. Буду рад услышать критику или дополнения к моему решению и надеюсь, что оно сможет кому-то помочь и упростить жизнь.ЗадачаПрежде чем начать, определимся с двумя терминами, чтобы в итоге не возникало путаницы. Окно — самое обычное окно в windows или любой другой os. Экран — какой-то контент, который может сменяться другим внутри окна. У меня была идея проекта, что-то вроде упрощенной версии Trello, но для ее реализации нужно было определиться с тем, как будет осуществляться смена контента в окне. Какие вообще есть варианты? Мы можем создавать разные окна на каждую задачу, но такой вариант плохо сказывается на пользовательском опыте и не нравится мне. Чтобы отказаться от главного окна, нужно написать навигатор, который будет уметь заменять экраны внутри QStackedWidget и предоставит различные сценарии переходов от одного экрана к другому. Также необходимо знать о предыдущих экранах для возможности вернуться к ним и как-то передавать данные.На изображении ниже представлена схема приложения и переходы. Когда приложение запускается, мы видим заставку, потом выполняется замена экрана на стартовый или на главный. При этом вернуться с главного экрана на загрузку мы не можем. В итоге, посмотрев на схему, мы можем составить небольшой список подзадач, которые хотим реализовать:
class BaseFragment: public QFrame {
Q_OBJECT signals: //тут мы потом определим сигналы для навигатора public: BaseFragment(); ~BaseFragment(); //тут реализуем что-то из жизненного цикла }; 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(); };
// 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; // идентификатор стартового экрана. } Нужные нам методы навигации:
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); }; 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); }; 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); } 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()); } } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 16:02
Часовой пояс: UTC + 5