QT快速入門

GeoFXR發表於2022-05-06

QT快速入門

本文件將介紹QT工程的建立、UI介面佈局,並以計數器為例瞭解QT中多執行緒的用法,最終完成一個基礎的QT專案。

1 建立QT工程檔案

在安裝好QT之後,能夠在其安裝元件中找到Qt Creator,點選

QT快速入門

設定專案名稱及路徑等,設定支援32位與64位,其他都直接下一步;

QT快速入門

建立完成,專案中包含以下幾個檔案:

QT專案檔案QTTEST.pro,主視窗標頭檔案mainwindow.h,主視窗程式mainwindow.cpp,主函式main.cpp以及視窗UI檔案mainwindow.ui

QT快速入門

我們當然可以直接在QT creator中編完這個工程,但推薦使用更加成熟、穩健的VS完成後續的程式設計與設計。

2 UI介面設計

首先,使用VS開啟新建的.pro檔案,同樣能看到這幾個檔案。在一切正常的情況下,此時點選執行程式會出現一個空白視窗(圖④)。

QT快速入門

隨後根據需求設計窗體介面與佈局,雙擊開啟UI檔案,預設UI操作介面如下:

QT快速入門

其中控制元件區域的所有元件為:

QT快速入門

然後回到主介面,UI設計的主要思路為:將控制元件從工具欄拖拽到主介面中,使介面能夠簡潔明瞭地反映工作流並反饋執行狀態;通過修改顯示名稱、物件屬性、訊號與槽函式使介面與背後的主程式連結。通常一個介面包括輸入、輸出、中間過程、計算、退出等操作,具體例子如下:

QT快速入門

3 訊號與槽函式、連線

(1) 內建的連線方法

以上述介面為例,重點演示Exit功能以及Input data的瀏覽和單行輸入框。

對於最簡單的Exit單擊(click)時只需執行關閉(close)介面即可,如圖:

QT快速入門

流程概括如下:

flowchart LR; Z(選中控制元件)-->A; A(修改屬性名)-->B(新增指定的訊號與槽); B-->C(儲存);

在QT設計師中新增的訊號與槽函式,只需通過簡單的點選即可建立連線。其實質是:

mainwindow.cpp檔案中,能夠看到#include "ui_mainwindow.h"引用了這樣一個標頭檔案,開啟之後,可以找到:

QObject::connect(ExitPushBotton, SIGNAL(clicked()), MainWindow, SLOT(close()));

其底層原理是通過connect單擊Exit按鈕這一訊號關閉介面這個槽函式相關聯,訊號由按鈕PushBotton發射,主視窗MainWindow接收。

(2) 自定義槽函式

期望達到的效果如圖,在點選Browse之後,我們能夠瀏覽檔案目錄並將檔名、路徑填入到前面的文字框中。而在之後的操作中,可以直接從介面上獲取檔案資訊。

QT快速入門

分析這個步驟,即是在單擊(click)按鈕Browse後,彈出選擇檔案/路徑的對話方塊,並將值傳給文字編輯框中顯示。

step1: 修改屬性名

命名的規則為:控制元件功能+控制元件名(如Inputdata+lineEdit),這是為了在後臺呼叫控制元件時能夠快速、準確定位。

QT快速入門

step2: 編寫槽函式

在主視窗標頭檔案中宣告槽函式:

class MainWindow : public QMainWindow
{
    Q_OBJECT
     ...
private slots:
	//3個Browse對應的槽函式
	void inputdataSelect();     //輸入檔案選擇格式
	void outputdataPathSelect(); //輸出檔案路徑選擇
	void waveletFileSelect();  //子波檔案選擇

};        

在mainwindow.cpp檔案中定義槽函式:

// 以輸入和輸出兩個為例,其他的瀏覽可類推
#include <QFileDialog>		//需引入QFileDialog標頭檔案才能使用對話視窗選擇檔案

void MainWindow::inputdataSelect() {   //輸入資料路徑及檔名選擇
	// 檔名將存為QString字串格式
	QString fileNameInput = QFileDialog::getOpenFileName(this,	//getOpenFileName獲取檔名
		tr("Input File"),
		"F:",									 // 預設啟動位置為F盤
		tr("Seismic(*sgy *segy *SEGY);;"));      //建立檔名及路徑選擇對話視窗、支援的格式為segy

	if (fileNameInput.isEmpty() == false) {
		ui->InputdatalineEdit->setText(fileNameInput);   //將選擇輸入資料的檔名路徑填入文字框
	}
}

void MainWindow::outputdataPathSelect(){  //輸出資料的路徑選擇

	QString fileNameInputPath = QFileDialog::getExistingDirectory(this, //getExistingDirectory獲取路徑
		tr("Select Output File Folder"), 
		tr("C:"));   //讀取輸出檔案儲存路徑,只有路徑因此無需預設檔案格式

	if (fileNameInputPath.isEmpty() == false) {
		ui->OutputlineEdit->setText(fileNameInputPath); //顯示選擇儲存路徑
	}
}

