Comment concevriez-vous un framework d’interface utilisateur très « pythonique » ?

StackOverflow https://stackoverflow.com/questions/58711

Question

J'ai joué avec les "chaussures" de la bibliothèque Ruby.Fondamentalement, vous pouvez écrire une application GUI de la manière suivante :

Shoes.app do
  t = para "Not clicked!"
  button "The Label" do
    alert "You clicked the button!" # when clicked, make an alert
    t.replace "Clicked!" # ..and replace the label's text
  end
end

Cela m'a fait réfléchir : comment pourrais-je concevoir un framework GUI tout aussi agréable à utiliser en Python ?Celui qui n'a pas les liens habituels consistant à être essentiellement des wrappers dans une bibliothèque C* (dans le cas de GTK, Tk, wx, QT, etc.)

Shoes s'inspire du développement Web (comme #f0c2f0 notation des couleurs de style, techniques de mise en page CSS, comme :margin => 10), et de Ruby (utilisant largement des blocs de manière judicieuse)

Le manque de "blocs rubis" de Python rend impossible un portage (métaphoriquement) direct :

def Shoeless(Shoes.app):
    self.t = para("Not clicked!")

    def on_click_func(self):
        alert("You clicked the button!")
        self.t.replace("clicked!")

    b = button("The label", click=self.on_click_func)

Nulle part aussi propre, et ne le serait pas presque aussi flexible, et je ne suis même pas sûr qu'il soit réalisable.

Utiliser des décorateurs semble être un moyen intéressant de mapper des blocs de code à une action spécifique :

class BaseControl:
    def __init__(self):
        self.func = None

    def clicked(self, func):
        self.func = func

    def __call__(self):
        if self.func is not None:
            self.func()

class Button(BaseControl):
    pass

class Label(BaseControl):
    pass

# The actual applications code (that the end-user would write)
class MyApp:
    ok = Button()
    la = Label()

    @ok.clicked
    def clickeryHappened():
        print "OK Clicked!"

if __name__ == '__main__':
    a = MyApp()
    a.ok() # trigger the clicked action

Fondamentalement, la fonction décorateur stocke la fonction, puis lorsque l'action se produit (par exemple, un clic), la fonction appropriée sera exécutée.

