26 août 2016

Python et le GUI : wxPython

Design & Code

Philippe

Boulanger

Image ordinateur.

26 août 2016

Python et le GUI : wxPython

Design & Code

Philippe

Boulanger

Image ordinateur.

26 août 2016

Python et le GUI : wxPython

Design & Code

Philippe

Boulanger

Image ordinateur.

Beaucoup de personnes partent du principe que Python n’est qu’un langage de script et le cantonnent à l’automatisation des tâches. Grâce à des frameworks, on peut aussi s’en servir pour faire des clients lourds. Le module tkInter qui permet de faire des boîtes de dialogues assez facilement et a longtemps été utilisée par les ingénieurs systèmes pour développer des mini-applications et faciliter leur travail. Il existe des portages de GTK, Qt et wxWidgets pour Python qui permettent de faire de belles applications. Spyder, par exemple, est un IDE pour Python qui est écrit en Python avec du Qt. Dans cet article nous allons travailler avec wxPython qui est le portage de wxWidgets ; une bibliothèque que j’ai utilisée à de nombreuses reprises pour faire un démonstrateur ou pour faire des outils internes pour des clients.

wxPython ?

> Installation

La bibliothèque wxPython est disponible sur le site www.wxPython.org . Vous y trouverez toutes les informations utiles pour l’installation ainsi que la documentation en ligne.

welcome-wxpython

Sous Windows et Mac, l’installation peut se faire via l’utilitaire pip grâce à la commande suivante :
pip install -U wxPython
Sous Windows, si vous utilisez Anaconda, vous pouvez utiliser l’outil de gestion de package fournit par cette distribution : Anaconda Navigator :

menu_anaconda

Suivant la façon dont est installé votre PC, il sera peut-être nécessaire d’exécuter Anaconda Navigator en mode Administrateur afin de permettre la mise à jour.

Pour Linux, compte-tenu des nombreuses distributions, l’installation peut s’avérer plus complexe :
pip install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04 \
wxPython

Il faut alors bien choisir sa version en fonction de la distribution cible.

> Les extras ?

À chaque nouvelle version de wxPython, vous trouverez un lien pour accéder aux extras.  

extras

L’un des gros intérêts de

wxPython est l’application « wxPython-demo » présente dans ces

extras.  Voici un aperçu de

l’application :


wxpython-demo

Cette application est écrite en

Python avec wxPython et permet de naviguer dans toutes les fonctionnalités de

la bibliothèque, de voir des exemples des contrôles graphiques et d’accéder au

code.


wxpython-demo2

Cela permet de développer plus rapidement son application 😊 sans avoir besoin de lire complètement la documentation.

Extras contient aussi la documentation offline pour ceux qui travaillent souvent en déplacement sans avoir une connexion internet.

> Outils ?

Créer des interfaces utilisateurs

peut être compliqué et il existe des outils pour faciliter leur création. Pour

wxPython, vous pouvez utiliser wxFormBuilder ou wxGlade qui sont, tous deux,

gratuits et téléchargeable sur le net.


« HELLO WORLD »

Avant de se lancer dans

l’écriture d’une application plus complexe, nous allons commencer par créer une

application ‘boîte de dialogue’ qui affiche le message « Hello

World ». Puis nous y rajouterons un bouton ce qui nous permettra de

comprendre le positionnement des contrôles.


> Au commencement, l’application

Avant toute chose, il faut créer

une instance de wx.App qui fournira les fonctionnalités de gestion des

événements : sans cet instance, rien ne fonctionnera.


import wx
class MyApplication( wx.App ):
    def OnInit( self ):
        print( "MyApplication.OnInit" )
        self.SetAppName( "HelloWordApp" )
        return True
if __name__ == '__main__

Pour l’instant, ce programme s’exécute mais n’affiche rien. La fonction OnInit de notre classe est appelée pour initialiser le GUI après le constructeur : elle doit retourner True pour que l’application puisse d’initialiser proprement.

> Créer une boîte de dialogue

Une fois l’application créée, il

faut afficher une fenêtre. Le type de fenêtre le plus simple que l’on puisse

utiliser est la boîte de dialogue wx.Dialog : nous allons créer une classe

qui en dérive.


