0%

qt学习

Qt学习笔记


一、qt概述

qt(跨平台的C++ GUI应用程序开发框架)——WPS

1. qt发展历史

  • 1991年诞生
  • 1994年创立奇趣科技(Trolltech)公司
  • 2005年qt4.0发布(500+类,9000+函数)
  • 2008年被诺基亚收购
  • 2009年将qt源代码开放
  • 2012年qt被出售给digia公司
  • 2013年5.0发布

2. 相关工具介绍

  • qt助手//帮助手册
  • qt构建器(qmake)//构建qt应用程序
  • qt设计师(designer)//图形界面编辑器
  • qt界面编辑器(uic)//将设计师得到的xml(.ui)文件转换为C++文件(.h)
  • qt元对象编译器(moc)//将qt中语法扩展还原为标准C++代码
  • qt资源编译器(rcc)//将图片资源转换为C++文件(.cpp)
  • qt创造器(qtcreator)//qt的集成开发环境(IDE),包含上面所有工具

3. 助手中常用的模块

  • qt SQL //数据库
  • qt network //网络
  • qt widgets //控件
  • qt core //代码
  • qt GUI //图形界面
  • qt serial port //串口编写

例如
类名:QApplication/QLabel

  • 先看第一句,了解该类的功能,若是不理解,就在”more”中查看详细介绍。
  • 了解头文件(和类名一致),构建选项(QT+=widgets),继承关系。
  • List of all menbers //列出所有的函数名
  • obsolete members //列出过时的函数,这些函数可能会在以后的版本中删除,同时里面有相应的替换函数
  • 类中的成员变量属性(properties)
  • 公用的成员函数(public functions)
  • 公有虚函数(Reimplemented Public Functions)
  • 槽函数(Public Slots) //当有个对象发生状态改变,会发射信号,发生某个特定的动作,是qt的语法扩展
  • 信号函数(signals)
  • 静态公有成员(Static Public Members) //相当于全局

第一个qt程序

  1. 创建工程目录(最好开头大写)
    注:每个qt应用程序都应该放在独立的工程目录下
  2. 进入工程目录编写代码(C++)
  3. 运行前需要qmake(构建)
    注:需要在Pro里面添加“QT += widgets”

qt字符串(QString)和字符编码

  1. 常见编码
  • Windows默认使用ANSI(中文gbk编码,英文ACKII)
  • Linux默认使用Unicode(utf-8)
  • Qt字符串QString内部默认使用Unicode(utf-16)用QString()将字符串用utf-8来编码
  1. 解决字符串乱码——编码转换(QTextCodec)

  2. 父窗口(容器窗口)

  • 常用的父窗口类
    QWidget //控件基类
    QDialog //对话框,QWidget的子类 //相对简单的界面
    QMainWindow //主窗口,QWidget的子类 //相对复杂的界面
  • QWidget中两个常用的函数
    调整控件或父窗口的位置
    void move(int x, int y); //相对于左上角的位置
    调整控制或父窗口的大小
    void resize(int w, int h) //相对于电脑的像素

二、信号和槽机制

1. 概念

信号和槽是Qt中自定义的一种通信机制,实现对象之间的交互,当某个对象发生改变时将会发送信号,该信号可以被其他对象接收,接收以后将执行一个执行的成员函数(槽函数)。

2. 定义

  • 包括信号或槽的类必须是Object的子类
  • 信号使用“signals:”标记,信号函数只需声明,不能写定义
  • 槽使用“slots:”标记,槽函数可以与某个信号建立连接,通过某个信号触发槽函数的执行;另外槽函数也可以当做是普通的成员函数,直接调用
  • 包含信号或槽的类,前面需要添加宏“Q_OBJECT”,将来构建工程时,会调用moc(元对象编译器)将语法扩展的信号或槽还原为标准的C++代码。
    1
    2
    3
    4
    5
    6
    7
    class XX::public Object{
    Q_OBJECT //moc
    signals:
    void sigFunc(void);//信号函数
    public slots:
    void slotFunc(void){…}//槽函数
    }

    3. 信号和槽连接

    1
    2
    3
    4
    5
    QObject::connect(
    const QObject* sender, //信号发送对象,可以是QObject所有的子类类型
    const QObject* signal, //信号函数
    const QObject* receiver, //信号的接收对象,可以是QObject所有的子类类型
    const QObject*method); //槽函数
    注: SIGNAL(信号函数(参数表)) //将信号函数转换为const char*
    SLOT(槽函数(参数表)) //将槽函数转换为const char*

    4. 信号与槽连接的语法要求

  • 信号和槽参数要一致(绝大多数情况下)
    1
    2
    QObject::connect(A,SIGNAL(sigFunc(int)),B,SLOT(slotFunc(int)));//ok
    QObject::connect(A,SIGNAL(sigFunc(void)),B,SLOT(slotFunc(int)));//error
  • 可以带有缺省参数,多余参数将被忽略
  • 信号函数的参数可以比槽函数参数多,多余参数将会被忽略
  • 一个信号可以同时连接多个槽函数(一对多)(同一线程上,按顺序执行)
    1
    2
    QObject::connect(A,SIGNAL(sigFunc(int)),B1,SLOT(slotFunc1(int)));//ok
    QObject::connect(A,SIGNAL(sigFunc(int)),B2,SLOT(slotFunc2(int)));//ok
  • 多个信号可以同时连接到同一个槽函数(多对一)
    1
    2
    QObject::connect(A1,SIGNAL(sigFunc1(int)),B,SLOT(slotFunc(int)));//ok
    QObject::connect(A2,SIGNAL(sigFunc2(int)),B,SLOT(slotFunc(int)));//ok
    注:无论A1或A2发送信号,B的槽函数都会被执行
  • 两个信号函数可以直接连接(信号串联)//了解
    1
    QObject::connect(A1,SIGNAL(sigFunc1(int)), A2,SIGNAL(sigFunc2(int)));//ok
    注:当A1发送信号时,所连接的A2对象的信号也将被发送
  • *A1->A2->B**

5. QSlider(滑块)/QSpinBox(选值框)

案例:
代码实现
main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "dialog.h"
#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
#include <QSpinBox>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Dialog w;
w.resize(320,240);

QLabel label("我是标签",&w);
label.move(20,40);
QPushButton button("关闭标签",&w);
button.move(20,150);
QPushButton* abutton = new QPushButton("关闭",&w);
abutton->resize(80,80);
abutton->move(200,100);
QSlider slider(Qt::Horizontal,&w);//Qt::Horizontal是个枚举常量,表示滑块是水平的
slider.move(200,50);
QSpinBox spin(&w);
spin.move(150,50);

w.show();

