目錄
1. 編寫第一個 QT 程序
1.1?使用 標簽 實現 ?
1.2 純代碼形式實現
1.3?使用 按鈕 實現?
1.3.1?圖形化界面實現?
1.3.2 純代碼形式實現
1.4?使用 編輯框 實現
1.4.1?圖形化界面實現
?1.4.2?純代碼形式實現?
1.4.3 內存泄露?
2. 認識對象模型(對象樹)
2.1 什么是對象樹
2.2?驗證對象樹
3. 解決編碼問題
4. Qt 編程注意事項
4.1 Qt 中的命名事項
4.2 Qt Creator 中的快捷鍵
4.3 使用幫助文檔
5. 小結
1. 編寫第一個 QT 程序
1.1?使用 標簽 實現 ?
1.創建好一個項目后,我們可以點擊 widget.ui 進入圖形化界面設計,可以直接通過拖拽的方式進行添加
2.拖拽 "標簽" 至 UI 設計界面中,并雙擊修改標簽內容
3. 此時ui界面就會生成對應的 XML 格式代碼,這個時候qmake就會根據這個XML代碼生成對應的C++代碼,我們也可以在同目錄下找到這個C++代碼
1.2 純代碼形式實現
我們點擊 widget.cpp 里面,會有一個 widget 的構造函數和析構函數,我們一般使用代碼進行編輯界面的時候,一般都是在 widget 的構造函數中實現,因為在 ?main 函數中調用了 widget 類之后就直接 show了,所以卸載構造函數中的時候,一旦執行到了 show 就一定可以顯示出設計的界面
#include "widget.h" // 創建生成時的文件
#include "ui_widget.h"
#include <QLabel> // 包含標簽的頭文件Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this); // 將form file生成的界面和我們當前的widget進行關聯起來// 創建對象的兩種方法// QLabel label; // 在棧上創建QLabel* label = new QLabel(this); // 在堆上創建,推薦這種方法,還要傳遞 一個 this,給當前這個 lable 對象指定 父對象// 1. 設置標簽內容label->setText((QString)("顯式 Hello world"));label->setText("隱式 Hello World"); // QString 也提供了 C 風格字符串作為參數的構造函數來不顯示構造 QString// 注意:由于QString 對應的頭文件,已經被很多 Qt 內置的其他類給間接包含了.因此一般不需要顯式包含 QString 頭文件// 這里雖然有兩次 setText,但是下面內容會覆蓋上面內容// 2. 設置窗口大小setFixedSize(500, 400);// 3. 設置字體大小QFont font("楷體", 16);label->setFont(font);// 4. 設置標簽內容顯式位置label->move(200, 150);// 5. 設計標簽字體顏色label->setStyleSheet("color:blue");
}Widget::~Widget()
{delete ui;
}
void setText(const QString &);
這里我們會發下使用字符串的時候并不是我們 C++ 使用的標準庫中的 string,而是 Qt 自己包裝好的字符串 QString 。這個其實也是歷史原因,Qt 誕生于1991年,那個時候 C++ 還沒有定標準,而 Qt 為了更好的開發就自己包裝了一些容器。但是我們也還是可以使用 C++ 的標準庫中的容器來使用
1.3?使用 按鈕 實現?
1.3.1?圖形化界面實現?
1.?雙擊:"widget.ui" 文件
2. 拖拽控件至 ui 界面窗口并修改內容
- 雖然那里有好幾個按鈕,但是我們這里用 Push Button(普通按鈕)
3. 構建并運行,效果如下所示
這里的按鈕的確可以點擊,但是卻沒有任何反應,這個就設計到我們后面學的信號槽知識,后面會說的
QT 的信號槽機制:本質上就是給按鈕的點擊操作,關聯上一個處理函數,當用戶點擊的時候,就會執行這個處理函數
這里我們的按鈕沒有任何功能,假如我們要實現一定的功能,那該怎么做呢?
打開 widget.ui 文件,查看設計的右下角,則有
如下代碼:
widget.hpp
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);void handleClick();~Widget();private:Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 按鈕的點擊操作 -- 信號槽// 在 Linux 網絡編程那也有個connect 函數,那里用來給 TCP socket 建立連接的,寫 TCP 客戶端的時候,就需要先建立連接才能讀寫數據// ui->pushButton:誰發出的信號// &QPushButton::clicked:發出了啥信號,點擊按鈕的時候自動觸發該信號// this: 誰來處理這個信號// Widget::handle:具體怎么處理connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick); // 訪問到 form file(ui 文件)中創建的控件}Widget::~Widget()
{delete ui;
}void Widget::handleClick()
{if(ui->pushButton->text() == "Hello World"){ui->pushButton->setText("Hello IsLand");}else{ui->pushButton->setText("Hello World");}
}
返回上級目錄查看?ui_widget.h?文件
因此我們也可以把 PushButton 改成其他的,如下:
再次查看?ui_widget.h?文件,如下:
結論:在 objectName 中,設置成什么值,生成的變量名就叫啥名字就可以根據這個名字來獲取到對應的控件的變量了
1.3.2 純代碼形式實現
widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QPushButton>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);void handleClick();~Widget();private:Ui::Widget *ui;QPushButton* myButton;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);myButton = new QPushButton(this);myButton->setText("Hello World");connect(myButton, &QPushButton::clicked, this, &Widget::handleClick); // 訪問到 form file(ui 文件)中創建的控件
}Widget::~Widget()
{delete ui;
}void Widget::handleClick()
{if(myButton->text() == QString("Hello World")){myButton->setText("Hello IsLand");}else{myButton->setText("Hello World");}
}
實現效果如下圖:
兩個版本比較:
圖形化實現:此時按鈕對象不需要咱們自己 new。new 對象的操作已經是被 Qt 自動生成了而且這個按鈕對象,已經作為 ui 對象里的一個成員變量了,也無需作為 Widget 的成員
純代碼實現:按鈕對象是咱們自己 new 的,為了保證其他函數中能夠訪問到這個變量,就需要把按鈕對象,設定為 Widget 類的成員變量
實際開發中,是通過代碼的方式構造界面為主,還是通過圖形化界面的方式構造界面為主??
這兩種都很主要,難分主次!!
如果你當前程序界面,界面內容是比較固定的,此時就會以?圖形化?的方式來構造界面
但是如果你的程序界面,經常要動態變化,此時就會以?代碼?的方式來構造界面
反正這兩種方式哪種方便用哪個,也可以配合來使用
1.4?使用 編輯框 實現
- 單行編輯框:?QLineEdit
- 多行編輯框:?QTextEdit
1.4.1?圖形化界面實現
-
當然輸出的文本框,我們也可以在輸出里面進行修改啥的,但是不會影響代碼里面的文本數據
?1.4.2?純代碼形式實現?
1.4.3 內存泄露?
在上面的代碼實現中,我們使用 new 創建了對象,在棧上開辟了一塊空間之后,但是我們沒有使用delete進行釋放控件,這樣不就會導致內存泄漏啊
其實上述代碼在 Qt 不會產生內存泄露, label ?對象會在合適的時候被析構釋放,之所以能夠把對象釋放掉,主要是因為把這個對象掛到了 對象樹 上?
前端開發(網頁開發)也涉及到 類似的 對象樹 (DOM),本質上也是一個樹形結構(N 又樹),通過樹形結構把界面上的各種元素組織起來
Qt 中也是類似,也是搞了一個對象樹,也是 N 又樹,把界面上的各種元素組織起來了
- 用對象樹把這些內容組織起來,最主要的目的:就是為了能夠在合適的時機(窗口關閉和銷毀),把這些對象統一進行釋放。通過這個樹形結構,就把界面上要顯示的這些控件對象都組織起來了。
- 這里的樹上的這些對象,統一銷毀是最好不過的,如果某個對象提前銷毀,此時就會導致對應的控件就在界面上不存在了。
2. 認識對象模型(對象樹)
2.1 什么是對象樹
在?Qt?中創建很多對象的時候會提供一個 Parent對象指針,下面來解釋這個 parent 到底是干什么的。
QObject ?是以對象樹的形式組織起來的。
當創建一個 QObject 對象時,會看到 QObject 的構造函數接收一個 QObject 指針作為參數,這個參數就是 parent,也就是父對象指針。
這相當于,在創建 QObject 對象時,可以提供一個其父對象,我們創建的這個 QObject 對象會自動添加到其父對象的 children()列表。
當父對象析構的時候,這個列表中的所有對象也會被析構。(注意,這里的父對象并不是繼承意義上的父類!)
這種機制在 GUI程序設計中相當有用。例如,一個按鈕有一個 QShortcut(快捷鍵)對象作為其子對象。當刪除按鈕的時候,這個快捷鍵理應被刪除。這是合理的。
- Qwidget 是能夠在屏幕上顯示的一切組件的父類。
- Qwidget 繼承自 QObject,因此也繼承了這種對象樹關系。一個孩子自動地成為父組件的-個子組件。因此,它會顯示在父組件的坐標系統中,被父組件的邊界剪裁。例如,當用戶關閉一個對話框的時候,應用程序將其刪除,那么,我們希望屬于這個對話框的按鈕、圖標等應該(-起被刪除。事實就是如此,因為這些都是對話框的子組件。
- 當然,我們也可以自己刪除子對象,它們會自動從其父對象列表中刪除。比如,當我們刪除了個工具欄時,其所在的主窗口會自動將該工具欄從其子對象列表中刪除,并且自動調整屏幕顯示。
Qt 引入對象樹的概念,在一定程度上解決了內存問題。
當一個 QObject 對象在堆上創建的時候,Qt 會同時為其創建一個對象樹。不過,對象樹中對象的順序是沒有定義的。這意味著,銷毀這些對象的順序也是未定義的。
任何對象樹中的 QObject 對象 delete 的時候,如果這個對象有 parent,則自動將其從 parent的children() 列表中刪除;如果有孩子,則自動 delete 每一個孩子。Qt 保證沒有 QObject 會被delete 兩次,這是由析構順序決定的。
如果?QObject?在棧上創建,Qt 保持同樣的行為。正常情況下,這也不會發生什么問題。來看下面的代碼片段:
{QWidget window;QLabel label("hello", &window); // 指定父類是widow
}
作為父組件的 ?window 和作為子組件的? label?都是 QObject 的子類(事實上,它們都是 Qwidget的子類,而 Qwidget 是 QObject 的子類)。這段代碼是正確的,label?的析構函數不會被調用兩次,因為標準 C++ 要求,局部對象的析構順序應該按照其創建順序的相反過程。因此,這段代碼在超出作用域時,會先調用 label?的析構函數,將其從父對象 window 的子對象列表中刪除,然后才會再調用 ?window 的析構函數。
- 但是一旦我們的代碼稍微修改一點就會出錯
{QLabel label("hello"); // 指定父類是widowQWidget window;label.setParent(&window);
}
情況又有所不同,析構順序就有了問題。我們看到,在上面的代碼中,作為父對象的 window 會首先被析構,因為它是最后一個創建的對象。在析構過程中,它會調用子對象列表中每一個對象的析構函數,也就是說,label此時就被析構了。然后,代碼繼續執行,在 window 析構之后,label也會被析構,因為 label也是一個局部變量,在超出作用域的時候當然也需要析構。但是,這時候已經是第二次調用 label的析構函數了,C++不允許調用兩次析構函數,因此,程序崩潰了。
由此我們看到,Qt?的對象樹機制雖然在一定程度上解決了內存問題,但是也引入了一些值得注意的事情。這些細節在今后的開發過程中很可能時不時跳出來煩擾一下,所以,我們最好從開始就養成良好習慣,即 在 Qt 中,盡量將其開辟在堆上,并指定好其?parent 父類對象
比如:
-
如果我們把最初的代碼改成在棧上開辟的話我們運行程序會發現什么都沒有
Qt 對象樹如下:
2.2?驗證對象樹
首先我們自定義一個label類,并在析構部分打上日志,如下步驟:
1. 選中工程名,鼠標右鍵------->"add new..."(或 "添加新文件" )
結果圖如下:?
- 上面 Qt Creator 是幫我們生成了一些代碼,但是沒完全生成,頭文件沒有給我們主動包含,上面的頭文件也是我自己手動包含的?
- 此時我們可以按F4來進行?.h文件?和?.cpp文件?來回切換
此時我們的?mylabel.cpp?中就是
#include "mylabel.h"
#include <iostream>MyLabel::MyLabel(QWidget* parent) : QLabel(parent)
{
}MyLabel::~MyLabel()
{std::cout << "MyLabel 被銷毀" << std::endl;
}
midget.cpp?中的代碼就是
#include "widget.h"
#include "ui_widget.h"
#include "mylabel.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this); // 將form file生成的界面和我們當前的widget進行關聯起來// 使用自己定義的 MyLabel 代替原來的 QLabel,所謂的 “繼承” 本質上是擴展,保持原有功能不變的基礎上// 給對象擴展出一個析構函數,通過這個析構函數,打印一個自定義日志,方便觀察程序運行結果MyLabel *label = new MyLabel(this);label->setText("Hello World");}Widget::~Widget()
{delete ui;
}
- 此時我們運行代碼,就可以看到窗口上有?Hello World?的字樣,只要我們關閉窗口,就會輸出我們的日志
這里也是驗證了對象樹自動釋放對象的能力
- 這里日志是有的,說明析構函數是執行了,雖然沒有 手動?delete,但是由于把 MyLabel 掛到了對象樹上,此時窗口被銷毀的時候,就會自動銷毀對象樹中的所有對象!!所以MyLabel 的析構是執行到了
但是這里似乎出現了亂碼的情況
- 亂碼問題出現的原因有且僅有一個?編碼方式不匹配(不僅僅局限于 C++)
- 比如字符串本身是 utf8 編碼的,但是終端(控制臺)是按照 gbk 的方式來解析顯示的,就會出現亂碼(相當于拿著 utf8 的數值 去查詢 gbk 的 碼表)
utf8 和 gbk?
目前,表示漢字字符集, 主要是兩種方式
- GBK(中國大陸) 使用 2 個字節表示一個漢字!Windows 簡體中文版,默認的字符集就是 GBK
- UTF-8 / utf8?變長編碼,表示一個符號,使用的字節數有變化,2-4但是在?utf8?中。一個漢字一般是 3 個字節
- Linux 默認就是?utf8
3. 解決編碼問題
我們用文本文件打開?mylabel.cpp?文件,可以看到這個文件的編碼方式
可看到這個文件的編碼方式是?utf8,但是??Qt?的這個終端的編碼方式肯定不是?utf8?,但是Qt不支持修改編碼方式,所以這里我們就需要借助?Qt?自己提供的打印日志的功能?qDebug,或者使用?QString?來處理編碼方式。
#include "mylabel.h"
#include <iostream>#include <QtDebug> // 頭文件MyLabel::MyLabel(QWidget* parent) : QLabel(parent)
{}MyLabel::~MyLabel()
{// std::cout << "MyLabel 被銷毀" << std::endl;qDebug() << "MyLabel 被銷毀"; // qDebug 這個宏封裝了 QDebug 對象,使用 qDebug 相當于使用 cout
}
#define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug
此時中文就不會出現亂碼情況,如下:?
后續在 Qt 中,如果想通過打印日志的方式,輸出一些調試信息,都優先使用 qDebug。雖然使用 cout 也行,但是 cout 對于編碼的處理不太好,在windows 上容易出現亂碼(如果是 Linux 使用 Qt Creator, 一般就沒事了,Linux 默認的編碼一般都是 utf8)
使用 qDebug, 還有一個好處:打印的調試日志是可以統一進行關閉的!!
輸出的日志,是開發階段、調試程序的時候使用的。如果你的程序發布給用戶,不希望用戶看到這些日志的!!
qDebug 可以通過編譯開關,來實現一鍵式關閉
之前調試程序, 都是用調試器.VS/gdb,這里為啥要打印日志調試呢??
- 調試器很多時候是有局限性的,是無法使用的,
- 假設當前 bug 是一個概率性的 bug。出現的概率是 1% 甚至更小要想調試,無法使用調試器了
- 使用?日志? 就可以很好的解決這種問題
- 無論是哪種方式本質上都是觀察程序執行的中間過程和中間結果~
4. Qt 編程注意事項
4.1 Qt 中的命名事項
類名:首字母大寫,單詞和單詞之間首字母大寫;
函數名及變量名:首字母小寫,單詞和單詞之間首字母大寫
起的名字要有描述性,不要使用 abc, xyz 這種比較無規律的名字來描述
如果名字比較長,由多個單詞構成的,就需要使用適當的方式來進行區分不同單詞
一般可以采用 蛇形命名法 或者 駝峰命名法
4.2 Qt Creator 中的快捷鍵
其里面內置 Vim 插件,因此我們也可以按照使用 Vim 操作來使用
注釋:ctrl+/
運行:ctrl+R
編譯:ctrl+B
字體縮放:ctrl+鼠標滑輪
查找:ctrl+F
整行移動:ctrl+shift+↑/↓
幫助文檔:F1
自動對齊:ctrl+i
同名之間的.h和.cpp 的切換:F4
生成函數聲明的對應定義:alt+enter
跳轉到控件定義: 鼠標左鍵 + ctrl,返回就是:alt + <-
4.3 使用幫助文檔
打開幫助文檔有三種方式,實際編程中使用哪種都可以
光標放到要查詢的類名/方法名上,直接按 F1
Qt Creator 左側邊欄中直接用鼠標單擊"幫助"按鈕
點擊 "幫助" 之后,出現如下圖:
3、找到?Qt Creator?的安裝路徑,在 "bin" 文件夾下找到 assistant.exe,雙擊打開
使用示例
-
新建項目,在新建的項目中使用 Qt 中的"QpushButton" 控件。
-
打開幫助手冊,在"索引"里面輸入"QpushButton":
注意:一定不要使用中文文檔!!!
-
閱讀英文文檔是每個程序員必備的專業技能,必須要練,不能退縮
-
Qt的文檔從通俗易懂的角度來說,是技術類文檔中非常出類拔萃的,只要大家稍微有點耐心,基本都能讀懂個八九不十
5. 小結
-
認識 QLabel 類,能夠在界面上顯示字符串。通過 setText 來設置的,參數 QString(Qt 中把 C++ 里的很多容器類,進行了重新封裝,歷史原因)
-
內存泄露/文件資源泄露
-
對象樹: Qt 中通過對象樹,來統一的釋放界面的控件對象,Qt 還是推薦使用 new 的方式在堆上創建對象,通過對象樹,統一釋放對象創建對象的時候,在構造函數中,指定父對象(此時才會掛到對象樹上),如果你的對象沒有掛到對象樹上,就必須要記得手動釋放!!
-
通過繼承自 Qt 內置的類,就可以達到對現有控件進行功能擴展效果Qt 內置的 QLabel,沒法看到銷毀過程的。為了看清楚,就創建類 MyLabel, 繼承自 QLabel 重寫析構函數。在析構函數中加上日志,直觀的觀察到對象釋放的過程了,也可以重寫控件中的任何功能。不僅僅是析構函數, 達到功能擴展目的
-
亂碼問題 和 字符集~ MySQL(很多地方都涉及到)
-
如何在 Qt 中打印日志,作為調試信息使用 cout 固然可以, 但是并不是上策(字符編碼處理的不好,也不方便統一進行關閉)Qt 中推薦使用 qDebug() 完成日志的打印