第十七章:Qt C++


第十七章:Qt C++

QtQt QuickQML

PySide6 。

QtCore class list 或 QtCore overview。
要用CMake和make来构建应用程序。CMake读取工程文件CMakeLists.txt,并生成一个Makefile,以用于构建应用程序。CMake也支持其它的编译系统,比如ninja。工程文件是平台无关的,并且CMake有一些规则应用于特定平台的设置,以生成makefile。项目也可以基于指定平台原则,在某些特定场景下来包含一些操作系统平台。
这有一个由Qt Creator生成的简单的工程文件。注意,Qt试图创建一个兼容Qt 5和Qt 6,以及兼容Android,OS X 的工程文件。

cmake_minimum_required(VERSION 3.14)

project(projectname VERSION 0.1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# QtCreator supports the following variables for Android, which are identical to qmake Android variables.
# Check https://doc.qt.io/qt/deployment-android.html for more information.
# They need to be set before the find_package(...) calls below.

#if(ANDROID)
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
#    if (ANDROID_ABI STREQUAL "armeabi-v7a")
#        set(ANDROID_EXTRA_LIBS
#            ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
#            ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
#    endif()
#endif()

find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Quick REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick REQUIRED)

set(PROJECT_SOURCES
        main.cpp
        qml.qrc
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(projectname
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
else()
    if(ANDROID)
        add_library(projectname SHARED
            ${PROJECT_SOURCES}
        )
    else()
        add_executable(projectname
          ${PROJECT_SOURCES}
        )
    endif()
endif()

target_compile_definitions(projectname
  PRIVATE $<$<OR:$,$>:QT_QML_DEBUG>)
target_link_libraries(projectname
  PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick)

set_target_properties(projectname 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}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_import_qml_plugins(projectname)
    qt_finalize_executable(projectname)
endif()

不必逐行去看这修文件。仅需要记住,Qt 使用CMake的CMakeLists.txt文件来生成特定平台的makefiles,之后它被用来构建工程。在编译系统章节,我们将涉及列基本的、手写的CMake文件。
上面简单的代码仅是写了一段文本并退出了应用程序。如果对于一个命令行工具,这已经够用了。对于用户界面,你需要事件循环来等待用户输入以及一些重绘工作操作。接下来是一个同样的例子,不过是用按钮来触发写文本操作。
main.cpp惊人地小。我们将代码移动到自定义类中以能够将Qt的信号和槽应用于用户输入,比如,处理鼠标点击。就象马上要见到的那样,信号和槽机制一般需要一个对象实例,但它也可以使用C++ lambdas 。

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


int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    MainWindow win;
    win.resize(320, 240);
    win.setVisible(true);

    return app.exec();
}

main函数中,我们创建了一个应用对象,一个窗体,然后使用exec()开始了事件循环。现在,程序运行于事件循环中等待用户输入。

int main(int argc, char** argv)
{
    QApplication app(argc, argv); // init application

    // create the ui

    return app.exec(); // execute event loop
}

在Qt里,即可以用QML也可以用控件来构建应用程序。本书我们聚集于QML,但本章我们将来看下控件。这让你仅用C++ 来构建程序。

主窗体本身是一个控件。它是最高层级的窗体,因为其没有任何的交类。这来源于Qt如何看待UI元素树上用户界面。这种情况下,主窗体是根元素,当点击按钮时,主窗体的子控件,成为主窗体内的一个控件。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include 

class MainWindow : public QMainWindow
{
public:
    MainWindow(QWidget* parent=0);
    ~MainWindow();
public slots:
    void storeContent();
private:
    QPushButton *m_button;
};

#endif // MAINWINDOW_H

另外,我们在头文件的自定义代码部分,定义了一个公共的名为storeContent()的槽。槽可以是公有的、保护的或私有的,并且可以象其它类方法那样被调用。你也会遇到有很多信号标识的signals节部分。这些方法不应该被调用,而且不能被实现。信号和槽都是被Qt的元数据信息系统处理,并能够在运行时被探测和调用。
storeContent()的目的就是,当按钮被点击时被调用。一起来实现它。

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    m_button = new QPushButton("Store Content", this);

    setCentralWidget(m_button);
    connect(m_button, &QPushButton::clicked, this, &MainWindow::storeContent);
}

