一、绘图和绘图设备
QPainter
QPainter用来执行绘制的操作;QPaintDevice是一个二维空间的抽象,QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口
上面的示意图告诉我们,Qt 的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。
下面我们通过一个实例来介绍QPainter的使用:
class PaintedWidget : public QWidget { Q_OBJECT public: PaintedWidget(QWidget *parent = 0); protected: void paintEvent(QPaintEvent *); }
注意我们重写了QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:
PaintedWidget::PaintedWidget(QWidget *parent) : QWidget(parent) { resize(800, 600); setWindowTitle(tr("Paint Demo")); } void PaintedWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.drawLine(80, 100, 650, 500); painter.setPen(Qt::red); painter.drawRect(10, 10, 100, 400); painter.setPen(QPen(Qt::green, 5)); painter.setBrush(Qt::blue); painter.drawEllipse(50, 150, 400, 200); }
构造函数中,我们仅仅设置了窗口的大小和标题。而paintEvent()函数则是绘制的代码。
绘图设备
绘图设备是指继承QPainterDevice的子类。
QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。
QPixmap:和平台无关,针对屏幕进行了优化了,不能对图片进行修改
QImage:和平台无关,在线程中绘图,可以对图片进行优化
QPicture:保存绘图的状态(二进制文件)
1、QPixmap -> QImage QPixmap a; a.toImage(); 2、QImage -> QPixmap QImage b; QPixmap::fromImage(b); 3、QPainter QPainter p; QPicture pic; p.begin(&pic); //绘图动作 p.end(); pic.save("路径"); 4、加载图片 QPicture temp; temp.load("路径");
不规则窗口
1、给窗口画一张背景图
2、去表框
3、设定属性(背景透明)
4、移动坐标是相对于屏幕而言
二、文件系统
I/O 设备的类图(Qt5):
- QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
- QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
- QFlie:访问本地文件或者嵌入资源;
- QTemporaryFile:创建和访问本地文件系统的临时文件;
- QBuffer:读写QbyteArray, 内存文件;
- QProcess:运行外部程序,处理进程间通讯;
- QAbstractSocket:所有套接字类的父类;
- QTcpSocket:TCP协议网络数据传输;
- QUdpSocket:传输 UDP 报文;
- QSslSocket:使用 SSL/TLS 传输数据;
QFile提供了从文件中读取和写入数据的能力。
我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改
我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取。
QFileInfo有很多类型的函数,举一些常用例子。比如:
- isDir()检查该文件是否是目录;
- isExecutable() 检查该文件是否是可执行文件等。
- baseName() 可以直接获得文件名;
- completeBaseName() 获取完整的文件名
- suffix() 则直接获取文件后缀名。
- completeSuffix() 获取完整的文件后缀
文件
1、QFile file; file.setFileName(); file.open(); file.write(); file.read(); file.close(); 2、QFileInfo info; info.size(); info.fileName(); QDataStream:二进制方式 QTextStream:文本方式(指定编码) QBuffer:内存文件(内容放在内存)
三、Socket通信
Qt中提供的所有的Socket类都是非阻塞的。
Qt中常用的用于socket通信的套接字类:
- QTcpServer 用于TCP通信, 作为服务器端套接字使用
- QTcpSocket 用于TCP通信,作为客户端套接字使用。
- QUdpSocket 用于UDP通信,服务器,客户端均使用此套接字。
TCP
在Qt中实现TCP服务器端通信的流程:
- 创建套接字
- 将套接字设置为监听模式
- 等待并接受客户端请求
可以通过QTcpServer提供的void newConnection()_信号*来检测是否有连接请求,如果有可以在对应的槽函数中调用nextPendingConnection函数获取到客户端的Socket信息(返回值为QTcpSocket_类型指针),通过此套接字与客户端之间进行通信。
- 接收或者向客户端发送数据
接收数据:使用read()或者readAll()函数
发送数据:使用write()函数
在Qt中实现TCP/IP客户端通信的流程:
- 创建套接字
- 连接服务器
可以使用QTcpSocket类的connectToHost()函数来连接服务器。
- 向服务器发送或者接受数据
下面例子为简单的TCP/IP通信的实现例子:
TCP服务端
//---------- tcpserver.h ------------ class TCPServer : public QMainWindow { Q_OBJECT public: explicit TCPServer(QWidget *parent = 0); ~TCPServer(); public slots: void slotNewConnection(); void slotReadyRead(); private: Ui::TCPServer *ui; // 负责监听的套接字 QTcpServer* m_server; // 负责通信的套接字 QTcpSocket* m_client; }; //---------- tcpserver.cpp ------------ TCPServer::TCPServer(QWidget *parent) : QMainWindow(parent), ui(new Ui::TCPServer), m_server(NULL), m_client(NULL) { ui->setupUi(this); //创建套接字对象 m_server = new QTcpServer(this); //将套接字设置为监听模式 m_server->listen(QHostAddress::Any, 9999); //通过信号接收客户端请求 connect(m_server, &QTcpServer::newConnection, this, &TCPServer::slotNewConnection); } TCPServer::~TCPServer() { delete ui; } void TCPServer::slotNewConnection() { if(m_client == NULL) { //处理客户端的连接请求 m_client = m_server->nextPendingConnection(); //发送数据 m_client->write("服务器连接成功!!!"); //连接信号, 接收客户端数据 connect(m_client, &QTcpSocket::readyRead, this, &TCPServer::slotReadyRead); } } void TCPServer::slotReadyRead() { //接收数据 QByteArray array = m_client->readAll(); QMessageBox::information(this, "Client Message", array); }
TCP客户端
//------------- tcpclient.h ------------ class TCPClient : public QMainWindow { Q_OBJECT public: explicit TCPClient(QWidget *parent = 0); ~TCPClient(); public slots: void slotReadyRead(); void slotSendMsg(); private: Ui::TCPClient *ui; QTcpSocket* m_client; }; //------------- tcpclient.cpp -------------- TCPClient::TCPClient(QWidget *parent) : QMainWindow(parent), ui(new Ui::TCPClient) { ui->setupUi(this); //创建套接字 m_client = new QTcpSocket(this); //连接服务器 m_client->connectToHost(QHostAddress("127.0.0.1"), 9999); //通过信号接收服务器数据 connect(m_client, &QTcpSocket::readyRead, this, &TCPClient::slotReadyRead); //发送按钮 connect(ui->btnSend, &QPushButton::clicked, this, &TCPClient::slotSendMsg); } TCPClient::~TCPClient() { delete ui; } void TCPClient::slotReadyRead() { //接收数据 QByteArray array = m_client->readAll(); QMessageBox::information(this, "Server Message", array); } void TCPClient::slotSendMsg() { QString text = ui->textEdit->toPlainText(); //发送数据 m_client->write(text.toUtf8()); ui->textEdit->clear(); }
UDP
在UDP方式下,客户端并不与服务器建立连接,它只负责调用发送函数向服务器发送数据。类似的服务器也不从客户端接收连接,只负责调用接收函数,等待来自客户端的数据的到达
在UDP通信中,服务器端和客户端的概念已经显得有些淡化,两部分做的工作都大致相同:、
- 创建套接字
- 绑定套接字
在UDP中如果需要接收数据则需要对套接字进行绑定,只发送数据则不需要对套接字进行绑定。
通过调用bind()函数将套接字绑定到指定端口上。
- 接收或者发送数据
- 接收数据:使用readDatagram()接收数据,函数声明如下:
qint64 readDatagram(char * data, qint64 maxSize, QHostAddress address = 0, quint16 port = 0)
参数:
data: 接收数据的缓存地址
maxSize: 缓存接收的最大字节数
address: 数据发送方的地址(一般使用提供的默认值)
port: 数据发送方的端口号(一般使用提供的默认值)
使用**pendingDatagramSize()可以获取到将要接收的数据的大小**,根据该函数返回值来准备对应大小的内存空间存放将要接收的数据。
* 发送数据: 使用writeDatagram()函数发送数据,函数声明如下:
qint64 writeDatagram(const QByteArray & datagram, const QHostAddress & host, quint16 port)
参数:
datagram:要发送的字符串
host:数据接收方的地址
port:数据接收方的端口号
广播
在使用QUdpSocket类的writeDatagram()函数发送数据的时候,其中第二个参数host应该指定为广播地址:QHostAddress::Broadcast此设置相当于QHostAddress("255.255.255.255")
使用UDP广播的的特点:
* 使用UDP进行广播,局域网内的其他的UDP用户全部可以收到广播的消息
* UDP广播只能在局域网范围内使用
组播
在使用QUdpSocket类的writeDatagram()函数发送数据的时候,其中第二个参数host应该指定为组播地址,关于组播地址的分类:
* 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
* 224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;
* 224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
* 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
注册加入到组播地址需要使用QUdpSocket类的成员函数:
bool joinMulticastGroup(const QHostAddress & groupAddress)
四、多线程的使用
在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。
例子中,信号由主线程的QTimer对象发出,之后Qt会将关联的事件放到worker所属线程的事件队列。由于队列连接的作用,在不同线程间连接信号和槽是很安全的。
示例代码如下:
class Worker : public QObject { Q_OBJECT private slots: void onTimeout() { qDebug()<<"Worker::onTimeout get called from?: " < } }; int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug()<<"From main thread: "< QThread t; QTimer timer; Worker worker; QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout())); // 启动定时器 timer.start(1000); // 将类对象移交个线程 worker.moveToThread(&t); // 启动线程 t.start(); return a.exec(); }
关于Qobject类的connect函数最后一个参数,连接类型:
* 自动连接(AutoConnection),默认的连接方式。
如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;
如果发送者与接受者处在不同线程,等同于队列连接。
* 直接连接(DirectConnection)
当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。
* 队列连接(QueuedConnection)
当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行。
总结
* 队列连接:槽函数在接受者所在线程执行。
* 直接连接:槽函数在发送者所在线程执行。
*自动连接:二者不在同一线程时,等同于队列连接
多线程使用过程中注意事项:
* 线程不能操作UI对象(从Qwidget直接或间接派生的窗口对象)
* 需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。
五、Qt数据库操作
操作数据库
Qt 提供了 QtSql 模块来提供平台独立的基于 SQL 的数据库操作。
Qt 使用QSqlDatabase表示一个数据库连接。
可以通过:
//打印Qt支持的数据库驱动 qDebug() << QSqlDatabase::drivers();
找到系统中所有可用的数据库驱动的名字列表。
封装一个连接数据库的函数:
bool connect(const QString &dbName) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // db.setHostName("host"); // db.setDatabaseName("dbname"); // db.setUserName("username"); // db.setPassword("password"); db.setDatabaseName(dbName); if (!db.open()) { QMessageBox::critical(0, QObject::tr("Database Error"), db.lastError().text()); return false; } return true; }
创建数据库表student后,插入数据,然后将其独取出来:
if (connect("demo.db")) { QSqlQuery query; query.prepare("INSERT INTO student (name, age) VALUES (?, ?)"); QVariantList names; names << "Tom" << "Jack" << "Jane" << "Jerry"; query.addBindValue(names); QVariantList ages; ages << 20 << 23 << 22 << 25; query.addBindValue(ages); if (!query.execBatch()) { QMessageBox::critical(0, QObject::tr("Database Error"), query.lastError().text()); } query.exec("SELECT name, age FROM student"); while (query.next()) { QString name = query.value(0).toString(); int age = query.value(1).toInt(); qDebug() << name << ": " << age; } } else { return 1; }
插入多条数据,此时可以使用QSqlQuery::exec()函数一条一条插入数据,但是这里我们选择了另外一种方法:批量执行。首先,我们使用**QSqlQuery::prepare()函数对这条 SQL 语句进行预处理,问号 ? 相当于占位符,预示着以后我们可以使用实际数据替换这些位置。
在上面的代码中,我们使用一个字符串列表 names 替换掉第一个问号的位置,一个整型列表 ages 替换掉第二个问号的位置,利用**QSqlQuery::addBindValue()我们将实际数据绑定到这个预处理的 SQL 语句上**。需要注意的是,names 和 ages 这两个列表里面的数据需要一一对应。然后我们调用**QSqlQuery::execBatch()批量执行 SQL,之后结束该对象。这样,插入操作便完成了。
使用模型操作数据库
基于QSqlTableModel 的模型处理更为高级,如果对 SQL 语句不熟悉,并且不需要很多复杂的查询,这种QSqlTableModel模型基本可以满足一般的需求。
1、查询操作
if (connect("demo.db")) { QSqlTableModel model; model.setTable("student"); model.setFilter("age > 20 and age < 25"); if (model.select()) { for (int i = 0; i < model.rowCount(); ++i) { QSqlRecord record = model.record(i); QString name = record.value("name").toString(); int age = record.value("age").toInt(); qDebug() << name << ": " << age; } } } else { return 1; }
* setTable()函数设置所需要操作的表格;
* setFilter()函数则是添加过滤器,也就是 WHERE 语句所需要的部分。
例如上面代码中的操作实际相当于 SQL 语句:
SELECT * FROM student WHERE age > 20 and age < 25 ```
注意:我们使用QSqlTableModel只能进行 SELECT * 的查询,不能只查询其中某些列的数据。
2、插入操作
QSqlTableModel model; model.setTable("student"); int row = 0; model.insertRows(row, 1); model.setData(model.index(row, 1), "Cheng"); model.setData(model.index(row, 2), 24); model.submitAll();
model.insertRows(row, 1);说明我们想在索引 0 的位置插入 1 行新的数据。使用setData()函数则开始准备实际需要插入的数据。最后,调用submitAll()函数提交所有修改。
例如上面代码中的操作实际相当于 SQL 语句:
INSERT INTO student (name, age) VALUES ('Cheng', 24)
3、更新操作
QSqlTableModel model; model.setTable("student"); model.setFilter("age = 25"); if (model.select()) { if (model.rowCount() == 1) { QSqlRecord record = model.record(0); record.setValue("age", 26); model.setRecord(0, record); model.submitAll(); } }
找到 age = 25 的记录,然后将 age 重新设置为 26,存入相同的位置(在这里都是索引 0 的位置),提交之后完成一次更新
例如上面代码中的操作实际相当于 SQL 语句:
UPDATE student SET age = 26 WHERE age = 25
4、删除操作
QSqlTableModel model; model.setTable("student"); model.setFilter("age = 25"); if (model.select()) { if (model.rowCount() == 1) { model.removeRows(0, 1); model.submitAll(); } }
removeRows()函数可以一次删除多行。
例如上面代码中的操作实际相当于 SQL 语句:
DELETE FROM student WHERE age = 25
如果你觉得文章还不错,记得"点赞关注"
关注我的微信公众号【 加班猿 】可以获取更多内容