Evgenii Legotckoi
Evgenii Legotckoi28 серпня 2016 р. 11:17

Qt/C++ - Урок 055. QSignalMapper проти лямбда-функцій

QSignalMapper є чудовим класом, щоб організувати роботу сигналів та слотів для динамічно створюваних об'єктів. Наприклад, динамічно створюваних кнопок або об'єктів у QStackedWidget. Особливо це було актуально у застарілих версіях програмного забезпечення, тобто базованого на Qt 4.8 , де система сигналів та слотів будувалася на застосуванні макросів. Але в поточних реаліях новий синтаксис на покажчиках значно зручніший, а також підтримує лямбда функції, що може дозволити взагалі позбутися застосування QSignalMapper, який буде виглядати як монструозний атавізм у нових проектах, які використовують останні версії фреймворку Qt та стандартів мови C++ .

А якщо врахувати ще й перевантаження map() і mapped() , це робить код з QSignalMapper ще страшнішим, якщо використовувати коннект сигналів і слотів з використанням покажчиків, оскільки необхідно кастувати як сигнали, так і слоти, але про це трохи згодом.

Тому давайте розглянемо невеликий проект, який ґрунтуватиметься на прикладі з офіційної документації Qt. Зокрема, приклад буде наступний. У нас є QLabel, QPushButton та Vertical Layout . Після натискання кнопки Vertical Layout будуть додаватися інші динамічні кнопочки, по натисканню на які в QLabel відображатиметься текст з номером кнопки в наступному вигляді: "Button 2". На наступному рисунку показаний приклад даного додатка, зовнішній вигляд якого не буде відрізнятися, тоді як реалізацій програмного коду буде кілька.


Варіант 1 - QSignalMapper та синтаксис на макросах

Для початку розберемо варіант, який пропонується як приклад офіційної документації. Коли сигнали та слоти підключаються за допомогою макросів, тобто варіант, сумісний з Qt 4.8.

Зовнішній вигляд програми створювався в графічному дизайнері, тому не дивуйтесь використанню ui об'єкта.

mainwindow.h

Отже, щоб програма запрацювала, нам знадобиться QSignalMapper та слот, у якому оброблятиметься натискання динамічної кнопки. Нюанс у тому, що в QLabel буде встановлюватися текст із цієї самої кнопки, тому скористаємося сигналом QSignalMapper::mapped(const QString &) , який прийматиметься слотом MainWindow::clicked(const QString & ), у цьому слоті віджет буде перетворено на об'єкт QPushButton, і ми заберемо з нього текст. Текст буде попередньо встановлюватися під час створення цієї кнопки. Для нумерації використовуватиметься лічильник створених кнопок (змінна int counter).

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSignalMapper>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();           // Слот, в котором будут создаваться кнопки
    void clicked(const QString &str);       // Слот, в котором будут обрабатываться клики кнопок

private:
    Ui::MainWindow *ui;
    int counter;
    QSignalMapper *mapper;                  // маппер, который будет обрабатываться сигналы от кнопок
};

#endif // MAINWINDOW_H

mainwindow.cpp

І розглянемо, як це все виглядає в коді.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    counter(0)
{
    ui->setupUi(this);

    mapper = new QSignalMapper(this);   // Инициализируем маппер
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    QPushButton *button = new QPushButton(this);            // Создаём кнопку
    button->setText("Button " + QString::number(counter));  // Устанавливаем в неё текст
    counter++;                                              // Инкрементируем счётчик
    ui->verticalLayout->addWidget(button);                  // Помещаем кнопку в vertical layout

    // подключаем сигнал клика кнопки к мапперу
    connect(button, SIGNAL(clicked(bool)), mapper, SLOT(map()));
    mapper->setMapping(button, button->text()); // по клику кнопки будем передавать текст из этой кнопки
    // передаём текст с кнопки из маппера в слот, где будет установлен текст
    connect(mapper, SIGNAL(mapped(QString)), this, SLOT(clicked(QString)));
}

void MainWindow::clicked(const QString &str)
{
    // конечно, у QLabel метод setText уже является слотом и
    // можно было бы сразу его подключить к сигналу,
    // но для обзора всех сложностей будет оптимальнее показать это отдельным слотом
    ui->label->setText(str);
}

Варіант 2 - QSignalMapper та новий синтаксис

У першому варіанті код сумісний з кодом на Qt 4.8 і в цілому досить читальний, але що якщо ми не збираємося підтримувати проект на версії 4.8? Тоді перше, що потрібно зробити, це переписати цей код за допомогою синтаксису на покажчиках і зробити невеликі вкраплення лямбда функцій. І тоді ви побачите, що я мав на увазі, говорячи, що QSignalMapper буде виглядати як монструозний атавізм.