使用ui->xxx可以調取介面中的控制元件,如ui->InputdatalineEdit則會指定到主視窗中的第一個文字框,這裡的命名為step1中修改後的屬性名,可從.ui介面中複製貼上。

step3:建立連線

在UI介面中建立clicked()與我們自定義的槽函式的連線,儲存後,重新執行程式,即可實現上述功能。

QT快速入門

(3)自定義訊號與槽函式

演算法計算(Compute)是這個QT工程的核心,按照同樣的方法將介面上的Compute按鈕與compute()槽函式連線;

而在計算過程中,我們希望能夠顯示進度並列印日誌,這部分通常沒有固定的連線,因此需要自定義發射訊號與接收槽函式。在展開介紹Compute的實現過程之前,再次強調以下Qt中使用多執行緒的注意事項:

  • 預設的執行緒在Qt中稱之為視窗執行緒,也就叫主執行緒,負責視窗事件處理或者視窗控制元件資料的更新;
  • 子執行緒負責後臺的業務邏輯處理,子執行緒中不能對視窗物件做任何操作,這些事情需要交給視窗執行緒處理;
  • 主執行緒和子執行緒之間如果要進行資料的傳遞,需要使用Qt中的訊號槽機制
  • 子執行緒一般不允許越級進行對視窗引數進行操作

簡單來說,當我們需要執行計算處理,並同步更新結果到視窗時,如果只使用一個執行緒,會出現視窗卡頓的情況。於是,我們將計算、處理放在子執行緒中,計算的中間過程與結果通過訊號槽機制傳遞到主執行緒進行顯示。

①mainwindow.h

  • 建立一個子執行緒類,它繼承自QThread,通過在protected成員方法中重新實現run()
  • 在主執行緒(Mainwindow)中宣告:子執行緒成員,以及接收訊號的主視窗上的槽函式。
#include <QThread>
#include <QProgressDialog>

/****************** 子執行緒--發射端 *****************/
class MyThread :public QThread {	//MyThread子類繼承自QThread
	Q_OBJECT
public:
	MyThread() {
        
    }
	~MyThread() {
        
    }

protected:			// 受保護的成員
	void run() {	//重寫run()方法,此處的方法為一個間隔0.1s從0~100的計數器
		for (int i = 0; i < 101; i++) {
			emit SendNumber(i);		//使用emit發射子執行緒中的訊號SendNumber。它將傳遞出當前的實參:一個1~100之間的整數
            						//emit是一個巨集定義,本質上會在moc_*.cpp檔案中生成一個SendNumber()訊號
            						//QT內部進行呼叫時,會找到底層的相應程式碼並進行訊號與槽函式的連線
            
			msleep(100);			//停滯0.1s
		}
	}
    
signals:
	void SendNumber(const int nNum);	//在類中宣告訊號函式
};

/****************** 主執行緒--主視窗--接收端 *****************/
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

	MyThread *_mthread;		//在主視窗中宣告一個子執行緒成員

private:
    Ui::MainWindow *ui;

private slots:				//宣告私有的槽函式

	void compute();  		//compute功能函式
	void updateProgress(int iter);  //重新整理進度條
	void printlog(int num); //列印日誌
};

②mainwindow.cpp

首先在compute訊號中啟動子執行緒;

void MainWindow::compute(){

	_mthread->start();		//使用start()啟動子執行緒

}

然後定義主執行緒中的槽函式方法;

void MainWindow::updateProgress(int iter) {  //Qt設定進度條的槽函式

	ui->computeprogressBar->setValue(iter);
}

void MainWindow::printlog(int num) {   //Qt列印計算日誌

	QString qs = "Process:";
	QString q1;

	q1 = q1.sprintf("%d", num);
	qs = qs + q1;

	ui->computeLogTextBrowser->append(tr("Reading Data Suceesed!"));
	ui->computeLogTextBrowser->append(qs);
}

最後在主視窗中連線子執行緒訊號與槽函式。

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

	this->_mthread = new MyThread();	//例項化一個子執行緒物件
	// 從子執行緒_mthread傳送一個訊號SendNumber
    // 由主執行緒(this)的槽函式updateProgress與printlog接收
	connect(_mthread, &MyThread::SendNumber, this, &MainWindow::updateProgress);
	connect(_mthread, &MyThread::SendNumber, this, &MainWindow::printlog);

}

關於connect官方給出的文件中包含5個引數,具體如下:

QObject::connect(const QObject *sender, 	// 傳送者
                 const char *signal, 		// 訊號
                 const QObject *receiver, 	// 接收者
                 const char *method, 		// 接收的槽函式
                 Qt::ConnectionType type)	/* 連線方式
                 Qt::ConnectionType type = Qt::AutoConnection (1)預設連線
                 						   Qt:: DirectConnection (2)立即呼叫 
                                           Qt::QueuedConnection (3)非同步呼叫
										   Qt::BlockingQueuedConnection (4)同步呼叫
										   Qt:: UniqueConnection (5)單一連線        */

