Au menu aujourd'hui...
Gagner du temps lors de la création d'interfaces... (oui oui j'insiste dessus !)
La création d'interfaces graphiques est toujours long, fastidieux et ennuyant lorsqu'on a beaucoup de contrôles à disposer pour modifier une structure de données comportant des éléments standards (nombres, strings, booléens, et autres objets composés de ces mêmes éléments).
Il faut créer l'objet contenant ces données, puis l'interface qui le modifie.
C'est pour simplifier et raccourcir ce dur labeur, que j'ai pensé à réunir les 2 choses dans le même objet.
Oh mon Dieu, il réunit les données et l'interface, quel désastre, on ne lui a jamais appris à programmer !
En fait, ce n'est pas vraiment une réunion totale des deux. Le principe est de donner des attributs simples aux données qui définiront la manière de les afficher.
Avant de poursuivre...
Lors de cet article, j'utiliserai Python comme langage de programmation, et wxPython comme toolkit graphique. Cependant, la technique peut sans difficulté être portée vers une autre librairie et un autre langage (supportant la réflexion pour plus de facilité).
Le principe en lui-même !
L'idée est très simple : on va, dans l'objet contenant les données, donner de simples informations sur la manière de présenter ces données.
- l'attribution d'attributs de représentation de données en lien avec les attributs affichables/modifiables d'un objet (un peu redondant dans la formulation j'en conviens)
- La création de l'interface à partir des données récupérées : l'afficheur / modificateur de données.
Les attributs de représentation
Ce sont eux qui vont indiquer à l'interface ce qu'elle doit présenter à l'utilisateur. L'idéal est de créer une classe de base commune à tous les types, et une classe héritée de celle-ci pour chaque type d'élément affichable.
Les éléments de base à rendre disponibles par l'interface sont le nom de l'attribut et de l'objet parent (pour que l'interface puisse elle même modifier les données), et la priorité d'affichage.
Petit truc pythonique
En python, il est très aisé de produire un mécanisme permettant d'utiliser des mots clefs pour les arguments du constructeur de ces attributs, afin d'avoir une meilleure lisibilité, et de permettre d'omettre certains paramètres afin de leur assigner une valeur par défaut.
Exemple :
class DataRepresentation:
def __init__(self, arg1=0, arg2=""):
# Traitement
class StringDataRepresentation:
def __init__(self, label="", **kwargs)
# Avec ça, les arguments spécifiques à DataRepresentation lui seront passés.
# S'il y en a trop, ou qu'ils ne correspondent pas à ceux attendus, des exceptions seront levées.
DataRepresentation.__init__(self, **kwargs)
# Traitement...
Pour que tous les attributs de données soient accessibles de la même manière par l'interface, créer une fonction simple qui les attribue elle-même selon une certaine règle est la bienvenue.
Le principe de cette fonction est simple :
- Elle prend en arguments l'objet parent de l'attribut, le nom de l'attribut, l'attribut de données à lui associer.
- Elle ajoute un attribut à l'objet parent, correspondant à l'attribut de données qui lui a été passé en argument. Le nom de cet attribut peut être celui de l'attribut qui lui est associé, suivi de "__data_attribute".
- Elle peut faire tout le pré-processing que vous désirez faire :)
Des infos pour les attributs à la masse...
C'est bien joli de dire qu'on va donner des informations à destination de l'interface, qui varient en fonction de l'objet à représenter, mais, qu'est-ce qu'on lui dit exactement ?
Déjà, pour certains types de données, on peut définir quel contrôle utiliser pour la représenter. Par exemple, pour un int, on peut utiliser un SpinCtrl, afin de le modifier manuellement, ou bien un Choice, afin de choisir entre des propositions correspondant chacune à un numéro...
Et puis, des informations peuvent s'avérer nécessaires, par exemple, pour le cas précédent, quelles chaines affiche-t-on pour quelles valeurs de l'int ?
Utiliser les attributs de représentation pour créer l'interface
Les attributs de représentation peuvent alors être disponibles pour l'interface, via simple passage en argument de l'objet.
L'afficheur de données se contente de :
- Lister les attributs d'un objet (au début celui passé en argument, puis ceux qu'il contient si c'est le cas !), et en extraire ceux qui sont affichables, c'est à dire, ceux qui ont des attributs de représentation.
- Pour chacun de ses attributs, vérifier son type et celui de son attribut de représentation, et de placer des contrôles en conséquence.
Modifier les données
Il est certain que le but d'une telle interface n'est pas seulement de visualiser des données, mais aussi de les modifier ! Une fonction de l'afficheur / modificateur d'interface pourrait permettre d'affecter les données dans les champs à l'objet de données, lorsque l'utilisateur cliquerait sur un bouton tel que 'OK'.
L'algorithme récupérant les données est simple, néanmoins, il lui faut certaines informations supplémentaires, qu'il serait bon de sauvegarder quelque part. Ces informations sont des tuples (controle, nom de l'attribut, objet parent), qui peuvent être créés lors de la création du contrôle.
A partir de ces information, il suffit de faire une itération sur chaque tuple, de récupérer et convertir la valeur contenue dans chaque champ, et de l'affecter à l'attribut de l'objet parent, d'où l'utilité de connaître le nom de l'attribut et son objet parent...
En python, cela se fait simplement via :
setattr(obj_parent, nom_attribut, valeur)
Bilan
Cette solution peut être utile lorsque beaucoup d'informations simples et hétérogènes sont à afficher.
Elle est simple à mettre en place, et permet d'économiser du temps en créant en un seul coup un moyen de stocker/afficher/modifier des données !
Code source (data_modifier.py) :
http://nuki.music-all.be/past/index.php?page=Sources&id=1
Ce script n'a pas à être utilisé tel quel, mais sert de support à l'article. Il doit servir d'exemple pour illustrer la théorie, mais il y a diverses manières spécifiques d'implémenter le concept.
Exemple du progrès :
Créer un interface se résumera alors à faire cela (si on prend le script posté plus haut):
# -*- coding: latin-1 -*- from wx import * from data_modifier import * class Obj2: def __init__(self): # Création de 10 attributs booléens for i in range(12): bool = i % 2 == 0 and True or False setattr(self, "Attr"+str(i), bool) data(self, "Attr"+str(i), BoolDataRepresentation("Attr "+str(i+1))) class Obj: def __init__(self): self.Attr1 = 5 data(self, "Attr1", IntDataRepresentation(label="Attribut 1", type=CHOICE, choices=["Choix 1", "Choice 2", "Choix 3", "Choice 4", "OMG TRO BI1", "Funky work Roxxx"])) self.Attr2 = False data(self, "Attr2", BoolDataRepresentation(label="Attribut 2", type=RADIO_BUTTON)) self.Attr3 = True data(self, "Attr3", BoolDataRepresentation(label="A la fin hohoho"), 15) self.Attr4 = "Rapide oh pinaise" data(self, "Attr4", StringDataRepresentation(label="Attribut 4")) self.Attr5 = 125.4 data(self, "Attr5", IntDataRepresentation(label="Un float", is_float=True)) self.Attr6 = Obj2() data(self, "Attr6", ObjectDataRepresentation(label="Un objet !", orientation=GridOrient(4, 4, 5, 5))) def __str__(self): return "Obj : " + "Attr1 = " + str(self.Attr1) + " Attr2 = " + str(self.Attr2) + " etc.." class TestObj: def __init__(self): self.Dat = [Obj(), Obj(), Obj(), Obj()]#[Obj(), Obj(), Obj(), Obj()] data(self, "Dat", ObjectDataRepresentation(label=u"Une liste d'objets tiens", obj_type=Obj)) self.Dat2 = Obj() data(self, "Dat2", ObjectDataRepresentation(label=u"Un objet", obj_type=Obj)) class TestApp(wx.App): def OnInit(self): frame = Frame(None, -1, "Funky Work") dat = TestObj() DataModifier(frame, dat) frame.Show() return True app = TestApp(redirect=False) app.MainLoop()
Enjoy !