mainwindow.h

Тепер у заголовному файлі вже немає слот для обробки натискання динамічної кнопки, оскільки вже тут будуть використовуватися лямбди.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSignalMapper>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();           // Слот, в котором будут создаваться кнопки

private:
    Ui::MainWindow *ui;
    int counter;
    QSignalMapper *mapper;                  // маппер, который будет обрабатываться сигналы от кнопок
};

#endif // MAINWINDOW_H

mainwindow.cpp

Подивимося, як тепер виглядає програмний код. Здавалося б, отримуємо однозначний WIN:

  1. Прибрали один слот, завдяки функції лямбда;
  2. отримали можливість відстежувати помилки вже на етапі компіляції, а не в рантаймі, чим грішать макроси сигналів і слотів;
  3. Привели код до стандарту Qt5.

Але при цьому код виглядає досить страшно, оскільки використовується static_cast для сигналів та слотів QSignalMapper. Це пов'язано з тим, що як слот map(), так і сигнал mapped() є перевантаженими і компілятору потрібно вказувати їхню сигнатуру. І нижче такі конструкції лаконічності та краси коду не надають. Але й цю ситуацію можна виправити – розглянемо третій варіант.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    counter(0)
{
    ui->setupUi(this);

    mapper = new QSignalMapper(this);   // Инициализируем маппер
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    QPushButton *button = new QPushButton(this);            // Создаём кнопку
    button->setText("Button " + QString::number(counter));  // Устанавливаем в неё текст
    counter++;                                              // Инкрементируем счётчик
    ui->verticalLayout->addWidget(button);                  // Помещаем кнопку в vertical layout

    // подключаем сигнал клика кнопки к мапперу
    connect(button, &QPushButton::clicked,
            mapper, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
    mapper->setMapping(button, button->text()); // по клику кнопки будем передавать текст из этой кнопки
    // передаём текст с кнопки из маппера в слот, где будет установлен текст
    connect(mapper, static_cast<void(QSignalMapper::*)(const QString &)>(&QSignalMapper::mapped),
            [=](const QString str){
        ui->label->setText(str);
    });
}

Варіант 3 - позбавляємося QSignalMapper

А тепер позбавимося QSignalMapper. Адже якщо використовувати всі можливості лямбда функцій, то для реалізації таких завдань, як у даному прикладі, QSignalMapper не потрібен.

mainwindow.h

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

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();           // Слот, в котором будут создаваться кнопки

private:
    Ui::MainWindow *ui;
    int counter;
};

#endif // MAINWINDOW_H

mainwindow.cpp

І як можете бачити, використання лямбда функції значно скорочує код, а також позбавляє в даній ситуації використання класу QSignalMapper зовсім. Цілком можливо, що цей клас завдяки розвитку мови C++ помре зовсім.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    counter(0)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    QPushButton *button = new QPushButton(this);            // Создаём кнопку
    button->setText("Button " + QString::number(counter));  // Устанавливаем в неё текст
    counter++;                                              // Инкрементируем счётчик
    ui->verticalLayout->addWidget(button);                  // Помещаем кнопку в vertical layout

    // В лямбде захватываем из внешней области переменных указатель на MainWindow,
    // что позволит использовать переменные ui, а также саму кнопку button
    connect(button, &QPushButton::clicked, [this, button](){
        ui->label->setText(button->text());
    });
}

Резюмуємо ...

Застосування нового синтаксису сигналів і слотів дозволяє підключити потужні можливості мови C++ і в застосуванні до Qt зовсім позбутися використання деяких класів, користь яких була безсумнівною в застарілих версіях Qt. А також використання лямбд дозволяє значно скоротити та спростити програмний код.

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

Вам це подобається? Поділіться в соціальних мережах!

АК
  • 28 серпня 2017 р. 08:18

Добрый день.
На

connect(ui->comboBoxProfile, &QComboBox::currentIndexChanged, [=](){ this->close();});
Ругается так
C:\Projects\Qt\BMR-1401LM-U\dialogfeatures.cpp:31: ошибка: no matching function for call to 'DialogFeatures::connect(QComboBox*&, <unresolved overloaded function type>, DialogFeatures::DialogFeatures(int, int, int, int, int, int, QWidget*)::<lambda()>)'
connect(ui->comboBoxProfile, &QComboBox::currentIndexChanged, [=](){ this->close();});
^

Не можете подсказать почему?

currentIndexChanged является сигналом с перегрузкой. Поэтому его нужно подключать через static_cast с указанием сигнатуры сигнала.

Вот такая петрушка получается:

connect(comboBox, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentIndexChanged), 
[=](const QString &text){ }); 

