一文入門Qt Quick

2022-09-27 06:06:00

以下內容為本人的著作,如需要轉載,請宣告原文連結 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16733091.html


初識Qt Quick

很高興可以來到這一章,終於可以開始講講最近幾年Qt的熱門技術Quick這一塊了。

啥是Qt?

哦,這是一個宣稱可以跨任意平臺,開發各種場景應用軟體的開發框架。從三個維度來講,就是開發庫framework,整合開發平臺IDE,以及成熟的開發思維模式。

Qt Quick最早出現在Qt的4.7版本中,目標是在UI設計者與開發者之間搭建一個更高效合作平臺,給開發者更好的UI開發體驗。雖然幾經易手,Qt在digia公司這些年的努力迭代更新下,Qt Quick終於迎來了成熟穩定的版本(這也是我願意在最近的專案裡轉用它的原因)。

至於Qt Quick和老一套開發核心Qwidget的區別,其中最重點的就是提供了新的UI描述語言QML(Qt Meta-object Language,Qt元物件描述語言)。QML乍看起來有點像json,但是核心思想卻是模仿web頁面。沒錯,在QML檔案中允許搭配Javascript程式碼,就可以輔助實現豐富的UI互動邏輯。

如果你以往習慣QWidget開發,那麼Qt Quick真的非常值得上手試試。

好了,口水吐多了招人厭,下面直入廬山一窺真面目!

手剝一個簡單的功能程式開發栗子

在Qt開發過程中,Qt官方IDE(Qt Creator)提供了好幾種工程構建工具,比如簡單易懂的qmake,火上天的cmake,還有貌似沒人聽說過的Qbs。而目前Qt主推的構建方式就是cmake,下面要講的例子也是用cmake。

1.開發環境設定

Win10
Qt 6.2.4
Qt Creator 8.0.1
Mingw 11.2.0 64bit
Cmake 3.23.2

這裡選用的Qt版本是寫作時最新的LTS版本,LTS意思就是官方長期支援更新。比如說,一兩年內還會發布一下修補程式和安全更新,至於新功能特性就別想了。

2.建立Qt Quick工程

先用Qt Creator建立一個簡單的quick工程,工程構建描述的內容就儲存在工程根目錄的組態檔CMakeLists.txt中,如下:

cmake_minimum_required(VERSION 3.16)

project(instance VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOMOC ON)
#set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 6.2 COMPONENTS Quick REQUIRED)

