Considérations sur les performances et suggestions
Considérations sur la synchronisation
En tant que développeur d'applications, vous vous efforcez généralement de permettre au moteur de rendu d'atteindre un taux de rafraîchissement constant de 60 images par seconde. En fonction de votre matériel et de vos exigences, ce chiffre peut être différent, mais 60 images par seconde est très courant. 60 FPS signifie qu'il y a environ 16 millisecondes entre chaque image pendant lesquelles un traitement peut être effectué, ce qui inclut le traitement requis pour télécharger les primitives de dessin vers le matériel graphique.
En pratique, cela signifie que le développeur de l'application doit :
- utiliser une programmation asynchrone et événementielle dans la mesure du possible
- utiliser des threads de travail pour effectuer les traitements importants
- ne jamais faire tourner manuellement la boucle d'événements
- ne jamais passer plus de quelques millisecondes par image dans des fonctions bloquantes.
Le non-respect de cette règle entraînera des sauts d'images, ce qui aura un effet considérable sur l'expérience de l'utilisateur.
Remarque : un modèle tentant, mais qui ne devrait jamais être utilisé, consiste à créer votre propre QEventLoop ou à appeler QCoreApplication::processEvents() afin d'éviter le blocage dans un bloc de code C++ invoqué à partir de QML. C'est dangereux, car lorsqu'une boucle d'événements est saisie dans un gestionnaire de signaux ou une liaison, le moteur QML continue à exécuter d'autres liaisons, animations, transitions, etc. Ces liaisons peuvent alors provoquer des effets secondaires qui, par exemple, détruisent la hiérarchie contenant votre boucle d'événements.
Profilage
Le conseil le plus important est le suivant : utilisez l'outil QML Profiler inclus dans Qt Creator. Le fait de savoir où le temps est passé dans une application vous permettra de vous concentrer sur les problèmes qui existent réellement, plutôt que sur les problèmes qui pourraient exister. Voir Qt Creator: Profiler les applications QML pour plus d'informations.
Déterminer quelles liaisons sont exécutées le plus souvent, ou quelles fonctions de votre application passent le plus de temps, vous permettra de décider si vous devez optimiser les zones problématiques, ou revoir certains détails de l'implémentation de votre application afin d'en améliorer les performances. Tenter d'optimiser le code sans profilage est susceptible d'entraîner des améliorations de performance très mineures plutôt que significatives.
Code JavaScript
La plupart des applications QML contiennent du code JavaScript, sous la forme d'expressions de liaison de propriétés, de fonctions et de gestionnaires de signaux. Cela ne pose généralement pas de problème. Grâce à des outils avancés tels que le compilateurQt Quick , les fonctions et les liaisons simples peuvent être très rapides. Toutefois, il faut veiller à ne pas déclencher accidentellement des traitements inutiles. L'outil QML Profiler peut fournir de nombreux détails sur l'exécution de JavaScript et sur ce qui l'a déclenchée.
Conversion de type
L'un des principaux coûts de l'utilisation de JavaScript est que, dans certains cas, lorsqu'on accède à une propriété d'un type QML, un objet JavaScript avec une ressource externe contenant les données C++ sous-jacentes (ou une référence à celles-ci) est créé. Dans la plupart des cas, cette opération est relativement peu coûteuse, mais dans d'autres cas, elle peut être très onéreuse. Il convient d'être prudent lorsque l'on manipule des types de valeurs ou des types de séquences complexes et de grande taille. Ceux-ci doivent être copiés par le moteur QML chaque fois que vous les modifiez ou que vous les affectez à une propriété différente. Lorsque cela devient un goulot d'étranglement, envisagez d'utiliser des types d'objets à la place. Les listes de types d'objets n'ont pas le même problème que les listes de types de valeurs car les listes de types d'objets sont implémentées à l'aide de QQmlListProperty.
La plupart des conversions entre des types de valeurs simples sont peu coûteuses. Il y a cependant des exceptions. La création d'une url à partir d'une chaîne de caractères peut impliquer la construction d'une instance QUrl, ce qui est coûteux.
Résolution des propriétés
La résolution des propriétés prend du temps. Bien que les recherches soient généralement optimisées pour s'exécuter beaucoup plus rapidement lors des exécutions suivantes, il est toujours préférable d'éviter tout travail inutile, dans la mesure du possible.
Dans l'exemple suivant, nous avons un bloc de code qui est souvent exécuté (dans ce cas, il s'agit du contenu d'une boucle explicite, mais il pourrait s'agir d'une expression de liaison fréquemment évaluée, par exemple) et dans lequel nous résolvons l'objet avec l'identifiant "rect" et sa propriété "color" à plusieurs reprises :
// bad.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { printValue("red", rect.color.r); printValue("green", rect.color.g); printValue("blue", rect.color.b); printValue("alpha", rect.color.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
Chaque fois que rect.color est récupéré, le moteur QML doit :
- Allouer une enveloppe de type valeur sur le tas JavaScript.
- Exécuter le getter de la propriété
colorde Rectangle. - Copier le résultat QColor dans le wrapper de type valeur.
Il n'est pas nécessaire d'effectuer cette opération quatre fois. Nous pouvons résoudre la base commune une seule fois dans le bloc :
// good.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { var rectColor = rect.color; // resolve the common base. printValue("red", rectColor.r); printValue("green", rectColor.g); printValue("blue", rectColor.b); printValue("alpha", rectColor.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
Cette simple modification permet d'améliorer considérablement les performances. Notez que le code ci-dessus peut être encore amélioré (puisque la propriété recherchée ne change jamais pendant le traitement de la boucle), en extrayant la résolution de la propriété de la boucle, comme suit :
// better.qml import QtQuick Item { width: 400 height: 200 Rectangle { id: rect anchors.fill: parent color: "blue" } function printValue(which: string, value: real) { console.log(which + " = " + value); } Component.onCompleted: { var t0 = new Date(); var rectColor = rect.color; // resolve the common base outside the tight loop. for (var i = 0; i < 1000; ++i) { printValue("red", rectColor.r); printValue("green", rectColor.g); printValue("blue", rectColor.b); printValue("alpha", rectColor.a); } var t1 = new Date(); console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations"); } }
Liaisons de propriétés
Une expression de liaison de propriété sera réévaluée si l'une des propriétés qu'elle référence est modifiée. C'est pourquoi les expressions de liaison doivent être aussi simples que possible.
Si vous avez une boucle dans laquelle vous effectuez un traitement, mais que seul le résultat final du traitement est important, il est souvent préférable de mettre à jour un accumulateur temporaire que vous affectez ensuite à la propriété que vous devez mettre à jour, plutôt que de mettre à jour progressivement la propriété elle-même, afin d'éviter de déclencher la réévaluation des expressions de liaison au cours des étapes intermédiaires de l'accumulation.
L'exemple suivant illustre ce point :
// bad.qml import QtQuick Item { id: root width: 200 height: 200 property int accumulatedValue: 0 Text { anchors.fill: parent text: root.accumulatedValue.toString() onTextChanged: console.log("text binding re-evaluated") } Component.onCompleted: { var someData = [ 1, 2, 3, 4, 5, 20 ]; for (var i = 0; i < someData.length; ++i) { accumulatedValue = accumulatedValue + someData[i]; } } }
La boucle du gestionnaire onCompleted entraîne la réévaluation de la liaison de la propriété "text" six fois (ce qui entraîne la réévaluation de toutes les autres liaisons de propriété qui dépendent de la valeur textuelle, ainsi que du gestionnaire de signal onTextChanged, à chaque fois, et la mise en page du texte pour l'affichage à chaque fois). Ceci est clairement inutile dans ce cas, puisque nous ne nous intéressons qu'à la valeur finale de l'accumulation.
Il pourrait être réécrit comme suit :
// good.qml import QtQuick Item { id: root width: 200 height: 200 property int accumulatedValue: 0 Text { anchors.fill: parent text: root.accumulatedValue.toString() onTextChanged: console.log("text binding re-evaluated") } Component.onCompleted: { var someData = [ 1, 2, 3, 4, 5, 20 ]; var temp = accumulatedValue; for (var i = 0; i < someData.length; ++i) { temp = temp + someData[i]; } accumulatedValue = temp; } }
Conseils de séquence
Comme indiqué précédemment, les séquences de types de valeurs doivent être manipulées avec précaution.
Tout d'abord, les types de séquences ont un comportement différent dans deux scénarios distincts :
- si la séquence est une Q_PROPERTY d'une QObject (nous l'appellerons une séquence de référence),
- si la séquence est renvoyée par une fonction Q_INVOKABLE d'une QObject (nous appellerons cela une séquence de copie).
Une séquence de référence est lue et écrite via QMetaObject chaque fois qu'elle change, soit dans votre code JavaScript, soit sur l'objet original. Pour optimiser les choses, les séquences de référence (ainsi que les types de valeurs de référence) peuvent être chargées paresseusement. Le contenu réel n'est alors récupéré qu'au moment de leur première utilisation. Cela signifie que la modification de la valeur de n'importe quel élément de la séquence à partir de JavaScript se traduira par :
- la lecture éventuelle du contenu du site QObject (s'il est chargé paresseusement).
- La modification de l'élément à l'index spécifié dans cette séquence.
- l'écriture de la séquence entière dans le site QObject.
Une séquence de copie est beaucoup plus simple, car la séquence réelle est stockée dans les données de ressources de l'objet JavaScript, de sorte qu'aucun cycle de lecture/modification/écriture ne se produit (au lieu de cela, les données de ressources sont modifiées directement).
Par conséquent, les écritures sur les éléments d'une séquence de référence seront beaucoup plus lentes que les écritures sur les éléments d'une séquence de copie. En fait, l'écriture sur un seul élément d'une séquence de référence à N éléments équivaut, en termes de coût, à l'affectation d'une séquence de copie à N éléments à cette séquence de référence, de sorte qu'il est généralement préférable de modifier une séquence de copie temporaire, puis d'affecter le résultat à une séquence de référence, au cours du calcul.
Supposons l'existence (et l'enregistrement préalable dans l'espace de noms "Qt.example") du type C++ suivant :
class SequenceTypeExample : public QQuickItem { Q_OBJECT Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged) public: SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; } ~SequenceTypeExample() {} QList<qreal> qrealListProperty() const { return m_list; } void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); } signals: void qrealListPropertyChanged(); private: QList<qreal> m_list; };
L'exemple suivant écrit sur les éléments d'une séquence de référence dans une boucle serrée, ce qui entraîne de mauvaises performances :
// bad.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); qrealListProperty.length = 100; for (var i = 0; i < 500; ++i) { for (var j = 0; j < 100; ++j) { qrealListProperty[j] = j; } } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
La lecture et l'écriture de la propriété QObject dans la boucle interne causée par l'expression "qrealListProperty[j] = j" rend ce code très sous-optimal. Il serait préférable d'utiliser un code fonctionnellement équivalent mais beaucoup plus rapide :
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); var someData = [1.1, 2.2, 3.3] someData.length = 100; for (var i = 0; i < 500; ++i) { for (var j = 0; j < 100; ++j) { someData[j] = j; } qrealListProperty = someData; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Un autre schéma courant à éviter est celui des boucles de lecture-modification-écriture où chaque élément est lu, modifié et réécrit dans la propriété de la séquence. Comme dans l'exemple précédent, cela entraîne la lecture et l'écriture de la propriété QObject à chaque itération :
// bad.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); qrealListProperty.length = 100; for (var i = 0; i < 500; ++i) { for (var j = 0; j < 100; ++j) { qrealListProperty[j] = qrealListProperty[j] * 2; } } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Au lieu de cela, créez une copie manuelle de la séquence, modifiez la copie, puis réaffectez le résultat à la propriété :
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root width: 200 height: 200 Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 500; ++i) { let data = [...qrealListProperty]; for (var j = 0; j < 100; ++j) { data[j] = data[j] * 2; } qrealListProperty = data; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Deuxièmement, un signal de changement pour la propriété est émis si l'un de ses éléments change. Si vous avez de nombreuses liaisons avec un élément particulier dans une propriété de séquence, il est préférable de créer une propriété dynamique liée à cet élément et d'utiliser cette propriété dynamique comme symbole dans les expressions de liaison au lieu de l'élément de séquence, car elle n'entraînera la réévaluation des liaisons que si sa valeur change.
Il s'agit d'un cas d'utilisation inhabituel que la plupart des clients ne devraient jamais rencontrer, mais qu'il vaut la peine de connaître, au cas où vous vous retrouveriez à faire quelque chose de ce genre :
// bad.qml import QtQuick import Qt.example SequenceTypeExample { id: root property int firstBinding: qrealListProperty[1] + 10; property int secondBinding: qrealListProperty[1] + 20; property int thirdBinding: qrealListProperty[1] + 30; Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { qrealListProperty[2] = i; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Notez que même si seul l'élément à l'index 2 est modifié dans la boucle, les trois liaisons seront toutes réévaluées puisque la granularité du signal de changement est que la propriété entière a changé. Ainsi, l'ajout d'une liaison intermédiaire peut parfois s'avérer bénéfique :
// good.qml import QtQuick import Qt.example SequenceTypeExample { id: root property int intermediateBinding: qrealListProperty[1] property int firstBinding: intermediateBinding + 10; property int secondBinding: intermediateBinding + 20; property int thirdBinding: intermediateBinding + 30; Component.onCompleted: { var t0 = new Date(); for (var i = 0; i < 1000; ++i) { qrealListProperty[2] = i; } var t1 = new Date(); console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds"); } }
Dans l'exemple ci-dessus, seule la liaison intermédiaire sera réévaluée à chaque fois, ce qui se traduit par une augmentation significative des performances.
Conseils relatifs au type de valeur
Les propriétés detype valeur (font, color, vector3d, etc.) ont une propriété similaire QObject et une sémantique de notification différente de celle des propriétés de type séquence. Ainsi, les conseils donnés ci-dessus pour les séquences s'appliquent également aux propriétés de type valeur. Bien que le problème soit généralement moins important avec les types de valeur (puisque le nombre de sous-propriétés d'un type de valeur est généralement bien inférieur au nombre d'éléments d'une séquence), toute augmentation du nombre de liaisons réévaluées inutilement aura un impact négatif sur les performances.
Conseils généraux en matière de performances
Les considérations générales sur les performances de JavaScript résultant de la conception du langage s'appliquent également à QML. Les plus importantes sont les suivantes :
- Éviter d'utiliser eval() dans la mesure du possible.
- Ne pas supprimer les propriétés des objets
Éléments d'interface communs
Éléments de texte
Le calcul des mises en page de texte peut être une opération lente. Dans la mesure du possible, envisagez d'utiliser le format PlainText au lieu de StyledText, car cela réduit la quantité de travail requise par le moteur de mise en page. Si vous ne pouvez pas utiliser PlainText (parce que vous devez incorporer des images ou utiliser des balises pour spécifier des plages de caractères à mettre en forme (gras, italique, etc.) plutôt que le texte entier), vous devez utiliser StyledText.
Vous ne devez utiliser AutoText que si le texte pourrait être (mais n'est probablement pas) StyledText, car ce mode entraîne un coût d'analyse. Le mode RichText ne doit pas être utilisé, car StyledText offre presque toutes ses fonctionnalités pour une fraction de son coût.
Images
Les images sont un élément essentiel de toute interface utilisateur. Malheureusement, elles sont aussi une grande source de problèmes en raison du temps nécessaire à leur chargement, de la quantité de mémoire qu'elles consomment et de la manière dont elles sont utilisées.
Chargement asynchrone
Les images sont souvent assez volumineuses et il est donc judicieux de s'assurer que le chargement d'une image ne bloque pas le thread de l'interface utilisateur. Définissez la propriété "asynchrone" de l'élément QML Image sur true pour permettre le chargement asynchrone d'images à partir du système de fichiers local (les images distantes sont toujours chargées de manière asynchrone) lorsque cela n'a pas d'impact négatif sur l'esthétique de l'interface utilisateur.
Les éléments d'image dont la propriété "asynchrone" est fixée à true chargeront les images dans un fil d'exécution à faible priorité.
Taille explicite de la source
Si votre application charge une grande image mais l'affiche dans un élément de petite taille, définissez la propriété "sourceSize" à la taille de l'élément rendu pour vous assurer que la version réduite de l'image est conservée en mémoire, plutôt que la grande.
Attention, la modification de la propriété "sourceSize" entraîne le rechargement de l'image.
Éviter la composition au moment de l'exécution
Rappelez-vous également que vous pouvez éviter le travail de composition à l'exécution en fournissant la ressource image précomposée avec votre application (par exemple, en fournissant des éléments avec des effets d'ombre).
Éviter de lisser les images
N'activez image.smooth que si cela est nécessaire. Le lissage est plus lent sur certains matériels et n'a aucun effet visuel si l'image est affichée dans sa taille naturelle.
Peinture
Évitez de peindre plusieurs fois la même zone. Utilisez Item comme élément racine plutôt que Rectangle pour éviter de peindre plusieurs fois l'arrière-plan.
Positionner les éléments à l'aide d'ancres
Il est plus efficace d'utiliser des ancres plutôt que des liaisons pour positionner les éléments les uns par rapport aux autres. Prenons l'exemple de l'utilisation de liens pour positionner rect2 par rapport à rect1 :
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
x: rect1.x
y: rect1.y + rect1.height
width: rect1.width - 20
height: 200
}Il est plus efficace d'utiliser des ancres :
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
height: 200
anchors.left: rect1.left
anchors.top: rect1.bottom
anchors.right: rect1.right
anchors.rightMargin: 20
}Le positionnement à l'aide de liaisons (en attribuant des expressions de liaison aux propriétés x, y, largeur et hauteur des objets visuels, plutôt que d'utiliser des ancres) est relativement lent, bien qu'il permette une flexibilité maximale.
Si la disposition n'est pas dynamique, la manière la plus performante de la spécifier est l'initialisation statique des propriétés x, y, width et height. Les coordonnées des éléments sont toujours relatives à leur parent, de sorte que si vous souhaitez un décalage fixe par rapport à la coordonnée 0,0 de votre parent, vous ne devez pas utiliser d'ancres. Dans l'exemple suivant, les objets Rectangle enfants sont au même endroit, mais le code des ancres présenté n'est pas aussi efficace en termes de ressources que le code qui utilise un positionnement fixe via une initialisation statique :
Rectangle {
width: 60
height: 60
Rectangle {
id: fixedPositioning
x: 20
y: 20
width: 20
height: 20
}
Rectangle {
id: anchorPositioning
anchors.fill: parent
anchors.margins: 20
}
}Modèles et vues
La plupart des applications ont au moins un modèle qui alimente une vue en données. Il existe une certaine sémantique que les développeurs d'applications doivent connaître afin d'obtenir des performances maximales.
Modèles C++ personnalisés
Il est souvent souhaitable d'écrire son propre modèle personnalisé en C++ pour l'utiliser avec une vue en QML. Bien que la mise en œuvre optimale d'un tel modèle dépende fortement du cas d'utilisation auquel il doit répondre, voici quelques lignes directrices générales :
- Être aussi asynchrone que possible
- Effectuer tous les traitements dans un fil d'exécution (à faible priorité).
- Regrouper les opérations de backend de manière à minimiser les E/S et les IPC (potentiellement lentes).
Il est important de noter qu'il est recommandé d'utiliser un fil d'exécution à faible priorité pour minimiser le risque d'affamer le fil d'exécution de l'interface graphique (ce qui pourrait se traduire par une baisse des performances perçues). N'oubliez pas non plus que les mécanismes de synchronisation et de verrouillage peuvent être une cause importante de lenteur, et qu'il faut donc veiller à éviter les verrouillages inutiles.
ListModel QML Type
Qt Qml Models fournit un type ListModel qui peut être utilisé pour alimenter en données un site ListView. Il est utile pour un prototypage rapide, mais ne convient pas pour de grandes quantités de données. Utilisez une QAbstractItemModel appropriée si nécessaire.
Remplissage au sein d'une file d'attente (Worker Thread)
ListModel Les éléments peuvent être remplis dans un fil d'exécution (à faible priorité) en JavaScript. Le développeur doit explicitement appeler sync() sur ListModel à partir de WorkerScript pour que les modifications soient synchronisées avec le fil d'exécution principal. Voir la documentation de WorkerScript pour plus d'informations.
Veuillez noter que l'utilisation d'un élément WorkerScript entraînera la création d'un moteur JavaScript distinct (étant donné que le moteur JavaScript fonctionne par thread). Il en résultera une utilisation accrue de la mémoire. Cependant, plusieurs éléments WorkerScript utiliseront tous le même thread de travail, de sorte que l'impact sur la mémoire de l'utilisation d'un deuxième ou d'un troisième élément WorkerScript est négligeable dès lors qu'une application en utilise déjà un. En revanche, les scripts de travailleur supplémentaires ne s'exécutent pas en parallèle.
Ne pas utiliser de rôles dynamiques
L'élément ListModel suppose que les types de rôles au sein de chaque élément d'un modèle donné sont stables à des fins d'optimisation. Si le type peut changer dynamiquement d'un élément à l'autre, les performances du modèle seront bien moins bonnes.
Par conséquent, le typage dynamique est désactivé par défaut ; le développeur doit définir spécifiquement la propriété booléenne dynamicRoles du modèle pour activer le typage dynamique (et subir la dégradation des performances qui en découle). Nous vous recommandons de ne pas utiliser le typage dynamique, sauf en cas d'absolue nécessité.
Vues
Les délégués de vues doivent être aussi simples que possible. Le délégué doit contenir juste assez de QML pour afficher les informations nécessaires. Toute fonctionnalité supplémentaire qui n'est pas immédiatement nécessaire (par exemple, si elle affiche plus d'informations lorsqu'on clique dessus) ne doit être créée qu'en cas de besoin (voir la section suivante sur l'initialisation paresseuse).
La liste suivante est un bon résumé des éléments à garder à l'esprit lors de la conception d'un délégué :
- Moins il y a d'éléments dans un délégué, plus vite ils peuvent être créés, et donc plus vite la vue peut être déroulée.
- Limitez au maximum le nombre de liaisons dans un délégué ; en particulier, utilisez des ancres plutôt que des liaisons pour le positionnement relatif au sein d'un délégué.
- Évitez d'utiliser des éléments ShaderEffect dans les délégués.
- N'activez jamais l'écrêtage sur un délégué.
Vous pouvez définir la propriété cacheBuffer d'une vue pour permettre la création asynchrone et la mise en mémoire tampon de délégués en dehors de la zone visible. L'utilisation de cacheBuffer est recommandée pour les délégués de vue qui ne sont pas triviaux et qui ont peu de chances d'être créés en une seule image.
N'oubliez pas qu'un site cacheBuffer conserve des délégués supplémentaires en mémoire. Par conséquent, la valeur dérivée de l'utilisation de cacheBuffer doit être mise en balance avec l'utilisation supplémentaire de mémoire. Les développeurs doivent utiliser l'analyse comparative pour trouver la meilleure valeur pour leur cas d'utilisation, car la pression accrue sur la mémoire causée par l'utilisation de cacheBuffer peut, dans de rares cas, entraîner une réduction du taux de rafraîchissement lors du défilement.
Pour améliorer encore les performances, envisagez d'activer la réutilisation des éléments dans les vues. Voir Réutilisation des éléments pour ListView et Réutilisation des éléments pour TableView et TreeView pour plus d'informations.
Effets visuels
Qt Quick comprend plusieurs fonctionnalités qui permettent aux développeurs et aux concepteurs de créer des interfaces utilisateur exceptionnellement attrayantes. La fluidité, les transitions dynamiques et les effets visuels peuvent être utilisés à bon escient dans une application, mais il convient d'être prudent lors de l'utilisation de certaines fonctionnalités de QML, car elles peuvent avoir des répercussions sur les performances.
Les animations
En général, l'animation d'une propriété entraîne la réévaluation de toutes les liaisons qui font référence à cette propriété. C'est généralement ce que l'on souhaite, mais dans d'autres cas, il peut être préférable de désactiver la liaison avant d'effectuer l'animation, puis de réaffecter la liaison une fois l'animation terminée.
Évitez d'exécuter du JavaScript pendant l'animation. Par exemple, il convient d'éviter d'exécuter une expression JavaScript complexe pour chaque image d'une animation de la propriété x.
Les développeurs doivent être particulièrement prudents lorsqu'ils utilisent des animations par script, car celles-ci sont exécutées dans le fil d'exécution principal (et peuvent donc entraîner le saut d'images si elles prennent trop de temps à se dérouler).
Particules
Le module Qt Quick Particles permet d'intégrer de magnifiques effets de particules dans les interfaces utilisateur. Cependant, les capacités du matériel graphique varient d'une plate-forme à l'autre et le module Particules ne peut pas limiter les paramètres à ce que votre matériel peut prendre en charge. Plus vous essayez de rendre de particules (et plus elles sont grosses), plus votre matériel graphique devra être rapide afin d'obtenir un rendu à 60 FPS. L'affectation d'un plus grand nombre de particules nécessite un processeur plus rapide. Il est donc important de tester soigneusement tous les effets de particules sur votre plate-forme cible, afin de calibrer le nombre et la taille des particules que vous pouvez rendre à 60 FPS.
Il convient de noter qu'un système de particules peut être désactivé lorsqu'il n'est pas utilisé (par exemple, sur un élément non visible) afin d'éviter toute simulation inutile.
Voir le guide des performances du système de particules pour des informations plus détaillées.
Contrôle de la durée de vie des éléments
En divisant une application en composants simples et modulaires, chacun contenu dans un seul fichier QML, vous pouvez accélérer le démarrage de l'application et mieux contrôler l'utilisation de la mémoire, tout en réduisant le nombre d'éléments actifs mais invisibles dans votre application.
Initialisation paresseuse
Le moteur QML effectue des opérations délicates pour s'assurer que le chargement et l'initialisation des composants n'entraînent pas le saut de trames. Cependant, il n'y a pas de meilleur moyen de réduire le temps de démarrage que d'éviter de faire le travail que vous n'avez pas besoin de faire, et de retarder le travail jusqu'à ce qu'il soit nécessaire. Pour ce faire, il est possible d'utiliser Loader.
Utilisation du chargeur
Le chargeur est un élément qui permet le chargement et le déchargement dynamiques des composants.
- La propriété "active" d'un chargeur permet de retarder l'initialisation jusqu'à ce qu'elle soit nécessaire.
- La version surchargée de la fonction "setSource()" permet de fournir les valeurs initiales des propriétés.
- La définition de la propriété du chargeur asynchronous à true peut également améliorer la fluidité pendant l'instanciation d'un composant.
Détruire les éléments inutilisés
Les éléments qui sont invisibles parce qu'ils sont enfants d'un élément non visible (par exemple, le deuxième onglet d'un widget à onglets, alors que le premier onglet est affiché) doivent être initialisés paresseusement dans la plupart des cas, et supprimés lorsqu'ils ne sont plus utilisés, afin d'éviter les coûts permanents liés au fait de les laisser actifs (par exemple, le rendu, les animations, l'évaluation de la liaison des propriétés, etc.)
Un élément chargé avec un élément Loader peut être libéré en réinitialisant la propriété "source" ou "sourceComponent" du Loader, tandis que d'autres éléments peuvent être explicitement libérés en appelant la fonction destroy(). Dans certains cas, il peut être nécessaire de laisser l'élément actif, auquel cas il doit au moins être rendu invisible.
Voir la prochaine section sur le rendu pour plus d'informations sur les éléments actifs mais invisibles.
Rendu
Le graphe de scène utilisé pour le rendu dans Qt Quick permet aux interfaces utilisateur animées et hautement dynamiques d'être rendues de manière fluide à 60 images par seconde. Certains éléments peuvent toutefois réduire considérablement les performances de rendu, et les développeurs doivent veiller à éviter ces écueils dans la mesure du possible.
Découpage
L'écrêtage est désactivé par défaut et ne doit être activé qu'en cas de besoin.
Le détourage est un effet visuel, PAS une optimisation. Il augmente (plutôt qu'il ne réduit) la complexité du moteur de rendu. Si l'écrêtage est activé, un élément écrêtera sa propre peinture, ainsi que celle de ses enfants, sur son rectangle de délimitation. Cela empêche le moteur de rendu de réorganiser librement l'ordre de dessin des éléments, ce qui se traduit par une traversée du graphe de la scène sous-optimale dans le meilleur des cas.
L'écrêtage à l'intérieur d'un délégué est particulièrement mauvais et doit être évité à tout prix.
Surdessin et éléments invisibles
Si vous avez des éléments qui sont totalement couverts par d'autres éléments (opaques), il est préférable de définir leur propriété "visible" à false ou ils seront dessinés inutilement.
De même, les éléments invisibles (par exemple, le deuxième onglet d'un widget d'onglet, alors que le premier onglet est affiché) mais qui doivent être initialisés au moment du démarrage (par exemple, si le coût d'instanciation du deuxième onglet est trop long pour pouvoir le faire uniquement lorsque l'onglet est activé), devraient avoir leur propriété "visible" fixée à false, afin d'éviter le coût de leur dessin (bien que, comme expliqué précédemment, ils subiront toujours le coût de l'évaluation des animations ou des liaisons, puisqu'ils sont toujours actifs).
Translucide ou opaque
Le contenu opaque est généralement beaucoup plus rapide à dessiner que le contenu translucide. La raison en est que le contenu translucide doit être estompé et que le moteur de rendu peut potentiellement mieux optimiser le contenu opaque.
Une image comportant un pixel translucide est considérée comme entièrement translucide, même si elle est principalement opaque. Il en va de même pour une image BorderImage dont les bords sont transparents.
Les shaders
Le type ShaderEffect permet de placer du code GLSL en ligne dans une application Qt Quick avec très peu de surcharge. Cependant, il est important de réaliser que le programme de fragmentation doit être exécuté pour chaque pixel de la forme rendue. Lors du déploiement sur du matériel bas de gamme et lorsque le shader couvre un grand nombre de pixels, il convient de limiter le shader de fragment à quelques instructions afin d'éviter des performances médiocres.
Les shaders écrits en GLSL permettent d'écrire des transformations et des effets visuels complexes, mais ils doivent être utilisés avec précaution. L'utilisation de ShaderEffectSource entraîne le pré-rendement d'une scène dans un FBO avant qu'elle ne puisse être dessinée. Ce surcoût peut s'avérer assez élevé.
Allocation de mémoire et collecte
La quantité de mémoire qui sera allouée par une application et la manière dont cette mémoire sera allouée sont des considérations très importantes. Outre les préoccupations évidentes concernant les conditions de sortie de mémoire sur les dispositifs à mémoire limitée, l'allocation de mémoire sur le tas est une opération assez coûteuse en termes de calcul, et certaines stratégies d'allocation peuvent entraîner une fragmentation accrue des données entre les pages. JavaScript utilise un tas de mémoire géré qui est automatiquement ramassé, ce qui présente certains avantages, mais aussi d'importantes implications.
Une application écrite en QML utilise de la mémoire provenant à la fois du tas C++ et du tas JavaScript géré automatiquement. Le développeur de l'application doit être conscient des subtilités de chacun afin de maximiser les performances.
Conseils pour les développeurs d'applications QML
Les conseils et suggestions contenus dans cette section ne sont que des lignes directrices et peuvent ne pas être applicables dans toutes les circonstances. Veillez à comparer et à analyser soigneusement votre application à l'aide de mesures empiriques, afin de prendre les meilleures décisions possibles.
Instancier et initialiser les composants paresseusement
Si votre application comporte plusieurs vues (par exemple, plusieurs onglets) mais qu'une seule est nécessaire à un moment donné, vous pouvez utiliser l'instanciation paresseuse pour minimiser la quantité de mémoire allouée à un moment donné. Voir la section précédente sur l'initialisation paresseuse pour plus d'informations.
Détruire les objets inutilisés
Si vous chargez paresseusement des composants ou créez des objets dynamiquement au cours d'une expression JavaScript, il est souvent préférable de les destroy() manuellement plutôt que d'attendre que le ramassage automatique des ordures le fasse. Voir la section précédente sur le contrôle de la durée de vie des éléments pour plus d'informations.
Ne pas invoquer manuellement le ramasse-miettes
Dans la plupart des cas, il n'est pas judicieux d'invoquer manuellement le ramasse-miettes, car cela bloquera le fil d'exécution de l'interface graphique pendant une longue période. Cela peut entraîner des sauts d'images et des animations saccadées, qu'il convient d'éviter à tout prix.
Dans certains cas, il est acceptable d'invoquer manuellement le ramasse-miettes (ce point est expliqué plus en détail dans une prochaine section), mais dans la plupart des cas, il est inutile et contre-productif d'invoquer le ramasse-miettes.
Éviter de définir plusieurs types implicites identiques
Si un élément QML possède une propriété personnalisée définie en QML, celle-ci devient son propre type implicite. Ce point est expliqué plus en détail dans une prochaine section. Si plusieurs types implicites identiques sont définis dans un site Component, de la mémoire sera gaspillée. Dans ce cas, il est généralement préférable de définir explicitement un nouveau composant qui pourra ensuite être réutilisé. Dans ce cas, envisagez de définir un composant en ligne à l'aide du mot-clé component.
La définition d'une propriété personnalisée peut souvent constituer une optimisation bénéfique des performances (par exemple, pour réduire le nombre de liaisons nécessaires ou réévaluées), ou améliorer la modularité et la maintenabilité d'un composant. Dans ces cas, l'utilisation de propriétés personnalisées est encouragée. Cependant, le nouveau type devrait, s'il est utilisé plus d'une fois, être divisé en son propre composant (en ligne ou fichier .qml) afin de conserver la mémoire.
Réutiliser les composants existants
Si vous envisagez de définir un nouveau composant, il convient de vérifier qu'un tel composant n'existe pas déjà dans l'ensemble des composants de votre plate-forme. Sinon, vous obligerez le moteur QML à générer et à stocker des données de type pour un type qui est essentiellement un duplicata d'un autre composant préexistant et potentiellement déjà chargé.
Utiliser des types singleton au lieu de scripts de bibliothèque pragma
Si vous utilisez un script de bibliothèque pragma pour stocker des données d'instance à l'échelle de l'application, envisagez d'utiliser un type singleton QObject à la place. Cela devrait permettre d'améliorer les performances et de réduire l'utilisation de la mémoire du tas JavaScript.
Allocation de mémoire dans une application QML
L'utilisation de la mémoire d'une application QML peut être divisée en deux parties : l'utilisation du tas C++ et l'utilisation du tas JavaScript. Une partie de la mémoire allouée dans chaque partie est inévitable, car elle est allouée par le moteur QML ou le moteur JavaScript, tandis que le reste dépend des décisions prises par le développeur de l'application.
Le tas C++ contiendra
- les frais généraux fixes et inévitables du moteur QML (structures de données d'implémentation, informations contextuelles, etc ;)
- les données compilées par composant et les informations de type, y compris les métadonnées de propriété par type, qui sont générées ou chargées à partir du cache du disque par le moteur QML en fonction des modules et des composants chargés par l'application ;
- les données C++ par objet (y compris les valeurs des propriétés) et la hiérarchie des métaobjets par élément, en fonction des composants instanciés par l'application ;
- toutes les données allouées spécifiquement par les importations QML (bibliothèques).
Le tas JavaScript contiendra
- les frais généraux fixes et inévitables du moteur JavaScript lui-même (y compris les types JavaScript intégrés) ;
- les frais généraux fixes et inévitables de notre intégration JavaScript (fonctions de construction pour les types chargés, modèles de fonctions, etc ;)
- les informations de mise en page par type et autres données de type internes générées par le moteur JavaScript au moment de l'exécution, pour chaque type (voir la note ci-dessous concernant les types) ;
- les données JavaScript par objet (propriétés "var", fonctions JavaScript et gestionnaires de signaux, et expressions de liaison non optimisées) ;
- les variables allouées lors de l'évaluation des expressions.
En outre, il y aura un tas JavaScript alloué pour l'utilisation dans le thread principal, et éventuellement un autre tas JavaScript alloué pour l'utilisation dans le thread WorkerScript. Si une application n'utilise pas d'élément WorkerScript, ce surcoût ne sera pas encouru. Le tas JavaScript peut atteindre plusieurs mégaoctets, de sorte que les applications écrites pour des dispositifs à mémoire restreinte ont tout intérêt à éviter l'élément WorkerScript.
Il convient de noter que le moteur QML et le moteur JavaScript génèrent automatiquement leurs propres caches de données sur les types observés. Chaque composant chargé par une application est un type distinct (explicite) et chaque élément (instance de composant) qui définit ses propres propriétés personnalisées en QML est un type implicite. Tout élément (instance d'un composant) qui ne définit aucune propriété personnalisée est considéré par les moteurs JavaScript et QML comme étant du type explicitement défini par le composant, plutôt que de son propre type implicite.
Prenons l'exemple suivant :
import QtQuick Item { id: root Rectangle { id: r0 color: "red" } Rectangle { id: r1 color: "blue" width: 50 } Rectangle { id: r2 property int customProperty: 5 } Rectangle { id: r3 property string customProperty: "hello" } Rectangle { id: r4 property string customProperty: "hello" } }
Dans l'exemple précédent, les rectangles r0 et r1 n'ont pas de propriétés personnalisées et les moteurs JavaScript et QML les considèrent donc tous deux comme étant du même type. En d'autres termes, r0 et r1 sont tous deux considérés comme appartenant au type Rectangle explicitement défini. Les rectangles r2, r3 et r4 ont chacun des propriétés personnalisées et sont considérés comme étant de types différents (implicites). Notez que r3 et r4 sont tous deux considérés comme étant de types différents, même s'ils ont des informations de propriété identiques, simplement parce que la propriété personnalisée n'a pas été déclarée dans le composant dont ils sont des instances.
Si r3 et r4 étaient tous deux des instances d'un composant RectangleWithString et que la définition de ce composant incluait la déclaration d'une propriété de chaîne nommée customProperty, alors r3 et r4 seraient considérés comme étant du même type (c'est-à-dire qu'ils seraient des instances du type RectangleWithString, plutôt que de définir leur propre type implicite).
Considérations approfondies sur l'allocation de mémoire
Lorsque l'on prend des décisions concernant l'allocation de mémoire ou les compromis de performance, il est important de garder à l'esprit l'impact des performances du cache de l'unité centrale, de la pagination du système d'exploitation et du ramassage des ordures du moteur JavaScript. Les solutions potentielles doivent faire l'objet d'une évaluation comparative minutieuse afin de garantir le choix de la meilleure solution.
Aucun ensemble de lignes directrices générales ne peut remplacer une solide compréhension des principes sous-jacents de l'informatique combinée à une connaissance pratique des détails de mise en œuvre de la plateforme pour laquelle le développeur d'applications travaille. En outre, aucun calcul théorique ne peut remplacer un bon ensemble de points de référence et d'outils d'analyse lors de la prise de décisions concernant les compromis.
Fragmentation
La fragmentation est un problème de développement en C++. Si le développeur de l'application ne définit pas de types ou de plugins C++, il peut ignorer cette section.
Au fil du temps, une application allouera de grandes portions de mémoire, écrira des données dans cette mémoire, puis libérera certaines portions de cette mémoire une fois qu'elle aura fini d'utiliser une partie des données. Il peut en résulter que la mémoire "libre" se trouve dans des morceaux non contigus, qui ne peuvent pas être restitués au système d'exploitation pour que d'autres applications puissent les utiliser. Cela a également un impact sur les caractéristiques de mise en cache et d'accès de l'application, car les données "vivantes" peuvent être réparties sur de nombreuses pages différentes de la mémoire physique. Cela peut obliger le système d'exploitation à échanger des données, ce qui peut entraîner des entrées/sorties du système de fichiers, ce qui est, comparativement, une opération extrêmement lente.
La fragmentation peut être évitée en utilisant des allocateurs de pool (et d'autres allocateurs de mémoire contiguë), en réduisant la quantité de mémoire allouée à tout moment en gérant soigneusement la durée de vie des objets, en nettoyant et en reconstruisant périodiquement les caches, ou en utilisant un moteur d'exécution à gestion de mémoire avec ramassage des ordures (comme JavaScript).
Ramassage des ordures
JavaScript propose un système de ramassage des ordures. La mémoire allouée sur le tas JavaScript (par opposition au tas C++) appartient au moteur JavaScript. Le moteur collecte périodiquement toutes les données non référencées sur le tas JavaScript.
Implications de la collecte de déchets
La collecte de déchets présente des avantages et des inconvénients. Il signifie que la gestion manuelle de la durée de vie des objets est moins importante. Cependant, cela signifie également qu'une opération potentiellement longue peut être lancée par le moteur JavaScript à un moment qui échappe au contrôle du développeur de l'application. Si le développeur de l'application ne prend pas garde à l'utilisation du tas de JavaScript, la fréquence et la durée du ramassage des ordures peuvent avoir un impact négatif sur l'expérience de l'application. Depuis Qt 6.8, le ramasse-miettes est incrémental, ce qui signifie qu'il subira des interruptions plus courtes, mais potentiellement plus nombreuses.
Invoquer manuellement le ramasse-miettes
Une application écrite en QML nécessitera (très probablement) l'exécution du ramasse-miettes à un moment ou à un autre. Bien que le ramassage des ordures soit automatiquement déclenché par le moteur JavaScript selon son propre calendrier, il est parfois préférable que le développeur de l'application décide du moment où il faut invoquer manuellement le ramasse-miettes (bien que ce ne soit généralement pas le cas).
Le développeur de l'application est probablement le mieux placé pour savoir quand une application va rester inactive pendant de longues périodes. Si une application QML utilise beaucoup de mémoire de tas JavaScript, provoquant des cycles de ramassage réguliers et perturbants lors de tâches particulièrement sensibles aux performances (par exemple, le défilement de listes, les animations, et ainsi de suite), le développeur de l'application peut être bien inspiré d'invoquer manuellement le ramasse-miettes pendant les périodes d'inactivité. Les périodes d'inactivité sont idéales pour effectuer le ramassage des ordures, car l'utilisateur ne remarquera pas la dégradation de son expérience (images sautées, animations saccadées, etc.) qui résulterait de l'invocation du ramasse-miettes pendant que l'activité est en cours.
Le ramasse-miettes peut être invoqué manuellement en appelant gc() dans JavaScript. Cela entraînera l'exécution d'un cycle de collecte complet et non incrémentiel, qui peut prendre de quelques centaines à plus d'un millier de millisecondes, et doit donc être évité dans la mesure du possible.
Compromis entre mémoire et performances
Dans certaines situations, il est possible de compenser l'augmentation de l'utilisation de la mémoire par une diminution du temps de traitement. Par exemple, la mise en cache du résultat d'une recherche de symboles utilisée dans une boucle serrée dans une variable temporaire d'une expression JavaScript se traduira par une amélioration significative des performances lors de l'évaluation de cette expression, mais cela implique l'allocation d'une variable temporaire. Dans certains cas, ces compromis sont judicieux (comme dans le cas ci-dessus, qui est presque toujours judicieux), mais dans d'autres cas, il peut être préférable de laisser le traitement prendre un peu plus de temps afin d'éviter d'augmenter la pression de la mémoire sur le système.
Dans certains cas, l'impact d'une pression accrue sur la mémoire peut être extrême. Dans certains cas, l'échange de l'utilisation de la mémoire contre un gain de performance supposé peut entraîner une augmentation du "page-thrash" ou du "cache-thrash", ce qui entraîne une réduction considérable des performances. Il est toujours nécessaire d'évaluer soigneusement l'impact des compromis afin de déterminer quelle est la meilleure solution dans une situation donnée.
Pour des informations plus détaillées sur les performances de la mémoire cache et les compromis entre mémoire et temps, consultez les articles suivants :
- L'excellent article d'Ulrich Drepper : "What Every Programmer Should Know About Memory" (Ce que tout programmeur devrait savoir sur la mémoire), à l'adresse : https://people.freebsd.org/~lstewart/articles/cpumemory.pdf.
- Les excellents manuels d'Agner Fog sur l'optimisation des applications C++ à l'adresse : http://www.agner.org/optimize/.
Optimisation de l'amorçage rapide et du démarrage
Sur la base d'une expérience réelle de l'optimisation des applications Qt Quick pour un démarrage rapide, il convient de prendre en considération les meilleures pratiques suivantes :
- Concevez votre application pour qu'elle démarre rapidement dès le début. Pensez à ce que vous voulez que l'utilisateur voie en premier.
- Utilisez l'outil QML Profiler pour identifier les goulots d'étranglement au démarrage.
- Utilisez le chargement en chaîne. Exécutez autant de loaders que vous avez de cœurs dans votre processeur (par exemple, deux cœurs : deux chargeurs fonctionnant en même temps).
- Le premier loader ne doit pas être asynchrone, afin que certains contenus soient affichés immédiatement. Déclenchez les chargeurs asynchrones après.
- Ne vous connectez aux services back-end que lorsque c'est nécessaire.
- Créer des modules QML qui sont importés lorsque cela est nécessaire. En utilisant des modules et des types à chargement paresseux, vous pouvez mettre des services non critiques à la disposition de votre application en fonction des besoins.
- Optimisez vos images PNG/JPG à l'aide d'outils tels que optipng.
- Optimisez vos modèles 3D en réduisant le nombre de sommets et en supprimant les parties non visibles.
- Optimisez le chargement du modèle 3D en utilisant glTF.
- Limitez l'utilisation du clip et de l'opacité, car ils peuvent avoir un impact sur les performances.
- Mesurez les limites du GPU et tenez-en compte lors de la conception de l'interface utilisateur. Voir Captures d'images et profilage des performances pour plus d'informations.
- Utilisez Qt Quick Compiler pour précompiler les fichiers QML.
- Déterminer si l'établissement de liens statiques est possible pour votre architecture.
- Privilégiez les liaisons déclaratives plutôt que les gestionnaires de signaux impératifs.
- Veillez à ce que les liaisons de propriétés soient simples. En général, il faut que le code QML soit simple, amusant et lisible. De bonnes performances s'ensuivent.
- Remplacez les contrôles complexes par des images ou des shaders si le temps de création est un problème.
Ne pas :
- Ne pas utiliser QML à outrance. Même si vous utilisez QML, vous n'avez pas besoin de faire absolument tout en QML.
- Initialisez tout dans votre main.cpp.
- Créez de gros singletons qui contiennent toutes les interfaces nécessaires.
- Créez des délégués complexes pour ListView ou d'autres vues.
- Utiliser le clip à moins que cela ne soit absolument nécessaire.
- Tomber dans le piège commun de la surutilisation des chargeurs. Loader est excellent pour charger paresseusement des choses plus importantes comme les pages d'application, mais introduit trop de surcharge pour le chargement de choses simples. Ce n'est pas de la magie noire qui accélère tout et n'importe quoi. Il s'agit d'un élément supplémentaire avec un contexte QML supplémentaire.
Ces pratiques permettent d'obtenir des temps de démarrage inférieurs à la seconde et des expériences utilisateur fluides, en particulier sur les appareils embarqués.
© 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.