Qt — окно и виджеты

в 8:03, , рубрики: GUI, qt, Программирование

Всем привет!

В одном из учебных проектов передо мной встала задача реализации кроссплатформенного GUI-приложения. В качестве инструмента был выбран Qt за свою скорость и обширную документацию.

Суть приложения подразумевает работу пользователя в несколько этапов, каждый из которых имел свой интерфейс, с возможностью перехода вперед-назад (по типу обыкновенного установщика на Windows). Для реализации было придумано решение, которым я хочу поделиться с вами.

Серьезного опыта в работе с Qt нету, поэтому в некоторых моментах могу ошибаться и буду рад любым вашим замечаниям.

Начнем.

Хотелось бы начать со схемы и пояснений общей идеи.

Qt — окно и виджеты - 1

В данном случае центральным классом будет 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js