//实现点击按钮,关闭标签
QObject::connect(&button,SIGNAL(clicked()),
&label,SLOT(close()));
//第一个参数是信号发送对象
//第二个参数是信号函数,用SIGNAL将信号函数转换为const char*格式
//第三个参数是信号接收对象
//第四个参数是槽函数,用SLOT将槽函数转换为const char*格式
//QObject::connect(abutton,SIGNAL(clicked()),&a,SLOT(closeAllWindows()));
QObject::connect(abutton,SIGNAL(clicked()),&a,SLOT(quit()));
//QObject::connect(abutton,SIGNAL(clicked()),&w,SLOT(close()));
//abutton本身就是指针,所以不需要取地址
//功能实现:将窗口全都关闭

//滑块滑动时,选值框数值跟随改变
//滑块滑动信号:valueChanged(int)
//设置选值框数值的槽函数:setValue(int)
QObject::connect(&slider,SIGNAL(valueChanged(int)),&spin,SLOT(setValue(int)));
//选值框数值改变时,滑块随之滑动
//选值框数值改变信号:valueChanged(int)
//设置滑块滑动位置槽函数:setValue(int)
QObject::connect(&spin,SIGNAL(valueChanged(int)),&slider,SLOT(setValue(int)));

return a.exec();
}

效果展示
关闭标签/滑动滑块


三、面向对象的Qt编程

1. 基于对象的Qt编程(之前的编写方式)//不推荐

2. 面向对象的Qt编程

案例:实现加法计算器
QLineEdit(行编辑)

  • 左右操作数只能输入数字形式的内容
  • 初始化等号按钮为禁用状态,等到左右操作数都输入有效数据再恢复按钮为正常状态
  • 点击等号按钮时,计算和显示结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    思路:
    class CalculatorDialog:public QDialog{
    Q_OBJECT//moc
    public:
    构造函数(){界面初始化,信号与槽连接}
    public slots:
    void 恢复等号按钮正常状态的槽函数(){}
    void 计算和显示结果的槽函数(){}
    private:
    QLineEdit、QLabel、QPushButton
    };
  • 自定义信号、自定义槽函数
    signals: public slots:

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef __CALCULATORDIALOG_H
#define __CALCULATORDIALOG_H

#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QLineEdit> //行编辑
#include <QDoubleValidator> //数字验证器
#include <QFont> //字体
#include <QHBoxLayout> //水平布局器
//自定义计算器窗口类
class CalculatorDialog:public QDialog{
Q_OBJECT //moc
public:
CalculatorDialog(void); //构造函数
public slots:
void enableButton(void);//恢复按钮为正常状态的槽函数
void calcClicked(void); //计算和显示结果的槽函数
private:
QLineEdit* m_editX; //左操作数
QLineEdit* m_editY; //右操作数
QLineEdit* m_editZ; //显示结果
QLabel* m_label; //“+”标签
QPushButton* m_button; //“=”按钮
QHBoxLayout* m_layout; //水平布局器,用于自动调整大小和位置
};


#endif // __CALCULATORDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "CalculatorDialog.h"

CalculatorDialog::CalculatorDialog(void){
setWindowTitle("加法计算器");//界面初始化
QFont font;//设置字体大小
font.setPointSize(20);
setFont(font);

m_editX = new QLineEdit(this);//左操作数,this指向正在创建的对象,就是父窗口指针
m_editX->setAlignment(Qt::AlignRight);//设置文本对齐方式为右对齐
m_editX->setValidator(new QDoubleValidator(this));//设置验证器,只能输入数字内容
m_editY = new QLineEdit(this);//右操作数
m_editY->setAlignment(Qt::AlignRight);
m_editY->setValidator(new QDoubleValidator(this));
m_editZ = new QLineEdit(this);//显示结果
m_editZ->setAlignment(Qt::AlignRight);
m_editZ->setReadOnly(true);//设置结果为只读,默认为false
m_label = new QLabel("+",this);//"+"
m_button = new QPushButton("=",this);
m_button->setEnabled(false);//设置按钮为禁用(不可点击)

//使用布局器自动调整控件的大小和位置
m_layout = new QHBoxLayout(this);
m_layout->addWidget(m_editX);//将控件按水平方向(从左到右)依次添加到布局器
m_layout->addWidget(m_label);
m_layout->addWidget(m_editY);
m_layout->addWidget(m_button);
m_layout->addWidget(m_editZ);
setLayout(m_layout);//设置布局器

//信号和槽连接
//左右操作数文本改变时,发送信号textChanged(QString)
//如果连接的槽函数是当前父窗口中自定义的,那么第三个参数一定是this
//多对一
//信号可以比槽函数的参数多
connect(m_editX,SIGNAL(textChanged(QString)),this,SLOT(enableButton(void)));
connect(m_editY,SIGNAL(textChanged(QString)),this,SLOT(enableButton(void)));
//点击等号按钮,发送信号clicked()
connect(m_button,SIGNAL(clicked(void)),this,SLOT(calcClicked(void)));

}//构造函数

void CalculatorDialog::enableButton(void){
bool bXok;//记录左操作数是否输入了有效数据
bool bYok;//记录右操作数是否输入了有效数据
m_editX->text().toDouble(&bXok);
m_editY->text().toDouble(&bYok);
//如果左右操作数都输入了有效数据,则恢复按钮为正常可用状态,否则设置为禁用状态
m_button->setEnabled(bXok && bYok);

}//恢复按钮为正常状态槽函数

void CalculatorDialog::calcClicked(void){
double res = m_editX->text().toDouble()+m_editY->text().toDouble();//计算相加结果(转换为double类型进行计算)
QString str = QString::number(res);//number()可以将double转换为QString
m_editZ->setText(str);//显示结果,setText()可以设置控件要显示的文本

}//计算和显示结果的槽函数

效果展示
加法器


四、qt设计师使用(designer)

案例:使用设计师重构加法计算器

  1. 创建工程目录

  2. 进入工程目录,启动设计师

  • 新建窗体
  • 在设计师界面中完成ui设计
  • 从“widget box”中拖拽出相应的控件到父窗口
  • 设置父窗口和每个控件的属性
    父窗口:objectname(对象名)
    注:将来会根据父窗口的对象名生成一个名字相同的类,包含在设计师完成的所有代码
  • 调整父窗口和控件大小和位置
    方法一:鼠标拖拽
    方法二:键盘,调整位置(方向键(一次十个像素),Ctrl+方向键(一次一个像素))
            调整大小(shift+方向键(一次十个像素),Ctrl+shift+方向键(一次一个像素))(左上角点不动)
    方法三:设置几何属性(geometry),调整位置(x,y)大小(宽度,高度)
    方法四:使用布局器(右键选择布局)//推荐
  1. 使用界面编辑器(uic),将ui文件转换为C++.h文件

ui_*.h文件中的内容

