# 应用程序类型

本节介绍了可以使用Qt 6编写的不同类型的应用程序。它不只是为了呈现所能做的选择,而会让您更好地了解使用Qt 6可以实现的总体目标。

# 控制台应用程序

控制台应用程序不提供图形化的用户界面,通常作为系统服务的一部分调用或从命令行调用。 Qt 6附带了一系列现成的组件,可帮助您非常高效地创建跨平台控制台应用程序。 例如,网络文件API、字符串处理和高效的命令行解析器。 由于Qt是C++之上的高级API,可以获得与执行速度相匹配的编程速度。不要将Qt视为只是一个 UI 工具包 - 它可以提供更多功能!

# 字符串处理

第一个示例演示了如何合并2个常量字符串。 诚然,这不是一个非常有用的应用程序,但它让您了解没有事件循环的本地C++应用程序可能是什么样子。

// module or class includes
// 包含模块或者类
include <QtCore>

// text stream is text-codec aware
// 文本流是文本编码器敏感的
QTextStream cout(stdout, QIODevice::WriteOnly);

int main(int argc, char** argv)
{
    // avoid compiler warnings
    // 避免编译器警告
    Q_UNUSED(argc)
    Q_UNUSED(argv)
    QString s1("Paris");
    QString s2("London");
    // string concatenation
    // 字符串合并
    QString s = s1 + " " + s2 + "!";
    cout << s << endl;
}

# 容器类

此示例向应用程序添加列表和列表迭代器。 Qt带有大量易于使用的容器类集合,并且具有与其他Qt类相同的API范例。

QString s1("Hello");
QString s2("Qt");
QList<QString> list;
// stream into containers
// 通过流放到容器中
list << s1 << s2;
// Java and STL like iterators
// 类似于Java和STL的迭代器
QListIterator<QString> iter(list);
while(iter.hasNext()) {
    cout << iter.next();
    if(iter.hasNext()) {
        cout << " ";
    }
}
cout << "!" << endl;

这是一个更高级的列表函数,它允许将字符串列表连接成一个字符串。 当需要进行基于行的文本输入时,这非常方便。 使用 QString::split() 函数也可以实现相反功能(字符串分隔成字符串列表)。

QString s1("Hello");
QString s2("Qt");
// convenient container classes
// 便捷的容器类
QStringList list;
list <<  s1 << s2;
// join strings
// 拼接字符串
QString s = list.join(" ") + "!";
cout << s << endl;

# 文件IO

在下一个代码片段中,从本地目录读取一个CSV文件并遍历每一行,从中提取单元格。 这样做,用了大约20行代码从csv文件中获取表数据。文件读取提供了一个字节流,为了能够将其转换为有效的Unicode文本,需要使用文本流并将文件作为低级别的流传入。要写入CSV文件,您只需要在写入模式下打开文件,并将这些行通过管道传输到文本流中即可。

QList<QStringList> data;
// file operations
// 文件操作
QFile file("sample.csv");
if(file.open(QIODevice::ReadOnly)) {
    QTextStream stream(&file);
    // loop forever macro
    // 指示永远循环的宏
    forever {
        QString line = stream.readLine();
        // test for null string 'String()'
        if(line.isNull()) {
            break;
        }
        // test for empty string 'QString("")'
        // 空字符串测试 'QString("")'
        if(line.isEmpty()) {
            continue;
        }
        QStringList row;
        // for each loop to iterate over containers
        // 对每一个循环,将每行的内容在容器里迭代
        foreach(const QString& cell, line.split(",")) {
            row.append(cell.trimmed());
        }
        data.append(row);
    }
}
// No cleanup necessary.
// 不需要清理

关于使用Qt基于控制台应用程序的部分到此结束。

# C++ Widget应用程序

基于控制台的应用程序非常方便,但有时您需要有图形用户界面 (GUI)。另外,基于GUI的应用程序可能需要后端来读取/写入文件、通过网络进行通信或将数据保存在容器中。

第一个片段中,是基于widget(小部件)的应用程序,尽可能少地创建一个窗口并显示它。在Qt中,没有父级的widget是一个窗口。我们使用作用域指针(QScopedPointer)来确保当指针超出范围时删除widget。应用程序对象(QApplication)封装了Qt运行时,通过调用exec()启动事件循环。之后,应用程序仅对由用户输入(例如鼠标或键盘)或其他事件提供程序(例如网络或文件IO)触发的事件做出反应。应用程序仅在退出事件循环时退出。退出是通过在应用程序上调用quit()或关闭窗口来完成。

运行代码时,您将看到一个大小为 240 x 120 像素的窗口。

include <QtGui>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    QScopedPointer<QWidget> widget(new CustomWidget());
    widget->resize(240, 120);
    widget->show();
    return app.exec();
}

# 自定义Widget

当处理用户界面时,可能需要创建定制的Widget。 通常,Widget是一个充满绘画调用的窗口区域。 此外,Widget具有如何处理键盘和鼠标输入以及如何对外部触发器做出反应的内部机制。 要在Qt中做到这一点,需要从QWidget派生类并覆盖几个用于绘制和事件处理的函数。

