Logo Spiria

Des Qwidget dans un QListWidget

25 mars 2020.

Qt est très flexible et complet. Cette flexibilité a cependant ses limites, et parfois la documentation de Qt peut mener à des impasses. C’est exactement ce qui m’est arrivé récemment. Mon but était de mettre des boutons et des champs de texte dans une liste de type QListWidget. Ça me semblait être une chose raisonnable à faire. Après tout, le QListWidget permet d’ajouter très facilement une QCheckBox à chaque élément. Ne serait-ce pas tout aussi simple d’ajouter quelques boutons supplémentaires ?

De plus, je voulais permettre de transférer les items entre plusieurs QlistWidget, via le glisser-déposer (drag and drop). Cela est facilement pris en charge dans le QListWidget standard. Le glisser-déposer permet de concevoir une interface utilisateur facile à utiliser et intuitive, et ça me semblait important à supporter.

Et pourtant, remplir ces deux conditions s’est avéré plus difficile que je ne l’avais prévu. Dans cet article, je vais survoler tous les obstacles que j’ai rencontrés et la solution finale, étonnamment simple, à tous ces problèmes.

Premier essai

La première approche naïve consiste à penser qu’il doit être possible d’ajouter simplement un QWidget contenant plusieurs sous-widgets dans un QListWidget. Il existe de nombreux exemples montrant une vue en liste contenant des cases à cocher. Ainsi, pour ajouter d’autres types de boutons, il faudrait simplement trouver la bonne fonction à appeler. Quelque chose doit être enterré dans la documentation Qt quelque part à ce sujet, n’est-ce pas ?

J’ai continué à fouiller dans la documentation de Qt, mais je n’ai jamais rien trouvé. Il s’avère que c’est tout simplement impossible.

Quand on y pense, il y a une raison pourquoi Qt ne permet pas de le faire. Le QListWidget peut contenir des milliers d’éléments dans une vue. Si chacun de ces éléments contenait plusieurs widgets, cela donnerait des milliers de widgets en même temps à l’écran. Étant donné que chaque widget fournit une interface riche, et donc beaucoup de données, ceci représente une charge très lourde. Au lieu de cela, Qt ne conserve que le strict minimum de données par élément dans la liste. Ce détail signifie que les widgets ne peuvent pas être placés directement dans une liste.

Deuxième essai

En cherchant dans la documentation de Qt, vous trouverez le concept de délégué d’item (QItemDelegate). C’est ainsi que Qt permet à l’usager de modifier les données d’un item dans une liste. Malheureusement, cette solution présente de nombreuses failles. L’interface utilisateur qui en résulte est non intuitive et difficile à découvrir, ce qui la rend inélégante.

L’utilisation d’un délégué est obscure, car les boutons ne sont pas visibles dans la liste. Pour faire apparaître les boutons, l’éditeur d’éléments doit être invoqué par l’utilisateur. L’utilisateur doit faire ce que Qt appelle une “action d’édition” : soit en appuyant sur F2, soit en double-cliquant. Ce ne sont pas des gestes qu’un utilisateur connaîtra. Il en résulte une interface utilisateur qui n’est pas intuitive ni facile à découvrir.

Il est également obscur, car tout cela nécessite que l’utilisateur sache déjà que l’item de la liste est modifiable…

Si seulement il y avait un moyen de montrer l’item en mode modifiable à tout moment…

Troisième essai

… mais il y en a un ! Le délégué d’item fournit une fonction de dessin qui peut être modifiée pour dessiner n’importe quoi. Mais comment dessiner n’importe quel autre widget ? Il y a en fait un moyen. Si vous regardez le code source pour savoir comment le widget de la liste standard peint la case à cocher, vous pouvez voir que Qt fournit des classes “style-option” pour dessiner les widgets. Mais si vous lisez la documentation, vous verrez qu’elle est très brève et plutôt incomplète. De plus, le support de toutes les fonctionnalités normales du widget, comme le survol et la sélection, nécessite beaucoup de code.

Et même si vous dessinez les widgets, ils ne sont toujours pas interactifs ! L’utilisateur doit toujours double-cliquer pour les rendre modifiables. Cela s’avère donc encore plus déroutant pour l’utilisateur.

Mais si cela ne suffisait pas, il y a encore un autre accroc. Le hic, c’est que les délégués ne peuvent être utilisés que par des modèles d’items (QAbstractItemModel) personnalisés. Vous ne pouvez pas utiliser un délégué avec un modèle d’items standard. Vous pourriez penser que ce n’est pas trop mal, que vous pourrez en créer un vous-même. Malheureusement, la mise en œuvre de votre propre modèle personnalisé n’est pas simple. Le problème est que Qt a des règles strictes sur la façon dont un modèle d’item doit se comporter. Il est difficile d’appliquer correctement toutes les règles, et tout écart entraîne un comportement étrange et des pépins très difficiles à diagnostiquer. Et ce, uniquement pour un modèle non éditable. Si vous voulez supporter le glisser-déposer, alors les règles sont encore plus complexes et impitoyables.

