欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Qt中connect函数不能传递参数的两种解决方法

程序员文章站 2022-04-29 21:46:18
...

Qt中的connect函数可以让我们动态地管理信号和槽。

比如现在界面上有一个标签,id为label。我现在想要动态地创建一个按键,id为push,然后利用connect函数,实现点击push以后,label上显示“Hello world!”,代码如下:

<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:
Ui::MainWindow *ui;

private slots:
void showLabel();
};

#endif // MAINWINDOW_H

<mainwindow.cpp>

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>

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

    //新建一个按钮,id为push
    QPushButton * push = new QPushButton(this);
    //设置按钮的(x,y)坐标、长、宽
    push->setGeometry(150, 170, 89, 24);
    //设置按键上显示的文字
    push->setText("button");
    //将信号和槽连接
    connect(push, SIGNAL(clicked()), this, SLOT(showLabel()));
}

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

void MainWindow::showLabel()
{
    ui->label->setText("Hello world!");
}

这就实现了动态创建一个控件并连接信号和槽。

题外:这里的信号(SIGNAL)可以通过如下方式找到,即右键ui编辑界面中的一个控件,然后点击转到槽:

Qt中connect函数不能传递参数的两种解决方法

但是有时候我们可能需要动态创建多个按键,数量我们也并不清楚。我们希望将其存放在一个数组中,每个按键的功能相似,但是也有略微区别,这个区别和它在数组中的下标有关。举个简单的例子,现在我们有一个私有变量,存放了一个QString的数组,一共5项。我们希望动态地创建一个大小为5的QPushButton数组,实现的功能是点击第i个按键就让label显示第i个QString。代码如下:

<mainwindow.h>

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

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

private:
Ui::MainWindow *ui;
QString list[5] = {"item1", "item2", "item3", "item4", "item5"};

private slots:
void showLabel(int i);    //注意这里也改了
};

#endif // MAINWINDOW_H

<mainwindow.cpp>

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>

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

    //新建一个按钮数组,id为push[i]
    QPushButton * push[5];
    for (int i = 0; i < 5; i++)
    {
        push[i] = new QPushButton(this);
        push[i]->setGeometry(300, 60 + 30 * i, 89, 24);
        push[i]->setText(QString("button%1").arg(i));
        connect(push[i], SIGNAL(clicked()), this, SLOT(showLabel(i)));
    }

}

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

void MainWindow::showLabel(int i)
{
    ui->label->setText(QString("button%1 is clicked").arg(i));
}

这是一个错误的方式,报错信息如下:

QObject::connect: No such slot MainWindow::showLabel(i) in ……

QObject::connect: (receiver name: 'MainWindow')

这是connect的机制导致的。网上有很多解释这个的教程,简单来说就是SLOT()中的槽函数如果需要参数,那么这个参数必须来自于SIGNAL()中信号函数的参数。也就是这样:

//这是一个错误的语句!
connect(sender, SIGNAL(signal()), context, SLOT(slot(int i)));

//这才是正确的!
connect(sender, SIGNAL(signal(int i, char c, ...)), context, SLOT(slot(int i)));

这就意味着如果信号函数不能传递我们需要的参数,我们就无法分别给每个按钮分配不同的任务。

这篇文章就是为了解决这种问题的。这里提供两种思路。(其实是2.5种)


QSignalMapper

我们可以将QSignalMapper理解为一个消息转发器,其中存储的是一系列的键值对,这里先来看代码。

<mainwindow.h>

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>
#include <QSignalMapper>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

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

private:
Ui::MainWindow *ui;
QString list[5] = {"item1", "item2", "item3", "item4", "item5"};
QSignalMapper * myMapper;

private slots:
void showLabel(int i);
};

#endif // MAINWINDOW_H

<mainwindow.cpp>

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
#include <QSignalMapper>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    myMapper = new QSignalMapper();

    //新建一个按钮数组,id为push[i]
    QPushButton * push[5];
    for (int i = 0; i < 5; i++)
    {
        push[i] = new QPushButton(this);
        push[i]->setGeometry(300, 60 + 30 * i, 89, 24);
        push[i]->setText(QString("button%1").arg(i));
        connect(push[i], SIGNAL(clicked()), myMapper, SLOT(map()));
        myMapper->setMapping(push[i], i);
    }
    connect(myMapper, SIGNAL(mapped(int)), this, SLOT(showLabel(int)));

}

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

void MainWindow::showLabel(int i)
{
    ui->label->setText(QString("button%1 is clicked").arg(i));
}

运行以后就可以发现,点击每个按键都会有不同的效果,如图所示:

Qt中connect函数不能传递参数的两种解决方法Qt中connect函数不能传递参数的两种解决方法

