Qt筆記
本筆記適用于Qt5,教材來源為嗶哩嗶哩Up主“愛編程的大丙”,視頻地址:https://www.bilibili.com/video/BV1Jp4y167R9?p=31&spm_id_from=pageDriver
## Qt下載和安裝
Qt在其官網即可下載,下載鏈接為:`http://download.qt.io/archive/qt/`,可根據自己的系統版本下載對應的安裝包。
下載完成之后,在Linux系統中,首先要將安裝包權限改為可執(zhí)行文件:
```powershell
sudo chomd +x xxxx.run
```
然后運行安裝命令進行安裝:
```powershell
./xxxx.run
```
運行之后會彈出安裝界面,會提示需要賬號和密碼,因此需要提前去官網注冊好,輸入完成之后一路下一步即可,此處不再贅述。
## Qt工程目錄及含義
### Qt工程創(chuàng)建基本流程
安裝完成后打開Qt,在File選項中選擇新建文件或工程,然后選擇新建工程中的Project->Application,選擇Qt Widgets Application選項,如下圖所示:

然后點擊choose進入下一步,設置項目名稱和存儲地址,進入下一步,Build system選擇qmake,Classname設定之后繼續(xù)下一步,剩下的一路下一步即可,其他的都不用操作。
Qt工程目錄分為四個部分:
- project文件:描述Qt項目的文件及依賴關系
- Headers:QT項目中的頭文件
- Sources:Qt項目中的源文件
- Forms:Qt項目中的ui文件,其可用QtDesinger打開編輯
## 信號和槽
### 概述
所謂信號槽,實際就是ROS的發(fā)布者和訂閱者的模式,分為信號和槽兩部分。
當某個事件發(fā)生之后對應的對象發(fā)出特定信號(Signal),這個信號沒有明確的接收者,所有接收到這個信號的觀察者只要需要都可以通過connect函數對這個信號做出響應,將想要處理的信號和自己的一個函數(稱為槽,slot)綁定來處理這個信號,也就是說**當信號發(fā)出時,被連接的槽函數會自動被回調**,這就類似觀察者模式:當感興趣的事發(fā)生時,某一個操作就會被自動觸發(fā)。
### 信號槽本質
#### 信號本質
信號是由于用戶對窗口或控件進行了某項操作,導致窗口或控件產生了某個特定事件,這時Qt對應的窗口類會發(fā)出某個信號,以此對用戶的操作做出反應。
因此根據上述的描述我們得到一個結論:信號的本質就是事件。比如:
- 單擊、雙擊按鈕
- 窗口刷新
- 鼠標移動、按下、釋放
- 鍵盤輸入
那么在Qt中信號通過什么方式呈獻給用戶呢?
- 我們對哪個窗口進行操作,該窗口就可以捕捉到這些被觸發(fā)的事件
- 對于使用者來說觸發(fā)了一個事件我們就可以得到Qt框架給我們發(fā)出的某個特定信號
- 信號的呈現形式就是函數,也就是說某個事件產生了,Qt框架就會調用某個對應的信號函數,通知使用者。
在Qt中信號的發(fā)出者是**某個實例化的類對象**,對象內部可以進行相關事件的檢測。
#### 槽本質
在Qt中槽是一種特殊的功能函數,在編碼中,也可以作為類的普通函數來使用。之所以稱為槽函數,是因為他們還有一個職責就是對Qt框架中產生的信號進行處理。
例如:
女朋友(一種不存在的東西)說:“我餓了”,于是我?guī)コ燥?/p>
上面例子中相當于女朋友發(fā)出了一個信號,我收到了信號并將其處理掉了。
- 女朋友->發(fā)送信號的對象
- 信號內容:我餓了
- 我->接收信號的對象
- 處理方式:帶她去吃飯
在Qt中槽函數的所有者也是**某個類的實例對象。**
#### 信號和槽的關系
在Qt中信號和槽都是獨立的個體,本身沒有任何聯系,但由于某種特性需求我們可以將二者連接到一起,好比牛郎和織女需要喜鵲為他們搭橋一樣。在Qt中我們需要QObject中的connect函數進行二者的鏈接。
connect函數的函數原型:
```C++
QMetaObject::Connection QObject::connect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
```
參數:
- sender:發(fā)出信號的對象
- signal:屬于sender對象,信號是一個函數,**這個參數的類型是函數指針,指向信號函數的地址**
- receiver:接收信號的對象
- method:屬于receiver對象,當檢測到sender發(fā)出了signal信號,receiver對象調用method方法,進行信號發(fā)出之后的處理動作
注意事項:
- connect函數相當于對信號處理動作的注冊;
- 調用connect函數的sender對象的信號并沒有馬上產生,因此receiver對象的method函數也不會馬上被調用;
- method槽函數的本質是一個回調函數,調用的時機是信號產生之后,調用是Qt框架來執(zhí)行的;
- connect中的sender和receiver兩個指針必須被實例化了,否則connect不會成功
函數原型精簡之后可以寫作:
```C++
connect (
const QObject *sender,const &QObject::signal,
const QObject *receiver,const &QObject::method
)
```
## 標準信號槽使用
### 標準信號/槽
Qt提供的很多標準類中紅都可以對用戶觸發(fā)的某些信號進行檢測,因此當用戶做了這些操作以后,事件被觸發(fā),類的內部就會產生對應的信號,這些信號都是Qt類內部自帶的,因此稱之為標準信號。
同樣的,在Qt的很多類內部為我們提供了很多功能函數,并且這些功能函數也可以作為觸發(fā)的信號的處理動作,有這類性質的函數在Qt中稱之為標準槽函數。
系統自帶的信號和槽如何查找呢?可以通過幫助文檔,如下圖所示:
#### 使用舉例
實現點擊按鈕關閉窗口的功能。
設計對象:按鈕和窗口。
- 按鈕發(fā)出點擊信號->`QPushbutton`,`QPushbutton::clicked`
- 窗口接收點擊信號并進行處理->`this`,`QMainwindow::close`
首先在創(chuàng)建的工程中的ui文件夾Forms中雙擊mainwindow.ui進行編輯,在其中添加一個按鈕:

