Вступление
Язык QML для Qt Quick позволяет c легкостью делать многие вещи, особенно что касается анимированных пользовательских интерфейсов. Однако, не всё возможно сделать или не всё подходит под реализацию в QML, в частности:
- получение доступа к функциональности извне окружения QML/Javascript
- реализация критических по производительности функций, где требуется нативный код для повышения эффективности
- большой или сложный не декларативный код, который было бы утомительно реализовывать в JavaScript
Как Вы увидите впоследствии, Qt легко отображает C++ код для QML. В этой статье мы создадим маленькое, но функциональное приложение, делающее это. Пример написан для Qt 5 и использует компоненты Qt Quick, поэтому для запуска примера Вам необходим как минимум Qt 5.1.0.
Обзор
Базовые шаги, для того, чтобы показать тип C++, с его свойствами, методами, сигналами и слотами, окружению QML:
- определение нового класса, унаследованного от QObject
- помещение макроса Q_OBJECT в объявление класса для поддержки сигналов, слотов и прочих сервисов метаобъектной системы Qt
- объявление свойств, используя макрос Q_PROPERTY
- вызов qmlRegisterType() в C++ коде приложения для регистрации типа в Qt Quick движке
Подробности находятся в разделе документации Qt Отображение атрибутов C++ для QML и в туториале Создание расширений C++ для QML.
Генератор SSH ключей
Для примера, создадим небольшое приложение, которое будет генерировать пару открытого/закрытого SSH ключей, с помощью GUI. Пользователю будут предоставлены элементы управления для соответствующих параметров, затем будет запущена программа ssh-keygen для генерации пары ключей.
Мы реализуем пользовательский интерфейс, используя новые элементы управления Qt Quick, посколько это было задумано как настольное приложение. Первоначально, для получения опыта взаимодействия, запустим qmlscene с исходным кодом QML. Скриншот показан ниже:
Пользовательский интерфейс запрашивает пользователя данные о типе ключа, имени файла для генерируемого приватного ключа и, опционально, секретную фразу, которая должна быть подтверждена.
C++ класс
Теперь, имея интерфейс, нам необходимо реализовать внутреннюю функциональность. Мы не можем вызвать внешнюю программу напрямую из QML, поэтому мы должны написать это на C++ (что есть целью нашего приложения).
Сперва, определим класс, инкапсулирующий функционал для генерации ключа (он будет отображен как новый класс KeyGenerator в QML). Это сделано в заголовочном файле KeyGenerator.h:
#ifndef KEYGENERATOR_H
#define KEYGENERATOR_H
#include <QObject>
#include <QString>
#include <QStringList>
// Simple QML object to generate SSH key pairs by calling ssh-keygen.
class KeyGenerator : public QObject
{
Q_OBJECT
Q_PROPERTY(QString type READ type WRITE setType NOTIFY typeChanged)
Q_PROPERTY(QStringList types READ types NOTIFY typesChanged)
Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged)
Q_PROPERTY(QString passphrase READ passphrase WRITE setPassphrase NOTIFY passphraseChanged)
public:
KeyGenerator();
~KeyGenerator();
QString type();
void setType(const QString &t);
QStringList types();
QString filename();
void setFilename(const QString &f);
QString passphrase();
void setPassphrase(const QString &p);
public slots:
void generateKey();
signals:
void typeChanged();
void typesChanged();
void filenameChanged();
void passphraseChanged();
void keyGenerated(bool success);
private:
QString _type;
QString _filename;
QString _passphrase;
QStringList _types;
};
#endif
Нам необходимо наследовать наш класс от QObject. Мы объявляем все свойства, которые нам нужны и все связанные с ними методы, методы уведомлений становятся сигналами. В нашем случае, мы хотим иметь свойства для выбранного типа ключа, списка всех допустимых типов SSH ключей, имени файла и секретной фразы. Тип ключа произвольно сделан строкой. Можно было бы сделать перечислением, но пример был бы более сложным.
Кстати, новая возможность макроса Q_PROPERTY в Qt 5.1.0 — это аргумент MEMBER. Он позволяет задать переменную, являющуюся членом класса, которая будет привязана к свойству, без реализации функций получения и установки. Эта возможность тут не используется.
Далее, объявим методы для установки и получения и для сигналов. Также объявим один слот с именем generateKey(). Всё это будет доступно для QML. Если бы мы хотели сделать обычный метод видимым для QML, его нужно бы было пометить как Q_INVOKABLE. В этом случае было решено сделать метод generateKey() слотом, посколько это возможно будет полезно в будущем, но он с легкостю может быть и вызываемым методом.
В конце, объявим все необходимые нам закрытые члены класса.
C++ реализация
Сейчас, давайте посмотрим на реализацию класса в KeyGenerator.cpp. Вот исходный код:
#include <QFile>
#include <QProcess>
#include "KeyGenerator.h"
KeyGenerator::KeyGenerator()
: _type("rsa"), _types{"dsa", "ecdsa", "rsa", "rsa1"}
{
}
KeyGenerator::~KeyGenerator()
{
}
QString KeyGenerator::type()
{
return _type;
}
void KeyGenerator::setType(const QString &t)
{
// Check for valid type.
if (!_types.contains(t))
return;
if (t != _type) {
_type = t;
emit typeChanged();
}
}
QStringList KeyGenerator::types()
{
return _types;
}
QString KeyGenerator::filename()
{
return _filename;
}
void KeyGenerator::setFilename(const QString &f)
{
if (f != _filename) {
_filename = f;
emit filenameChanged();
}
}
QString KeyGenerator::passphrase()
{
return _passphrase;
}
void KeyGenerator::setPassphrase(const QString &p)
{
if (p != _passphrase) {
_passphrase = p;
emit passphraseChanged();
}
}
void KeyGenerator::generateKey()
{
// Sanity check on arguments
if (_type.isEmpty() or _filename.isEmpty() or
(_passphrase.length() > 0 and _passphrase.length() < 5)) {
emit keyGenerated(false);
return;
}
// Remove key file if it already exists
if (QFile::exists(_filename)) {
QFile::remove(_filename);
}
// Execute ssh-keygen -t type -N passphrase -f keyfileq
QProcess *proc = new QProcess;
QString prog = "ssh-keygen";
QStringList args{"-t", _type, "-N", _passphrase, "-f", _filename};
proc->start(prog, args);
proc->waitForFinished();
emit keyGenerated(proc->exitCode() == 0);
delete proc;
}
В конструкторе инициализируются некоторые переменные, члены класса. Для интереса, используется возможность C++11 список инициализации для инициализации переменной члена класса _types, которая имеет тип QStringList. Деструктор сейчас ничего не делает, но присутствует для полноты и будущего расширения.
Функции получения, такие как type() просто возращают значение соответсвующей переменной члена класса. Функции установки устанавливают соответсвующие переменные, заботясь о проверке, что новое значение отличается от старого и, если это так, испускают соответсвующий сигнал. Обратите внимание, что сигналы, созданные MOC, как и всегда, не нуждаются в реализации, их можно только испускать в нужное время.
Только одним не тривиальным методом есть слот generateKey(). Он делает некоторые проверки аргументов и создает QProcess для запуска внешней программы ssh-keygen. Для простоты и так как это обычно выполняется быстро, мы делаем это синхронно и ожидаем завершения ssh-keygen. Когда эта программа завершится, излучаем сигнал, имеющий аргумент с типом boolean, который показывает была ли генерация ключа успешной или нет.
Код QML
Теперь взглянем на QML код в main.qml:
// SSH key generator UI
import QtQuick 2.1
import QtQuick.Controls 1.0
import QtQuick.Layouts 1.0
import QtQuick.Dialogs 1.0
import com.ics.demo 1.0
ApplicationWindow {
title: qsTr("SSH Key Generator")
statusBar: StatusBar {
RowLayout {
Label {
id: status
}
}
}
width: 369
height: 166
ColumnLayout {
x: 10
y: 10
// Key type
RowLayout {
Label {
text: qsTr("Key type:")
}
ComboBox {
id: combobox
Layout.fillWidth: true
model: keygen.types
currentIndex: 2
}
}
// Filename
RowLayout {
Label {
text: qsTr("Filename:")
}
TextField {
id: filename
implicitWidth: 200
onTextChanged: updateStatusBar()
}
Button {
text: qsTr("&Browse...")
onClicked: filedialog.visible = true
}
}
// Passphrase
RowLayout {
Label {
text: qsTr("Pass phrase:")
}
TextField {
id: passphrase
Layout.fillWidth: true
echoMode: TextInput.Password
onTextChanged: updateStatusBar()
}
}
// Confirm Passphrase
RowLayout {
Label {
text: qsTr("Confirm pass phrase:")
}
TextField {
id: confirm
Layout.fillWidth: true
echoMode: TextInput.Password
onTextChanged: updateStatusBar()
}
}
// Buttons: Generate, Quit
RowLayout {
Button {
id: generate
text: qsTr("&Generate")
onClicked: keygen.generateKey()
}
Button {
text: qsTr("&Quit")
onClicked: Qt.quit()
}
}
}
FileDialog {
id: filedialog
title: qsTr("Select a file")
selectMultiple: false
selectFolder: false
nameFilters: [ "All files (*)" ]
selectedNameFilter: "All files (*)"
onAccepted: {
filename.text = fileUrl.toString().replace("file://", "")
}
}
KeyGenerator {
id: keygen
filename: filename.text
passphrase: passphrase.text
type: combobox.currentText
onKeyGenerated: {
if (success) {
status.text = qsTr('<font color="green">Key generation succeeded.</font>')
} else {
status.text = qsTr('<font color="red">Key generation failed</font>')
}
}
}
function updateStatusBar() {
if (passphrase.text != confirm.text) {
status.text = qsTr('<font color="red">Pass phrase does not match.</font>')
generate.enabled = false
} else if (passphrase.text.length > 0 && passphrase.text.length < 5) {
status.text = qsTr('<font color="red">Pass phrase too short.</font>')
generate.enabled = false
} else if (filename.text == "") {
status.text = qsTr('<font color="red">Enter a filename.</font>')
generate.enabled = false
} else {
status.text = ""
generate.enabled = true
}
}
Component.onCompleted: updateStatusBar()
}
Приведенный выше код немного большой, однако, большая часть работы связана с расстановкой компонентов GUI. Этот код должен быть предельно простым, чтобы двигаться дальше.
Заметьте, что мы импортируем com.ics.demo версии 1.0. Вскоре, мы увидим, где появляется имя этого модуля. Это делает новый QML тип KeyGenerator доступном и мы объявляем его экземпляр. Имея доступ к его C++ свойствам, как к QML свойствам, можно вызывать его методы и работать с сигналами, как это сделано с onKeyGenerated.
Более полное приложение следует делать с дополнительными проверками ошибок и осмысленными уведомлениями об ошибках, если генерация ключа была неудачной (мы могли бы легко добавить новый метод или свойство для этого). Пользовательский интерфейс также можно улучшить, делая его более гибким.
Наша основая программа, является, по сути, враппером, таким как qmlscene. Всё что нам необходимо, это зарегистрировать наш тип, для доступа к нему в движке QML:
qmlRegisterType<KeyGenerator>("com.ics.demo", 1, 0, "KeyGenerator");
Это делает C++ тип KeyGenerator доступным как QML тип KeyGenerator, в модуле com.ics.demo версии 1.0, который будет импортирован.
Как правило, для запуска QML кода из исполняемого файла, в главной программе Вы создаете QGuiApplication и QQuickView. Сейчас же, для использования компонентов Qt Quick необходимо немного дополнительной работы, если элемент верхнего уровня ApplicationWindow или Window. Вы можете посмотреть исходный код, как это реализуется. Это урезанная версия qmlscene, необходимый минимум для примера.
Вот полный листинг главной программы, main.cpp:
#include <QApplication>
#include <QObject>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQuickWindow>
#include <QSurfaceFormat>
#include "KeyGenerator.h"
// Main wrapper program.
// Special handling is needed when using Qt Quick Controls for the top window.
// The code here is based on what qmlscene does.
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
// Register our component type with QML.
qmlRegisterType<KeyGenerator>("com.ics.demo", 1, 0, "KeyGenerator");
int rc = 0;
QQmlEngine engine;
QQmlComponent *component = new QQmlComponent(&engine);
QObject::connect(&engine, SIGNAL(quit()), QCoreApplication::instance(), SLOT(quit()));
component->loadUrl(QUrl("main.qml"));
if (!component->isReady() ) {
qWarning("%s", qPrintable(component->errorString()));
return -1;
}
QObject *topLevel = component->create();
QQuickWindow *window = qobject_cast<QQuickWindow *>(topLevel);
QSurfaceFormat surfaceFormat = window->requestedFormat();
window->setFormat(surfaceFormat);
window->show();
rc = app.exec();
delete component;
return rc;
}
Если это не очевидно, при использовании модулей, написанных на C++, с QML, Вы не можете использовать программу qmlscene, потому что C++ код не будет слинкован. Если же Вы попробуете это, то получите сообщение об ошибке, что модуль не установлен.
Выводы
Этот пример показывает, как легко можно создавать новые QML компоненты на C++ и отображать свойства, сигналы и слоты. Хотя многое можно сделать с QML, C++ по прежнему полезен и, как правило, будет использован в сочетании с QML в любом не тривиальном приложении.
Вы можете скачать весь исходный код примера отсюда.