1
2
3
4
5
6
7
8
class CalculatorDialog:public Ui_CaculatorDialog(){
public:
图形控件相关对象指针声明;
void setupUi(QDialog*){//界面初始化、创建图形控件、属性设置…}
};
namespace Ui{
class CalculatorDialog:public Ui_CalculatorDialog();
}

注:Ui名称空间的子类(Ui::CalculatorDialog)和上面基类(Ui_CalculatorDialog)相同

  1. 使用“ui_CaculatorDialog.h”文件,复用里面代码方法
  • 方法1:继承
    1
    2
    3
    	class MyClass:public Ui::CalculatorDialog{
    //将界面相关代码继承过来直接使用
    };
  • 方法2:组合
    1
    2
    3
    4
    5
    6
    	class MyClass{
    public:
    MyClass():ui(new Ui::CalculatorDialog){}
    private:
    Ui::CalculatorDialog* ui;//之后可以通过“ui->”访问和界面相关代码
    };
  • *案例:登录对话框**
  • 创建工程目录
  • 进入工程目录,启动设计师,完成ui设计
    echoMode: Password//用密码的方式显示文本
    layoutDirection: LeftToRight//改为从左向右的布局方式

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef __LOGINDIALOG_H
#define __LOGINDIALOG_H

#include "ui_LoginDialog.h"
#include <QMessageBox>//消息提示框
#include <QDebug>//打印调试

namespace Ui {
class LoginDialog;
}

class LoginDialog : public QDialog,public Ui::LoginDialog
{
Q_OBJECT

public:
LoginDialog(void);
~LoginDialog();
public slots:
void onAccepted(void);//处理ok按钮的槽函数
void onRejected(void);//处理Cancel按钮的槽函数
//点击也分为很多种,最普通的就是clicked(),但是对于有明确接受与拒绝信号的按钮,我们可以用accepted()和rejected()来表示相应的点击

private:
Ui::LoginDialog *ui;
};

#endif // __LOGINDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "LoginDialog.h"

LoginDialog::LoginDialog(void){
setupUi(this);
connect(m_btnBox,SIGNAL(accepted()),this,SLOT(onAccepted()));
connect(m_btnBox,SIGNAL(rejected()),this,SLOT(onRejected()));

}//构造函数

void LoginDialog::onAccepted(){
//如果属于tarena/123456打印提示登录成功,如果不是,弹出消息框显示登录失败
if(m_editUserName->text()=="tarena" && m_editPassword->text()=="123456"){
qDebug("登陆成功");//相当于printf
qDebug() << "Login Success!";//相当于cout
close();//关闭登录对话框
}
else {
QMessageBox msgBox(QMessageBox::Critical,"登录失败","用户名或密码错误,请重试!",QMessageBox::Ok,this);
//参数:图标,标题,提示信息,按钮,父窗口
msgBox.exec();//显示消息提示框,并进入事件循环
}

}//处理ok按钮的槽函数

void LoginDialog::onRejected(){
//弹出消息提示框,提示用户是否确定要取消登录
QMessageBox msgBox(QMessageBox::Question,"取消登录","确定要取消登录么?",QMessageBox::No|QMessageBox::Yes);
//显示消息提示框,并进入事件循环,点击No或Yes都会退出事件循环;点击Yes退出时,关闭登录对话框
if(msgBox.exec() == QMessageBox::Yes){
close();//关闭登录对话框
}

}//处理cancel按钮的槽函数

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

效果展示
登录对话框


五、Qt创造器使用(qtcreator)

使用技巧
F4可以实现在头文件与源文件之间快速切换
对象用.
对象指针用->
QString str;
qDebug() << str;//类似C++的信息流,打印消息到通知栏

案例:获取系统时间

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef __timeDIALOG_H
#define __timeDIALOG_H

#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>//垂直布局器
#include <QFont>
#include <QDateTime>

class TimeDialog:public QDialog{
Q_OBJECT
public:
TimeDialog(void);
public slots:
void getTimeButton(void);
private:
QLabel* m_label;
QPushButton* m_button;
QVBoxLayout* m_layout;
QDateTime m_time;

};



#endif // __timeDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "timedialog.h"

TimeDialog::TimeDialog(void){
setWindowTitle("时间");
QFont font;
font.setPointSize(20);
setFont(font);

m_label = new QLabel(this);
m_label->setFrameStyle(QFrame::Panel|QFrame::Sunken);//设置边框效果:凹陷面板
m_label->setAlignment(Qt::AlignHCenter|Qt::AlignVCenter);//设置文本水平居中对齐且垂直居中对齐
m_button = new QPushButton("获取系统时间",this);

connect(m_button,SIGNAL(clicked(void)),this,SLOT(getTimeButton(void)));
m_layout = new QVBoxLayout(this);
m_layout->addWidget(m_label);
m_layout->addWidget(m_button);
setLayout(m_layout);
}

void TimeDialog::getTimeButton(void){
m_time=QDateTime::currentDateTime();
QString str=m_time.toString("yyyy-MM-dd hh:mm:ss ddd");
m_label->setText(str);

}

效果展示
获取系统时间


六、资源和图像

  1. 资源编译器(rcc)
  • 创建资源文件(.qrc)
  • 将资源文件中描述的图片资源编辑器转换为C++源代码

2.绘图事件(paintEvent)和画家类(QPainter)

  • 当应用程序窗口改变时(启动,拉伸,拖拽,最大化最小化…)将会触发绘图事件,对应的事件处理函数paintEvent将会被执行。
    virtual void QWidget::paintEvent(QPaintEvent*);
  • 通过调用update()/repaint()函数也可以手动触发绘图事件
  • 绘图事件处理函数是虚函数,如果希望在自定义子类中实现指定的图像绘制,可以重写绘图事件处理函数,对基类中的版本形成覆盖,并在其中通过画家类完成绘制操作。
  • QPainter是Qt中二维图形引擎,可以实现各种图像、图片的绘制。

案例:图图秀