完善Compute,在讀取檔名稱、路徑中加入判空:

if(filenameInput.isEmpty()==true){				//如果輸入檔名為空

    QMessageBox msgbox;
    msgbox.setText("no select input data");
    msgbox.exec();
    return;
}

if(filenameOutputPath.isEmpty() == true) {		//如果輸出路徑為空

    QMessageBox msgbox;
    msgbox.setText("no select output data path");
    msgbox.exec();
    return;
}

if (filenameOutput.isEmpty() == true){			//如果輸出檔名為空

    QMessageBox msgbox;
    msgbox.setText("no input output data name");
    msgbox.exec();
    return;
}

最終效果:

QT快速入門

完整程式碼

Ⅰ mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QFileDialog>
#include <QProgressDialog>
#include <QDebug>
#include <QMessageBox>
#include <QThread>

class MyThread :public QThread {

	Q_OBJECT

public:

	MyThread() {

	}
	~MyThread() {

	}

protected:
	void run() {
		for (int i = 0; i < 101; i++) {

			emit SendNumber(i);
			msleep(100);
		}
	}

signals:

	void SendNumber(const int nNum);

};

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

	MyThread *_mthread;

private:
    Ui::MainWindow *ui;

private slots:

	void inputdataSelect();     //輸入檔案選擇格式
	void outputdataPathSelect(); //輸出檔案路徑選擇
	void waveletFileSelect();  //子波檔案選擇

	void compute();  //計算函式
	void updateProgress(int iter);  //重新整理進度條
	void printlog(int num); //列印日誌

};
#endif // MAINWINDOW_H

Ⅱ mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

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

	this->_mthread = new MyThread();

	connect(_mthread, &MyThread::SendNumber, this, &MainWindow::updateProgress);
	connect(_mthread, &MyThread::SendNumber, this, &MainWindow::printlog);

}

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

void MainWindow::inputdataSelect(){   //輸入資料路徑及檔名選擇

	QString fileNameInput = QFileDialog::getOpenFileName(this,
		tr("Input File"),
		"F:",
		tr("Seismic(*sgy *segy *SEGY);;"));      //建立檔名及路徑選擇對話視窗


	if (fileNameInput.isEmpty() == false) {

		ui->InputdatalineEdit->setText(fileNameInput);   //將選擇輸入資料的檔名路徑
		qDebug() << "filename : " << fileNameInput;
	}
	else {

	}//end if(fileNameInput.isEmpty()==false) 


}

void MainWindow::outputdataPathSelect(){  //輸出資料的路徑選擇

	QString fileNameInputPath = QFileDialog::getExistingDirectory(this, 
		tr("Select Output File Folder"), 
		tr("C:"));   //讀取輸出檔案儲存路徑

	if (fileNameInputPath.isEmpty() == false) {

		ui->OutputlineEdit->setText(fileNameInputPath); //顯示選擇儲存路徑
	}
	else {

	}//end if(fileNameInputPath.isEmpty()==false)

}

void  MainWindow::waveletFileSelect(){

	QString fileNameInput = QFileDialog::getOpenFileName(this,
		tr("Input File"),
		"F:",
		tr("wavelet file(*dat *txt);;"));      //建立檔名及路徑選擇對話視窗

	if (fileNameInput.isEmpty() == false){

		ui->waveletFileNamelineEdit->setText(fileNameInput);   //將選擇輸入資料的檔名路徑
		qDebug() << "filename : " << fileNameInput;
	}

}

void MainWindow::compute(){

	QString filenameInput = ui->InputdatalineEdit->text();         //從介面獲取輸入模型的SEGY檔案
	QString filenameOutputPath = ui->OutputlineEdit->text();       //從介面獲取輸出模型的SEGY檔案檔案路徑
	QString filenameOutput = ui->outputDataFilenamelineEdit->text(); //從介面獲取輸出資料的SEGY的檔名

	if(filenameInput.isEmpty()==true){

		QMessageBox msgbox;
		msgbox.setText("no select input data");
		msgbox.exec();
		return;
	}

	if(filenameOutputPath.isEmpty() == true) {

		QMessageBox msgbox;
		msgbox.setText("no select output data path");
		msgbox.exec();
		return;
	}

	if (filenameOutput.isEmpty() == true){

		QMessageBox msgbox;
		msgbox.setText("no input output data name");
		msgbox.exec();
		return;
	}

	_mthread->start();
}

void MainWindow::updateProgress(int iter) {  //Qt設定進度條的槽函式

	ui->computeprogressBar->setValue(iter);
}

void MainWindow::printlog(int num) {   //Qt列印計算日誌

	QString qs = "Process:";
	QString q1;

	q1 = q1.sprintf("%d", num);
	qs = qs + q1;

	ui->computeLogTextBrowser->append(tr("Reading Data Suceesed!"));
	ui->computeLogTextBrowser->append(qs);

}