Le recours à un délégué entraîne donc l’écriture d’un grand nombre de lignes de codes dont la mise en œuvre et la maintenance sont très complexes. Il doit y avoir un meilleur moyen !

Essai final

Quelle est donc la bonne façon de mettre des widgets complexes dans une liste ? La réponse est d’une simplicité déconcertante : n’utilisez pas un widget de liste. Utilisez un widget simple. Oui, un simple QWidget.

Après tout, un QWidget peut contenir une liste arbitraire de sous-widgets. Bien entendu, vous devez alors ajouter la fonctionnalité standard d’une vue en liste par-dessus. Il faut supporter la sélection, le glisser-déposer et le défilement. Mais il s’avère que c’est beaucoup plus simple à écrire que les autres approches. Et, en prime, vous avez la garantie que le comportement de toutes les interfaces utilisateur que vous mettrez dans vos articles sera exactement le même que le comportement normal auquel l’utilisateur s’attend.

La solution

Ma solution au problème de la création d’un widget semblable à QListWidget pouvant contenir des widgets complexes est la suivante. Il y a quatre classes qui interagissent entre elles pour former la solution.

Classe But
QWidgetListWidget Le widget qui contient les items.
QWidgetListItem Les éléments qui peuvent être mis dans la liste. Peut contenir un nombre quelconque de sous-widgets, de n’importe quel type.
QWidgetScrollListWidget Une enveloppe autour de QWidgetListWidget pour permettre le défilement.
QWidgetListMimeData Les données MIME utilisées pour supporter le glisser-déposer entre plusieurs QWidgetListWidget.

 

QWidgetListWidget

C’est l’un des deux principaux points d’intérêt de la conception : le widget de liste. Cette vue de liste permet de sélectionner des éléments et de les faire glisser. Il fournit une interface simple, composée de quelques fonctions. Voici la déclaration en C++ de ces fonctions :

// Créer une liste de widgets.
QWidgetListWidget(ListModifiedCallbackFunction modifCallback, bool stretch,
 QBoxLayout::Direction dir, QWidget * parent);

// Vérifiez si la liste est verticale ou horizontale.
bool isVertical() const;

// Enlever tous les éléments de la liste.
void clear();

// Ajouter un item.
QWidgetListItem* addItem(QWidgetListItem* item, int index = -1);

// Supprimer un élément.
void removeItem(QWidgetListItem* item);

// Récupérer tous les éléments conservés dans cette liste.
std::vector<QWidgetListItem*> getItems(bool onlySelected = false) const;

// Récupérer tous les éléments sélectionnés conservés dans cette liste.
std::vector<QWidgetListItem*> getSelectedItems() const;

 

QWidgetLisItem

C’est le deuxième point d’intérêt principal. C’est un élément qui peut être sélectionné et qui peut être cloné. Le clonage est utilisé lors du glisser-déposer pour copier un élément d’une liste à une autre. Voici l’interface complète en C++. Elle est assez courte :

// Créer un item.
QWidgetListItem(QWidget* parent);

// Sélection.
bool isSelected() const;
void select(bool sel);

// Clonage d’objets pour le glisser-déposer.
virtual QWidgetListItem* clone() const;

 

QWidgetScrollListWidget

Cette classe n’existe que pour que le défilement de la liste soit facultatif. Il peut sembler étrange de rendre le défilement optionnel, mais c’est assez pratique lorsque vous voulez intégrer la vue de la liste dans les éléments d’une autre vue de la liste. Toute l’interface C++ est simplement ceci :

// Créer un widget défilant autour d’un autre widget.
QWidgetScrollListWidget(QWidget * widget, QWidget* parent);

 

QWidgetListMimeData

La dernière classe n’existe que pour le glisser-déposer. Vous ne devriez jamais avoir à l’utiliser directement. L’ensemble de son implémentation en C++ est la suivante :

static constexpr char MimeType[] = "application/x-qwidget-list-item";

QWidgetListItem* Widget = nullptr;
QPoint HotSpot;

 

Le code

L’intégralité de la mise en œuvre est disponible sur GitHub.

Le code est accompagné d’un exemple d’application montrant comment utiliser les classes. Voir la description sur GitHub sur la façon de construire le projet.

Un exemple plus complexe peut être trouvé dans le projet TreeFilterApp. Il montre même comment placer des listes dans d’autres listes. Le code est également sur GitHub.