  1. 使用qt创造器创建工程
  • 工程名:ShowImage
  • 类名:ShowImageDialog
  1. 向工程中添加资源
  • 将需要使用资源图片(images)拷贝到工程目录下
  • 添加新文件:qt-》qt resource file
  • 指定资源文件名字:showImage(将来会自动生成showImage.qrc的资源文件)
  • 下一步完成,默认进入文件编辑界面
  • 添加:添加前缀-》添加文件,选择“image”目录下的所有图片并打开
  1. 双击.ui文件,进入设计模式,完成界面设计
  • 拖拽需要使用的控件
    frame(显示框架)pushbutton(实现图片上一张下一张的切换)
  • 设置属性
    1
    2
    3
    frame:
    sizePolicy:垂直策略(Expanding)//让控件在设置布局的过程中,尽可能的多占据空间,而不是跟其他的控件平均分配空间
    FrameShape:Box//一种窗口的显示主题
  • 使用布局器调整大小和位置
  1. 编码、测试

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef DIALOGSHOWIMAGEDIALOG_H
#define DIALOGSHOWIMAGEDIALOG_H

#include <QDialog>
#include <QImage>//图片对象
#include <QPainter>//画家类

namespace Ui {
class DialogShowImageDialog;
}

class DialogShowImageDialog : public QDialog
{
Q_OBJECT

public:
explicit DialogShowImageDialog(QWidget *parent = nullptr);
~DialogShowImageDialog();

private slots:
void on_m_btnPrev_clicked();

void on_m_btnNext_clicked();

private:
void paintEvent(QPaintEvent *);//绘图事件处理函数,窗口改变会自动运行或者调用update触发运行
private:
Ui::DialogShowImageDialog *ui;
int m_index;//图片索引
};

#endif // DIALOGSHOWIMAGEDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "dialogshowimagedialog.h"
#include "ui_dialogshowimagedialog.h"

DialogShowImageDialog::DialogShowImageDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::DialogShowImageDialog)
{
ui->setupUi(this);
m_index = 1;
}

DialogShowImageDialog::~DialogShowImageDialog()
{
delete ui;
}
//自动生成上一张按钮的槽函数
void DialogShowImageDialog::on_m_btnPrev_clicked(){

if(--m_index < 0){
m_index=3;
}
update();//触发绘图事件
}
//下一张
void DialogShowImageDialog::on_m_btnNext_clicked(){

if(++m_index > 3){
m_index = 1;
}
update();
}
void DialogShowImageDialog::paintEvent(QPaintEvent *){

QPainter painter(this); //创建画家对象
QRect rect = ui->frame->frameRect(); //获取绘图的矩形区域,QRrect表示矩形区域的对象
rect.translate(ui->frame->pos()); //平移rect坐标值,让其和父窗口(painter)坐标系一致
//准备绘制的图片对象,QImage表示图片对象,":/new/prefix1/images/"为路径名(千万不要打错路径,打错路径后并不会报错,但是不会显示图片)
QImage image(":/new/prefix1/images/"+QString::number(m_index)+".png");
painter.drawImage(rect,image); //使用painter将image绘制到rect上
}

效果展示
图图秀


七、目录(QDir)和定时器(QTime)

  1. 目录QDir
  • 创建目录对象
    QDir dir(“/home/tarena/csd1911/qt/photos”);//绝对路径
    QDir dir(“./photos”);//相对路径
  • 遍历目录下内容
    QStringList list1 = dir.entryList(QDir::dirs|QDir::NoDotAndDotDot);//遍历子目录,并且排除.与..目录
    QStringList list1 = dir.entryList(QDir::Files);//遍历普通文件
  1. 定时器
  • 定时器事件:timerEvent
    //定时器事件处理函数,定时器到时后将自动触发执行
    virtual void timeEvent(QTimerEvent*);
    //开启定时器,参数定时器到时的间隔时间(ms,是连续触发的,所以需要关闭),返回定时器ID
    int startTimer(int interval);
    //关闭定时器
    void killTimer(int id);

  • 定时器类:QTimer(对上一个事件进行封装)(实现循环操作,相当于while(1),但是while会使窗口卡死)(比定时器事件更灵活)
    //创建定时器对象
    QTimer timer;
    //连接定时器信号和槽函数
    connect(&timer,SIGNAL(timeout()),this,SLOT(自定义定时器处理槽函数()));
    //开启定时器
    timer.start(int msec);
    //关闭定时器
    timer.stop();

案例:摇摇乐

  • 工程名:Lottery
  • 类名:LotteryDialog
  • 将“photos”目录拷贝到工程目录下,然后到qt创造器的“项目”模式中,去掉Shadow build选项(因为要用相对路径)
  • 双击“.ui”文件,进入设计模式,完成ui设计

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#ifndef LOTTERYDIALOG_H
#define LOTTERYDIALOG_H

#include <QDialog>
#include <QPainter> //画家
#include <QDir> //目录
#include <QTimer> //定时器
#include <QTime> //时间
#include <QVector> //向量容器
#include <QDebug> //打印调试

namespace Ui {
class LotteryDialog;
}

class LotteryDialog : public QDialog
{
Q_OBJECT

public:
explicit LotteryDialog(QWidget *parent = nullptr);
~LotteryDialog();

private slots:
void on_pushButton_clicked();
void onTimeout(void); //定时器到时后执行的槽函数
private:
void loadPhotos(const QString& path); //从参选者目录中加载照片到容器中
//void timerEvent(QTimerEvent *); //定时器事件处理函数
void paintEvent(QPaintEvent *); //绘图事件处理函数
private:
Ui::LotteryDialog *ui;
QVector<QImage> m_vecPhotos; //保存所有参选者照片的容器(相当于动态数组)
int m_index; //照片在容器中索引
QTimer m_timer; //int m_timer;//定时器ID
bool isStarted; //状态标记,true(开始摇奖),false(停止摇奖)
};

#endif // LOTTERYDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include "lotterydialog.h"
#include "ui_lotterydialog.h"

LotteryDialog::LotteryDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::LotteryDialog)
{
ui->setupUi(this);
isStarted = false; //初始化未开始摇奖
m_index = 0; //初始化索引号
qsrand(QTime::currentTime().msec()); //设置随机数种子
loadPhotos("./photos"); //动态的从photos目录加载图片,注意路径
qDebug() << "加载到照片个数:" << m_vecPhotos.size();
//定时器到时后将会发送信号timeout
connect(&m_timer,SIGNAL(timeout()),this,SLOT(onTimeout()));
}

LotteryDialog::~LotteryDialog()
{
delete ui;
}
//开始按钮对应的槽函数
void LotteryDialog::on_pushButton_clicked()
{
if(isStarted == false){
isStarted = true;//开始摇奖
ui->pushButton->setText("停止摇奖"); //修改按钮文本
//m_timer = startTimer(50); //开始定时器,每隔50ms触发一次
m_timer.start(50);
}
else {
isStarted = false; //停止摇奖
ui->pushButton->setText("开始摇奖");
//killTimer(m_timer); //关闭定时器
m_timer.stop();
}
}
//从参选者目录中加载照片到容器中
void LotteryDialog::loadPhotos(const QString& path){
QDir dir(path);//创建目录对象
//遍历访问当前目录下所有照片
QStringList l1 = dir.entryList(QDir::Files);
for (int i=0;i<l1.size();i++) {
QImage image(path+"/"+l1.at(i)); //根据照片文件名字构造图片对象
m_vecPhotos << image; //保存照片到容器中
}
//递归偏离子目录下的所有照片,不包括“.”和“..”
QStringList l2 = dir.entryList(QDir::Dirs|QDir::NoDotAndDotDot);
for (int i=0;i<l2.size();i++) {
loadPhotos(path+"/"+l2.at(i));
}
}
//定时器事件处理函数(每次定时器到时都会自动被触发执行)
/*void LotteryDialog::timerEvent(QTimerEvent *){
//随机从容器中获取一个照片对象的索引
m_index = qrand()%m_vecPhotos.size();
//触发绘图事件,绘制m_index对应的照片
update();
}*/
//定时器到时后执行的槽函数
void LotteryDialog::onTimeout(void){
m_index = qrand()%m_vecPhotos.size(); //随机从容器中获取一个照片对象的索引
update(); //触发绘图事件,绘制m_index对应的照片
}
//绘图事件处理函数
void LotteryDialog::paintEvent(QPaintEvent *){
QPainter painter(this);//创建绘图的画家对象
QRect rect = ui->frame->frameRect(); //绘图的矩形区域
rect.translate(ui->frame->pos()); //坐标值平移,让其和painter坐标系一致
painter.drawImage(rect,m_vecPhotos[m_index]); //绘制m_index对应的照片
}