class MyDialog( wx.Dialog ):
    def __init__( self,
                  parent,
                  id,
                  title,
                  size  = wx.DefaultSize,
                  pos   = wx.DefaultPosition,
                  style = wx.DEFAULT_DIALOG_STYLE,
                  name  = 'My dialog' ):
        wx.Dialog.__init__

‘parent’ désigne l’objet parent de la boîte de dialogue : la première fenêtre n’a pas de parent, on lui passe None. ‘title’ contient le nom qui sera affiché dans la barre de titre. ‘id’

est un identifiant unique qui désigne une fenêtre, cela permet d’aiguiller les

événements aux bons composants. Si on définit ‘id’ à -1, on demande à wxPython

de générer automatiquement un identifiant.


Pour permettre à l’application d’afficher la boîte de dialogue il faut modifier la fonction OnInit :


La première étape consiste à créer l’objet MyDialog et de

stocker l’instance dans ‘dlg’. Cela ne suffit pas à déclencher l’affichage, il

faut appeler la fonction ShowModal. Tant que la boîte n’est pas fermée, la

ligne après n’est pas exécuté : le ShowModal est une action bloquante. Une

fois la boîte fermée, il faut penser à supprimer les objets systèmes liés en

appelant la fonction Destroy.


Lorsque l’on exécute le programme on obtient :

hello_world_wxpython

> Et mon « Hello World »

Maintenant que ma boîte de dialogue s’affiche, il nous

faut rajouter un champ texte avec « Hello World » à l’intérieur. En

regardant dans l’application wxPython-demo, on trouve ceci :


wxpython-demo3

wx.StaticText

est un contrôle qui nous permet d’afficher un texte non-modifiable. Il nous

faut ajouter un appel dans la fonction ‘__init__’ de MyDialog :


class MyDialog( wx.Dialog ):
    def __init__( self,
                  parent,
                  id,
                  title,
                  size  = wx.DefaultSize,
                  pos   = wx.DefaultPosition,
                  style = wx.DEFAULT_DIALOG_STYLE,
                  name  = 'My dialog' ):
        wx.Dialog.__init__

Ce qui nous permet d’obtenir :

wxpython-helloworld2

> Rajoutons un bouton OK et un bouton Cancel

En

recherchant dans wxPython-demo le bon contrôle, nous trouverons wx.Button. Et

son utilisation est assez simple :


class MyDialog( wx.Dialog ):
    def __init__( self,
                  parent,
                  id,
                  title,
                  size  = wx.DefaultSize,
                  pos   = wx.DefaultPosition,
                  style = wx.DEFAULT_DIALOG_STYLE,
                  name  = 'My dialog' ):
        wx.Dialog.__init__

A l’affichage on obtient :

helloworld-wxpython3

Tous les contrôles s’affichent au même endroit. Outre le

fait que l’interface est illisible, il est impossible d’appuyer sur les

boutons ☹.


> Positionnement des contrôles et gestion des événements

Pour positionner les contrôles les uns par rapports aux autres, wxPython propose les ‘sizer’ :

  • wx.Sizer : une classe de base

  • wx.BoxSizer :

c’est une

‘boîte’ qui permet de ranger les contrôles les uns après les autres soit

horizontalement :


horizontal-boxes

soit verticalement :

vertical-boxes
  • wx.GridSizer : c’est une grille de contrôles

wx.gridsizer

Mettons en œuvre les sizers :

import wx
class MyDialog( wx.Dialog ):
    def __init__( self,
                  parent,
                  id,
                  title,
                  size  = wx.DefaultSize,
                  pos   = wx.DefaultPosition,
                  style = wx.DEFAULT_DIALOG_STYLE,
                  name  = 'My dialog' ):
        wx.Dialog.__init__( self )
        self.Create( parent, id, title, pos, size, style, name )
        staticText   = wx.StaticText( self, -1, "Hello world!" )
        okButton     = wx.Button( self, wx.ID_OK, "OK" )
        cancelButton = wx.Button( self, wx.ID_CANCEL, "Cancel" )
        self.Bind( wx.EVT_BUTTON, self.OnOK, okButton )
        self.Bind( wx.EVT_BUTTON, self.OnCancel, cancelButton )
        topSizer = wx.BoxSizer( wx.VERTICAL )
        topSizer.Add( staticText,   0, wx.EXPAND )
        hSizer = wx.BoxSizer( wx.HORIZONTAL )
        hSizer.Add( okButton,     0, wx.EXPAND )
        hSizer.Add( cancelButton, 0, wx.EXPAND )
        topSizer.Add( hSizer, 0, wx.EXPAND )
        self.SetSizer( topSizer )
        topSizer.Fit( self )
    def OnOK( self, event ):
        print( "OnOK" )
        self.EndModal( wx.ID_OK )
    def OnCancel( self, event ):
        print( "OnCancel" )
        self.EndModal( wx.ID_CANCEL )
