QT-自定义信号和槽对象树图形化开发计算器
1. 自定义信号和槽
-
核心逻辑: 需要有两个类,一个提供信号,另一个提供槽。 然后在窗口中将 信号和槽 链接起来。
-
示例目标: 创建一个 Teacher 类,提供信号。 创建一个 Student 类,提供槽。
实现步骤:
-
创建 Teacher 类,在 singals 中提供信号的定义
-
创建 Student 类,在 slots 中提供处理的槽函数
-
在 窗口(Widget)中,链接信号和槽,并触发 (使用 emit 来发射信号)
1.1 创建 Teacher 类
1)右键 项目名称 ----> Add New (添加新的)
2)选择 C++ ----> C++ Class
3)设置类名以及继承关系
1.2 Teacher中定义信号
自定义信号的特点:
-
自定义信号写在 signals 下
-
自定义信号是一个无返回值的函数声明,不需要实现
-
自定义信号允许重载
// teacher,h
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject
{
Q_OBJECT //元对象系统
public:
explicit Teacher(QObject *parent = nullptr);
//信号定义在signals节点下
//信号是一个无返回值的函数
//信号不需要实现
//信号函数允许重载
signals:
void mySignal();
void mySignal(QString str);
public slots:
void response();
void response(QString str);
};
#endif // TEACHER_H
//teacher.c
#include "teacher.h"
#include <QDebug>
Teacher::Teacher(QObject *parent) : QObject(parent)
{
}
void Teacher::response()
{
qDebug() << "我是老师";
}
void Teacher::response(QString str)
{
if(str == "学生")
{
qDebug() << "学生,你好";
}
else if (str == "鲨鱼") {
qDebug() << "鲨鱼来袭";
}
}
1.3 创建 Student 类
创建过程同 Teacher
1.4 Student 中定义槽函数
槽函数特点:
-
槽函数写在 slots 下, 高级版本中也可以写在 public 下
-
槽函数是一个无返回值的函数,可以重载
// student,h
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
signals:
void mySignal();
void mySignal(QString str);
//槽函数定义在 public/pretected/private slots 节点下
//槽函数是一个无返回值的函数
//在槽函数中编写实际的业务逻辑
//在槽函数中允许重载
public slots:
void response();
void response(QString str);
};
#endif // STUDENT_H
//student。c
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{
}
void Student::response()
{
qDebug() <<"收到...";
}
void Student::response(QString str)
{
//qDebug() << str;
if (str == "下课")
{
qDebug()<<"下楼";
}
else if(str == "放学")
{
qDebug()<<"回家";
}
}
1.5 窗口中链接信号和槽
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
//widget.c
#include "widget.h"
#include "teacher.h"
#include "student.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
auto *t = new Teacher(this);
auto *s = new Student(this);
//链接信号
//connect(t, &Teacher::mySignal, s, &Student::response);
//发送信号
//emit t->mySignal();
//如果信号和槽有多个版本的重载,需要使用SIGNAL()和SLOT()函数来指定信号和槽
connect(t,SIGNAL(mySignal(QString)), s, SLOT(response(QString)));
//emit t->mySignal("下课");
QPushButton *btn1 = new QPushButton("下课",this);
btn1->move(20,20);
connect(btn1, &QPushButton::clicked, [t](){
emit t->mySignal("下课");
});
QPushButton *btn2 = new QPushButton("放学",this);
btn2->move(120,20);
connect(btn2, &QPushButton::clicked, [t](){
emit t->mySignal("放学");
});
//信号到槽 -> 学生到老师
connect(s,SIGNAL(mySignal(QString)), t, SLOT(response(QString)));
QPushButton *btn3 = new QPushButton("学生",this);
btn3->move(20,80);
connect(btn3, &QPushButton::clicked, [s](){
emit s->mySignal("学生");
});
QPushButton *btn4 = new QPushButton("鲨鱼",this);
btn4->move(120,80);
connect(btn4, &QPushButton::clicked,[s](){
emit s->mySignal("鲨鱼");
});
}
Widget::~Widget()
{
}
核心: 使用 emit 触发信号
1.6 信号和槽的重载
信号和槽都允许重载
1)在 Teacher 中重载信号
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
// 重载信号, 信号始终不需要实现
void say();
void say(QString str);
};
2)在 Student 头文件中重载槽函数
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
public slots:
// 重载槽函数
void deal();
void deal(QString str);
};
3)在Student 源文件中实现重载
void Student::deal()
{
qDebug() << "去吃饭";
}
void Student::deal(QString str)
{
if (str == "下课")
{
qDebug() << "抽烟去咯";
}
else if (str == "休息")
{
qDebug() << "吃饭去咯";
}
}
4)链接信号与槽
注意点: 此时,单纯的使用 connect 就无法区分哪个信号 链接 哪个槽
解决方案: 使用 SIGNAL() 函数包装要发射的信号; 使用 SLOT() 函数包装要执行的槽函数
注意事项: 要使用 SIGNAL() 和 SLOT() , 必须将槽函数放在 public slots 节点下
// 链接无参的信号和槽
connect(t, SIGNAL(say()), s, SLOT(deal()));
// 链接有参的信号和槽
connect(t, SIGNAL(say(QString)), s, SLOT(deal(QString)));
发射哪个信号,由 emit 决定
emit t->say(); // 发射无参的信号
emit t->say("放学"); // 发射有参的信号, 实参会被槽函数的形参接收
2. 对象树
-
创建图形化应用程序时,我们只 new 了各种控件,但是没有释放过。当关闭应用时系统会自动释放
-
通过 new 得到的对象,又通过 setParent 方法添加到了父组件中。实际上是添加到了父组件的 children()列表中,最终形成一个父子结构的树,称之为 对象树
-
构建图形界面时,从父开始执行,父创建好了再创建子;当关闭图形界面时,析构函数会从子开始执行,再到父。系统会自动进行释放操作。
注:1.所有的部件会添加到父部件的children列表中 2.创建窗口时,被分越高的越先创建出来 3.销毁窗口时,辈分越低的越先被销毁.系统会自动找到 children 列表中的子,进行销毁。
示例: 自定义组件验证
1)创建 MyButton 组件,继承 QPushButton 组件
//mybutton.h
#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QObject>
#include <QWidget>
//引入 QPushButton
#include <QPushButton>
// 修改继承为 QPushButton
class MyButton : public QPushButton
{
Q_OBJECT
public:
explicit MyButton(QWidget *parent = nullptr);
// 添加析构函数
~MyButton();
signals:
public slots:
};
#endif // MYBUTTON_H
2)mybutton.cpp
#include "mybutton.h"
#include <QDebug>
MyButton::MyButton(QWidget *parent) : QPushButton (parent)
{
qDebug() << "MyButton的构造函数执行";
}
MyButton::~MyButton()
{
qDebug() << "MyButton的析构函数执行";
}
3)MyWidget.cpp 中调用自定义组件
#include "MyWidget.h"
#include <QDebug>
#include "mybutton.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
qDebug() << "MyWidget构造函数";
this->resize(500, 300);
auto myBtn = new MyButton;
myBtn->setText("我的按钮");
myBtn->move(0, 100);
myBtn->setParent(this);
}
Widget::~Widget()
{
qDebug() << "MyWidget析构函数";
}
4)测试结果
启动应用时: 先执行 MyWidget 构造函数,再执行 MyButton 构造函数
关闭应用时: 先执行 MyWidget 析构函数,注意此时并没有结束。 在执行 MyWidget 析构函数时,会发现还有子控件,子控件析构函数会先执行完毕后,再完成父控件析构函数。
3. 图形化开发
3.1 创建图形化项目
-
重点: 勾选 ui
-
优势: 能够使用拖拽的形式向窗口添加部件
-
注意点:
-
勾选 ui 创建出来的项目多了 Form / widget.ui。 该文件使用 xml 形式来描述窗口结构
-
点击 "设计" 按钮,能够切换到 图形化开发界面。
-
widget.cpp 源文件额外继承了 ui 类,调用 ui->setupUi(this) 方法将窗口设置到ui对象中
-
调用窗口中任意部件时需要使用: ui->部件对象名
-
3.2 图形化创建常用部件
3.3 代码编写
-
图形化只能进行布局,所有的功能还要依靠代码编写
-
首先需要给所有的部件起名,再通过 ui 对象找到 具体的部件进行编码
选中任意部件,可以在如图的两个地方进行修改对象名称
使用图形化设置, 图形化设置都匹配有对应的方法
为部件设置信号和槽
右键某个部件 ---> 跳转槽...
选择使用哪个槽函数
在 .cpp 源文件中自动成功槽函数
4. 计算器
4.1 ui 布局
4.2 数字 和 运算符
-
当用户点击数字按钮 和 运算符按钮时,能够将对应的数字保存到 num1 、 num2 ,将运算符保存到 opt 中
-
解决方案:
-
在类中设置三个私有属性, num1、num2、opt
-
在num0~num9按钮上设置信号,当用户点击时,就将当前的数字拼接到 num1 或者 num2 中。 根据 opt 中是否保存了运算符来区分,到底保存在 num1 还是 num2 中。
-
在加减乘除按钮上设置信号,点击时保存对应的运算符到 opt 中
-
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
void setNum(QString n);
void setOpt(QString o);
private:
Ui::Widget *ui;
//定义操作数1、操作数2、运算符
QString num1;
QString num2;
QString opt;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("计算器");
this->setFixedSize(363,660);
//mid(start, length):字符串截取
//参数1:截取的起始索引号
//参数2:截取的长度,可选。如果不给参数2,则会截取到数据末尾
//处理数字按钮
connect(ui->btn9, &QPushButton::clicked,[this](){this->setNum("9");});
connect(ui->btn8, &QPushButton::clicked,[this](){this->setNum("8");});
connect(ui->btn7, &QPushButton::clicked,[this](){this->setNum("7");});
connect(ui->btn6, &QPushButton::clicked,[this](){this->setNum("6");});
connect(ui->btn5, &QPushButton::clicked,[this](){this->setNum("5");});
connect(ui->btn4, &QPushButton::clicked,[this](){this->setNum("4");});
connect(ui->btn3, &QPushButton::clicked,[this](){this->setNum("3");});
connect(ui->btn2, &QPushButton::clicked,[this](){this->setNum("2");});
connect(ui->btn1, &QPushButton::clicked,[this](){this->setNum("1");});
connect(ui->btn0, &QPushButton::clicked,[this](){this->setNum("0");});
connect(ui->pointBtn, &QPushButton::clicked,[this](){this->setNum(".");});
//处理运算符按钮
connect(ui->addBtn, &QPushButton::clicked,[this](){this->setOpt("+");});
connect(ui->subBtn, &QPushButton::clicked,[this](){this->setOpt("-");});
connect(ui->mulBtn, &QPushButton::clicked,[this](){this->setOpt("*");});
connect(ui->divBtn, &QPushButton::clicked,[this](){this->setOpt("÷");});
connect(ui->perBtn, &QPushButton::clicked,[this](){this->setOpt("%");});
4.3 等号处理
-
点击 等号 开始运算
-
运算逻辑: 根据不同的运算符,进行不同的运算,使用 if 或者 switch 进行分支操作
-
str.toDouble() 将数字型字符串转为浮点型
-
QString::number(num) 将数字转为字符串
//等号
connect(ui->equarBtn, &QPushButton::clicked,[this](){
double result = 0;
if (this->opt == "+")
{
result = this->num1.toDouble() + this->num2.toDouble();
}
else if (this->opt == "-") {
result = this->num1.toDouble() - this->num2.toDouble();
}
else if (this->opt == "*") {
result = this->num1.toDouble() * this->num2.toDouble();
}
else if (this->opt == "÷") {
if(this->num2 == "0")
{
ui->lineEdit->setText("除数不能为0");
return;
}
result = this->num1.toDouble() / this->num2.toDouble();
}
else if (this->opt == "%") {
result = this->num1.toInt() % this->num2.toInt();
}
ui->lineEdit->setText(QString::number(result));
//转存结果到num1,并且清空num2和opt
this->num1 = QString::number((result));
this->num2 = "";
this->opt = "";
//qDebug() << this->num1 << this->opt <<this->num2;
});
4.4 清空按钮
清空: 将 num1 、num2 、 opt 都置为空
//清空
connect(ui->clsBtn, &QPushButton::clicked,[this](){
this->num1 = "";
this->num2 = "";
this->opt = "";
ui->lineEdit->clear();
});
4.5 连续运算
-
点击等号时,进行加减乘除运算
-
运算完毕之后,将 结果保存到 num1 中, 然后清空 num2、opt
ui->lineEdit->setText(QString::number(result));
//转存结果到num1,并且清空num2和opt
this->num1 = QString::number((result));
this->num2 = "";
this->opt = "";
4.6 输入优化
1)对 . 运算符的优化
当 num1 为空时,不能输入 . ; 当 num1 中已经有 . 时,不能再输入 .
2)对 0 的优化
当0作为第一个输入的数字时,第二个字符必须为 .
void Widget::setNum(QString n)
{
//对 . 的特殊验证:1)如果屏幕中有点,不能输入 . 2)屏幕为空 不能输入 .
//如果屏幕为空,且输入为 . 不能向下执行(返回)
if (ui->lineEdit->text().isEmpty() && n == ".")
{
return;
}
//如果屏幕中有 . ,并且当前输入的是 . ,则放回
if (ui->lineEdit->text().count(".") == 1 && n == ".")
{
return;
}
//对 0 特殊验证:屏幕中如果只有一个0,则下一个输入必须是点
// if(ui->lineEdit->text().isEmpty() && n == "0")
// {
// if (this->opt.isEmpty())
// {
// this->num1 += "0.";
// ui->lineEdit->setText(this->num1);
// }
// else {
// this->num2 += "0.";
// ui->lineEdit->setText(this->num2);
// }
// return;
// }
if (ui->lineEdit->text() == "0" && n != ".")
{
return;
}
//判断运算符是否为空
if(this->opt.isEmpty())
{
this->num1 += n;
ui->lineEdit->setText(this->num1);
}
else {
this->num2 += n;
ui->lineEdit->setText(this->num2);
}
}
4.7 back 退格
-
核心逻辑: 截取掉 num1 或者 num2 的最后一位
-
使用方法:
-
num1.mid(start, length);
-
num2.chop(1)
-
//Back
connect(ui->backBtn, &QPushButton::clicked,[this](){
QString tmp = ui->lineEdit->text();
if (tmp.isEmpty())
{
return;
}
tmp=tmp.mid(0,tmp.size()-1);
if (tmp.size() > 1 && tmp[tmp.size() -1] == ".")
{
tmp = tmp.mid(0,tmp.size()-1);
}
if (this->opt.isEmpty())
{
this->num1 = tmp;
ui->lineEdit->setText(this->num1);
}
else {
this->num2 = tmp;
ui->lineEdit->setText(this->num2);
}
});
4.8 完整的计算器代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("计算器");
this->setFixedSize(363,660);
//mid(start, length):字符串截取
//参数1:截取的起始索引号
//参数2:截取的长度,可选。如果不给参数2,则会截取到数据末尾
//处理数字按钮
connect(ui->btn9, &QPushButton::clicked,[this](){this->setNum("9");});
connect(ui->btn8, &QPushButton::clicked,[this](){this->setNum("8");});
connect(ui->btn7, &QPushButton::clicked,[this](){this->setNum("7");});
connect(ui->btn6, &QPushButton::clicked,[this](){this->setNum("6");});
connect(ui->btn5, &QPushButton::clicked,[this](){this->setNum("5");});
connect(ui->btn4, &QPushButton::clicked,[this](){this->setNum("4");});
connect(ui->btn3, &QPushButton::clicked,[this](){this->setNum("3");});
connect(ui->btn2, &QPushButton::clicked,[this](){this->setNum("2");});
connect(ui->btn1, &QPushButton::clicked,[this](){this->setNum("1");});
connect(ui->btn0, &QPushButton::clicked,[this](){this->setNum("0");});
connect(ui->pointBtn, &QPushButton::clicked,[this](){this->setNum(".");});
//处理运算符按钮
connect(ui->addBtn, &QPushButton::clicked,[this](){this->setOpt("+");});
connect(ui->subBtn, &QPushButton::clicked,[this](){this->setOpt("-");});
connect(ui->mulBtn, &QPushButton::clicked,[this](){this->setOpt("*");});
connect(ui->divBtn, &QPushButton::clicked,[this](){this->setOpt("÷");});
connect(ui->perBtn, &QPushButton::clicked,[this](){this->setOpt("%");});
//清空
connect(ui->clsBtn, &QPushButton::clicked,[this](){
this->num1 = "";
this->num2 = "";
this->opt = "";
ui->lineEdit->clear();
});
//Back
connect(ui->backBtn, &QPushButton::clicked,[this](){
QString tmp = ui->lineEdit->text();
if (tmp.isEmpty())
{
return;
}
tmp=tmp.mid(0,tmp.size()-1);
if (tmp.size() > 1 && tmp[tmp.size() -1] == ".")
{
tmp = tmp.mid(0,tmp.size()-1);
}
if (this->opt.isEmpty())
{
this->num1 = tmp;
ui->lineEdit->setText(this->num1);
}
else {
this->num2 = tmp;
ui->lineEdit->setText(this->num2);
}
});
//等号
connect(ui->equarBtn, &QPushButton::clicked,[this](){
double result = 0;
if (this->opt == "+")
{
result = this->num1.toDouble() + this->num2.toDouble();
}
else if (this->opt == "-") {
result = this->num1.toDouble() - this->num2.toDouble();
}
else if (this->opt == "*") {
result = this->num1.toDouble() * this->num2.toDouble();
}
else if (this->opt == "÷") {
if(this->num2 == "0")
{
ui->lineEdit->setText("除数不能为0");
return;
}
result = this->num1.toDouble() / this->num2.toDouble();
}
else if (this->opt == "%") {
result = this->num1.toInt() % this->num2.toInt();
}
ui->lineEdit->setText(QString::number(result));
//转存结果到num1,并且清空num2和opt
this->num1 = QString::number((result));
this->num2 = "";
this->opt = "";
//qDebug() << this->num1 << this->opt <<this->num2;
});
}
void Widget::setNum(QString n)
{
//对 . 的特殊验证:1)如果屏幕中有点,不能输入 . 2)屏幕为空 不能输入 .
//如果屏幕为空,且输入为 . 不能向下执行(返回)
if (ui->lineEdit->text().isEmpty() && n == ".")
{
return;
}
//如果屏幕中有 . ,并且当前输入的是 . ,则放回
if (ui->lineEdit->text().count(".") == 1 && n == ".")
{
return;
}
//对 0 特殊验证:屏幕中如果只有一个0,则下一个输入必须是点
// if(ui->lineEdit->text().isEmpty() && n == "0")
// {
// if (this->opt.isEmpty())
// {
// this->num1 += "0.";
// ui->lineEdit->setText(this->num1);
// }
// else {
// this->num2 += "0.";
// ui->lineEdit->setText(this->num2);
// }
// return;
// }
if (ui->lineEdit->text() == "0" && n != ".")
{
return;
}
//判断运算符是否为空
if(this->opt.isEmpty())
{
this->num1 += n;
ui->lineEdit->setText(this->num1);
}
else {
this->num2 += n;
ui->lineEdit->setText(this->num2);
}
}
void Widget::setOpt(QString o)
{
this->opt = o;
ui->lineEdit->clear();
}
Widget::~Widget()
{
delete ui;
}