第十七章: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()
要用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。
注意
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());
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());
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字符。这样你需要记住在QChar
和QString
类中如何标记它们。
// 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
对于初学者来说应该够了。
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;
}
#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.Controls
和QtQuick.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()
}
}
}
}
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可用,这不仅是技术范畴,也是编程范式的变化:从实现式到声明式编程的变化。所以要为挫折与心惊喜做好准备:-)