class MyApplication( wx.App ):
    def OnInit( self ):
        # initialize
        print( "MyApplication.OnInit" )
        self.SetAppName( "HelloWordApp" )
        # create dialog box
        dlg = MyDialog( None, -1, "Hello world Application!" )
        print( "before ShowModal" )
        res = dlg.ShowModal()
        print( "after ShowModal" )
        if res == wx.ID_OK:
            print( "exit OK" )
        elif res == wx.ID_CANCEL:
            print( "exit CANCEL" )
        else:
            print( "exit %d" % res )
        dlg.Destroy()
        return True
if __name__ == '__main__

‘self.Bind’

permet de rediriger les événements. Dans le cas des boutons, il y a 3

paramètres :


  • wx.EVT_BUTTON : l’identifiant de l’événement « appui sur un bouton »

  • la fonction qui doit recevoir l’événement

  • l’objet wx.Button dont on veut capter l’appui

Les handlers

d’événements OnOK et OnCancel servent à clore la boîte de dialogue. Pour se

faire il faut appeler la fonction self.EndModal en lui passant l’identifiant de

l’événement.


L’ajout de

contrôles dans les sizers se fait via la fonction ‘Add’ ; les paramètres

d’appel sont :


  • le contrôle à ajouter

  • ‘proportion’ : un entier qui définit la proportion

  • ‘flag’ : un masque d’options

  • ‘border’ : l’épaisseur de la bordure

  • ‘userData’ : une donnée utilisateur que l’on peut attacher

Le sizer le plus externe (celui qui contient tous les

contrôles) doit être ajouté à la fenêtre via la fonction ‘SetSizer’. Pour

ajuster la taille de chaque contrôle ainsi que celle de la fenêtre, nous appelons

la fonction ‘Fit’ sur le sizer le plus externe en lui passant en paramètre

l’objet de la fenêtre.


PASSONS AUX CHOSES SERIEUSES

Après cette mise en bouche avec

une application relativement simple, nous allons créer une application qui

permet de visualiser des images. Mon objectif est d’avoir un explorateur de

fichiers qui me permette de parcourir les fichiers du disque dur. Si je

double-clique sur un fichier « image » celle-ci s’affichera dans une

fenêtre et nous pouvons avoir plusieurs images ouvertes en même temps.


Il se trouve que wxPython propose

une classe wx.Image qui supporte plusieurs formats de fichiers bitmaps (BMP,

PNG, JPEG, GIF, ICO, TGA, TIFF, etc…). Par contre, pour afficher une image nous

utiliserons la classe wx.StaticBitmap qui est un contrôle.


Afin de se rapprocher d’un design

plus professionnel, nous ajouterons une barre d’outils ainsi qu’une barre de

menu. Cela nous permettra de comprendre leurs créations ainsi que la gestion

des événements associés.


# -*- coding: utf-8 -*-
import os
import wx
import wx.adv
ID_Menu_New   = 5000
ID_Menu_Open  = 5001
ID_Menu_Exit  = 5002
wildcard = "Bitmap files (*.bmp)|*.bmp|"              \
           "JPEG files (*.jpg, *jpeg)|*.jpg, *.jpeg|" \
           "PNG files (*.png)|*.png|"                 \
           "GIF files (*.gif)|*.gif|"                 \
           "Icon files (*.ico)|*.ico|"                \
           "Targa files (*.tga)|*.tga|"               \
           "TIFF files (*.tif,*tiff)|*.tif,*tiff|"    \
           "All files (*.*)|*.*"