效果展示
抽奖效果


八、鼠标和键盘

  1. 鼠标事件
    #include <QMouseEvent>
    //鼠标按下时执行的事件处理函数
    virtual void mousePressEvent(QMouseEvent *);
    //鼠标抬起时执行的事件处理函数
    virtual void mouseReleaseEvent(QMouseEvent *);
    //鼠标移动时执行的事件处理函数
    virtual void mouseMoveEvent(QMouseEvent *);
    //鼠标双击时执行的事件处理函数
    virtual void mouseDoubleClickEvent(QMouseEvent *);
    QRect(x,y,w,h);//矩形区域
    QPoint(x,y);//位置
    QSize(w,h);//大小

案例:鼠标测试,实现用鼠标左键拖拽“标签”移动

  • 工程名:Mouse
  • 类名:MouseDialog
  • 界面设计:设置label控件背景颜色
    方法1:样式表(styleSheet)
    方法2:调色板(palette)(只能二选一)
    autoFillBackground:勾选
    palette:点击“继承”->“改变调色板”
    编辑调色板:选择颜色

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef MOUSEDIALOG_H
#define MOUSEDIALOG_H

#include <QDialog>
#include <QMouseEvent>

namespace Ui {
class MouseDialog;
}

class MouseDialog : public QDialog
{
Q_OBJECT

public:
explicit MouseDialog(QWidget *parent = nullptr);
~MouseDialog();
private:
void mousePressEvent(QMouseEvent *event); //鼠标按下时执行的事件处理函数
void mouseReleaseEvent(QMouseEvent *event); //鼠标抬起时执行的事件处理函数
void mouseMoveEvent(QMouseEvent *event); //鼠标移动时执行的事件处理函数
private:
Ui::MouseDialog *ui;
bool m_drag; //标记是否可以拖拽
QPoint m_pos; //记录鼠标和label的相对位置
};

#endif // MOUSEDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "mousedialog.h"
#include "ui_mousedialog.h"

MouseDialog::MouseDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::MouseDialog)
{
ui->setupUi(this);
m_drag = false;
}

MouseDialog::~MouseDialog()
{
delete ui;
}
//鼠标按下时执行的事件处理函数
void MouseDialog::mousePressEvent(QMouseEvent *event){

if(event->button() == Qt::LeftButton){ //是否为鼠标左键按下,event->button()读取目前按下的按键,验证是否是左键
QRect rect = ui->label->frameRect(); //获取label所在矩形区域
rect.translate(ui->label->pos()); //坐标值平移,让rect和鼠标坐标系一致
if(rect.contains(event->pos()) == true){ //判断鼠标点击位置是否在rect范围中
m_drag = true;
m_pos = ui->label->pos() - event->pos(); //记录label和鼠标的相对位置
}
}
}
//鼠标抬起时执行的事件处理函数
void MouseDialog::mouseReleaseEvent(QMouseEvent *event){

if(event->button() == Qt::LeftButton){
m_drag = false;
}
}
//鼠标移动时执行的事件处理函数
void MouseDialog::mouseMoveEvent(QMouseEvent *event){

if(m_drag == true){
QPoint newPos = m_pos + event->pos(); //计算label移动的新位置
QSize s1 = size(); //获取窗口大小
QSize s2 = ui->label->size(); //获取label大小

//设置label移动范围:不能超出父窗口
if(newPos.x()<0){ //x:最小值(0)最大值(窗口宽-label宽)
newPos.setX(0);
}
else if (newPos.x() > s1.width()-s2.width()) {
newPos.setX(s1.width()-s2.width());
}

if(newPos.y()<0){ //y:最小值(0)最大值(窗口高-label高)
newPos.setY(0);
}
else if (newPos.y()>s1.height()-s2.height()) {
newPos.setY(s1.height()-s2.height());
}
ui->label->move(newPos); //移动label到新位置
}
}

效果展示
鼠标拖动方块

  1. 键盘事件
    #include <QKeyEvent>
    virtual void keyPressEvent (QKeyEvent *);//按键按下时执行的事件处理函数
    virtual void keyReleaseEvent(QKeyEvent *);//按键抬起时执行的事件处理函数

案例:键盘按键测试,实现通过键盘方向键控制label移动

  • 工程名:Keyboard
  • 类名:KeyboardDialog

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef KEYBOARDDIALOG_H
#define KEYBOARDDIALOG_H

#include <QDialog>
#include <QKeyEvent>

namespace Ui {
class KeyboardDialog;
}

class KeyboardDialog : public QDialog
{
Q_OBJECT

public:
explicit KeyboardDialog(QWidget *parent = nullptr);
~KeyboardDialog();
private:
void keyPressEvent(QKeyEvent *event);//键盘按键按下时执行的事件处理函数
private:
Ui::KeyboardDialog *ui;
};

#endif // KEYBOARDDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "keyboarddialog.h"
#include "ui_keyboarddialog.h"

KeyboardDialog::KeyboardDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::KeyboardDialog)
{
ui->setupUi(this);
}

KeyboardDialog::~KeyboardDialog()
{
delete ui;
}
void KeyboardDialog::keyPressEvent(QKeyEvent *event){
//获取label的当前位置
int x = ui->label->pos().x();
int y = ui->label->pos().y();
if(event->key() == Qt::Key_Left){
ui->label->move(x-10,y);
}
else if (event->key() == Qt::Key_Right) {
ui->label->move(x+10,y);
}
else if (event->key() == Qt::Key_Up) {
ui->label->move(x,y-10);
}
else if (event->key() == Qt::Key_Down) {
ui->label->move(x,y+10);
}
}

效果展示
键盘操控方块


