De quoi parle-t-on ?
Partons du cas concret qui nous a occupé. Soit un composant graphique chargé de présenter des items de données sous forme d'arbre hiérarchique (un outline) ; c'est ce qu'on appelle une « TreeView ». Les noeuds ayant des enfants peuvent être soient pliés (« collapsed ») - les enfants sont alors invisibles - soient dépliés (« expanded ») - les enfants sont alors visibles. L'état du noeud peut varier au cours du temps :
- si l'utilisateur (dé)plie le noeud à l'aide d'un click de souris.
- si un code appelle une API dédiée pour (dé)plier le noeud.
Ce changement d'état s'accompagne de l'émission d'un événement. La question est alors de savoir si cet événement doit être émis uniquement quand l'utilisateur interragit graphiquement avec le composant (1) ou s'il doit aussi l'être quand on fait appel à l'API publique de la TreeView permettant de définir l'état (2). Voyons comment certains toolkits graphiques répondent à la question...
Exemple en SWT
package org.rnb.tests.treeview;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
public class SwtTree implements TreeListener, SelectionListener {
private Display display;
private Shell shell;
private Tree tree;
private Button button;
public SwtTree() {
this.display = new Display();
this.shell = new Shell(this.display);
this.shell.setLayout(new FillLayout());
this.appendTree();
this.appendButton();
this.shell.setSize(220, 150);
this.shell.open();
while (!this.shell.isDisposed()) {
if (!this.display.readAndDispatch()) {
this.display.sleep();
}
}
this.display.dispose();
}
private void appendTree() {
this.tree = new Tree(this.shell, SWT.BORDER);
this.tree.addTreeListener(this);
TreeItem root = new TreeItem(this.tree, 0);
root.setText("Root");
for (int i = 0; i < 4; i++) {
TreeItem iItem = new TreeItem(root, 0);
iItem.setText("TreeItem " + i);
}
}
private void appendButton() {
this.button = new Button(this.shell, SWT.PUSH);
this.button.setText("Toggle");
this.button.addSelectionListener(this);
}
private void toggleTreeRoot() {
TreeItem root = this.tree.getItem(0);
root.setExpanded(!root.getExpanded());
}
@Override
public void treeExpanded(TreeEvent e) {
System.out.println("expanded");
}
@Override
public void treeCollapsed(TreeEvent e) {
System.out.println("collapsed");
}
@Override
public void widgetSelected(SelectionEvent e) {
System.out.println("Selected");
this.toggleTreeRoot();
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
System.out.println("selected");
}
public static void main(String[] args) {
new SwtTree();
}
}
je ne suis pas spécialiste de SWT : le nombre de fois que j'ai eu à utiliser ce toolkit graphique doit se compter sur les doigts d'une main, l'écriture de ce petit exemple inclut ! Les APIs sont cependant suffisamment explicites pour ne pas trop se tromper. Il s'agit ici d'attacher un écouteur TreeListener
à la TreeView pour être informé de la fermeture (treeCollapsed
) et de l'ouverture (treeExpanded
) d'un noeud avec enfants.
Et que constate-t-on ? Que l'événement TreeEvent
est émis lorsque l'on plie / déplie le noeud par interaction graphique (click sur la petite boîte du noeud racine) MAIS PAS lorsque l'on plie / déplie le noeud via l'API (setExpanded
).
Exemple en Swing
package org.rnb.tests.treeview;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.DefaultMutableTreeNode;
public class SwingTree implements ActionListener, TreeExpansionListener {
private JFrame frame;
private JPanel panel;
private JTree tree;
private JButton button;
public SwingTree() {
this.frame = new JFrame();
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.panel = new JPanel(new FlowLayout());
this.appendTree();
this.appendButton();
this.frame.add(this.panel);
this.frame.setSize(220, 150);
this.frame.setVisible(true);
}
private void appendTree() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
for (int i = 0; i < 4; i++) {
root.add(new DefaultMutableTreeNode("Item " + i));
}
this.tree = new JTree(root);
this.tree.addTreeExpansionListener(this);
this.panel.add(tree);
}
private void appendButton() {
this.button = new JButton("Toggle");
this.button.addActionListener(this);
this.panel.add(button);
}
private void toggleTreeRoot() {
if (this.tree.isExpanded(0)) {
this.tree.collapseRow(0);
} else {
this.tree.expandRow(0);
}
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("action");
this.toggleTreeRoot();
}
@Override
public void treeExpanded(TreeExpansionEvent e) {
System.out.println("expanded");
}
@Override
public void treeCollapsed(TreeExpansionEvent e) {
System.out.println("collapsed");
}
public static void main(String args[]) {
new SwingTree();
}
}
Idem que précédemment : je ne suis pas du tout spécialiste de Swing mais là encore les objets à manipuler sont assez courants. Nous devons cette fois-ci attacher un écouteur TreeExpansionListener
à la TreeView pour connaître la fermeture et l'ouverture des noeuds de l'arborescence.
Ici, l'évenement TreeExpansionEvent
est émis lors d'une interaction graphique (click sur le noeud racine) MAIS AUSSI lorsque l'on passe par l'API dédiée (collapseRow
ou expandRow
).
Exemple en Qt4
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtGui
class QtTreeViewTest(QtGui.QWidget):
def __init__(self):
super(QtTreeViewTest, self).__init__()
self.append_tree()
self.append_button()
self.resize(220, 150)
self.move(300, 300)
self.setWindowTitle('QtTreeView test')
self.show()
def append_button(self):
self.button = QtGui.QPushButton('Toggle', self)
self.button.move(160, 0)
self.button.clicked.connect(self.on_button_clicked)
def append_tree(self):
self.tree = QtGui.QTreeWidget(self)
self.tree.resize(150,150)
self.tree.collapsed.connect(self.on_collapsed)
self.tree.expanded.connect(self.on_expanded)
self.tree.itemCollapsed.connect(self.on_item_collapsed)
self.tree.itemExpanded.connect(self.on_item_expanded)
self.root = QtGui.QTreeWidgetItem(['Root'])
self.tree.addTopLevelItem(self.root)
for i in range(4):
self.root.addChild(QtGui.QTreeWidgetItem(['Item ' + str(i)]))
def toggle_tree_root(self):
self.root.setExpanded(self.root.isExpanded() is False)
def on_collapsed(self, index):
print('collapsed')
def on_expanded(self, index):
print('expanded')
def on_item_collapsed(self, item):
print('collapsed')
def on_item_expanded(self, item):
print('expanded')
def on_button_clicked(self):
print('toggle')
self.toggle_tree_root()
def main():
app = QtGui.QApplication(sys.argv)
win = QtTreeViewTest()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Mon expérience de Qt4 est encore moins grande que celles sur les toolkits graphiques java mais quand on a vu une TreeView, on les a toutes vues. L'écriture de ce petit test a d'ailleurs balayé les a priori que j'avais à propos de cette librairie graphique, qui est plutôt agréable à utiliser. Ici, nous pouvons au choix être à l'écoute des événements itemCollapsed
et itemExpanded
émis par QTreeWidget
ou à l'écoute des événements collapsed
et expanded
émis par QTreeView
dont QTreeWidget
hérite.
Les événements sont émis à la fois quand on plie / déplier le noeud via l'interface graphique ET quand on appelle l'API dédiée (setExpanded
sur les objets QTreeWidgetItem
).
Exemple en GTK 3
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from gi.repository import Gtk
class GtkTreeViewWindow(Gtk.ApplicationWindow):
def __init__(self, app):
Gtk.Window.__init__(self, title='GTK TreeView test', application=app)
self.set_default_size(220, 150)
self.button = None
self.tree = None
self.box = Gtk.Box(spacing=5)
self.add(self.box)
self.append_tree()
self.append_button()
def append_button(self):
self.button = Gtk.Button('Toggle')
self.button.connect('clicked', self.on_button_clicked)
self.box.pack_start(self.button, True, True, 0)
def append_tree(self):
self.tree = Gtk.TreeView()
self.tree.connect('row-expanded', self.on_row_expanded)
self.tree.connect('row-collapsed', self.on_row_collapsed)
col = Gtk.TreeViewColumn('Items', Gtk.CellRendererText(), text=0)
self.tree.append_column(col)
store = Gtk.TreeStore(str)
self.root = store.append(None, ['Root'])
for i in range(4):
store.append(self.root, ['Item ' + str(i)])
self.tree.set_model(store)
self.box.pack_start(self.tree, True, True, 1)
def toggle_tree_root(self):
model = self.tree.get_model()
path = model.get_path(self.root)
if self.tree.row_expanded(path):
self.tree.collapse_row(path)
else:
self.tree.expand_row(path, False)
def on_row_expanded(self, tree, treeiter, path, data=None):
print('expanded')
def on_row_collapsed(self, tree, treeiter, path, data=None):
print('collapsed')
def on_button_clicked(self, button, data=None):
print('action')
self.toggle_tree_root()
class GtkTreeViewApplication(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(self)
def do_activate(self):
win = GtkTreeViewWindow(self)
win.show_all()
def do_startup(self):
Gtk.Application.do_startup(self)
if __name__ == "__main__":
app = GtkTreeViewApplication()
exit_status = app.run(sys.argv)
sys.exit(exit_status)
Avec GTK, je me retrouve avec un toolkit graphique que je connais beaucoup mieux et je savais déjà comment allait être émis les événements marquant la fermeture (row-collapsed
) et l'ouverture (row-expanded
) d'une TreeView. Ces événements sont émis quand l'action à une source graphique (click sur le triangle de pliage du noeud) ET quand on appelle l'API dédiée (expand_row
ou collapse_row
).
Discussions
Les exemples ci-dessus ne permettent pas de faire le tour de la question et ce n'est de toute façon pas le but. Ce que je souhaite simplement (dé)montrer, c'est que les 2 cas de figures peuvent exister et qu'il n'y a rien d'incohérent
à voir un événement associé à une action partir à la fois quand cette action est induite par une interaction graphique et quand elle l'est par l'appel à une API.
Toolkit | Interaction graphique | Interaction par API |
---|---|---|
SWT | oui | non |
Swing | oui | oui |
Qt4 | oui | oui |
GTK 3 | oui | oui |
Ressources et références
- Titre
- GTK+ 3
- Chapitre
- GtkTreeView
- Editeur
- Gnome Developer
- Titre
- Swing
- chapter
- JTree
- Éditeur
- docs.oracle.com
- Titre
- SWT
- chapter
- Tree
- Éditeur
- Eclipse Documentation
- Titre
- QTreeView Class Reference
- Éditeur
- Qt Project
- Titre
- QTreeWidget Class Reference
- Éditeur
- Qt Project