MainWindow::~MainWindow()
{

}

void MainWindow::storeContent()
{
    qDebug() << "... store content";
    QString message("Hello World!");
    QFile file(QDir::home().absoluteFilePath("out.txt"));
    if(!file.open(QIODevice::WriteOnly)) {
        qWarning() << "Can not open file with write access";
        return;
    }
    QTextStream stream(&file);
    stream << message;
}

在主窗体里,先创建了一个点击按钮,然后用connect方法将信号clicked()注册到槽storeConnect()。每当信号clicked释放时,槽storeConnect()也被调用。现在这两个对象(按钮和主窗体)通过信号和槽交流,而彼此并未在意对方的存在。这就是松耦合,这使得多数Qt类使用QObject为其基类。

CMake将工程文件CMakeLists.txt转换成指定平台的make文件,然后被特定平台的工具编译。

注意
Qt自带三种不同的编译系统。原生的Qt编译系统被称为qmake。另一种Qt指定的编译系统是QBS,它可以使用声明式的方法来描述编译顺序。从Qt 6开始,Qt将官方指定编译器从qmake转换为CMake。

Unix下的Qt的典型编译流程是:

vim CMakeLists.txt
cmake . // generates Makefile
make

使用 Qt,我们鼓励您使用影子构建。影子构建是指在源码路径之外来构建。假定我们有一个内有CMakeLists.txt的myproject文件夹。流程应该是这样:

mkdir build
cd build
cmake ..

我们创建了一个build文件夹,并从工程文件夹路径中进入到build文件夹,从那里调用cmake。所有build文件(工件)都存储在buid文件夹而不是我们的源代码文件夹中,将以方式设置的方式生成文件。这允许我们同时为不同的 qt 版本创建buildt和build配置,而且它不会弄乱我们的源代码文件夹,这总是一件好事。
当你使用Qt Creator时,它在幕后为你做了这些事情,而你在大多数的时候无须担心这些步骤。对于大型的工程以及地构建流程更深层次的理解,建议你从命令行来构建你的Qt工程,以保证你对正在发生的事情有所掌控。

CMake Help可在线使用但仍然是Qt Help的格式
Running CMake
KDE CMake Tutorial
CMake Book
CMake and Qt

QMake Manual - QMake 手册目录
QMake Language- 赋值、范围及其它
QMake Variables- TEMPLATE, CONFIG, QT 等变量说明

Implicit Sharing,这被用于很多Qt类中。

QString data("A,B,C,D"); // create a simple string
// split it into parts
QStringList list = data.split(",");
// create a new string out of the parts
QString out = list.join(",");
// verify both are the same
QVERIFY(data == out);
// change the first character to upper case
QVERIFY(QString("A") == out[0].toUpper());

以下是将数值与字符相互转换的例子。float、double及其它类型也有相应的转换函数。可以到Qt文档中查找这里所用到的以及其它函数。

// create some variables
int v = 10;
int base = 10;
// convert an int to a string
QString a = QString::number(v, base);
// and back using and sets ok to true on success
bool ok(false);
int v2 = a.toInt(&ok, base);
// verify our results
QVERIFY(ok == true);
QVERIFY(v = v2);

在文本中经常遇到参数字符。一种方式是可以使用QString("Hello" + name)的形式,而更灵活一点的方法是arg标记法。它在翻译时保留了参数的顺序。

// create a name
QString name("Joe");
// get the day of the week as string
QString weekday = QDate::currentDate().toString("dddd");
// format a text using paramters (%1, %2)
QString hello = QString("Hello %1. Today is %2.").arg(name).arg(weekday);
// This worked on Monday. Promise!
if(Qt::Monday == QDate::currentDate().dayOfWeek()) {
    QCOMPARE(QString("Hello Joe. Today is Monday."), hello);
} else {
    QVERIFY(QString("Hello Joe. Today is Monday.") !=  hello);
}