九、Qt多线程(QThread)

  1. 创建线程方法1:QObject::moveToThread
    class Myclass:public QObject{
    Q_OBJECT
    public slots:
    void func(void){

     //耗时或阻塞操作,需要放在子线程中执行

    }
    };
    QThread thread;//子线程对象
    Myclass myobject;//需要放在子线程中工作的对象
    myobject.moveToThread(&thread);//将myobject对象移动到子线程中
    connect(xx,SIGNAL(XX),&myobject,SLOT(func()));//连接信号和槽函数
    thread.start();//开启子线程(exec),麻烦,但是更灵活

  2. 创建线程方法2:继承QThread,重写线程入口函数run

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass::public QThread{
    protected:
    virtual void run(void){
    //耗时或阻塞操作,需要放在子线程中执行
    }
    };
    MyClass mythread;//创建子线程对象
    mythread.start();//开启子线程,子类中重写run函数将会在子线程中执行(不够灵活,但是简单)

案例:多线程打印消息

  • 方法1:工程(Thread1)
  • 方法2:工程(Thread2)
  1. 线程同步
  • 互斥锁:QMutex(加锁lock()会让其他线程在同样的事件中,进入阻塞…)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    eg:
    QMutex mutex;
    void run(void){
    mutex.lock();//加锁
    //访问共享资源start
    if("Error1"){
    mutex.unlock();//解锁
    return;
    }
    else if("Error2"){
    mutex.unlock();//解锁
    return;
    }
    //访问共享资源end
    mutex.unlock();//解锁
    }
  1. QMutexLocker简化加锁和解锁过程;
  • 互斥锁:QMutex(加锁lock()会让其他线程在同样的事件中,进入阻塞…)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    eg:
    QMutex mutex;
    void run(void){
    QMutexLocker locker(&mutex);//加锁(不需要写解锁)
    //访问共享资源start
    if("Error1"){
    return;
    }
    else if("Error2"){
    return;
    }
    //访问共享资源end
    }//析构locker时,在析构函数中自动解锁
  • 信号量:QSemaphore
    //初始化信号量计数值5,表示有5个可用共享资源
    QSemaphore sem(5); // sem.available() == 5
    //获取3个共享资源,剩余2个可用的共享资源
    sem.acquire(3); // sem.available() == 2
    sem.acquire(2); // sem.available() == 0
    //释放5个共享资源,剩余5个可用的共享资源
    sem.release(5); // sem.available() == 5
    sem.release(5); // sem.available() == 10
    //尝试获取1个共享资源,剩余9个可用的共享资源,获取成功
    sem.tryAcquire(1); // sem.available() == 9, returns true
    //尝试获取250个共享资源,获取失败,会返回错误
    sem.tryAcquire(250); // sem.available() == 9, returns false

案例:生产者和消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <QtCore>
#include <stdio.h>
#include <stdlib.h>

const int DataSize = 100000;//要生产的产品数量

const int BufferSize = 8192;//缓冲区大小8K
char buffer[BufferSize];

QSemaphore freeBytes(BufferSize);//控制生产者的信号量,初始化为8K
QSemaphore usedBytes;//控制消费者的信号量,初始化为0

class Producer : public QThread//生产者线程
{
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();//获取生产者的信号量,计数-1
buffer[i % BufferSize] = "ACGT"[QRandomGenerator::global()->bounded(4)];
usedBytes.release();//释放消费者信号量,计数+1
}
}
};

class Consumer : public QThread//消费者线程
{
Q_OBJECT
public:
void run() override
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();//获取消费者信号量,计数-1
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();//释放生产者的信号量。计数+1
}
fprintf(stderr, "\n");
}
};

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);//创建没有GUI应用的程序
Producer producer;//生产者
Consumer consumer;//消费者
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

十、Qt数据库

  1. 数据库简介
  • 概念
    数据库是以一定的方式存储在一起、能为用户共享、具有尽可能小的冗余特性,是与应用程序彼此独立的数据集合。
  • 相关术语
    DB:数据库(database)
    DBA:数据库管理员
    RDB:关系式数据库
    DBMS:数据库管理系统
  • 常见的数据库
    //大型的商业数据库
    甲骨文的Oracle
    IBM的DB2
    微软的Sqlserver
    //免费的数据库
    Sun的Mysql
    开源的Sqlite
  • 数据库操作语言(SQL)
  1. Sqlite数据库
  • Windows安装Sqlite
    请访问 SQLite 下载页面https://www.sqlite.org/download.html,从 Windows 区下载预编译的二进制文件。
    需要下载 sqlite-tools-win32-.zip 和 sqlite-dll-win32-.zip 压缩文件。
    创建文件夹 C:\sqlite,并在此文件夹下解压上面两个压缩文件,将得到 sqlite3.def、sqlite3.dll 和 sqlite3.exe 文件。
    添加 C:\sqlite 到 PATH 环境变量,最后在命令提示符下,使用 sqlite3 命令,将显示如下结果。

  • Linux安装SQLite
    目前,几乎所有版本的 Linux 操作系统都附带 SQLite。所以,只要使用下面的命令来检查您的机器上是否已经安装了 SQLite。
    $ sqlite3

  • 测试
    在“sqlite>”命令界面中可以输入两种指令,一种指令是以“.”开头,可以实现对数据库配置和设置数据显示方式;
    另一种指令是“Sql语句”,这些指令以“;”结尾,实现对数据的增删改查等管理操作。

  • 常用的sqlite自身配置和格式显示相关命令
    .help//打开帮助
    .exit//退出数据库
    .database//查看当前数据库名称
    .open testDB.db//打开数据库文件.db
    .table//查看数据库中数据表的名字
    .mode MODE//设置数据表显示模式,MODE:list(默认)/column/tab/html…
    .header on//显示数据的表头
    .schema//查看数据表创建时的详细信息
    .nullvalue “NULL”//设置数据表空白位置显示数值“NULL”

