QtGrpc Chat
Une application de chat pour partager des messages de toutes sortes dans une salle de chat.
L'exemple Chat démontre l'utilisation avancée de l'API client QtGrpc. Le serveur permet aux utilisateurs de s'enregistrer et de s'authentifier, ce qui leur permet de rejoindre le salon de discussion. Une fois inscrits, les utilisateurs peuvent partager avec tous les autres participants différents types de messages dans le salon de discussion, tels que des messages textuels, des images, des activités d'utilisateur ou tout autre fichier de leur disque.

Exécution de l'exemple
- Assurez-vous que le site
qtgrpc_chat_serverest en cours d'exécution et qu'il écoute avec succès. - Si vous êtes sur la même machine que le serveur, l'adresse par défaut
localhostdevrait suffire pour exécuter l'exempleqtgrpc_chat_client. Si vous utilisez un périphérique autre que celui qui héberge le serveur, spécifiez l'adresse IP correcte de l'hôte qui exécute le serveur dans la boîte de dialogue Paramètres. - Assurez-vous que l'option
GRPC_CHAT_USE_EMOJI_FONTCMake est activée sur le client pour construire avec une expérience emoji fluide 🚀.

Pour exécuter l'exemple à partir de Qt CreatorOuvrez le mode Welcome et sélectionnez l'exemple de Examples. Pour plus d'informations, voir Qt Creator: Tutoriel : Construire et exécuter.
Modules et classes pertinents.
Cet exemple présente les modules et classes Qt suivants.
- Communication par l'intermédiaire de QGrpcBidiStream à longue durée de vie.
- Utilisation du client QtGrpc à partir d'un travailleur thread.
- Utilisation du module QtProtobufQtCoreTypes dans le schéma protobuf.
- Communication sécurisée par SSL.
- Visualisation des messages QtProtobuf dans un QML ListView.
Schéma Protobuf
Le schéma Protobuf définit la structure des messages et des services utilisés dans l'application de chat. Le schéma est divisé en deux fichiers :
syntax = "proto3";
package chat;
import "chatmessages.proto";
service QtGrpcChat {
// Register a user with \a Credentials.
rpc Register(Credentials) returns (None);
// Join as a registered user and exchange \a ChatMessage(s)
rpc ChatRoom(stream ChatMessage) returns (stream ChatMessage) {}
}Le fichier qtgrpcchat.proto spécifie le service QtGrpcChat, qui fournit deux méthodes RPC :
Register: Enregistrement d'un utilisateur à l'adresseCredentials. Le serveur stocke et vérifie les utilisateurs à partir d'une base de données en texte clair.ChatRoom: Établit un flux bidirectionnel pour l'échange deChatMessage(s) entre tous les clients connectés. Le serveur diffuse tous les messages entrants aux autres clients connectés.
syntax = "proto3";
package chat;
import "QtCore/QtCore.proto";
message ChatMessage {
string username = 1;
int64 timestamp = 2;
oneof content {
TextMessage text = 3;
FileMessage file = 4;
UserStatus user_status = 5;
}
}Le fichier chatmessages.proto définit ChatMessage, qui est une union étiquetée (également connue sous le nom de type somme). Il représente tous les messages individuels qui peuvent être envoyés par le biais de la RPC en continu ChatRoom. Chaque ChatMessage doit inclure un username et un timestamp pour identifier l'expéditeur.
Vous devez inclure l'importation QtCore/QtCore.proto pour activer les types du module QtProtobufQtCoreTypes, ce qui permet une conversion transparente entre les types spécifiques à QtCore et leurs équivalents Protobuf.
message FileMessage {
enum Type {
UNKNOWN = 0;
IMAGE = 1;
AUDIO = 2;
VIDEO = 3;
TEXT = 4;
}
Type type = 1;
string name = 2;
bytes content = 3;
uint64 size = 4;
message Continuation {
uint64 index = 1;
uint64 count = 2;
QtCore.QUuid uuid = 3;
}
optional Continuation continuation = 5;
}FileMessage est l'un des types de messages pris en charge pour le type de somme ChatMessage. Il permet d'envelopper n'importe quel fichier local dans un message. Le champ optionnel Continuation garantit une livraison fiable en traitant les transferts de fichiers volumineux par morceaux.
Note : Pour plus de détails sur l'utilisation du module ProtobufQtCoreTypes dans votre schéma Protobuf et votre code d'application, voir Qt Core usage.
Serveur
Note : L'application serveur décrite ici utilise la bibliothèque gRPC™ bibliothèque.
L'application serveur utilise l'API de rappel asynchrone gRPC. Cela nous permet de bénéficier des avantages de performance de l'API asynchrone sans la complexité de la gestion manuelle des files d'attente d'achèvement.
class QtGrpcChatService final : public chat::QtGrpcChat::CallbackService
Vous déclarez la classe QtGrpcChatService, qui sous-classe la classe CallbackService du service QtGrpcChat généré.
grpc::ServerBidiReactor<chat::ChatMessage, chat::ChatMessage> * ChatRoom(grpc::CallbackServerContext *context) override { return new ChatRoomReactor(this, context); } grpc::ServerUnaryReactor *Register(grpc::CallbackServerContext *context, const chat::Credentials *request, chat::None * /*response*/) override
Vous remplacez également les fonctions virtuelles pour implémenter la fonctionnalité des deux méthodes gRPC fournies par le service :
- La méthode
Registervérifie et stocke les utilisateurs dans une base de données en texte brut. - La méthode
ChatRoomvérifie les informations d'identification fournies dans les métadonnées par rapport à la base de données. En cas de succès, elle établit un flux bidirectionnel pour la communication.
// Broadcast \a message to all connected clients. Optionally \a skip a client void broadcast(const std::shared_ptr<chat::ChatMessage> &message, const ChatRoomReactor *skip) { for (auto *client : activeClients()) { assert(client); if (skip && client == skip) continue; client->startSharedWrite(message); } }
L'implémentation du service suit tous les clients actifs qui se connectent ou se déconnectent via la méthode ChatRoom. Cela permet la fonctionnalité broadcast, qui partage les messages avec tous les clients connectés. Pour réduire le stockage et les frais généraux, la méthode ChatMessage est enveloppée dans une méthode shared_ptr.
// Share \a response. It will be kept alive until the last write operation finishes. void startSharedWrite(std::shared_ptr<chat::ChatMessage> response) { std::scoped_lock lock(m_writeMtx); if (m_response) { m_responseQueue.emplace(std::move(response)); } else { m_response = std::move(response); StartWrite(m_response.get()); } }
La méthode startSharedWrite est une fonction membre de la méthode ChatRoomReactor. Si le réacteur est en cours d'écriture, le message est mis en mémoire tampon dans une file d'attente. Sinon, une opération d'écriture est lancée. Un seul et unique message est partagé par tous les clients. Chaque copie du message response augmente la valeur de use_count. Une fois que tous les clients ont fini d'écrire le message et que use_count tombe à 0, ses ressources sont libérées.
// Distribute the incoming message to all other clients. m_service->broadcast(m_request, this); m_request = std::make_shared<chat::ChatMessage>(); // detach StartRead(m_request.get());
Cet extrait fait partie de la méthode virtuelle ChatRoomReactor::OnReadDone. Chaque fois que cette méthode est appelée, un nouveau message a été reçu du client. Le message est diffusé à tous les autres clients, sans passer par l'expéditeur.
std::scoped_lock lock(m_writeMtx); if (!m_responseQueue.empty()) { m_response = std::move(m_responseQueue.front()); m_responseQueue.pop(); StartWrite(m_response.get()); return; } m_response.reset();
Cet extrait fait partie de la méthode virtuelle ChatRoomReactor::OnWriteDone. Chaque fois que cette méthode est appelée, un message a été écrit au client. S'il y a des messages en mémoire tampon dans la file d'attente, le message suivant est écrit. Dans le cas contraire, m_response est réinitialisé pour signaler qu'aucune opération d'écriture n'est en cours. Un verrou est utilisé pour éviter les conflits avec la méthode broadcast.
Client
L'application client utilise le schéma Protobuf fourni pour communiquer avec le serveur. Elle fournit des capacités frontales et dorsales pour enregistrer les utilisateurs et gérer le flux bidirectionnel à longue durée de vie de la méthode ChatRoom gRPC . Cela permet la visualisation et la communication de ChatMessages.
Configuration
add_library(qtgrpc_chat_client_proto STATIC)
qt_add_protobuf(qtgrpc_chat_client_proto
QML
QML_URI QtGrpcChat.Proto
PROTO_FILES
../proto/chatmessages.proto
PROTO_INCLUDES
$<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)
qt_add_grpc(qtgrpc_chat_client_proto CLIENT
PROTO_FILES
../proto/qtgrpcchat.proto
PROTO_INCLUDES
$<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)Tout d'abord, générez les fichiers sources à partir du schéma Protobuf. Comme le fichier qtgrpcchat.proto ne contient aucune définition de message, seule la génération de qtgrpcgen est nécessaire. Fournissez également le PROTO_INCLUDES du module ProtobufQtCoreTypes pour vous assurer que l'importation de "QtCore/QtCore.proto" est valide.
target_link_libraries(qtgrpc_chat_client_proto
PUBLIC
Qt6::Protobuf
Qt6::ProtobufQtCoreTypes
Qt6::Grpc
)Assurez-vous que la cible indépendante qtgrpc_chat_client_proto est publiquement liée à ses dépendances, y compris le module ProtobufQtCoreTypes. La cible de l'application est ensuite liée à cette bibliothèque.
Logique du backend
Le backend de l'application est construit autour de quatre éléments cruciaux :
ChatEngine: Un singleton orienté QML qui gère la logique de l'application.ClientWorker: Un objet travailleur qui fournit la fonctionnalité client gRPC de manière asynchrone.ChatMessageModel: UnQAbstractListModelpersonnalisé pour gérer et stocker lesChatMessage.UserStatusModel: UnQAbstractListModelpersonnalisé pour gérer l'activité de l'utilisateur.
explicit ChatEngine(QObject *parent = nullptr); ~ChatEngine() override; // Register operations Q_INVOKABLE void registerUser(const chat::Credentials &credentials); // ChatRoom operations Q_INVOKABLE void login(const chat::Credentials &credentials); Q_INVOKABLE void logout(); Q_INVOKABLE void sendText(const QString &message); Q_INVOKABLE void sendFile(const QUrl &url); Q_INVOKABLE void sendFiles(const QList<QUrl> &urls); Q_INVOKABLE bool sendFilesFromClipboard();
L'extrait ci-dessus montre une partie de la fonctionnalité Q_INVOKABLE qui est appelée à partir de QML pour interagir avec le serveur.
explicit ClientWorker(QObject *parent = nullptr); ~ClientWorker() override; public Q_SLOTS: void registerUser(const chat::Credentials &credentials); void login(const chat::Credentials &credentials); void logout(); void sendFile(const QUrl &url); void sendFiles(const QList<QUrl> &urls); void sendMessage(const chat::ChatMessage &message);
Les slots fournis par ClientWorker reflètent quelque peu l'API exposée par ChatEngine. ClientWorker fonctionne dans un thread dédié pour traiter les opérations coûteuses, telles que la transmission ou la réception de fichiers volumineux, en arrière-plan.
m_clientWorker->moveToThread(&m_clientThread); m_clientThread.start(); connect(&m_clientThread, &QThread::finished, m_clientWorker, &QObject::deleteLater); connect(m_clientWorker, &ClientWorker::registerFinished, this, &ChatEngine::registerFinished); connect(m_clientWorker, &ClientWorker::chatError, this, &ChatEngine::chatError); ...
Dans le constructeur de ChatEngine, assignez ClientWorker à son thread de travail dédié et continuez à gérer et à transmettre ses signaux pour les rendre disponibles du côté de QML.
void ChatEngine::registerUser(const chat::Credentials &credentials) { QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::registerUser, credentials); } ... void ClientWorker::registerUser(const chat::Credentials &credentials) { if (credentials.name().isEmpty() || credentials.password().isEmpty()) { emit chatError(tr("Invalid credentials for registration")); return; } if ((!m_client || m_hostUriDirty) && !initializeClient()) { emit chatError(tr("Failed registration: unabled to initialize client")); return; } auto reply = m_client->Register(credentials, QGrpcCallOptions{}.setDeadlineTimeout(5s)); const auto *replyPtr = reply.get(); connect( replyPtr, &QGrpcCallReply::finished, this, [this, reply = std::move(reply)](const QGrpcStatus &status) { emit registerFinished(status); }, Qt::SingleShotConnection); }
Ceci démontre comment ChatEngine interagit avec ClientWorker pour enregistrer les utilisateurs. Étant donné que ClientWorker fonctionne dans son propre thread, il est important d'utiliser invokeMethod pour appeler ses fonctions membres en toute sécurité.
Dans ClientWorker, vous vérifiez si le client n'est pas initialisé ou si l'URI de l'hôte a changé. Si l'une de ces conditions est remplie, appelez initializeClient, qui crée un nouveau QGrpcHttp2Channel. Comme il s'agit d'une opération coûteuse, il convient d'en réduire le nombre d'occurrences.
Pour gérer la RPC Register, utilisez l'option setDeadlineTimeout pour vous prémunir contre l'inactivité du serveur. Il est généralement recommandé de fixer une date limite pour les RPC unaires.
void ClientWorker::login(const chat::Credentials &credentials) { if (credentials.name().isEmpty() || credentials.password().isEmpty()) { emit chatError(tr("Invalid credentials for login")); return; } ... QGrpcCallOptions opts; opts.setMetadata({ { "user-name", credentials.name().toUtf8() }, { "user-password", credentials.password().toUtf8() }, }); connectStream(opts); }
Lorsque vous vous connectez à ChatRoom, vous pouvez utiliser l'option setMetadata pour fournir les informations d'identification de l'utilisateur, comme l'exige le serveur pour l'authentification. L'appel et l'établissement de la connexion sont gérés par la méthode connectStream.
void ClientWorker::connectStream(const QGrpcCallOptions &opts) { ... m_chatStream = m_client->ChatRoom(*initialMessage, opts); ... connect(m_chatStream.get(), &QGrpcBidiStream::finished, this, [this, opts](const QGrpcStatus &status) { if (m_chatState == ChatState::Connected) { // If we're connected retry again in 250 ms, no matter the error. QTimer::singleShot(250, [this, opts]() { connectStream(opts); }); } else { setState(ChatState::Disconnected); m_chatResponse = {}; m_userCredentials = {}; m_chatStream.reset(); emit chatStreamFinished(status); } }); ...
Pour mettre en œuvre une logique de reconnexion de base au cas où le flux se termine brusquement alors que vous êtes toujours connecté. Pour ce faire, il suffit d'appeler à nouveau connectStream avec le QGrpcCallOptions de l'appel initial. Cela garantit que toutes les connexions nécessaires sont également mises à jour.
Remarque : le mode Doze/App-Standby d'Android peut être déclenché, par exemple, en utilisant le FileDialog ou en basculant vers une autre application. Ce mode coupe l'accès au réseau, ferme toutes les connexions QTcpSocket actives et provoque l'affichage du flux à l'adresse finished. Vous pouvez résoudre ce problème grâce à la logique de reconnexion.
connect(m_chatStream.get(), &QGrpcBidiStream::messageReceived, this, [this] { ... switch (m_chatResponse.contentField()) { case chat::ChatMessage::ContentFields::UninitializedField : qDebug("Received uninitialized message"); return; case chat::ChatMessage::ContentFields::Text : if (m_chatResponse.text().content().isEmpty()) return; break; case chat::ChatMessage::ContentFields::File : // Télécharger tout message de fichier et stocker l'URL téléchargée dans le // contenu, permettant au modèle d'y faire référence. m_chatResponse.file() . setContent(saveFileRequest(m_chatResponse.file()).toString().toUtf8()) ; break; ... emit chatStreamMessageReceived(m_chatResponse) ; }) ; setState(Backend::ChatState::Connecting) ; }
Lorsque des messages sont reçus, ClientWorker effectue certains traitements préliminaires, comme la sauvegarde du contenu de FileMessage, de sorte que ChatEngine n'ait plus qu'à se concentrer sur les modèles. Utilisez l'enum ContentFields pour vérifier en toute sécurité le champ oneof content de notre type de somme ChatMessage.
void ChatEngine::sendText(const QString &message) { if (message.trimmed().isEmpty()) return; if (auto request = m_clientWorker->createMessage()) { chat::TextMessage tmsg; tmsg.setContent(message.toUtf8()); request->setText(std::move(tmsg)); QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendMessage, *request); m_chatMessageModel->appendMessage(*request); } } ... void ClientWorker::sendMessage(const chat::ChatMessage &message) { if (!m_chatStream || m_chatState != ChatState::Connected) { emit chatError(tr("Unable to send message")); return; } m_chatStream->writeMessage(message); }
Lors de l'envoi de messages, ChatEngine crée des requêtes correctement formatées. Par exemple, la méthode sendText accepte un QString et utilise la fonction createMessage pour générer un message valide avec les champs username et timestamp définis. Le client est ensuite invité à envoyer le message et une copie est mise en file d'attente dans notre propre ChatMessageModel.
Frontal QML
import QtGrpc import QtGrpcChat import QtGrpcChat.Proto
Les importations suivantes sont utilisées dans le code QML :
QtGrpc: Fournit la fonctionnalité QtGrpc QML, telle que StatusCode.QtGrpcChat: Notre module d'application, qui comprend des composants tels que le singletonChatEngine.QtGrpcChat.Proto: Fournit un accès QML à nos types de protobuf générés.
Connections { target: ChatEngine function onChatStreamFinished(status) { root.handleStatus(status) loginView.clear() } function onChatStateChanged() { if (ChatEngine.chatState === Backend.ChatState.Connected && mainView.depth === 1) mainView.push("ChatView.qml") else if (ChatEngine.chatState === Backend.ChatState.Disconnected && mainView.depth > 1) mainView.pop() } function onRegisterFinished(status) { root.handleStatus(status) } function onChatError(message) { statusDisplay.text = message statusDisplay.color = "yellow" statusDisplay.restart() } }
Main.qml gère les signaux principaux émis par ChatEngine. La plupart de ces signaux sont gérés globalement et sont visualisés dans n'importe quel état de l'application.
Rectangle { id: root property credentials creds ... ColumnLayout { id: credentialsItem ... RowLayout { id: buttonLayout ... Button { id: loginButton ... enabled: nameField.text && passwordField.text text: qsTr("Login") onPressed: { root.creds.name = nameField.text root.creds.password = passwordField.text ChatEngine.login(root.creds) } }
Les types de messages générés à partir du schéma protobuf sont accessibles en QML car il s'agit de QML_VALUE_TYPEs (une version camelCase de la définition du message). Le message LoginView.qml utilise la propriété de type de valeur credentials pour lancer le message login sur le message ChatEngine.
ListView { id: chatMessageView ... component DelegateBase: Item { id: base required property chatMessage display default property alias data: chatLayout.data ... } ... // We use the DelegateChooser and the 'whatThis' role to determine // the correct delegate for any ChatMessage delegate: DelegateChooser { role: "whatsThis" ... DelegateChoice { roleValue: "text" delegate: DelegateBase { id: dbt TextDelegate { Layout.fillWidth: true Layout.maximumWidth: root.maxMessageBoxWidth Layout.preferredHeight: implicitHeight Layout.bottomMargin: root.margin Layout.leftMargin: root.margin Layout.rightMargin: root.margin message: dbt.display.text selectionColor: dbt.lightColor selectedTextColor: dbt.darkColor } } }
Dans ChatView.qml, le ListView affiche les messages dans le ChatRoom. C'est un peu plus complexe, car vous devez traiter le type de somme ChatMessage de manière conditionnelle.
Vous pouvez utiliser un DelegateChooser, qui nous permet de sélectionner le délégué approprié en fonction du type de message. Utilisez le rôle whatThis par défaut dans le modèle, qui fournit le type de message pour chaque instance ChatMessage. Le composant DelegateBase accède ensuite au rôle display du modèle, rendant les données chatMessage disponibles pour le rendu.
TextEdit { id: root required property textMessage message text: message.content color: "#f3f3f3" font.pointSize: 14 wrapMode: TextEdit.Wrap readOnly: true selectByMouse: true }
Voici l'un des composants qui visualise le type TextMessage. Il utilise le type de valeur textMessage du module protobuf pour visualiser le texte.
TextArea.flickable: TextArea { id: inputField function sendTextMessage() : void { if (text === "") return ChatEngine.sendText(text) text = "" } ... Keys.onPressed: (event) => { if (event.key === Qt.Key_Return && event.modifiers & Qt.ControlModifier) { sendTextMessage() event.accepted = true } else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { if (ChatEngine.sendFilesFromClipboard()) event.accepted = true } }
Le client Chat fournit divers points d'accès pour l'envoi de messages tels que :
- Acceptation de fichiers déposés sur l'application.
- <Ctrl + V> pour envoyer tout ce qui est stocké dans le QClipboard.
- <Ctrl + Entrée> pour envoyer le message à partir de l'écran d'accueil.
inputField - Cliquer sur le bouton d'envoi du message.
inputField - Sélection de fichiers à l'aide d'un FileDialog
SSL
Pour sécuriser la communication entre le serveur et les clients, le cryptage SSL/TLS est utilisé. Pour ce faire, il faut au minimum les éléments suivants
- Clé privée: contient la clé privée du serveur, qui est utilisée pour établir des connexions sécurisées. Elle doit rester confidentielle et ne doit jamais être partagée.
- Certificat: contient le certificat public du serveur, qui est partagé avec les clients pour vérifier l'identité du serveur. Il est généralement signé par une autorité de certification (AC) ou peut être auto-signé à des fins de test.
- Certificat d'autorité de certification racine facultatif : Si vous utilisez une autorité de certification personnalisée pour signer le certificat de votre serveur, le certificat de l'autorité de certification racine est requis du côté client pour valider la chaîne de certificats du serveur. Cela garantit que le client peut faire confiance au certificat du serveur, car le certificat racine de l'autorité de certification personnalisée n'est pas préinstallé dans le magasin de confiance du client, comme c'est le cas pour les autorités de certification publiques.
Utilisez OpenSSL pour créer ces fichiers et configurer notre communication gRPC pour utiliser SSL/TLS.
grpc::SslServerCredentialsOptions sslOpts; sslOpts.pem_key_cert_pairs.emplace_back(grpc::SslServerCredentialsOptions::PemKeyCertPair{ LocalhostKey, LocalhostCert, }); builder.AddListeningPort(QtGrpcChatService::httpsAddress(), grpc::SslServerCredentials(sslOpts)); builder.AddListeningPort(QtGrpcChatService::httpAddress(), grpc::InsecureServerCredentials());
Vous fournissez la clé privée et le certificat au serveur gRPC. Avec cela, vous pouvez construire le SslServerCredentials pour activer TLS du côté du serveur. En plus d'une communication sécurisée, il est également possible d'autoriser un accès non crypté.
Le serveur écoute les adresses suivantes :
- HTTPS :
0.0.0.0:65002 - HTTP :
0.0.0.0:65003
Le serveur se lie à 0.0.0.0 pour écouter sur toutes les interfaces réseau, ce qui permet l'accès à partir de n'importe quel appareil sur le même réseau.
if (m_hostUri.scheme() == "https") { if (!QSslSocket::supportsSsl()) { emit chatError(tr("The device doesn't support SSL. Please use the 'http' scheme.")) ; return false; } QFile crtFile(":/res/root.crt") ; if (!crtFile.open(QFile::ReadOnly)) { qFatal("Unable to load root certificate"); return false; } QSslConfiguration sslConfig ; QSslCertificate crt(crtFile.readAll()) ; sslConfig.addCaCertificate(crt) ; sslConfig.setProtocol(QSsl::TlsV1_2OrLater) ; sslConfig.setAllowedNextProtocols({ "h2" }) ; // Allow HTTP/2 // Disable hostname verification to allow connections from any local IP. // Acceptable for development but avoid in production for security.sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone) ; opts.setSslConfiguration(sslConfig) ; }
Le client charge le certificat de l'autorité de certification racine, puisque vous avez auto-signé l'autorité de certification. Ce certificat est utilisé pour créer le protocole QSslCertificate. Il est important de fournir le protocole "h2" avec setAllowedNextProtocols, car vous utilisez HTTP/2.
Fichiers sources
Voir aussi Tous les exemples de Qt.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.