Wiki

Замещение нескольких сигналов одним

Автор: Jasmin Blanchette Перевод: Andi Peredri Qt позволяет связывать несколько сигналов с одним определенным сигналом или слотом. Это может оказаться полезным при реализации нескольких способов вызова пользователем одной и той же операции. Однако, иногда исполняемая слотом операция может незначительно отличаться в зависимости от виджета, инициировавшего сигнал. В этой статье мы рассмотрим различные решения этой проблемы, включая решение с использованием класса QSignalMapper. > Неофициальный перевод статьи Mapping Many Signals to One выполнен с любезного разрешения Trolltech. В качестве примера мы рассмотрим реализацию виджета Keypad с десятью кнопками QPushButton, пронумерованными от 0 до 9. При нажатии на одну из кнопок виджет должен генерировать сигнал вида digitClicked(int). Мы рассмотрим четыре возможных решения и обсудим преимущества и недостатки каждого.
Keypad

Простейшее решение

Самое простейшее и прямолинейное решение нашей проблемы заключается в связывании десяти сигналов clicked() объектов QPushButton с десятью различными слотами. Каждый из слотов от button0Clicked() до button9Clicked() посылает сигнал digitClicked(int) с соответствующим значением целого параметра (от 0 до 9). Вот определение класса Keypad:
class Keypad : public QWidget
{
    Q_OBJECT
public:
    Keypad(QWidget *parent = 0);
 
signals:
    void digitClicked(int digit);
 
private slots:
    void button0Clicked();
    void button1Clicked();
    ...
    void button9Clicked();
 
private:
    void createLayout();
 
    QPushButton *buttons[10];
};
Ниже представлен конструктор класса Keypad:
Keypad::Keypad(QWidget *parent) : QWidget(parent)
{
    for (int i = 0; i < 10; ++i) {
        QString text = QString::number(i);
        buttons[i] = new QPushButton(text, this);
    }
 
    connect(buttons[0], SIGNAL(clicked()), this, SLOT(button0Clicked()));
    ...
    connect(buttons[9], SIGNAL(clicked()), this, SLOT(button9Clicked()));
 
    createLayout();
}
В конструкторе мы создаем одну за другой кнопки QPushButton и для каждой из них связываем сигнал clicked() с соответствующим закрытым слотом.
void Keypad::button0Clicked()
{
    emit digitClicked(0);
}
 
...
 
void Keypad::button9Clicked()
{
    emit digitClicked(9);
}
Каждый слот генерирует сигнал digitClicked(int) с жестко заданным аргументом. Разумеется, это решение является негибким и подвержено ошибкам. Оно применимо при небольшом числе связей, но даже для 10-кнопочного виджета Keypad операции вставки могут показаться утомительными. Давайте рассмотрим более приемлемое решение.

Решение с помощью sender()

На следующем этапе мы заменим слоты вида buttonNClicked() одним закрытым слотом, генерирующим сигнал digitClicked(int) с параметром, соответствующим нажатой кнопке. Как вскоре мы увидим, это станет возможным благодаря функции QObject::sender(). Вот новый конструктор Keypad:
Keypad::Keypad(QWidget *parent) : QWidget(parent)
{
    for (int i = 0; i < 10; ++i) {
        QString text = QString::number(i);
        buttons[i] = new QPushButton(text, this);
        connect(buttons[i], SIGNAL(clicked()), this, SLOT(buttonClicked()));
    }
    createLayout();
}
А это код слота buttonClicked():
void Keypad::buttonClicked()
{
    QPushButton *button = (QPushButton *)sender();
    emit digitClicked(button->text()[0].digitValue());
}
Функция sender() возвращает указатель на объект QObject, который сгенерировал сигнал и привел к вызову данного слота. В данном случае мы знаем, что источником сигнала была одна из кнопок QPushButton, поэтому мы приводим тип значения, возвращаемого функцией sender() к типу QPushButton*. Затем мы генерируем сигнал digitClicked(int) с значением, указанным на кнопке. Недостатком этого решения является необходимость в реализации закрытого слота для выполнения задачи демультиплексирования. Код слота buttonClicked() не отличается изяществом; если вы вдруг замените кнопки QPushButton другими виджетами и забудете изменить тип приведения, это повлечет за собой крах программы. Аналогично, если вы измените надписи на кнопках (например, "NIL" вместо "0"), сигнал digitClicked(int) будет сгенерирован с некорректным параметром. И наконец, использование функции sender() приводит к тесному связыванию компонентов, что по мнению многих разработчиков является плохим стилем программирования. В данном случае это не так критично, потому что объект Keypad уже знает о кнопках. Но если бы слот buttonClicked() был определен в другом классе, использование функции sender() привело бы к нежелательной зависимости логики этого класса от деталей реализации класса Keypad.