注:清屏“Ctrl+L”
注:执行“SELECT * FROM company;”可以查看company数据中所有的数据。
注:可以将上述指令写入配置文件中,将来重新进入数据库界面时会自动执行。

  1. 使用SQL语言操作数据库//重点
  • 创建数据表
    语法:
    CREATE TABLE 表名 (列名1 数据类型 [约束], 列名2 数据类型 [约束],…);
    常用数据类型:
    INT(整型数)TEXT(字符串)REAL(浮点数)
    常用约束:
    NOT NULL:非空约束,表示该类数据不能为空
    PRIMARY KEY:主键约束,表示该列数据唯一,可以加快对数据的访问
  • 删除数据表
    语法:
    DROP TABLE 表名;
    注:慎用,数据表一旦删除,里面所包含的数据也将随之消失!
  • 向数据表插入数据
    语法:
    INSERT INTO 表名(列名1,列名2,…) VALUES(数值1,数值2,…);
  • 从数据表中删除数据
    语法:
    DELETE FROM 表名WHERE 条件表达式;//删除满足条件的若干条数据
    DELETE FROM 表名WHERE 条件1 and 条件2;//删除同时满足两个条件的数据
    DELETE FROM 表名WHERE 条件1 or 条件2;//删除满足条件1或条件2的数据
  • 修改数据表中已存在的数据
    语法:
    UPDATE 表名 SET 列名1=新数值,列名2=新数值,… WHERE 条件表达式;//条件表达式中一般是要修改数据的id号
  • 查询数据表中的数据
    语法:
    SELET 列名1,列名2,… FROM 表名;
    SELET 列名1,列名2,… FROM 表名 WHERE 条件表达式;
    SELET 列名1,列名2,… FROM 表名 WHERE 条件表达式 ORDER BY 列名 排列方式;
    注:排序方式关键字ASC(升序)/DESC(降序)
  1. 在Qt应用程序中使用Sqlite数据库(QT += sql)
  • 建立Qt应用程序和数据库连接 QSqlDatabase
    //添加数据库驱动
    QSqlDatabase db = QSqlDatabase: :addDatabase (“OSQLITE”‘);
    //设置数据库名字(对于sqlite就是数据库文件名)
    db.setDatabaseName ( “testDB.db” ) ;
    //打开数据库
    bool ok = db.open () ;

  • 执行SQL语句 QSqlQuery
    //创建QsqlQuery对象
    QsqlQuery query;
    //准备要执行的sQL语句字符串
    QString str = Qstring ( “SQL语句”);//执行sql语句
    query.exec (str) ;

  • 保存查询结果集 QSqlQueryModel
    //创建QSqlQueryModel对象
    QSqlQueryModel *model = new QSqlQueryModel;1l执行SELECT语句并保存结果集到model
    model->setQuery ( “SELECT语句”);
    /将model保存结果显示图形控件(QTableView)上QTableview *view = new QTableview ;
    view->setModel (model) ;
    view->show () ;

案例:学生成绩管理系统

  • 工程名:Student
  • 类名:StudentDailog
  • .pro文件中QT += sql
  • 界面设计
    拖拽需要使用的控件
    ComboBox(2个,组合框)
    PushButton(4个)
    Label(3个)
    LineEdit(3个)
    TableView(1个,表格:用于显示查询结果集)
    设置对象名
    ComboBox:valueComboBox condComboBox
    PushButton:sortButton insertButton updateButton deleteButton
    LineEdit:idEdit nameEdit scoreEdit
    Label:默认
    TableView:默认

十一、Qt网络编程

  1. 网络编程基础
  • 网络协议模型(OSI七层)
    应用层:HTTP
    表示层
    会话层
    传输层:TCP、UDP(TCP安全、UDP效率高)
    网络层:IP(IPV4/IPV6)
    数据链路层
    物理层
  1. IP地址
  • 概念:
    互联网中的唯一地址标识
  • IP地址表示方式:
    IPV4(32位整数)、IPV6(128位整数)
    点分十进制:“192.168.15.100”
    无符号整数:0xC0A80F64
  • 查看IP地址命令
    windows系统:ipconfig
    linux系统:ifconfig
  • 通过IP地址判断两台主机能否通信
    ping 对方的IP地址;
  • 特殊的IP地址
    “0.0.0.0”//任意地址,INADDR_ANY,常用于服务器
    “127.0.0.1”//本地环回地址,常用于测试
    “255.255.255.255”//广播地址
  1. Qt中和网络编程相关类(Qt+= network)
  • 套接字基类:QAbstractsocket
  • TCP通信套接字:Tcpsocket
  • TCP服务器:TcpServer
  • UDP通信套接字:QUdpsocket
  • IP地址:QHostAddress
  1. 常见问题
  • windows空闲端口号查询方式
    如下图所示,在cmd中使用指令查询所有端口号
    查询所有端口号
    如下图所示,在cmd中使用指令查询所需端口号是否为空闲状态
    查询端口号是否空闲

qt同时运行多个实例时,需要将工具-选项-构建和运行中的stop application before building设置为None
qt同时运行多个实例

案例:基于TCP协议的网络聊天室

  • TCP服务器(Server)(服务器:IP、port固定)
    使用QTcpServer创建服务器
    响应客户端连接请求,保存和客户端通信套接字(socket)
    实时接收客户端发来的消息
    转发消息给所有的客户端

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef SERVERDIALOG_H
#define SERVERDIALOG_H

#include <QDialog>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QTimer>

namespace Ui {
class ServerDialog;
}

class ServerDialog : public QDialog
{
Q_OBJECT

public:
explicit ServerDialog(QWidget *parent = nullptr);
~ServerDialog();

private slots:
void on_pushButton_clicked(); //创建服务器按钮对应的槽函数
void onNewConnection(); //响应客户端连接请求的槽函数
void onReadyRead(); //接收客户端聊天消息的槽函数
void sendMessage(const QByteArray&); //转发聊天消息给其他客户端的槽函数
void onTimeout(void); //定时器检查客户端套接字是否为正常连接状态的槽函数
private:
Ui::ServerDialog *ui;
QTcpServer tcpServer; //TCP服务器
quint16 port; //服务器端口,quint16-->unsigned short
QList<QTcpSocket*>tcpClientList; //容器:保存和客户端通信的套接字
QTimer timer; //定时器,定时检查容器中和客户端通信的套接字是否为正常连接状态
};

#endif // SERVERDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include "serverdialog.h"
#include "ui_serverdialog.h"

ServerDialog::ServerDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ServerDialog)
{
ui->setupUi(this);
}

