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:
- Прибрали один слот, завдяки функції лямбда;
- отримали можливість відстежувати помилки вже на етапі компіляції, а не в рантаймі, чим грішать макроси сигналів і слотів;
- Привели код до стандарту 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. А також використання лямбд дозволяє значно скоротити та спростити програмний код.
Добрый день.
Ругается такНа
Не можете подсказать почему?
currentIndexChanged является сигналом с перегрузкой. Поэтому его нужно подключать через static_cast с указанием сигнатуры сигнала.
Вот такая петрушка получается:
Спасибо! Знал же, что сигнал с перегрузкой. Думал : "А как компилятор поймет какую именно вызывать?",- но не знал такой прием как указать сигнатуру через static_cast :)
Этот приём в официальной документации на QComboBox указан ;-)
Лямбда удобная штука. Только вчера научился, мне нравится :)
Можешь сказать, когда лучше использовать слот и когда лучше лямбду.
По-моему, связывание сигнала с лямбдой нарушает принцип сигнал слотов.
А писать слот - это значит, что слот может быть вызван из вне. А это не всегда нужно. Я искал про private slots, думал, что private slot может быть связан только с сигналом своего класса, но я ошибался.
По сути лямбда ограничивает область видимости , но нарушает принцип qt сигнал-слот. Это так?
Был бы рад услышать твое мнение по этой теме :)
Я бы не сказал, что лямбда нарушает принципы сигналов и слотов. Это таже самая функция, просто анонимная.
На мой взгляд, принцип сигналов и слотов - это связать некоторые части кода, чтобы при определённом сигнале выполнился необходимый код. А всё остальное это уже домыслы, поскольку лямда подчиняется всем остальным особенностям работы сигналов и слотов, как последовательность подключений и т.д.
Плюс лямбды в том, что она может захватывать необходимые объекты, которые являются локальными в определённом методе. Если же использовать полноценный слот, то будет происходит разрастание кода, поскольку все эти локальные объекты нужно будет объявлять в заголовочном файле класса, а это по сути не нужно. Например, как в этом примере с динамическими кнопками.
В примере нужно получить сигнал от кнопки, и что-то сделать с той же самой кнопкой. Если использовать слот, то придётся держать какой-то вектор объектов в заголовочном файле, либо как сделано здесь, использовать QSignalMapper, а это опять же разрастание кода и повышение избыточности кода. А так лямбда захватывает кнопку и спокойно выполняет необходимый код. Когда кнопка будет уничтожена, а память освобождена, то лямбда автоматически будет отключена от сигнала этой кнопки и так же будет уничтожена. Так что проблем здесь никаких не возникнет.
Конечно, нужен несколько больший профессиональный уровень, чтобы понимать лямбды и работать с ними. Но профит здесь очевиден. Кода меньше. Работа с кодом становится гибче. Не происходит разрастания заголовочного файла методами, которые используются в одном единственном месте и больше нигде.
Плюс некоторые возможности шаблонизации, которые присущи лямбдам с аргументами auto. Плюсов слишком много, чтобы игнорировать использование лямбд в сигналах и слотах.
Как вам такое
Может и кривовато но чёрт побери работает и класс от ненужной больше ни где фигни не разбухает.
Вы используете стандартную практику замыканий, когда нет никакой необходимости объявлять функции в классе, поскольку они используются в одном единственном месте класса, а объявление всех эти лямбд вело бы, как вы подметили, к разбуханию класса.
Мы у себя на проекте такими же приёмами пользуемся, чтобы не загромождать код подобным мусором.