class MyParentFrame( wx.MDIParentFrame ):
    def __init__( self ):
        wx.MDIParentFrame.__init__( self,
                                    None,
                                    -1,
                                    "MDI Parent",
                                    size  = (600,400),
                                    style = wx.DEFAULT_FRAME_STYLE | wx.HSCROLL | wx.VSCROLL )
        self.create_menu_bar()
        self.create_toolbar()
    def create_menu_bar( self ):
        # create the "File" menu
        menuFile = wx.Menu()
        menuFile.Append( ID_Menu_New,  "&New Window" )
        menuFile.Append( ID_Menu_Open, "&Open file" )
        menuFile.AppendSeparator()
        menuFile.Append( ID_Menu_Exit, "E&xit" )
        # create the menu bar
        menubar = wx.MenuBar()
        menubar.Append( menuFile, "&File" )
        self.SetMenuBar( menubar )
        # bind the events
        self.Bind( wx.EVT_MENU, self.OnNewWindow, id = ID_Menu_New )
        self.Bind( wx.EVT_MENU, self.OnOpenFile,  id = ID_Menu_Open )
        self.Bind( wx.EVT_MENU, self.OnExit,      id = ID_Menu_Exit )
    def create_toolbar( self ):
        # create the toolbar
        tsize = ( 32, 32 )
        tb    = self.CreateToolBar( True )
        tb.SetToolBitmapSize( tsize )
        # new window
        new_bmp =  wx.ArtProvider.GetBitmap( wx.ART_NEW, wx.ART_TOOLBAR, tsize )
        tb.AddTool( ID_Menu_New,
                    "New",
                    new_bmp,
                    wx.NullBitmap,
                    wx.ITEM_NORMAL,
                    "New",
                    "Long help for 'New'",
                    None )
        # open file
        open_bmp = wx.ArtProvider.GetBitmap( wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize )
        tb.AddTool( ID_Menu_Open,
                    "Open",
                    open_bmp,
                    wx.NullBitmap,
                    wx.ITEM_NORMAL,
                    "Open",
                    "Long help for 'Open'",
                    None )
        # display the toolbar
        tb.Realize()
        # bind the events
        self.Bind( wx.EVT_TOOL, self.OnNewWindow, id = ID_Menu_New )
        self.Bind( wx.EVT_TOOL, self.OnOpenFile,  id = ID_Menu_Open )
    def OnNewWindow( self, event ):
        win    = wx.MDIChildFrame( self, -1, "Child Window" )
        canvas = wx.ScrolledWindow( win )
        win.Show( True )
    def OnOpenFile( self, event ):
        # choose the file
        dlg = wx.FileDialog( self,
                             message     = "Choose a file",
                             defaultDir  = os.getcwd(),
                             defaultFile = "",
                             wildcard    = wildcard,
                             style       = wx.FD_OPEN | wx.FD_MULTIPLE | wx.FD_CHANGE_DIR |
                                           wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW )
        if dlg.ShowModal() == wx.ID_OK:
            for path in dlg.GetPaths():
                self.read_file( path )
        dlg.Destroy()
    def OnExit( self, event ):
        self.Close( True )
    def read_file( self, filename ):
        # read image if possible
        try:
            image = wx.Image( filename, wx.BITMAP_TYPE_ANY )
        except:
            return
        # create the window
        win     = wx.MDIChildFrame( self, -1, filename )
        canvas  = wx.ScrolledWindow( win )
        sizer   = wx.BoxSizer( wx.HORIZONTAL )
        statBmp = wx.StaticBitmap( canvas,
                                   wx.ID_ANY,
                                   image.ConvertToBitmap() )
        sizer.Add( statBmp, 1, wx.EXPAND )
        canvas.SetSizer( sizer )
        sizer.Fit( canvas )
        win.Show( True )
class MyApp( wx.App ):
    def OnInit( self ):
        frame = MyParentFrame()
        frame.Show( True )
        self.SetTopWindow( frame )
        return True
if __name__ == '__main__

CONCLUSION

wxPython est une bibliothèque qui vous permettra de développer rapidement des applications avec un look&feel professionnel. Elle est très intéressante tant pour faire un prototype/démonstrateur que pour faire une application interne. wxPython-demo est un formidable outil qui permet de rapidement trouver les contrôles qui correspondent à notre besoin ainsi que le code qui permet de les construire.