Всем привет!
В одном из учебных проектов передо мной встала задача реализации кроссплатформенного GUI-приложения. В качестве инструмента был выбран Qt за свою скорость и обширную документацию.
Суть приложения подразумевает работу пользователя в несколько этапов, каждый из которых имел свой интерфейс, с возможностью перехода вперед-назад (по типу обыкновенного установщика на Windows). Для реализации было придумано решение, которым я хочу поделиться с вами.
Серьезного опыта в работе с Qt нету, поэтому в некоторых моментах могу ошибаться и буду рад любым вашим замечаниям.
Начнем.
Хотелось бы начать со схемы и пояснений общей идеи.
В данном случае центральным классом будет MainWindow ( наследник QMainWindow) — это главное окно приложения, которое и будет провайдером для всех виджетов.
AbstractWidget расширяет QWidget и обеспечивает всех его наследников возможностью генерировать сигнал changeCurrentWidget(int widgetId, bool deleteFlag).
Этот сигнал очень важен в данной реализации. Он говорит главному окну — «Поменяй меня на другой виджет (widgetId), удали меня (deleteFlag)».
Возможность удаления очень важна, т.к. достаточно накладно держать в памяти крупные объекты интерфейса, и наоборот — удалять каждый раз легкие часто используемые виджеты может быть нецелесообразно
WidgetContainer — служит контейнером уникальных идентификаторов для наших виджетов. Здесь все совсем просто.
Резюмирую — главное окно является провайдером для виджетов, виджеты реализуют интерфейс для смены друг друга. Для обозначения, каждый из них имеет свой идентификатор, который хранится в WidgetContainer. (Очень похоже на архитектуру андроид, а именно, взаимодействие фрагментов с активити).
Надеюсь, все понятно, и можно перейти к реализации.
Начнем с виджетов.
Объявление AbstractWidget примитивно, как и его реализация.
class AbstractWidget : public QWidget
{
Q_OBJECT
public:
explicit AbstractWidget(QWidget *parent = 0);
signals:
void changeCurrentWidget(int widgetId, bool deleteSelfFlag);
};
AbstractWidget::AbstractWidget(QWidget *parent) :
QWidget(parent) {
setVisible(false);
}
Реализация его наследника так же не содержит в себе ничего сложного. Просто при нажатии на кнопку генерирует сигнал, который в дальнейшем будет обработан в MainWindow.
SecondWidget::SecondWidget(QWidget *parent) :
AbstractWidget(parent),
ui(new Ui::SecondWidget) {
ui->setupUi(this);
connect(ui->backToFirst, SIGNAL(clicked()), SLOT(buttonClickedSlot()));
}
void SecondWidget::buttonClickedSlot() {
emit changeCurrentWidget(WidgetIdContainer::FIRST_WIDGET, true); // Поменяй меня на первый виджет и удали
}
SecondWidget::~SecondWidget() {
delete ui;
}
Теперь, самое интересное — MainWindow.
Из объявления видно, что все виджеты будут хранится в хэш таблице по их ID. Это обеспечит нам быстрый доступ к ним.
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
AbstractWidget* getWidgetById(int idWidget);
private:
QHash<int, AbstractWidget*> widgetsHash;
AbstractWidget* currentWidget;
void setMainWidget(int idWidget);
public slots:
void setMainWidgetSlot(int idWidget, bool deleteFlag);
};
Все методы по порядку:
В конструкторе мы устанавливаем стартовый виджет окна.
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent), currentWidget(nullptr) {
setMainWidget(WidgetIdContainer::FIRST_WIDGET);
}
Данный слот реагирует на генерацию сигнала changeCurrentWidget(int widgetId, bool deleteSelfFlag) в текущем активном виджете. В зависимости от флага deleteFlag — сохраняем виджет в памяти, либо удаляем его.
// Слот callback - устанавливаем виджет
void MainWindow::setMainWidgetSlot(int idWidget, bool deleteFlag) {
AbstractWidget* buffer = currentWidget;
if(deleteFlag) {
currentWidget = nullptr;
}
setMainWidget(idWidget);
if(deleteFlag && buffer != nullptr) {
delete buffer;
}
}
Данный метод вызывается для установки виджета в качестве центрального. Мы проверяем корректность сиглально — слотовых соединений, получаем виджет по id и устанавливаем его в наше окно.
void MainWindow::setMainWidget(int idWidget) {
// Очищаем старые сигнально - слотовые соединения
if(currentWidget) {
disconnect(currentWidget, SIGNAL(changeCurrentWidget(int,bool)), this, SLOT(setMainWidgetSlot(int,bool)));
}
// Получаем виджет по айди
currentWidget = getWidgetById(idWidget);
currentWidget->setVisible(true);
// Делаем виджет центральным для этого окна
setCentralWidget(currentWidget);
connect(currentWidget, SIGNAL(changeCurrentWidget(int,bool)), SLOT(setMainWidgetSlot(int,bool)));
}
Данный метод получает на вход ID и возвращает объект виджета. При наличии в хэш таблице сохраненного объекта — вернет его, иначе будет создан новый инстанс нужного класса.
AbstractWidget* MainWindow::getWidgetById(int idWidget) {
// Проверка на наличие виджета в таблице
if(widgetsHash.contains(idWidget) && widgetsHash[idWidget] != nullptr) {
return widgetsHash[idWidget];
}
AbstractWidget* returnWidget = nullptr;
// Возвращаем нужный инстанс
switch(idWidget) {
case WidgetIdContainer::FIRST_WIDGET:
returnWidget = new FirstWidget(this);
break;
case WidgetIdContainer::SECOND_WIDGET:
returnWidget = new SecondWidget(this);
break;
}
return returnWidget;
}
Вообщем, все.
Данная архитектура позволяет нам легко встроить новый виджет в окно и использовать его, когда нам это нужно. Для этого нужно унаследовать виджет от AbstractWidget, определить его id в WidgetContainer и добавить возвращение инстанса в AbstractWidget* MainWindow::getWidgetById(int idWidget).
Это будет выглядеть примерно так:
switch(idWidget) {
case WidgetIdContainer::FIRST_WIDGET:
returnWidget = new FirstWidget(this);
break;
case WidgetIdContainer::SECOND_WIDGET:
returnWidget = new SecondWidget(this);
break;
case WidgetIdContainer::THIRD_WIDGET:
returnWidget = new ThirdWidget(this);
break;
}
Надеюсь, данное решение будет кому-нибудь полезно.
Спасибо за внимание.
Автор: Dimorinny