mercredi 31 août 2011

Gagner du temps lors de la création d'interfaces

Bonjour à tous, c'est avec beaucoup d'émotion que je poste sur funky work pour la première fois. On m'appelle Scriptopathe, Scripto pour les intimes.

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...

 
Un raccourci de raccourci....
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.
C'est aussi simple que ça en théorie, et ça permet d'afficher n'importe quel objet.

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):
  1. # -*- coding: latin-1 -*-
  2. from wx import *
  3. from data_modifier import *
  4. class Obj2:
  5.     def __init__(self):
  6.         # Création de 10 attributs booléens
  7.         for i in range(12):
  8.             bool = i % 2 == 0 and True or False
  9.             setattr(self, "Attr"+str(i), bool)
  10.             data(self, "Attr"+str(i), BoolDataRepresentation("Attr "+str(i+1)))
  11. class Obj:
  12.     def __init__(self):
  13.         self.Attr1 = 5
  14.         data(self, "Attr1", IntDataRepresentation(label="Attribut 1", type=CHOICE, choices=["Choix 1", "Choice 2", "Choix 3", "Choice 4", "OMG TRO BI1", "Funky work Roxxx"]))
  15.         self.Attr2 = False
  16.         data(self, "Attr2", BoolDataRepresentation(label="Attribut 2", type=RADIO_BUTTON))
  17.         self.Attr3 = True
  18.         data(self, "Attr3", BoolDataRepresentation(label="A la fin hohoho"), 15)
  19.         self.Attr4 = "Rapide oh pinaise"
  20.         data(self, "Attr4", StringDataRepresentation(label="Attribut 4"))
  21.         self.Attr5 = 125.4
  22.         data(self, "Attr5", IntDataRepresentation(label="Un float", is_float=True))
  23.         self.Attr6 = Obj2()
  24.         data(self, "Attr6", ObjectDataRepresentation(label="Un objet !", orientation=GridOrient(4, 4, 5, 5)))
  25.     def __str__(self):
  26.         return "Obj : " + "Attr1 = " + str(self.Attr1) + " Attr2 = " + str(self.Attr2) + " etc.."
  27. class TestObj:
  28.     def __init__(self):
  29.         self.Dat = [Obj(), Obj(), Obj(), Obj()]#[Obj(), Obj(), Obj(), Obj()]
  30.         data(self, "Dat", ObjectDataRepresentation(label=u"Une liste d'objets tiens", obj_type=Obj))
  31.         self.Dat2 = Obj()
  32.         data(self, "Dat2", ObjectDataRepresentation(label=u"Un objet", obj_type=Obj))
  33. class TestApp(wx.App):
  34.     def OnInit(self):
  35.         frame = Frame(None, -1, "Funky Work")
  36.         dat = TestObj()
  37.         DataModifier(frame, dat)
  38.         frame.Show()
  39.         return True
  40. app = TestApp(redirect=False)
  41. app.MainLoop()



Enjoy !

3 commentaires:

  1. Ça fait plaisir un si bon premier article :)
    Bravo et merci :)

    RépondreSupprimer
  2. Article intéressant, mais plusieurs points perfectibles dans la présentation.

    - D'abord, le but de l'article n'est pas assez mis en avant. "Gagner du temps lors de la création d'interfaces" ressemble à un simple sous titre sans aucun intérêt alors qu'il s'agit du but de l'article.

    - C'est un article consacré aux interfaces, et on ne voit une interface qu'à la toute fin du projet.

    - Obligation de récupérer un code sur un site externe, alors que le code pourrait être affiché sur le site.

    - Le code sur le site externe n'a aucune indentation, ni colorisation.

    - L'article entier est tourné de la façon la plus hermétique possible.
    Il y a deux façons d'expliquer ce qu'est le soleil : commencer par expliquer ce qu'est l'hydrogène ou commencer par expliquer que le soleil est ce qui brille dans le ciel.
    Cet article termine sur la partie la plus facile à comprendre.

    Bref, le fond est bon. Mais l'intégralité de la forme est à revoir. Dans l'état actuel des choses :
    - les principes évoqués ne sont pas exploitables à cause de la formulation
    - la démo à récupérer n'est pas exploitable pour cause de non indentation

    Personnellement, j'ai compris l'article à partir du moment où j'ai commencé à lire les paragraphes dans l'ordre inverse.

    Mais continue, c'est utile, il faut juste que tu trouves le moyen de formuler ça. ;)

    RépondreSupprimer
  3. Hello KK, merci pour le commentaire constructif :)

    1) Oui pour le titre c'est pas un bon choix.
    2) C'est pas vraiment consacré aux interfaces, mais à une manière d'accélérer leur développement
    3, 4) Ça j'avais pas vu, mais c'était ma seule solution pour l'instant (le div de code n'existait pas encore à l'heure où j'ai écris ça, et le code aurait alors pris beaucoup de pages). Un outil est en développement et je mettrai le code directement sur le site une fois qu'il sera terminé.

    Bon, c'est vrai qu'en relisant attentivement y'a certains points à corriger et je vais m'y atteler (et surtout fournir un exemple plus pertinent)

    RépondreSupprimer