在其中修改兩個部分:
1. PushButton的名字,
2. PushButton的objectName選項
修改之后的界面如下圖所示:

然后對source中的mainwindow.cpp文件進行編輯,加入如下代碼片段
```C++
connect(ui->closeButton,&QPushButton::clicked,this,&QMainWindow::close);
//信號發(fā)出對象為ui->closeButtopn按鈕,發(fā)送的信號為&QPushButton::clicked
//信號接收對象為this,也就是本窗口,接收信號后的處理槽函數為&QMainWindow::close
//注意,connect函數中的信號函數和槽函數參數分別是這兩個函數的地址。
```
然后編譯程序并運行,其界面如下:

### 自定義信號/槽
Qt框架提供的信號槽在某些特定的場景下無法滿足我們的項目需求,因此我們還需要設計自己需要的信號和槽,同樣還是使用connect函數對自定義的信號槽進行鏈接
如果要使用自己定義的信號槽,首先要編寫新的類并讓其繼承Qt的某些標準類,如果我們自己編寫的類想要在Qt中使用信號槽機制,那么必須要滿足如下條件:
- 這個類必須從QObject類或者是其子類進行派生
- 在自定義的頭文件中加入Q_OBJECT宏
在頭文件派生的時候,首先像下面那樣引入Q_OBJECT宏:
```C++
class MyMainWindow : public QWidget
{
? ? Q_OBJECT
? ? ......
}
```
#### 自定義信號
自定義信號的要求:
- 信號是類的成員函數
- 返回值必須是void類型
- 信號的名字可以根據實際情況進行指定
- 信號可以隨意指定,信號也支持重載
- 信號需要使用signal關鍵字進行生命,使用方法類似于public等關鍵字
- 信號函數只需要生命,不需要定義
- 在程序中發(fā)送自定義信號:發(fā)從信號的本質就是調用信號函數,習慣性在信號前面加上關鍵字emit,emit提示這是在發(fā)送信號,沒有其他含義和功能
舉例:信號重載
Qt中的類想要使用信號槽機制必須要從QObject類派生(直接或間接派生都可以)
```C++
class test:public QObject
{
? ? Q_OBJECT
? ? signals:
? ? ? ? void testsignal();
? ? ? ? void testsignal(int a); //重載
};
```
#### 自定義槽
槽就是信號的處理函數,自定義槽和自定義其他函數一樣,沒什么區(qū)別
自定義槽的要求:
- 返回值是void類型
- 槽也是函數,也支持重載:
? - 槽函數需要指定多少個參數,需要看鏈接的信號的參數個數
? - 槽函數的參數是用來接收信號發(fā)送的數據的,信號發(fā)送的數據就是信號的參數。例如:
? ? - 信號函數:`void testsig(int a , double b)`
? ? - 槽函數:`void testslot(int a , double b)`
? - 總結:
? ? - 槽函數的參數應該和對應的信號的參數個數和數據類型一一對應
? ? - 信號的參數可以大于等于槽函數的參數個數,信號傳遞的數據被忽略了
? ? ? - 信號函數:void testsig(int a, double b);
? ? ? - 槽函數:void testslot(int a);
- Qt中槽函數的類型:
? - 類的成員函數
? - 全局函數
? - 靜態(tài)函數
? - labmda表達式(匿名函數
- 槽函數可以使用關鍵字進行聲明:slots(Qt5中可以忽略不寫)
? - public slots
? - private slots
? - protected slots
舉例:類中的這三個函數都可以作為槽函數來使用:
```C++
class Test:public QObject
{
? ? public:
? ? ? ? void testSlot();
? ? ? ? static void testFunc();
? ? public slots:
? ? ? ? void testSlots(int id);
};
```
場景舉例:女朋友餓了 我請他吃飯
```c++
class GirlFriend;
class Me;
```
創(chuàng)建自定義類流程:
1. 首先在項目名右鍵點擊Add New,在彈出的窗口選擇C++類,
2. 在define class 中填入類名并選擇父類QObject:

3. 最后點擊下一步完成創(chuàng)建。
創(chuàng)建完成之后首先修改GirlFriend類,在頭文件中的signals中添加函數hungry:
```c++
//GirlFriend.h
#ifndef GIRLFRIEND_H
#define GIRLFRIEND_H
#include <QObject>
class GirlFriend : public QObject
{
? ? Q_OBJECT
public:
? ? explicit GirlFriend(QObject *parent = nullptr);
signals:
? ? void hungry();
};
#endif // GIRLFRIEND_H
```
此處需注意,**寫在signals里的hungry函數不需要定義,只需要聲明就好了**。
然后在Me類中添加處理動作,編輯Me.h,在其中加入處理動作(槽函數)聲明:
```c++
public slots:
? ? void feed();
```
在Me.cpp文件中添加槽函數的定義:
```c++
void Me::feed()
{
? ? qDebug()<<"let's go for food!"<<endl;
}
```
編輯完成之后的文件如下:
```c++
//Me.h
#ifndef ME_H
#define ME_H
#include <QObject>
class Me : public QObject
{
? ? Q_OBJECT
public:
? ? explicit Me(QObject *parent = nullptr);
public slots:
? ? void feed();
};
#endif // ME_H
```
```C++
//Me.cpp
#include "me.h"
#include <QDebug>
Me::Me(QObject *parent) : QObject(parent)
{
}
void Me::feed()
{
? ? qDebug()<<"let's go for food!"<<endl;
}
```
最后在窗口中用connect來鏈接這兩個對象。
首先在頭文件中添加這兩個對象:
```C++
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "me.h"
#include "girlfriend.h"
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
? ? Q_OBJECT
public:
? ? MainWindow(QWidget *parent = nullptr);
? ? ~MainWindow();
private:
? ? Ui::MainWindow *ui;
? ? Me *m_me;
? ? GirlFriend *m_girl;
};
#endif // MAINWINDOW_H
```
最后在CPP文件中鏈接二者:
```C++
connect(m_girl,&GirlFriend::hungry,m_me,&Me::feed);
```
此時有一個問題,hungry信號是自定義的,框架無法直接自動發(fā)送,因此需要設計一些觸發(fā)機制觸發(fā)hungry信號,這里使用按鈕觸發(fā)信號。
打開ui文件,添加新的按鈕如下:

并在mainwindow文件中添加按鈕和hungry信號的鏈接:
```C++
connect(ui->hungryButton,&QPushButton::clicked,this,&MainWindow::hungry_slot);
```
信號發(fā)出者為按鈕,發(fā)出信號為按鈕按下,信號接受者為m_girl,接收信號后的處理函數為&GirlFriend::hungry,即觸發(fā)hungry函數。
其中`MainWindow::hungry_slot`函數為自定義的槽函數,其作用是使m_girl對象發(fā)送hungry信號:
```c++
void MainWindow::hungry_slot()
{
? emit m_girl->hungry();//加不加emit沒區(qū)別,唯一的作用就在于提示程序員現在是在發(fā)送信號
}
```
### 信號槽拓展
#### 信號槽使用拓展
1. 一個信號可以鏈接多個槽函數,發(fā)送一個信號可以有多個處理動作
? - 需要寫多個connect函數
? - 槽函數的執(zhí)行順序是隨機的,和connect函數的調用順序沒有關系
? - 信號的接收者可以使一個對象,也可以是多個對象
2. 一個槽可以連接多個不同的信號
3. 信號可以連接信號,
? - 信號接收者可以不處理接收的信號,繼續(xù)發(fā)出新的信號,其傳遞了參數,但并未對其進行處理。
```C++
connect(m_girl,&GirlFriend::hungry,m_me,&Me::feed);
connect(ui->hungryButton,&QPushButton::clicked,this,&MainWindow::hungry_slot);
```
精簡為:
```C++
connect(ui->hungryButton,&QPushButton::clicked,m_girl,&GirlFriend::hungry);
```
4. 信號槽是可以斷開的
```C++
disconnect (
const QObject *sender,const &QObject::signal,
const QObject *receiver,const &QObject::method
)
```
斷開信號槽和鏈接信號槽除了函數名不同其他完全相同。
#### 信號槽的兩種連接方式
1. Qt5的連接方式:推薦的使用方式
? ```C++
? connect(ui->hungryButton,&QPushButton::clicked,m_girl,&GirlFriend::hungry);
? ```
2. Qt4的連接方式:不推薦的使用方式
? ```C++
? connect(ui->hungryButton,SIGNAL(QPushButton::clicked()),m_girl,SLOT(GirlFriend::hungry()));
? ```
### 信號槽之間如何傳遞信號
Qt5信號和槽的連接方式如下:
```C++
connect(ui->hungryButton,&QPushButton::clicked,m_girl,&GirlFriend::hungry);
```
這里面看不到信號是如何傳遞的,那兩個函數的信號如何傳遞呢?是通過兩個函數的參數來傳遞的。
如,信號函數為:
```c++
void hungry(QString msg);
```
則槽函數可以寫為:
```c++
void eat(QString msg);
```
然后在觸發(fā)信號函數中使用函數`hungry("XXXX")`,框架會將`"XXXX"`從hungry函數傳遞到eat函數,實現參數傳遞。
這里存在一個問題,如果是Qt4的鏈接方式,鏈接函數可寫為:
```C++
connect(m_girl,SIGNAL(GirlFriend::hungry(QString)),m_me,SLOT(Me::feed(String)));
```
這種調用方式可以明確的指出調用的是函數的哪個重載類型,這里就可以直接運行了。
但是對于QT5的連接方式,其鏈接函數寫為:
```C++
connect(m_girl,GirlFriend::hungry,m_me,Me::feed);
```
這種情況會報錯,因為編譯器搞不清楚你到底想調用signal和slot函數的哪個重載類型,針對這個問題有兩個解決方法:
1. 用Qt4的調用方法;
2. 創(chuàng)建函數指針,指明到底要采用哪個函數重載類型。
一般選擇第二種方法,其代碼如下:
```C++
void (GirlFriend::*girl_1)()=&GirlFriend::hungry;
void (GirlFriend::*girl_2)(QString msg)=&GirlFriend::hungry;
void (Me::*feed_1)()=&Me::feed;
void (Me::*feed_2)(QString msg)=&Me::feed;
connect(m_girl,girl_1,m_me,feed_1);
connect(m_girl,girl_2,m_me,feed_2);
```
## QT界面與ROS鏈接