file(GLOB_RECURSE SOURCE_FILES
    ./src/*.cpp
    ./src/*.h
)

qt_add_resources(SOURCE_FILES instance.qrc)

qt_add_executable(instance
    ${SOURCE_FILES}
)

set_target_properties(instance PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(instance
    PRIVATE Qt6::Quick)

install(TARGETS instance
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

cmake_minimum_required用於宣告當前的組態檔適用於的cmake最低版本,同時為了防止使用過於低階的版本來構建當前工程,避免某些指令不支援或者不相容。

project用於宣告當前工程名稱,和開發語言。CXX代表了C++。

CMAKE_AUTOMOC用於標記是否開啟自動MOC。Qt不僅僅是開發庫,它同時也對開發語言(比如C++)做了拓展,那麼原始碼檔案中就會多多少少會包含有通用編譯器無法識別的部分內容。MOC就是用於對Qt的擴充套件內容進行轉換的工具。

CMAKE_AUTORCC用於標記是否開啟自動RCC。在Qt工程中會包含有被最終輸出的執行程式所需要的資源內容,比如音視訊,圖片等等。那麼為了高效呼叫這些資源,勢必需要對原本的資原始檔進行處理再儲存到額外的二進位制檔案中,甚至內嵌到執行檔案中。RCC就是對這些資原始檔進行處理和再輸出工具。

由於我的例子工程中需要用到MOC和RCC,所以CMAKE_AUTOMOCCMAKE_AUTORCC都開啟。

CMAKE_AUTOUIC用於標記是否開啟自動UIC。如果開發介面用的技術棧是QWidget,那麼在Qt工程中就需要建立.ui檔案並儲存設計內容到其中,編譯的時候也需要用UIC把.ui檔案轉換成.h檔案。不過,這裡用Quick技術棧開發介面,因此無需開啟CMAKE_AUTOUIC,在最前面新增#表示註釋掉該行語句(該行語句會被解析器忽略)。

find_package匯入Qt的Quick模組。

由於本工程需要用到多個C++原始檔,所以這裡採用了遞迴參照檔案的方式把特定資料夾下面的所有.cpp、.h等檔案都囊括進來,需要輔以萬用字元*。所有被囊括的檔案路徑被追加到動態陣列SOURCE_FILES中,方便後邊參照。語法格式如下:

file(GLOB_RECURSE <variable> [FOLLOW_SYMLINKS]
     [LIST_DIRECTORIES true|false] [RELATIVE <path>]
     [<globbing-expressions>...])

格式裡的variable實際使用SOURCE_FILES代替。

qt_add_resources的作用是呼叫RCC對資原始檔(.qrc)編譯成qrc_開頭的原始檔再輸出,並且把輸出的原始碼檔案路徑追加到動態陣列SOURCE_FILES中。

當然,動態陣列SOURCE_FILES這個名字可以按照需求自定義設定,這裡取名為原始檔。

qt_add_executable指明構建的目標是二進位制檔案instance,參照的原始檔來自於動態陣列SOURCE_FILES。

target_link_libraries用於指明構建時連結Qt6::Quick的相關庫。

剩餘的語句都是Qt Creator建立工程時自動新增的內容,這裡略過。

然後看看我的工程目錄結構在Qt Creator中的展示:

如果用VSCODE開啟工程目錄,可以看到:

3.使用元物件描述檔案(QML)描述介面

使用Qt Creator自動建立的Quick應用,除了會自動生成組態檔CMakeLists.txt之外,還包含了main.cpp和main.qml檔案,原始碼只實現了啟動之後彈出一個視窗。

這裡稍作修改,實現簡單的檔案選擇,以及將選中的檔案路徑名顯示出來。這個功能用QWidget技術棧來實現其實是很簡單的啦,不過我們這裡目的是演示Quick技術框架怎麼用,所以下面來具體看看介面這塊怎麼玩:

1)主頁面

// main.qml

import QtQuick

Window {
    width: 640
    height: 200
    visible: true
    title: qsTr("Tool V1.0.0")
    Viewer {
        anchors.fill: parent
    }
}

main.qml 這個檔案是首頁元物件的描述檔案,一般QML引擎載入的第一個元物件所在的檔案就命名為main.qml。不過,命名為main.qml不是硬性規定。

首先,可以看到這裡通過import匯入了模組QtQuick。同一行,後邊還可以加上版本號。

Window是一種模組裡預定義的型別,用於表單描述。當然,型別也可以自定義,下面會說到。這裡通過對型別Window的範例化來描述一個表單物件。

型別後邊的{}內部包含了型別範例化後的成員屬性、函數、訊號、訊號處理控制程式碼等,比如width、height、visible、title等都是預定義的屬性,這些屬性如字面意思比較簡單易懂就不一一展開了,大夥要是有興趣可以反饋給我,我再看看意見給大家細聊。

Viewer其實是自定義的型別,這裡通過對型別Viewer的範例化來補充新增新的介面元素。Viewer物件內部的屬性anchors.fill: parent描述的是物件在其父物件(Window)中把父物件填充滿。QML一般通過anchors屬性來錨定物件的位置。

上面這個物件裡並沒有定義或者參照到函數或者訊號等。

2)自定義型別

下面來看看怎麼自定義型別

// Viewer.qml

import QtQuick 2.15
import QtQuick.Controls 6.2
import Qt.labs.platform 1.1

Item {
    function log(...msg) {
        let msgs = "";
        msg.forEach((item) => {
                        if (msgs.length != 0) {
                            msgs += " ";
                        }
                        msgs += item;
                    });
        console.log(msgs);
    }

    FileDialog {
        id: fileDialog
        objectName: "fileDialog"
        currentFile: selectedFileTextArea.text

        onFileChanged: {
            log(objectName + ".file =", file.toString().slice(8));
            fileMgrInstance.run(file.toString().slice(8));
        }
    }

    Label {
        id: fileLable
        x: 292
        y: 26
        text: qsTr("檔案:")
        verticalAlignment: Text.AlignVCenter
        font.pointSize: 14
    }

    TextField {
        id: selectedFileTextArea
        x: 70
        y: 70
        width: 500
        objectName: "selectedFileTextArea"
        text: fileDialog.file.toString().slice(8)
        font.pointSize: 12
        placeholderText: qsTr("選擇檔案")
    }

    Button {
        id: selectFileButton
        x: 268
        y: 124
        width: 105
        height: 54
        text: qsTr("選擇")
        font.pointSize: 10

        onClicked: {
            log(text, "clicked");
            fileDialog.open();
        }
    }
}

Item是類庫QtQuick的預定義元件型別,描述的是一個基礎可視元件,quick中所有的可視元件都繼承於它。

一般在QML中自定義型別都會使用基礎的型別Item,然後在其基礎上客製化內部屬性、函數、訊號、訊號處理控制程式碼等。

Item的繼承鏈是這樣的:

Item -> QtObject -> QObject

看到這裡,可以猜測一下,其實所有的Quick預定義元件都是繼承於QObject,和QWidget裡提供的類庫太相似了。

function log(...msg)定義了函數log,function是關鍵詞,log是函數名,後邊小括號裡的...表示引數不定,這樣子在呼叫log時就可以不限制輸入的引數個數了。

要注意的是,QML內部的函數使用的語法是ECMAScript,也就是我們常常聽到的JavaScript。

FileDialog是類庫Qt.labs.platform的預定義元件型別,描述的是一個檔案選擇視窗。屬性id,繼承於QObject,用於標記唯一的物件,也就是說所有物件的id都不能重複,無論物件是否處於同一個QML檔案。objectName描述的屬性可用於對物件樹中的物件進行查詢。currentFile描述了當前選中的檔名(包括路徑),在確定最後選中的檔案之前,此屬性也會跟隨選擇而改變。onFileChanged描述了當屬性file值改變時,自動產生的訊號的處理控制程式碼(handle),用{}限定處理範圍。log是上面定義的函數呼叫,輸入兩個引數。fileMgrInstance是C++原始檔暴露給QML引擎的特定物件id,通過該id可以呼叫C++中的相應物件的方法屬性(程式碼中呼叫了run方法,方法的詳情定義看下文)。

C++和QML原始檔之間的物件相互呼叫,會有後續的文章專門介紹,這裡不再細聊,敬請關注。

Label是類庫QtQuick.Controls的預定義元件型別,描述的是一個文字標籤。x、y描述的是座標。text描述的是顯示文字。qsTr()用於標識文字可被翻譯,類似Qt C++裡的tr()。verticalAlignment描述的是垂直排列方式,這裡的屬性值標識垂直中間排列。font.pointSize描述字型的大小。

TextField是類庫QtQuick.Controls的預定義元件型別,描述的是一個單行文字編輯窗。width是幾何寬度。placeholderText描述的是預留位置。

Button是類庫QtQuick.Controls的預定義元件型別,描述的是一個可點選的按鍵。height描述的是幾何高度。onClicked描述了當訊號clicked發生時,該訊號的處理控制程式碼(handle),用{}限定處理範圍,這裡呼叫了上面的檔案選擇窗的開啟函數。

訊號的處理控制程式碼(handle)中,在on後邊書寫時訊號的首字母需要大寫。

3)預覽介面

什麼?程式碼沒寫完就可以預覽介面了?

是的,QML檔案支援用工具預覽,非常方便於UI設計過程中的偵錯。

開啟預覽的方式是呼叫qmlscene或者用Qt Design Studio,如下圖用的是qmlscene。

看看Viewer.qml頁面預覽的實際效果。

4.使用C++程式碼實現邏輯處理

Qt Quick使用QML的目的是為了簡化介面的設計開發,而軟體除了介面的互動之外還有大量的後臺邏輯處理功能也需要實現,針對這塊業務,Qt其實還是推薦使用C++,正所謂術業有專攻,畢竟C++對效能的利用還是有兩把刷子的。廢話不多說,馬上看下文。。。

既然如此,那麼就來看看負責邏輯處理功能的C++程式碼部分。不過,這裡假設各位看官已經熟悉C++的各項業務技能,所以下面只針對和QML物件的互動來簡單介紹一下。

後續也會有更加詳細的專題文章介紹這部分,敬請留意哈!

1)QML物件的載入和C++物件傳遞

QML物件的建立和展現是通過QML引擎來載入的,一般每個程式會由單個引擎物件負責管理。不過,QML引擎物件不是直接管理QML物件,而是通過管理上下文(context)物件來分別管理QML物件。所以在C++裡邊如果需要往QML物件傳遞資訊也是直接傳給對應的上下文物件,然後再在QML物件中通過傳入物件時指定的id名呼叫對應的方法屬性。

// main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "./FileMgr/FileMgr.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/src/QML/main.qml"_qs);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        qInfo("%s start\n", QCoreApplication::applicationName().toLatin1().data());

        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);

    FileMgr fileMgr;
    engine.rootContext()->setContextProperty(QStringLiteral("fileMgrInstance"), &fileMgr);
    engine.load(url);

    return app.exec();
}

上面的main.cpp檔案程式碼中,把物件fileMgr傳入引擎的根上下文中,並設定id為fileMgrInstance。傳入根上下文,意味著引擎載入的所有QML物件都可以通過id=fileMgrInstance存取fileMgr物件內容。這裡要注意,fileMgr物件是在C++原始碼中範例化了的。

把C++物件暴露給QML物件的方法,除了上面這種通過上下文的方式外,還有一種是通過直接往元物件系統(Meta-Object System)註冊型別的方式,這種方式也是最根本的方式,因為Qt Quick框架的底層實現原理就依賴於元物件系統。

使用的介面原型:

template <typename T> int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)

如果通過這種註冊的方式實現,上面的栗子可以掰成這樣:

 qmlRegisterType<FileMgr>("com.englyf.qmlcomponents", 1, 0, "FileMgrItem");

根據上面的定義,com.englyf.qmlcomponents是名稱空間,1.0是名稱空間的版本號,FileMgrItem是QML中的型別名。

然後在QML檔案內,匯入並範例化這個類:

 import com.englyf.qmlcomponents 1.0

 FileMgrItem {
     // ...
 }

要強調的是,通過註冊的方式,型別的範例化會放在QML裡邊做,而C++原始碼就不需要再對類FileMgr作範例化了

2)C++類別型的定義

既然Qt Quick依賴於元物件系統,那麼對C++類別型的定義就有必然的要求了。

C++類別型需要繼承於QObject,並且類開頭應該宣告宏Q_OBJECT,這樣才可以使用元物件系統提供的服務,包括訊號槽機制等等。

需要被QML物件呼叫的方法應該新增修飾Q_INVOKABLE。這個修飾符表明該方法可被元物件系統呼叫。同時,該方法的引數型別和返回型別,都推薦使用型別QVariant。

如有開放給QML物件的可存取屬性,那麼也需要對屬性宣告為Q_PROPERTY。這裡暫不舉例,可關注後續的專題文章。

// FileMgr.h

#ifndef FILEMGR_H
#define FILEMGR_H

#include <QObject>
#include <QVariant>

class FileMgr : public QObject
{
    Q_OBJECT
public:
    explicit FileMgr(QObject *parent = nullptr);

    Q_INVOKABLE QVariant run(QVariant file);
};

#endif // FILEMGR_H

// FileMgr.cpp

#include "FileMgr.h"

FileMgr::FileMgr(QObject *parent)
    : QObject{parent}
{}

QVariant FileMgr::run(QVariant file)
{
    QString fileStr = file.toString();
    qDebug("C++ get file:%s selected", fileStr.toStdString().data());

    return 0;
}

自動化部署

這部分講點高階的內容,以往看到網上的教學都是教初學者部署的時候,進入exe生成的目錄,然後手動呼叫windeployqt執行部署。這個程式是Qt自帶的,會自動把所有依賴的動態庫拷貝過來存放在指定目錄下。

這裡就介紹一下怎麼在Qt Quick軟體工程編譯結束時自動部署所有依賴項。

首先,debug開發模式下是不需要部署軟體的,那麼我們就先切換到release模式下。

然後,在Build的步驟下,Build步驟之後新新增一個Custom Process Step的步驟。

我把設定都拷過來:

Command:            windeployqt
Arguments:          --qmldir %{ActiveProject:NativePath}\src\QML\ %{ActiveProject:RunConfig:Executable:NativeFilePath}
Working directory:  %{Qt:QT_INSTALL_BINS}

由於Qt Quick工程涉及到QML檔案,所以這裡需要帶上選項--qmldir,這個選項後邊緊跟著引數值是程式碼工程中存放自定義的QML檔案的根目錄。

%{ActiveProject:NativePath}代表著當前工程的主目錄的在地化路徑。

%{ActiveProject:RunConfig:Executable:NativeFilePath}代表著當前工程的exe檔案輸出目錄的在地化路徑。

Working directory項意思是Command命令的工作目錄,這裡填上%{Qt:QT_INSTALL_BINS},代表Qt安裝目錄下的bin目錄。

按照上面的介紹過程設定完整,以後如果需要部署輸出,只需要切換到release模式下,然後點選編譯,等編譯完成就會自動進入部署流程,整個過程就是這麼舒心。

生活簡單才是美好,部署也不例外!


到最後,一起來看看跑起來的程式: