Содержание

Qr-коды на Qt

Для генерации и отображения Qr-кодов будем использовать библиотеку qt-qrcode. Эта библиотека основана, как и большинство библиотек Qr-кодов, на основе libqrencode. Но проблема большинства библиотек в отвратном качестве кода. А вот эта либа вполне прилично выглядит.

Поддержка библиотеки прекратилась 8 лет назад, поэтому сделал я форк. Сейчас из изменений там есть фикс бага (если просто создать qr-код через конструктор, он будет невалиден, раньше приходилось заполнять его через сеттеры), и изменены pro/pri-файлы, чтобы можно было без лишних телодвижений подключить библиотеку в свой проект и как dll(so), и просто как исходные файлы.

О приложении

Само приложение будет выполнено в виде списка токен + expiration. Будут иметься функции создания нового токена, удаления старого, обновления всего списка и показа выбранного токена в виде Qr-кода.

Список токенов

Для хранения списка токенов создадим ListModel. Модель будет сама ходить на заданный в конструкторе адрес, доставая, создавая или удаляя токены.

TokenModel
TokenModel.h
class TokenModel: public QAbstractListModel {
private:
    QPointer<QNetworkAccessManager> m_manager;
    QMap<QUuid, QDateTime> m_tokens;
    const QString m_tokenApi;
public:
    TokenModel(const QString& address, QObject* parent = nullptr);
public:
    QPair<QUuid, QDateTime> tokenAt(qint32 index) const;
public:
    void create(qint64 expirationSpan);
    void remove(const QUuid& token);
    void refresh();
public:
    int rowCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
private:
    void parseTokensFromJson(const QJsonDocument& document);
};

TokenModel.cpp
TokenModel::TokenModel(const QString& address, QObject* parent)
    :QAbstractListModel{ parent },
     m_manager{ new QNetworkAccessManager{ this } },
     m_tokenApi{ "http://" + address + "/auth/token" } {}
QPair<QUuid, QDateTime> TokenModel::tokenAt(qint32 index) const {
    const auto token = std::next(m_tokens.begin(), index);
    return { token.key(), token.value() };
}

void TokenModel::create(qint64 expirationSpan) {
    const auto request = QNetworkRequest{ QString{ "%1/%2/%3" }.arg(m_tokenApi).arg("create").arg(expirationSpan) };
    const auto reply = m_manager->get(request);
    connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
    connect(reply, &QNetworkReply::finished, this, &TokenModel::refresh);
}
void TokenModel::remove(const QUuid &token) {
    const auto request = QNetworkRequest{ QString{ "%1/%2/%3" }.arg(m_tokenApi).arg("remove").arg(token.toString(QUuid::StringFormat::WithoutBraces)) };
    const auto reply = m_manager->get(request);
    connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
    connect(reply, &QNetworkReply::finished, this, &TokenModel::refresh);
}
void TokenModel::refresh() {
    const auto reply = m_manager->get(QNetworkRequest{ QString{ "%1/%2" }.arg(m_tokenApi).arg("all") });
    QObject::connect(reply, &QNetworkReply::finished, [this, reply]{
        const auto result = QJsonDocument::fromJson(reply->readAll());
        this->parseTokensFromJson(result);
        reply->deleteLater();
    });
}

int TokenModel::rowCount(const QModelIndex&/*parent*/) const {
    return m_tokens.size();
}
QVariant TokenModel::data(const QModelIndex &index, int role) const {
    if(not index.isValid()) return {};
    if(role != Qt::ItemDataRole::DisplayRole) return {};

    const auto item = std::next(m_tokens.begin(), index.row());
    return QString{ "[%1 <=> %2]" }.arg(item.key().toString(), item.value().toString());
}

void TokenModel::parseTokensFromJson(const QJsonDocument& document) {
    this->beginResetModel();
    m_tokens.clear();
    const auto tokens = document.object();
    for(const auto& token: tokens.keys())
        m_tokens.insert(QUuid::fromString(token), QDateTime::fromSecsSinceEpoch(tokens[token].toInteger()));

    this->endResetModel();
}