Спасибо! Знал же, что сигнал с перегрузкой. Думал : "А как компилятор поймет какую именно вызывать?",- но не знал такой прием как указать сигнатуру через static_cast :)

Этот приём в официальной документации на QComboBox указан ;-)

Лямбда удобная штука. Только вчера научился, мне нравится :)

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

А писать слот - это значит, что слот может быть вызван из вне. А это не всегда нужно. Я искал про private slots, думал, что private slot может быть связан только с сигналом своего класса, но я ошибался.

По сути лямбда ограничивает область видимости , но нарушает принцип qt сигнал-слот. Это так?

Был бы рад услышать твое мнение по этой теме :)

Я бы не сказал, что лямбда нарушает принципы сигналов и слотов. Это таже самая функция, просто анонимная.
На мой взгляд, принцип сигналов и слотов - это связать некоторые части кода, чтобы при определённом сигнале выполнился необходимый код. А всё остальное это уже домыслы, поскольку лямда подчиняется всем остальным особенностям работы сигналов и слотов, как последовательность подключений и т.д.

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

В примере нужно получить сигнал от кнопки, и что-то сделать с той же самой кнопкой. Если использовать слот, то придётся держать какой-то вектор объектов в заголовочном файле, либо как сделано здесь, использовать QSignalMapper, а это опять же разрастание кода и повышение избыточности кода. А так лямбда захватывает кнопку и спокойно выполняет необходимый код. Когда кнопка будет уничтожена, а память освобождена, то лямбда автоматически будет отключена от сигнала этой кнопки и так же будет уничтожена. Так что проблем здесь никаких не возникнет.

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

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

D
  • 21 вересня 2017 р. 16:29

Как вам такое


 enum {
        PROFILE_TOOLPATH_FORM,
        POCKET_TOOLPATH_FORM,
        DRILLING_TOOLPATH_FORM
    };
    QToolBar* toolpathToolBar = addToolBar(tr("Toolpath"));
    toolpathToolBar->setIconSize(QSize(24, 24));
    toolpathToolBar->setObjectName(QStringLiteral("toolpathToolBar"));

    static QVector<QAction*> ToolpathActionList;
    auto createDockWidget = [=](QWidget* dwContents, int type) {
        QDockWidget* dwCreatePath = nullptr;
        foreach (QDockWidget* dw, findChildren<QDockWidget*>()) {
            if (dw->objectName() == "dwCreatePath") {
                dwCreatePath = dw;
                break;
            }
        }
        foreach (QAction* action, ToolpathActionList) {
            action->setChecked(false);
        }
        ToolpathActionList[type]->setChecked(true);
        if (dwCreatePath == nullptr) {
            dwCreatePath = new QDockWidget(this);
            dwCreatePath->setObjectName(QStringLiteral("dwCreatePath"));
            dwCreatePath->setFloating(false);
            dwCreatePath->setWindowTitle(tr("Create Toolpath"));
            dwCreatePath->connect(dwCreatePath, &QDockWidget::visibilityChanged,
                [=](bool fl) {
                    if (!fl) {
                        dwCreatePath->deleteLater();
                        foreach (QAction* action, ToolpathActionList) {
                            action->setChecked(false);
                        };
                    }
                });
            addDockWidget(Qt::RightDockWidgetArea, dwCreatePath);
        }
        else {
            dwCreatePath->widget()->deleteLater();
        }
        dwContents->setObjectName(QStringLiteral("dwContents"));
        dwCreatePath->setWidget(dwContents);
        dwCreatePath->show();
    };

    action = toolpathToolBar->addAction(QIcon::fromTheme("object-to-path"), tr("Profile"), [=]() { createDockWidget(new ProfileToolpathForm(), PROFILE_TOOLPATH_FORM); });
    ToolpathActionList.append(action);
    //    action->setShortcut(QKeySequence::FullScreen);
    action = toolpathToolBar->addAction(QIcon::fromTheme("stroke-to-path"), tr("Pocket"), [=]() { createDockWidget(new PocketToolpathForm(), POCKET_TOOLPATH_FORM); });
    ToolpathActionList.append(action);
    //    action->setShortcuts(tr("Ctrl+0"));
    action = toolpathToolBar->addAction(QIcon::fromTheme("roll"), tr("Drilling"), [=]() { createDockWidget(new DrillingToolpathForm(), DRILLING_TOOLPATH_FORM); });
    ToolpathActionList.append(action);
    action = toolpathToolBar->addAction(QIcon::fromTheme("view-form"), tr("Tool Base"), [=]() { ToolDatabase tdb(this); tdb.exec(); });
    //    action->setShortcut(QKeySequence::ZoomIn);
    foreach (QAction* action, ToolpathActionList) {
        action->setCheckable(true);
    }
Или

