logo le blog invivoo blanc

Python et le GUI : wxPython

19 novembre 2019 | Python | 0 comments

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__':
    app = MyApplication()
    app.MainLoop()

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__( self )
        self.Create( parent, id, title, pos, size, style, name )

‘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 :

class MyApplication( wx.App ):
    
    def OnInit( self ):
        # initialize
        print( "MyApplication.OnInit" )
        self.SetAppName( "HelloWordApp" )

        # create the dialog box
        dlg = MyDialog( None, -1, "Hello world Application!" )
        print( "before ShowModal" )
        dlg.ShowModal()
        print( "after ShowModal" )
        dlg.Destroy()
        return True

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__( self )
        self.Create( parent, id, title, pos, size, style, name )
        wx.StaticText( self, -1, 'Hello world !' )

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__( self )
        self.Create( parent, id, title, pos, size, style, name )
        wx.StaticText( self, -1, 'Hello world !' )
        okButton     = wx.Button( self, -1, 'OK' ) 
        cancelButton = wx.Button( self, -1, 'Cancel' )

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__':
    app = MyApplication()
    app.MainLoop()

‘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__':
    app = MyApp( False )
    app.MainLoop()

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.