Отображения Qr-кода

Для отображения напишем два класса: QrCodeWidget и QrCodeDialog. Первый будет просто рисовать Qr-код на себе, а второй — использовать первый для отображения qr-кода в диалоговом окне.

Создание классов тривиально. Сам qr-код рисуется средствами класса QtQrCodePainter из библиотеки qt-qrcode. Этот класс может нарисовать qr-код на любом QPainterDevice, достаточно лишь запихнуть QPainterDevice в QPaint, и передать QtQrCodePainter.

QrCodeWidget
class QrCodeWidget: public QWidget {
private:
    QtQrCode m_code;
public:
    QrCodeWidget(const QtQrCode &code = {}, QWidget *parent = nullptr);
protected:
    virtual void paintEvent(QPaintEvent *event) override;
};

QrCodeWidget::QrCodeWidget(const QtQrCode &code, QWidget *parent)
    :QWidget{ parent }, m_code{ code } {}

void QrCodeWidget::paintEvent(QPaintEvent *event) {
    QWidget::paintEvent(event);
    QPainter painter{ this };
    QtQrCodePainter{}.paint(m_code, painter, width(), height());
}

QrCodeDialog
class QrCodeDialog : public QDialog {
public:
    QrCodeDialog(const QByteArray &data, QWidget* parent = nullptr);
};

QrCodeDialog::QrCodeDialog(const QByteArray &data, QWidget* parent)
    :QDialog{ parent } {
    auto codeView = new QrCodeWidget{ QtQrCode{ data } };
    auto layout = new QVBoxLayout{};
    layout->addWidget(codeView);
    this->setLayout(layout);
}

Основное окно приложения

Основное окно будет содержать 4 кнопки для обновления списка, удаления, добавления и отображения токенов.

Определять, какой код удалить или отобразить можно за счёт модели выделения (QAbstractSelectionModel). Эта модель предоставляется классами отображения (и может быть заменена на любую другую), например, QListView.

TokenView.h
class TokenView: public QWidget {
private:
    QPointer<TokenModel> m_tokens;
    QPointer<QListView> m_view;
public:
    TokenView(QWidget* parent = nullptr);
private slots:
    void showToken();
    void createToken();
    void removeToken();
};

Конструктор создаёт 4 кнопки, модель и представления, задаёт соединения (connect) для операций и раскидывает элементы по layout-ам.

Иконки взяты с сайта https://icons8.com.

Конструктор TokenView
TokenView::TokenView(QWidget* parent)
    :QWidget{ parent },
     m_tokens{ new TokenModel{ "127.0.0.1:5555" } },
     m_view{ new QListView{} } {
        m_view->setModel(m_tokens);
        auto showTokenButton = new QPushButton{ QIcon{ ":/images/icon-qr-code.png" }, {} };
        auto refreshModelButton = new QPushButton{ QIcon{ ":/images/icon-update.png" }, {} };
        auto createTokenButton = new QPushButton{ QIcon{ ":/images/icon-plus.png" }, {} };
        auto removeTokenButton = new QPushButton{ QIcon{ ":/images/icon-minus.png" }, {} };

        connect(m_view, &QAbstractItemView::doubleClicked, this, &TokenView::showToken);
        connect(showTokenButton, &QPushButton::clicked, this, &TokenView::showToken);
        connect(refreshModelButton, &QPushButton::clicked, m_tokens, &TokenModel::refresh);
        connect(createTokenButton, &QPushButton::clicked, this, &TokenView::createToken);
        connect(removeTokenButton, &QPushButton::clicked, this, &TokenView::removeToken);

        auto layout = new QVBoxLayout{};
            auto buttonLayout = new QHBoxLayout{};
            buttonLayout->addWidget(refreshModelButton);
            buttonLayout->addWidget(createTokenButton);
            buttonLayout->addWidget(removeTokenButton);
            buttonLayout->addWidget(showTokenButton);
            buttonLayout->addStretch(1);
        layout->addLayout(buttonLayout, 0);
        layout->addWidget(m_view, 1);
        this->setLayout(layout);
        m_tokens->refresh();
    }