La portée de diverses choses (par exemple, le la étiquette dans l'exemple ci-dessus) pourrait être plutôt compliqué, mais cela semble réalisable d'une manière assez soignée.

Était-ce utile?

La solution

Vous pourriez réellement y parvenir, mais cela nécessiterait l'utilisation de métaclasses, qui sont profond magie (il y a des dragons).Si vous souhaitez une introduction aux métaclasses, il existe une série de articles d'IBM qui parviennent à introduire les idées sans faire fondre votre cerveau.

Le code source d'un ORM comme SQLObject peut également être utile, car il utilise ce même type de syntaxe déclarative.

Autres conseils

## All you need is this class:

class MainWindow(Window):
    my_button = Button('Click Me')
    my_paragraph = Text('This is the text you wish to place')
    my_alert = AlertBox('What what what!!!')

    @my_button.clicked
    def my_button_clicked(self, button, event):
        self.my_paragraph.text.append('And now you clicked on it, the button that is.')

    @my_paragraph.text.changed
    def my_paragraph_text_changed(self, text, event):
        self.button.text = 'No more clicks!'

    @my_button.text.changed
    def my_button_text_changed(self, text, event):
        self.my_alert.show()


## The Style class is automatically gnerated by the framework
## but you can override it by defining it in the class:
##
##      class MainWindow(Window):
##          class Style:
##              my_blah = {'style-info': 'value'}
##
## or like you see below:

class Style:
    my_button = {
        'background-color': '#ccc',
        'font-size': '14px'}
    my_paragraph = {
        'background-color': '#fff',
        'color': '#000',
        'font-size': '14px',
        'border': '1px solid black',
        'border-radius': '3px'}

MainWindow.Style = Style

## The layout class is automatically generated
## by the framework but you can override it by defining it
## in the class, same as the Style class above, or by
## defining it like this:

class MainLayout(Layout):
    def __init__(self, style):
        # It takes the custom or automatically generated style class upon instantiation
        style.window.pack(HBox().pack(style.my_paragraph, style.my_button))

MainWindow.Layout = MainLayout

if __name__ == '__main__':
    run(App(main=MainWindow))

Ce serait relativement facile à faire en python avec un peu de ce savoir-faire magique en python de métaclasse.Ce que j'ai.Et une connaissance de PyGTK.Ce que j'ai aussi.Vous avez des idées ?

Je n'ai jamais été satisfait des articles de David Mertz chez IBM sur les métaclasses, j'ai donc récemment écrit le mien article de métaclasse.Apprécier.

C'est extrêmement artificiel et pas du tout pythonique, mais voici ma tentative de traduction semi-littérale en utilisant la nouvelle instruction "with".

with Shoes():
  t = Para("Not clicked!")
  with Button("The Label"):
    Alert("You clicked the button!")
    t.replace("Clicked!")

Le plus difficile est de gérer le fait que Python ne nous donnera pas de fonctions anonymes contenant plus d'une instruction.Pour contourner ce problème, nous pourrions créer une liste de commandes et les parcourir...

Quoi qu'il en soit, voici le code backend avec lequel j'ai exécuté ceci :

context = None

class Nestable(object):
  def __init__(self,caption=None):
    self.caption = caption
    self.things = []

    global context
    if context:
      context.add(self)

  def __enter__(self):
    global context
    self.parent = context
    context = self

  def __exit__(self, type, value, traceback):
    global context
    context = self.parent

  def add(self,thing):
    self.things.append(thing)
    print "Adding a %s to %s" % (thing,self)

  def __str__(self):
    return "%s(%s)" % (self.__class__.__name__, self.caption)


class Shoes(Nestable):
  pass

class Button(Nestable):
  pass

class Alert(Nestable):
  pass

class Para(Nestable):
  def replace(self,caption):
    Command(self,"replace",caption)

class Command(Nestable):
  def __init__(self, target, command, caption):
    self.command = command
    self.target  = target
    Nestable.__init__(self,caption)

  def __str__(self):
    return "Command(%s text of %s with \"%s\")" % (self.command, self.target, self.caption)

  def execute(self):
    self.target.caption = self.caption

Avec un peu de magie Metaclass pour conserver l'ordre, j'ai ce qui suit qui fonctionne.Je ne sais pas à quel point c'est pythonique, mais c'est très amusant pour créer des choses simples.

class w(Wndw):
  title='Hello World'
  class txt(Txt):  # either a new class
    text='Insert name here'
  lbl=Lbl(text='Hello') # or an instance
  class greet(Bbt):
    text='Greet'
    def click(self): #on_click method
      self.frame.lbl.text='Hello %s.'%self.frame.txt.text

app=w()

La seule tentative de faire cela que je connaisse est La cire de Hans Nowak (qui est malheureusement mort).

Le plus proche que vous puissiez obtenir des blocs rubis est la déclaration with de pep343 :

http://www.python.org/dev/peps/pep-0343/

Si tu utilises PyGTK avec clairière et cet emballage de clairière, alors PyGTK devient en fait quelque peu pythonique.Un peu au moins.

Fondamentalement, vous créez la disposition de l'interface graphique dans Glade.Vous spécifiez également des rappels d'événements dans Glade.Ensuite, vous écrivez une classe pour votre fenêtre comme ceci :

class MyWindow(GladeWrapper):
    GladeWrapper.__init__(self, "my_glade_file.xml", "mainWindow")
    self.GtkWindow.show()

    def button_click_event (self, *args):
        self.button1.set_label("CLICKED")

Ici, je suppose que j'ai un bouton GTK appelé quelque part bouton1 et que j'ai précisé bouton_clic_event comme le cliqué rappeler.Le wrapper Glade demande beaucoup d'efforts en matière de mappage d'événements.

Si je devais concevoir une bibliothèque d'interface graphique Pythonic, je prendrais en charge quelque chose de similaire, pour faciliter un développement rapide.La seule différence est que je veillerais à ce que les widgets aient également une interface plus pythonique.Les classes PyGTK actuelles me semblent très C, sauf que j'utilise foo.bar(...) au lieu de bar(foo, ...) même si je ne sais pas exactement ce que je ferais différemment.Autoriser probablement un moyen déclaratif de style de modèles Django pour spécifier des widgets et des événements dans le code et vous permettre d'accéder aux données via des itérateurs (là où cela a du sens, par exemple des listes de widgets peut-être), même si je n'y ai pas vraiment pensé.

Peut-être pas aussi élégant que la version Ruby, mais que diriez-vous de quelque chose comme ceci :

from Boots import App, Para, Button, alert

def Shoeless(App):
    t = Para(text = 'Not Clicked')
    b = Button(label = 'The label')

    def on_b_clicked(self):
        alert('You clicked the button!')
        self.t.text = 'Clicked!'

Comme Justin l'a dit, pour implémenter cela, vous devrez utiliser une métaclasse personnalisée sur la classe App, et un tas de propriétés sur Para et Button.En fait, ce ne serait pas trop difficile.

Le problème que vous rencontrez ensuite est le suivant :comment gardez-vous une trace de commande que les choses apparaissent dans la définition de classe ?Dans Python 2.x, il n'y a aucun moyen de savoir si t devrait être au-dessus b ou l'inverse, puisque vous recevez le contenu de la définition de classe sous forme de python dict.

Cependant, dans Python 3.0 les métaclasses sont en cours de modification de plusieurs manières (mineures).L'un d'eux est le __prepare__ , qui vous permet de fournir votre propre objet de type dictionnaire personnalisé à utiliser à la place - cela signifie que vous pourrez suivre l'ordre dans lequel les éléments sont définis et les positionner en conséquence dans la fenêtre.

Cela pourrait être une simplification excessive, je ne pense pas que ce serait une bonne idée d'essayer de créer une bibliothèque d'interface utilisateur à usage général de cette façon.D'un autre côté, vous pouvez utiliser cette approche (métaclasses et amis) pour simplifier la définition de certaines classes d'interfaces utilisateur pour une bibliothèque d'interface utilisateur existante et en fonction de l'application, ce qui pourrait en fait vous faire gagner beaucoup de temps et de lignes de code.

J'ai le même problème.Je souhaite créer un wrapper autour de n'importe quelle boîte à outils GUI pour Python qui soit facile à utiliser et inspiré de Shoes, mais qui doit être une approche POO (contre les blocs Ruby).

Plus d'informations dans : http://wiki.alcidesfonseca.com/blog/python-universal-gui-revisited

Tout le monde est invité à rejoindre le projet.

Si vous voulez vraiment coder l'interface utilisateur, vous pouvez essayer d'obtenir quelque chose de similaire à l'ORM de Django ;quelque chose comme ceci pour obtenir un navigateur d'aide simple :

class MyWindow(Window):
    class VBox:
        entry = Entry()
        bigtext = TextView()

        def on_entry_accepted(text):
            bigtext.value = eval(text).__doc__

L'idée serait d'interpréter certains conteneurs (comme les fenêtres) comme de simples classes, certains conteneurs (comme les tables, les v/hbox) reconnus par des noms d'objets et de simples widgets comme des objets.

Je ne pense pas qu'il serait nécessaire de nommer tous les conteneurs à l'intérieur d'une fenêtre, donc certains raccourcis (comme les classes à l'ancienne reconnues comme des widgets par leurs noms) seraient souhaitables.