有时想在代码中直接使用Unicode字符。这样你需要记住在QCharQString类中如何标记它们。

// Create a unicode character using the unicode for smile :-)
QChar smile(0x263A);
// you should see a :-) on you console
qDebug() << smile;
// Use a unicode in a string
QChar smile2 = QString("\u263A").at(0);
QVERIFY(smile == smile2);
// Create 12 smiles in a vector
QVector smilies(12);
smilies.fill(smile);
// Can you see the smiles
qDebug() << smilies;

这里给出了在Qt如何处理Unicode的例子。对于非Unicode,QByteArray类有许多函数用于转换。可以查阅QString在Qt文档中的说明,那里有许多很好的例子。

QObject,QString,QByteArrary
  • QFile, QDir, QFileInfo, QIODevice
  • QTextStream, QDataStream
  • QDebug, QLoggingCategory
  • QTcpServer, QTcpSocket, QNetworkRequest, QNetworkReply
  • QAbstractItemModel, QRegExp
  • QList, QHash
  • QThread, QProcess
  • QJsonDocument, QJSValue
  • 对于初学者来说应该够了。

    model.name”),列表视图将会查找“name”的映射,并使用NameRole请求模型数据。用户定义角色应该以Qt::UserRole开头,且必须在模型中唯一 。

    #include "roleentrymodel.h"
    
    RoleEntryModel::RoleEntryModel(QObject *parent)
        : QAbstractListModel(parent)
    {
        // Set names to the role name hash container (QHash)
        // model.name, model.hue, model.saturation, model.brightness
        m_roleNames[NameRole] = "name";
        m_roleNames[HueRole] = "hue";
        m_roleNames[SaturationRole] = "saturation";
        m_roleNames[BrightnessRole] = "brightness";
    
        // Append the color names as QColor to the data list (QList)
        for(const QString& name : QColor::colorNames()) {
            m_data.append(QColor(name));
        }
    
    }
    
    RoleEntryModel::~RoleEntryModel()
    {
    }
    
    int RoleEntryModel::rowCount(const QModelIndex &parent) const
    {
        Q_UNUSED(parent);
        return m_data.count();
    }
    
    QVariant RoleEntryModel::data(const QModelIndex &index, int role) const
    {
        int row = index.row();
        if(row < 0 || row >= m_data.count()) {
            return QVariant();
        }
        const QColor& color = m_data.at(row);
        qDebug() << row << role << color;
        switch(role) {
        case NameRole:
            // return the color name as hex string (model.name)
            return color.name();
        case HueRole:
            // return the hue of the color (model.hue)
            return color.hueF();
        case SaturationRole:
            // return the saturation of the color (model.saturation)
            return color.saturationF();
        case BrightnessRole:
            // return the brightness of the color (model.brightness)
            return color.lightnessF();
        }
        return QVariant();
    }
    
    QHash<int, QByteArray> RoleEntryModel::roleNames() const
    {
        return m_roleNames;
    }

    实现代码仅改动了两个地方。第一是初始化。现在以QColor数据类型来初始化数据列表(m_data)。并且,定义了角色名称映射以备QML访问。这个映射稍后由::roleNames函数返回。
    第二个变化是::data函数。增加了switch语句以涵盖其它角色(如,色调hue, 饱和度saturation, 亮度brightness)。无法从color获取SVG name,因为颜色color可以是任意颜色,而SVG names是有限的(不足以描述所有颜色)。所以我们跳过这个。需要一个结构struct { QColor, QString }来存储名字,以便识别有名字的颜色。
    注册类型后,就可以在用户界面里使用模型及其条目了。

    ListView {
        id: view
        anchors.fill: parent
        model: RoleEntryModel {}
        focus: true
        delegate: ListDelegate {
            text: 'hsv(' +
                  Number(model.hue).toFixed(2) + ',' +
                  Number(model.saturation).toFixed() + ',' +
                  Number(model.brightness).toFixed() + ')'
            color: model.name
        }
        highlight: ListHighlight { }
    }

    将返回的类型转换为 JS 数字类型,以便能够使用定点表示法格式化数字。不调用Number函数代码也能正常工作(比如:model.saturation.toFixed(2))。选择哪种格式,取决于对返回的数据的信赖程度。

    可以到Github来找相关资源)。本例使用QtQuick.ControlsQtQuick.Layout模块使得代码更紧凑。控件模块提供了Qt Quick里一系列桌面相关的UI元素。布局模块提供了一些非常有用的布局管理器。

    import QtQuick
    import QtQuick.Window
    import QtQuick.Controls
    import QtQuick.Layouts
    
    // our module
    import org.example 1.0
    
    Window {
        visible: true
        width: 480
        height: 480
    
        Background { // a dark background
            id: background
        }
    
        // our dyanmic model
        DynamicEntryModel {
            id: dynamic
            onCountChanged: {
                // we print out count and the last entry when count is changing
                print('new count: ' + dynamic.count)
                print('last entry: ' + dynamic.get(dynamic.count - 1))
            }
        }
    
        ColumnLayout {
            anchors.fill: parent
            anchors.margins: 8
            ScrollView {
                Layout.fillHeight: true
                Layout.fillWidth: true
                ListView {
                    id: view
                    // set our dynamic model to the views model property
                    model: dynamic
                    delegate: ListDelegate {
                        required property var model
                        width: ListView.view.width
                        // construct a string based on the models proeprties
                        text: 'hsv(' +
                              Number(model.hue).toFixed(2) + ',' +
                              Number(model.saturation).toFixed() + ',' +
                              Number(model.brightness).toFixed() + ')'
                        // sets the font color of our custom delegates
                        color: model.name
    
                        onClicked: {
                            // make this delegate the current item
                            view.currentIndex = model.index
                            view.focus = true
                        }
                        onRemove: {
                            // remove the current entry from the model
                            dynamic.remove(model.index)
                        }
                    }
                    highlight: ListHighlight { }
                    // some fun with transitions :-)
                    add: Transition {
                        // applied when entry is added
                        NumberAnimation {
                            properties: "x"; from: -view.width;
                            duration: 250; easing.type: Easing.InCirc
                        }
                        NumberAnimation { properties: "y"; from: view.height;
                            duration: 250; easing.type: Easing.InCirc
                        }
                    }
                    remove: Transition {
                        // applied when entry is removed
                        NumberAnimation {
                            properties: "x"; to: view.width;
                            duration: 250; easing.type: Easing.InBounce
                        }
                    }
                    displaced: Transition {
                        // applied when entry is moved
                        // (e.g because another element was removed)
                        SequentialAnimation {
                            // wait until remove has finished
                            PauseAnimation { duration: 250 }
                            NumberAnimation { properties: "y"; duration: 75
                            }
                        }
                    }
                }
            }
            TextEntry {
                id: textEntry
                onAppend: function (color) {
                    // called when the user presses return on the text field
                    // or clicks the add button
                    dynamic.append(color)
                }
    
                onUp: {
                    // called when the user presses up while the text field is focused
                    view.decrementCurrentIndex()
                }
                onDown: {
                    // same for down
                    view.incrementCurrentIndex()
                }
            }
        }
    }

    模型视图编程在Qt开发中是较复杂的一种模式。模型不象其它应用开发那样,它是不需要实现接口的少量类之一。所有其它类可正常使用。应该在QML端来对模型进行刻画。你应该关注你的用户如何在QML中使用你的模型。为此,应该首先使用ListModel创建一个原型来看下在QML如它如何最好地运作。当定义QML API的时候,这条也适用。让C++ 端数据到QML可用,这不仅是技术范畴,也是编程范式的变化:从实现式到声明式编程的变化。所以要为挫折与心惊喜做好准备:-)