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