ServerDialog::~ServerDialog()
{
delete ui;
}
//创建服务器按钮对应的槽函数
void ServerDialog::on_pushButton_clicked()
{
port = ui->lineEdit->text().toUShort(); //获取服务器端口
//设置监听服务器的IP和端口;QHostAddress::Any==>QHostAddress("0.0.0.0")
if(tcpServer.listen(QHostAddress::Any,port)==false){
qDebug() << "创建服务器失败";
return;
}
else {
qDebug() << "创建服务器成功";
//当有客户端向服务器发送连接请求时,发送信号newConnection
connect(&tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
ui->lineEdit->setEnabled(false); //禁用端口输入和创建服务器的按钮
ui->pushButton->setEnabled(false);

connect(&timer,SIGNAL(timeout()),this,SLOT(onTimeout())); //定时器到时发送信号:timeout
timer.start(3000); //开启定时器,每隔3秒检查一次
}
}
//响应客户端连接请求的槽函数
void ServerDialog::onNewConnection(){
QTcpSocket* tcpClient = tcpServer.nextPendingConnection(); //获取和客户端通信的套接字
tcpClientList.append(tcpClient); //保存套接字到容器中
//当客户端给服务器发送消息时,tcpClient发送信号readyRead
connect(tcpClient,SIGNAL(readyRead()),this,SLOT(onReadyRead()));
}
//接收客户端聊天消息的槽函数
void ServerDialog::onReadyRead(){
//遍历检查哪个客户端有消息
for (int i=0;i<tcpClientList.size();i++) {
//at(i):获取容器中第i套接字(QTcpSocket*)
//bytesAvailable:获取当前套接字等待读取消息的字节数,如果为0表示没有消息,如果大于0表示有消息
if(tcpClientList.at(i)->bytesAvailable()){
QByteArray buf = tcpClientList.at(i)->readAll(); //读取消息并保存
ui->listWidget->addItem(buf); //显示消息
ui->listWidget->scrollToBottom(); //回滚到最底部(最新消息)
sendMessage(buf); //转发消息给其他客户端
}
}
}
//转发聊天消息给其他客户端的槽函数
void ServerDialog::sendMessage(const QByteArray& msg){
for (int i=0;i<tcpClientList.size();i++) {
tcpClientList.at(i)->write(msg);
}
}
void ServerDialog::onTimeout(void){
for (int i=0;i<tcpClientList.size();i++) {
//state():获取第i个套接字的连接状态
//UnconnectedState:表示未连接(断开连接)
if(tcpClientList.at(i)->state() == QAbstractSocket::UnconnectedState){
tcpClientList.removeAt(i);//从容器中将断开连接的套接字移除
--i;
}
}
}
  • TCP客户端(Client)
    使用QTcpSocket创建和服务器通信的套接字
    向服务器发送连接请求
    输入聊天消息,发送到服务器
    接收服务器转发的消息并显示

代码实现
头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef CLIENTDIALOG_H
#define CLIENTDIALOG_H

#include <QDialog>
#include <QTcpSocket>
#include <QHostAddress>
#include <QMessageBox>

namespace Ui {
class ClientDialog;
}

class ClientDialog : public QDialog
{
Q_OBJECT

public:
explicit ClientDialog(QWidget *parent = nullptr);
~ClientDialog();

private slots:
void on_sendButton_clicked(); //发送按钮对应的槽函数
void on_connectButton_clicked(); //连接按钮对应的槽函数
void onConnected(void); //和服务器连接成功时执行的槽函数
void onDisconnected(void); //和服务器断开连接时执行的槽函数
void onReadyRead(void); //接收服务器转发聊天消息的槽函数
void onError(void); //网络通信异常时执行的槽函数
private:
Ui::ClientDialog *ui;
bool status; //客户端状态标记,true在线,false离线
QTcpSocket tcpSocket; //和服务器通信的tcp套接字
QHostAddress serverIp; //服务器地址
quint16 serverPort; //服务器端口
QString username; //聊天室昵称

};

#endif // CLIENTDIALOG_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include "clientdialog.h"
#include "ui_clientdialog.h"

ClientDialog::ClientDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ClientDialog)
{
ui->setupUi(this);
status = false;
connect(&tcpSocket,SIGNAL(connected()),this,SLOT(onConnected())); //和服务器连接成功时,发送connected
connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(onDisconnected())); //和服务器断开连接时,发送disconnected
connect(&tcpSocket,SIGNAL(readyRead()),this,SLOT(onReadyRead())); //收到聊天消息时,发送readyRead
connect(&tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(onError())); //网络通信异常时,发送error
}

ClientDialog::~ClientDialog()
{
delete ui;
}
//发送按钮对应的槽函数
void ClientDialog::on_sendButton_clicked()
{
QString msg = ui->messageEdit->text(); //获取用户输入的消息
if(msg == ""){
return;
}
msg = username + ":" + msg;
tcpSocket.write(msg.toUtf8()); //发送消息
ui->messageEdit->clear(); //清空已输入的消息
}
//连接按钮对应的槽函数
void ClientDialog::on_connectButton_clicked()
{
if(status == false){//如果当前是离线状态,则连接服务器
if(serverIp.setAddress(ui->serveripEdit->text()) == false){ //获取服务器IP
QMessageBox::critical(this,"Error","IP地址错误"); //critical()表示错误的消息提示框
return;
}
serverPort = ui->serverportEdit->text().toUShort(); //获取服务器端口
if(serverPort < 1024){
QMessageBox::critical(this,"Error","端口格式错误");
return;
}
username = ui->usernameEdit->text(); //获取聊天室昵称
if(username == ""){
QMessageBox::critical(this,"Error","聊天室昵称不能为空");
return;
}
//向服务器发送连接请求
//如果成功,发送信号connected
//如果失败,发送信号error
tcpSocket.connectToHost(serverIp,serverPort);
}
else { //如果当前是在线状态,则断开和服务器连接
QString msg = username + ":离开了聊天室"; //向服务器发送离开聊天室的消息
tcpSocket.write(msg.toUtf8()); //toUtf8():将QString(unicode)转换QByteArray(utf-8)
tcpSocket.disconnectFromHost(); //断开连接,断开后发送信号:disconnected
}
}
//和服务器连接成功时执行的槽函数
void ClientDialog::onConnected(void){
status = true; //设置状态标记:在线
ui->sendButton->setEnabled(true); //恢复"发送"按钮为正常可用状态
ui->serveripEdit->setEnabled(false); //禁用ip输入
ui->serverportEdit->setEnabled(false); //禁用端口输入
ui->usernameEdit->setEnabled(false); //禁用昵称输入
ui->connectButton->setText("离开聊天室"); //修改连接服务器按钮文本
QString msg = username + ":进入了聊天室"; //向服务器发送进入聊天室的提示消息
tcpSocket.write(msg.toUtf8()); //toUtf8():将QString(unicode)转换QByteArray(utf-8)
}
//和服务器断开连接时执行的槽函数
void ClientDialog::onDisconnected(void){
status = false; //设置离线状态
ui->sendButton->setEnabled(false); //禁用"发送"按钮
ui->serveripEdit->setEnabled(true); //恢复ip输入
ui->serverportEdit->setEnabled(true); //恢复端口输入
ui->usernameEdit->setEnabled(true); //恢复昵称输入
ui->connectButton->setText("连接服务器"); //修改离开聊天室按钮文本
}
//接收服务器转发聊天消息的槽函数
void ClientDialog::onReadyRead(void){
if(tcpSocket.bytesAvailable()){ //bytesAvailable()获取等待读取消息的字节数
QByteArray buf = tcpSocket.readAll(); //读取消息
ui->listWidget->addItem(buf); //显示消息到界面
ui->listWidget->scrollToBottom();
}
}
//网络通信异常时执行的槽函数
void ClientDialog::onError(void){
QMessageBox::critical(this,"Error",tcpSocket.errorString()); //errorString()获取网络通信异常原因的字符串
}

效果展示
聊天界面展示