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. Плюсов слишком много, чтобы игнорировать использование лямбд в сигналах и слотах.
Как вам такое
Может и кривовато но чёрт побери работает и класс от ненужной больше ни где фигни не разбухает.
Вы используете стандартную практику замыканий, когда нет никакой необходимости объявлять функции в классе, поскольку они используются в одном единственном месте класса, а объявление всех эти лямбд вело бы, как вы подметили, к разбуханию класса.
Мы у себя на проекте такими же приёмами пользуемся, чтобы не загромождать код подобным мусором.