À propos de l'ordre des éléments :dans MyWindow ci-dessus, vous n'avez pas besoin de suivre cela (la fenêtre est conceptuellement un conteneur à un emplacement).Dans d'autres conteneurs, vous pouvez essayer de suivre l'ordre en supposant que chaque constructeur de widgets a accès à une liste globale de widgets.C'est ainsi que cela se fait dans Django (AFAIK).

Quelques hacks par-ci, quelques ajustements par-là...Il y a encore peu de choses à penser, mais je crois que c'est possible...et utilisable, tant que vous ne créez pas d'interface utilisateur compliquée.

Cependant, je suis plutôt satisfait de PyGTK+Glade.L'interface utilisateur n'est qu'une sorte de données pour moi et elle doit être traitée comme des données.Il y a tout simplement trop de paramètres à modifier (comme l'espacement à différents endroits) et il est préférable de gérer cela à l'aide d'un outil GUI.Par conséquent, je construis mon interface utilisateur dans Glade, je l'enregistre au format XML et je l'analyse à l'aide de gtk.glade.XML().

Le déclaratif n'est pas nécessairement plus (ou moins) pythonique que fonctionnel à mon humble avis.Je pense qu'une approche en couches serait la meilleure (de bas en haut) :

  1. Une couche native qui accepte et renvoie les types de données Python.
  2. Une couche dynamique fonctionnelle.
  3. Une ou plusieurs couches déclaratives/orientées objet.