Решение с помощью наследования

Наше третье решение не требует наличия закрытого слота в Keypad; вместо этого мы заставим кнопки самостоятельно генерировать сигнал clicked(int), который может быть напрямую связан с сигналом digitClicked(int) виджета Keypad. (При связывании двух сигналов генерация целевого сигнала будет происходить при возникновении первого.) Для реализации нам потребуется наследование от QPushButton:
class KeypadButton : public QPushButton
{
    Q_OBJECT
public:
    KeypadButton(int digit, QWidget *parent);
 
signals:
    void clicked(int digit);
 
private slots:
    void reemitClicked();
 
private:
    int myDigit;
};
 
KeypadButton::KeypadButton(int digit, QWidget *parent)
    : QPushButton(parent)
{
    myDigit = digit;
    setText(QString::number(myDigit));
    connect(this, SIGNAL(clicked()), this, SLOT(reemitClicked()));
}
 
void KeypadButton::reemitClicked()
{
    emit clicked(myDigit);
}
В классе KeypadButton мы перехватываем сигнал clicked() класса QPushButton и вместо него генерируем сигнал clicked(int) с корректным числовым параметром. Конструктор Keypad преобразуется к следующему виду:
Keypad::Keypad(QWidget *parent) : QWidget(parent)
{
    for (int i = 0; i < 10; ++i) {
        buttons[i] = new KeypadButton(i, this);
        connect(buttons[i], SIGNAL(clicked(int)), this, SIGNAL(digitClicked(int)));
    }
    createLayout();
}
Такое решение является прозрачным и гибким, однако оно немного громоздко, потому что принуждает нас наследовать от класса QPushButton.

Решение с помощью QSignalMapper

Четвертое и последнее решение не требует каких-либо закрытых слотов и наследования от класса QPushButton. Вместо этого вся логика, имеющая отношение к сигналам, реализована в конструкторе класса Keypad:
Keypad::Keypad(QWidget *parent) : QWidget(parent)
{
    QSignalMapper *signalMapper = new QSignalMapper(this);
    connect(signalMapper, SIGNAL(mapped(int)), this, SIGNAL(digitClicked(int)));
 
    for (int i = 0; i < 10; ++i) {
        QString text = QString::number(i);
        buttons[i] = new QPushButton(text, this);
        signalMapper->setMapping(buttons[i], i);
        connect(buttons[i], SIGNAL(clicked()), signalMapper, SLOT(map()));
    }
 
    createLayout();
}
Во-первых, мы создаем объект QSignalMapper. Класс QSignalMapper является потомком QObject и обеспечивает связь непараметрических сигналов с однопараметрическими слотами и сигналами. Вызов setMapping() в цикле связывает каждую кнопку с целым числом; например, кнопке buttons[3] ставится в соответствие число 3.
QSignalMapper
Генерация кнопкой сигнала clicked() приведет к вызову слота QSignalMapper::map() (это становится возможным благодаря вызову connect() в цикле). Если для объекта, сгенерировавшего сигнал, будет найдено соответствие, сгенерируется сигнал mapped(int) с параметром, ранее переданным с помощью setMapping(). В свою очередь, этот сигнал связывается с сигналом digitClicked(int) класса Keypad. Есть две версии функции setMapping(): первая версия в качестве второго аргумента принимает целое число, а вторая - строку QString. Благодаря этому становится возможным поставить в соответствие объекту-источнику сигнала вместо целого числа произвольную строку. В этом случае QSignalMapper будет генерировать сигнал mapped(const QString &).
Palette
QSignalMapper не поддерживает напрямую какие-либо другие типы данных. Поэтому, если, например, вам нужно реализовать палитру выбора цвета из набора стандартных цветов с генерацией сигнала colorSelected(const QColor &), используйте одно из рассмотренных ранее решений на основе sender() или на основе наследования. Если класс виджета представления цвета вы, скажем, уже унаследовали от QToolButton, то вам не составит особого труда добавить в него сигнал clicked(const QColor &).

Компоновка виджетов

Несмотря на то, что этот материал не о компоновке виджетов, вам не удастся скомпилировать представленный выше код без вызываемой из конструктора функции createLayout(). Вот ее код:
void Keypad::createLayout()
{
    QGridLayout *layout = new QGridLayout(this, 3, 4);
    layout->setMargin(6);
    layout->setSpacing(6);
 
    for (int i = 0; i < 9; ++i)
        layout->addWidget(buttons[i + 1], i / 3, i % 3);
    layout->addWidget(buttons[0], 3, 1);
}
Код функции main() также отсутствует; мы оставляем его написание в качестве упражнения читателям.