#pragma once

include <QtWidgets>

class CustomWidget : public QWidget
{
    Q_OBJECT
public:
    explicit CustomWidget(QWidget *parent = 0);
    void paintEvent(QPaintEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
private:
    QPoint m_lastPos;
};

在类的实现中,在Widget上绘制了一个小边框,并在鼠标的最后位置上绘制了一个小矩形。 这是非常典型的低级自定义Widget。 鼠标和键盘事件会更改Widget的内部状态并触发绘画更新。 这里不会详细介绍此代码,如果你可能了解它的含义的话更好。Qt附带了大量现成的桌面Widget,因此可能不必这样自定义Widget。

include "customwidget.h"

CustomWidget::CustomWidget(QWidget *parent) :
    QWidget(parent)
{
}

void CustomWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    QRect r1 = rect().adjusted(10,10,-10,-10);
    painter.setPen(QColor("#33B5E5"));
    painter.drawRect(r1);

    QRect r2(QPoint(0,0),QSize(40,40));
    if(m_lastPos.isNull()) {
        r2.moveCenter(r1.center());
    } else {
        r2.moveCenter(m_lastPos);
    }
    painter.fillRect(r2, QColor("#FFBB33"));
}

void CustomWidget::mousePressEvent(QMouseEvent *event)
{
    m_lastPos = event->pos();
    update();
}

void CustomWidget::mouseMoveEvent(QMouseEvent *event)
{
    m_lastPos = event->pos();
    update();
}

# 桌面Widget

Qt开发人员已经为您完成了事件相应等所有这些工作,提供了一组桌面Widget,在不同的操作系统上都具有本地外观。 那么,使用者的工作就是将Widget容器中的这些不同的Widget排列成更大的面板。Qt中的Widget也可以作为其他Widget的容器,这是通过父子关系实现的。 这意味着准备这些制作现成的Widget(例如按钮、复选框、单选按钮、列表和网格)当成其他Widget的子部件。下面展示了实现此目的的一种方法。

这是所谓的widget容器的头文件。

class CustomWidget : public QWidget
{
    Q_OBJECT
public:
    explicit CustomWidget(QWidget *parent = 0);
private slots:
    void itemClicked(QListWidgetItem* item);
    void updateItem();
private:
    QListWidget *m_widget;
    QLineEdit *m_edit;
    QPushButton *m_button;
};

在类的实现中,使用布局来更好地排布widget。 当容器widget重新调整尺寸时,布局管理器根据一些尺寸策略重新布局widget。 在此示例中,我们有一个列表、一个行编辑器和一个按钮,它们垂直排列并允许用户编辑城市列表。 使用Qt的signalslots来连接发送方和接收方对象。

CustomWidget::CustomWidget(QWidget *parent) :
    QWidget(parent)
{
    QVBoxLayout *layout = new QVBoxLayout(this);
    m_widget = new QListWidget(this);
    layout->addWidget(m_widget);

    m_edit = new QLineEdit(this);
    layout->addWidget(m_edit);

    m_button = new QPushButton("Quit", this);
    layout->addWidget(m_button);
    setLayout(layout);

    QStringList cities;
    cities << "Paris" << "London" << "Munich";
    foreach(const QString& city, cities) {
        m_widget->addItem(city);
    }

    connect(m_widget, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(itemClicked(QListWidgetItem*)));
    connect(m_edit, SIGNAL(editingFinished()), this, SLOT(updateItem()));
    connect(m_button, SIGNAL(clicked()), qApp, SLOT(quit()));
}

void CustomWidget::itemClicked(QListWidgetItem *item)
{
    Q_ASSERT(item);
    m_edit->setText(item->text());
}

void CustomWidget::updateItem()
{
    QListWidgetItem* item = m_widget->currentItem();
    if(item) {
        item->setText(m_edit->text());
    }
}

# 绘制图形

有些问题如果可视化展示会更好。 如果手头的问题看起来像是几何对象,Qt图形视图是一个很好选择。 图形视图在场景中排列简单的几何形状。 用户可以与这些形状交互,或者使用算法定位它们。 要填充图形视图,您需要一个图形视图(graphics view)和一个图形场景(graphics scene)。 场景附加到视图并填充图形项。

这是一个简短的例子。 首先是带有视图(view)和场景(scene)声明的头文件。

class CustomWidgetV2 : public QWidget
{
    Q_OBJECT
public:
    explicit CustomWidgetV2(QWidget *parent = 0);
private:
    QGraphicsView *m_view;
    QGraphicsScene *m_scene;

};

在类的实现中,场景首先附加到视图。 视图是一个小部件,并排列在我们的容器Widget中。 最后,在场景中添加一个小矩形,然后在视图上进行渲染。

include "customwidgetv2.h"

CustomWidget::CustomWidget(QWidget *parent) :
    QWidget(parent)
{
    m_view = new QGraphicsView(this);
    m_scene = new QGraphicsScene(this);
    m_view->setScene(m_scene);

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setMargin(0);
    layout->addWidget(m_view);
    setLayout(layout);

    QGraphicsItem* rect1 = m_scene->addRect(0,0, 40, 40, Qt::NoPen, QColor("#FFBB33"));
    rect1->setFlags(QGraphicsItem::ItemIsFocusable|QGraphicsItem::ItemIsMovable);
}