Semblable à Élixir + SQLAlchimie.

Personnellement, j'essaierais de mettre en œuvre JQuery comme l'API dans un framework GUI.

class MyWindow(Window):
    contents = (
        para('Hello World!'),
        button('Click Me', id='ok'),
        para('Epilog'),
    )

    def __init__(self):
        self['#ok'].click(self.message)
        self['para'].hover(self.blend_in, self.blend_out)

    def message(self):
        print 'You clicked!'

    def blend_in(self, object):
        object.background = '#333333'

    def blend_out(self, object):
        object.background = 'WindowBackground'

Voici une approche qui aborde les définitions de l'interface graphique un peu différemment en utilisant une métaprogrammation basée sur les classes plutôt que l'héritage.

Il s'agit d'une grande inspiration de Django/SQLAlchemy dans la mesure où il est fortement basé sur la méta-programmation et sépare votre code GUI de votre "code de code".Je pense également qu'il devrait faire un usage intensif des gestionnaires de mise en page comme le fait Java, car lorsque vous supprimez du code, personne ne veut constamment modifier l'alignement des pixels.Je pense aussi que ce serait cool si nous pouvions avoir des propriétés de type CSS.

Voici un exemple approximatif de brainstorming qui montrera une colonne avec une étiquette en haut, puis une zone de texte, puis un bouton sur lequel cliquer en bas qui affiche un message.

from happygui.controls import *

MAIN_WINDOW = Window(width="500px", height="350px",
    my_layout=ColumnLayout(padding="10px",
        my_label=Label(text="What's your name kiddo?", bold=True, align="center"),
        my_edit=EditBox(placeholder=""),
        my_btn=Button(text="CLICK ME!", on_click=Handler('module.file.btn_clicked')),
    ),
)
MAIN_WINDOW.show()

def btn_clicked(sender): # could easily be in a handlers.py file
    name = MAIN_WINDOW.my_layout.my_edit.text
    # same thing: name = sender.parent.my_edit.text
    # best practice, immune to structure change: MAIN_WINDOW.find('my_edit').text
    MessageBox("Your name is '%s'" % ()).show(modal=True)

Une chose intéressante à remarquer est la façon dont vous pouvez référencer l'entrée de my_edit en disant MAIN_WINDOW.my_layout.my_edit.text.Dans la déclaration de la fenêtre, je pense qu'il est important de pouvoir nommer arbitrairement les contrôles dans la fonction kwargs.

Voici la même application utilisant uniquement le positionnement absolu (les contrôles apparaîtront à des endroits différents car nous n'utilisons pas de gestionnaire de mise en page sophistiqué) :

from happygui.controls import *

MAIN_WINDOW = Window(width="500px", height="350px",
    my_label=Label(text="What's your name kiddo?", bold=True, align="center", x="10px", y="10px", width="300px", height="100px"),
    my_edit=EditBox(placeholder="", x="10px", y="110px", width="300px", height="100px"),
    my_btn=Button(text="CLICK ME!", on_click=Handler('module.file.btn_clicked'), x="10px", y="210px", width="300px", height="100px"),
)
MAIN_WINDOW.show()

def btn_clicked(sender): # could easily be in a handlers.py file
    name = MAIN_WINDOW.my_edit.text
    # same thing: name = sender.parent.my_edit.text
    # best practice, immune to structure change: MAIN_WINDOW.find('my_edit').text
    MessageBox("Your name is '%s'" % ()).show(modal=True)

Je ne suis pas encore tout à fait sûr qu’il s’agisse d’une très bonne approche, mais je pense vraiment que c’est sur la bonne voie.Je n'ai pas le temps d'explorer davantage cette idée, mais si quelqu'un entreprenait cela comme projet, je l'adorerais.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top