QList<QString> GerberParser::Format(QString data)
{
    QList<QString> gerberLines;

    enum SATE {
        PARAM,
        MACRO,
        DATA,
    };

    SATE state = DATA;
    QString lastLine;

    auto gerberLinesAppend = [&gerberLines, &lastLine](SATE& state, const QString& val) -> void {
        switch (state) {
        case MACRO:
            lastLine.push_back(val);
            if (lastLine.endsWith('%')) {
                gerberLines << lastLine;
                state = DATA;
            }
            break;
        case PARAM:
            lastLine.push_back(val);
            if (lastLine.endsWith('%')) {
                foreach (QString tmpline, lastLine.remove('%').split('*')) {
                    if (!tmpline.isEmpty()) {
                        gerberLines << ('%' + tmpline + "*%");
                    }
                }
                state = DATA;
            }
            break;
        case DATA:
            break;
        }
    };

    auto lastLineClose = [&gerberLines](SATE state, QString& val) -> void {
        switch (state) {
        case MACRO:
            if (!val.endsWith('%'))
                val.push_back('%');
            if (!val.endsWith("*%"))
                val.insert(val.length() - 2, '*');
            gerberLines << val;
            break;
        case PARAM:
            foreach (QString tmpline, val.remove('%').split('*')) {
                if (!tmpline.isEmpty()) {
                    gerberLines << ('%' + tmpline + "*%");
                }
            }
            break;
        case DATA:
            break;
        }
        val.clear();
    };

    auto dataClose = [&gerberLines](const QString& val) -> void {
        if (val.count('*') > 1) {
            foreach (QString tmpline, val.split('*')) {
                if (!tmpline.isEmpty()) {
                    gerberLines << (tmpline + '*');
                }
            }
        }
        else {
            gerberLines << val;
        }
    };

    foreach (QString line, data.replace('\r', '\n').replace("\n\n", "\n").replace('\t', ' ').split('\n')) {
        line = line.trimmed();
        if (line.isEmpty()) {
            continue;
        }
        if (line.startsWith('%') && line.endsWith('%') && line.size() > 1) {
            lastLineClose(state, lastLine);
            if (line.startsWith("%AM")) {
                lastLineClose(MACRO, line);
            }
            else {
                lastLineClose(PARAM, line);
            }
            state = DATA;
            continue;
        }
        else if (line.startsWith("%AM")) {
            lastLineClose(state, lastLine);
            state = MACRO;
            lastLine = line;
            continue;
        }
        else if (line.startsWith('%')) {
            lastLineClose(state, lastLine);
            state = PARAM;
            lastLine = line;
            continue;
        }
        else if (line.endsWith('*') && line.length() > 1) {
            switch (state) {
            case MACRO:
            case PARAM:
                gerberLinesAppend(state, line);
                continue;
            case DATA:
                dataClose(line);
                continue;
            }
        }
        else {
            switch (state) {
            case MACRO:
            case PARAM:
                gerberLinesAppend(state, line);
                continue;
            case DATA:
                qDebug() << "Хрен его знает:" << line;
                continue;
            }
        }
    }
    return gerberLines;
}
D
  • 21 вересня 2017 р. 16:35

Может и кривовато но чёрт побери работает и класс от ненужной больше ни где фигни не разбухает.

Evgenii Legotckoi
  • 22 вересня 2017 р. 02:45

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


Есть кое-какие замечания и мысли
  • Раз уж вы используете минимум стандарт C++11 , то перепишите foreach , на range-based for цикл. foreach - это Qt-шный макрос, который является deprecated , поскольку в стандарте C++11 наконец-то ввели нормальные for циклы для контейнеров и foreach теперь не нужен.
  • Не то, чтобы замечание, но как-то непривычно наблюдать enum в теле метода, разве где-то в другом месте вы не используете подобный enum ? Возможно стоит проанализировать и немного перепроектировать класс?
  • В первом примере возможно, можно обойтись без статического пула Action`ов, если они больше нигде не используются. Вообще по возможности лучше работать без подобных статических пулов объектов настолько, насколько это возможно. Но естественно всё зависит от конкретного User Case и условий разработки.
  • Также название методов и переменных с большой буквы... Полагаю причина в том, что изначально разработка проекта велась под Visual Studio и сложился подобный код стайл в команде, но Qt использует несколько иные правила кодстайла , который немного удобнее, поскольку не приходится гадать порой, что это такое? - переменная, объявление класса, метод или конструктор в коде. Особенно, если косяк в подсветке синтаксиса кода имеется в самой IDE. Плюс при использовании Qt получаем разнобой в кодстайле от Qt и вашего проекта. Поразмышляйте в команде на досуге об этом.

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
AD

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Слідкуйте за нами в соціальних мережах