# 适配数据

到目前为止,主要介绍了基本数据类型以及如何使用widget和图形视图。 在应用程序中,经常需要大量的结构化数据,这些数据也可能需要持久存储。 最后,还需要显示这些数据。 为此,Qt使用模型。 一个简单的模型是字符串列表模型,它用字符串填充然后附加到列表视图。

m_view = new QListView(this);
m_model = new QStringListModel(this);
view->setModel(m_model);

QList<QString> cities;
cities << "Munich" << "Paris" << "London";
m_model->setStringList(cities);

另一种流行的数据存储和检索的方法是SQL。 Qt内置了SQLite,并且还支持其他数据库引擎(例如MySQL和PostgreSQL)。首先,需要使用schema创建数据库,如下所示:

CREATE TABLE city (name TEXT, country TEXT);
INSERT INTO city value ("Munich", "Germany");
INSERT INTO city value ("Paris", "France");
INSERT INTO city value ("London", "United Kingdom");

要使用SQL,需要将SQL模块添加到我们的.pro文件中。

QT += sql

然后可以使用C++打开数据库。 首先,需要为指定的数据库引擎获取一个新的数据库对象。 使用这个数据库对象,打开数据库。 对于SQLite,指定数据库文件的路径就足够了。 Qt提供了一些高级数据库模型,其中之一是表模型。 表模型使用表标识符和可选的where子句来选择数据。 与之前的其他模型一样,生成的模型可以附加到列表视图。

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("cities.db");
if(!db.open()) {
    qFatal("unable to open database");
}

m_model = QSqlTableModel(this);
m_model->setTable("city");
m_model->setHeaderData(0, Qt::Horizontal, "City");
m_model->setHeaderData(1, Qt::Horizontal, "Country");

view->setModel(m_model);
m_model->select();

对于更高级别的模型操作,Qt提供了一个排序文件代理模型(sorting file proxy model),允许对模型进行排序、过滤和转换。

QSortFilterProxyModel* proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(m_model);
view->setModel(proxy);
view->setSortingEnabled(true);

过滤是基于要成为过滤器的列和作为过滤器参数的字符串来完成的。

proxy->setFilterKeyColumn(0);
proxy->setFilterCaseSensitive(Qt::CaseInsensitive);
proxy->setFilterFixedString(QString)

过滤器代理模型比此处演示的要强大得多。 目前,记住它的存在就足够了。

提示

这是可以使用Qt 5开发的不同类型的经典应用程序的概述。桌面正在移动,很快移动设备将成为明天的桌面。 移动设备具有不同的用户界面设计。 它们比桌面应用程序简单得多。 他们只做一件事,而且简单而专注。 动画是移动体验的重要组成部分。 用户界面需要感觉生动和流畅。 传统的Qt技术不太适合这个市场。

接下来: Qt Quick来拯救这种情况。

# Qt Quick应用程序

现代软件开发存在固有的冲突。 用户界面的发展速度比我们的后端服务快得多。 在传统技术中,您以与后端相同的速度开发所谓的前端。 当客户想要在项目期间更改用户界面,或在项目期间有开发用户界面的想法时,这会导致冲突。 敏捷项目,需要敏捷的方法。

Qt Quick提供了一个声明式环境,其中用户界面(前端)像HTML一样声明,而后端则是本地C++代码。 这样可以两全其美。

下面是一个简单的Qt Quick UI。

import QtQuick

Rectangle {
    width: 240; height: 240
    Rectangle {
        width: 40; height: 40
        anchors.centerIn: parent
        color: '#FFBB33'
    }
}

这个声明语言称为QML,它需要一个运行时来执行它。 Qt提供了一个名为qml的标准运行时,还可以编写自定义运行时。 为此,需要快速视图(quick view)并将主QML文档设置为来自C++方法的源,然后就可以显示用户界面了。

#include <QtGui>
#include <QtQml>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine("main.qml");
    return app.exec();
}

回到之前的例子。 示例中,使用了C++城市模型。 如果可以在声明性QML代码中使用这个模型,那就太好了。

为了实现这一点,首先对前端进行编码,以了解希望如何使用城市模型。 在这种情况下,前端需要一个名为cityModel的对象,可以在列表视图中使用它。

import QtQuick

Rectangle {
    width: 240; height: 120
    ListView {
        width: 180; height: 120
        anchors.centerIn: parent
        model: cityModel
        delegate: Text { text: model.city }
    }
}

为了启用cityModel,主要可以重用以前的模型,并将上下文属性添加到我们的根上下文中。 根上下文是主文档中的另一个根元素。

m_model = QSqlTableModel(this);
... // some magic code
QHash<int, QByteArray> roles;
roles[Qt::UserRole+1] = "city";
roles[Qt::UserRole+2] = "country";
m_model->setRoleNames(roles);
engine.rootContext()->setContextProperty("cityModel", m_model);
最后更新: 11/21/2021, 8:32:49 PM