这里对几个重要的语句进行分开解释。

connect(push[i], SIGNAL(clicked()), myMapper, SLOT(map()));

这句话中的信号是按键的点击事件,槽则可以理解为查询QSignalMapper键值对。也就是每次点击都会触发对QSignalMapper的查询。

myMapper->setMapping(push[i], i);

QSignalMapper的内容就是由这句话来设置。它为其添加了一个映射项,键是按键的id,值是一个int类型的值。这里可以根据需要修改数据类型。这句话执行完以后就建立了一个键值对,将每个按钮喝它们各自的下标关联了起来。

connect(myMapper, SIGNAL(mapped(int)), this, SLOT(showLabel(int)));

槽函数map()查询QSignalMapper成功后会返回一个信号mapped(...),这里的参数是一个int,这个整型变量就是之前映射项中的值。这样就能构造出一个带参数的信号,就可以通过connect传递了。

整个过程大概就是:每建立一个按键,就执行一个connect,让它们的点击信号能触发一个查询QSignalMapper的槽。而QSignalMapper中的内容为按键和整型变量的键值对。根据点击的按键可以查询到唯一一个映射项,并发射一个信号,其参数为按键对应的值。这个信号就可以触发自己定义的槽函数,实现参数的传递。

也就是说,这里所做的全部工作就是让connect的信号函数拥有我们需要的参数,那么如果它本身自带参数不就完美了?这里我给大家推荐一个控件,名为Table Widget。这是一个表格,点击每一个格子都可以触发一个信号 cellEntered(int, int) ,参数分别是格子所在的行号和列号。发现了吗?这个信号函数自带参数,并且可以通过这个参数确定点击的位置。详细操作这里不再赘述,作为第0.5个方法。


Lambda

lambda表达式是c++11的新增特性,用于创建匿名函数。

先看代码:

<mainwindow.h>

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

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

private:
Ui::MainWindow *ui;
void showLabel(int i);
};

#endif // MAINWINDOW_H

<mainwindow.cpp>

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>

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

    //新建一个按钮数组,id为push[i]
    QPushButton * push[5];
    for (int i = 0; i < 5; i++)
    {
        push[i] = new QPushButton(this);
        push[i]->setGeometry(300, 60 + 30 * i, 89, 24);
        push[i]->setText(QString("button%1").arg(i));
        connect(push[i], &QPushButton::clicked, this, [ = ] {
            showLabel(i);
        });
    }

}

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

void MainWindow::showLabel(int i)
{
    ui->label->setText(QString("button%1 is clicked").arg(i));
}

这和之前的操作差别在于这句话:

connect(push[i], &QPushButton::clicked, this, [ = ] {
            showLabel(i);
        });

这里的槽就是一个Lambda匿名函数,完整形式如下:

[capture](parameters) mutable ->return-type{statement}

它可以分解为一下几个部分:

  1. [capture]:方括号,其内容是捕捉列表,总是出现在Lambda函数的开始处,是Lambda的引出符。它能够捕捉上下文中的变量供函数体使用。这里可以填入具体的变量名,也可以使用“=”,代表以值传递方式捕捉所有父作用域的变量,还可以使用“&”代表以引用传递方式捕捉所有父作用域的变量。
  2. (parameters):参数列表,表示传递给函数体的参数。如果不需要参数传递,可以省略这一部分。
  3. mutable:修饰符,默认情况下,Lambda函数是一个const函数,使用mutable可以取消其常量性。比如使用引用传递的时候就可以修改参数值等。使用它时参数列表不可省略,即使无参数传递。
  4. ->return-type:返回类型。不需要返回或返回类型明确时可以省略这一部分。
  5. {statement}:函数体,可以使用所有捕获与传递的变量。

也就是说我这里定义了一个Lambda匿名函数,捕获了所有父作用域的变量,在函数体内调用了showLabel(int i)函数。这里也可以将showLabel函数嵌入到Lambda函数内,如下所示:

connect(push[i], &QPushButton::clicked, this, [ = ] {
            ui->label->setText(QString("button%1 is clicked").arg(i));
        });

由于可以直接使用父作用域的变量,这里就不用担心signal没有参数传递了。

需要注意的是,这里的connect参数里的槽函数不能使用如下的SIGNAL()……SLOT() 形式:

connect(push[i], SIGNAL(clicked()), this, SLOT([i] {
            showLabel[i];
        }));

因为这是现场定义的函数,并不属于mainwindow的槽函数。这样写会有如下报错:

QObject::connect: No such slot MainWindow::[i] (){ showLabel[i]; } in ……

QObject::connect: (receiver name: 'MainWindow')


以上就是connect函数给槽函数传递参数的2.5种方法。