Обновление списка вызывается напрямую у модели при нажатии на кнопку, а остальным операциям нужно немного больше логики, поэтому для них создаются отдельные методы.

Qr код отображается в диалоговом окне, развёрнутом на максимальный размер. Закрыть можно по крестику, или нажав Escape.

При создании токена запрашивается время его жизни в секундах, а при удалении нужно лишь, чтобы был один выделенный токен.

Операции TokenView
void TokenView::showToken() {
    const auto selection = m_view->selectionModel()->selection();
    if(selection.size() != 1)
        return;

    const auto token = m_tokens->tokenAt(selection.indexes().front().row());
    QrCodeDialog qrCoreView{ token.first.toRfc4122(), this };
    qrCoreView.showMaximized();
    qrCoreView.exec();
}
void TokenView::createToken() {
    bool isValidExpirationSpan{};
    const auto expirationSpan = QInputDialog::getInt(this, "Expiration span", "Seconds:", 86400,
        0, std::numeric_limits<qint32>::max(), 1, &isValidExpirationSpan);
    if(not isValidExpirationSpan)
        return;

    m_tokens->create(expirationSpan);
}
void TokenView::removeToken() {
    const auto selection = m_view->selectionModel()->selection();
    if(selection.size() != 1)
        return;

    const auto token = m_tokens->tokenAt(selection.indexes().front().row());
    m_tokens->remove(token.first);
}

Список токенов
Список токенов
Qr-код
Qr-код

Немного стилизации

Дефолтные окна для Windows — довольно шакальная вещь (поэтому мне нравятся такие дистрибутивы, как Manjaro Linux, на которых даже приложения без стилей выглядят вполне приятно).

Ну а на Windows мы можем в любой момент добавить набор qss-файлов. Для простоты воспользуемся инструментом qt_material. С его помощью можно сгенерировать qss-файлы с кучей настроек сразу под (ну вроде как) все виджеты.

Для этого делаем:

pip install qt_material

Затем переходим в папку, куда хотим сгенерировать стили, открываем в ней консоль питона и вбиваем, например, такие настройки (подробнее на сайте проекта):

Пример настроек qt_material
from qt_material import export_theme

extra = {

    # Button colors
    'danger': '#dc3545',
    'warning': '#ffc107',
    'success': '#17a2b8',

    # Font
    'font_family': 'monospace',
    'font_size': '14px',
    'line_height': '13px',

    # Density Scale
    'density_scale': '0',

    # environ
    'pyside6': True,
    'linux': True,

}

export_theme(
    theme='dark_teal.xml', qss='dark_teal.qss', rcc='styles.qrc',
    output='theme', prefix='icon:/', invert_secondary=False, extra=extra)

На выходе получаем файлы с ресурсами, которые нужно подключить к своему приложению (как будет выглядеть pro-файл, можно посмотреть на GitHub). Тут ещё стоит отметить, что файлы картинок (иконки и т.п), будут подключены как DISTFILES, но для распространения может быть удобно подключить их как RESOURCES.

Теперь устанавливаем для приложения сгенерированный стиль:

main
int main(int argc, char *argv[]) {
    QApplication app{ argc, argv };
    {
        QDir::addSearchPath("icon", ":/icon/theme");
        QFile file(":/file/dark_teal.qss");
        file.open(QFile::ReadOnly);
        QString styleSheet = QLatin1String(file.readAll());
        app.setStyleSheet(styleSheet);
    }

    TokenView window{};
    window.show();
    return app.exec();
}

И оно начинает выглядеть как-то так:

Основное окно
Основное окно
Диалог для ввода числа
Диалог для ввода числа
Qr-код
Qr-код

Заключение

В реальном проекте для генерации токена должны использоваться какие-то личные данные, чтобы в случае утечки токена было понятно, кому давать по шапке. Ну и, конечно, никакого отображения токенов в виде списка быть не должно, это сделано просто для простоты восприятия.

Если дойдут руки, посмотрим, как можно сделать Android-приложения на Qt для сканирования такого токена и использования его в запросах.

Комментарии (0)