Ceci est une ancienne révision du document !
[Python v2.7.3, PyQt4 v4.9.5, Qt4 v4.8.3]
[modification le 28/1/2013 16h]
On est en train d'utiliser un programme, et on a un petit calcul à faire. Oui, je sais, tous les PC ont une calculatrice, mais le calcul dont on a besoin ne s'y trouve pas forcément: calcul de crédit, calcul en nombres complexes, calcul arithmétique (PGCD, loi de Poisson, …), etc… Selon le domaine dans lequel on est, il suffira d'avoir le module supplémentaire à importer.
Le but de ce projet est de permettre à un programme quelconque Python-PyQt4 d'ouvrir une fenêtre à l'intérieur de laquelle on disposera de l'interpréteur Python, comme la partie interactive de IDLE.
Et ça marchera, même dans un programme autonome traité par cx_freeze, et exécuté sur un PC qui n'a pas Python!
On utilise le module “code” qui est fait pour ça.
La fenêtre est toute simple: QWidget avec à l'intérieur un QTextEdit. Mais le QTextEdit est particulier, parce qu'il a fallu traiter certains caractères comme la touche “entrée” pour envoyer la dernière ligne tapée à l'interpréteur. J'ai donc sous-classé le QTextEdit pour ça: classe “Visu” dans le code ci-dessous. Cette classe devra aussi lancer le thread qui portera l'interpréteur, et recevoir ses réponses pour les afficher.
l'interpréteur sera porté par un thread. Pourquoi un thread? Parce que l'attente de la prochaine instruction à exécuter (sorte de “raw_input”) ferait figer le graphique. Et puis ce serait dommage qu'une exécution un peu longue fige aussi le graphique.
Mais cela posera un problème particulier: un code s'exécutant dans un thread ne doit pas toucher directement au graphique PyQt4. Les échanges d'information se feront exclusivement par échange de messages. J'ai donc choisi une thread “QThread”.
La partie interpréteur utilisée est la classe “InteractiveConsole” du module code. Elle est sous-classée et certaines méthodes surchargées pour assurer les échanges d'entrée/sortie avec la partie graphique, y compris avec un détournement des sys.stdout et sys.stderr.
Pour traiter les 2 points précédents, j'ai utilisé une classe, appelée ici “Interpy”, qui hérite en même temps de “QtCore.QThread” et “code.InteractiveConsole”. Les programmes en PyQt4 supportent très bien l'héritage multiple, à condition qu'il n'y ait pas plusieurs ancêtres issus de QWidget.
Dernier point à résoudre, le détournement des sys.stdout et sys.stderr. Ce sont des objets de type “fichier”. On peut toujours les détourner sur un “StringIO.StringIO” et récupérer le contenu après exécution pour l'afficher, mais l'impression à l'intérieur d'une boucle ne marchera pas. Il faut donc que chaque demande d'affichage soit satisfaite, même à l'intérieur d'une exécution complexe. La solution choisie est de sous-classer la classe StringIO, ce qui donne la classe “MonStringIO”, et de surcharger sa méthode write.
Voilà pour les principes. le reste est largement commenté dans le code lui-même.
Ce code fonctionne bien, mais il n'est pas encore parfait: il faut le considérer en version “beta”. Les remarques sont bienvenues!
La seule chose que je n'ai pas réussi à faire, c'est d'arrêter un code trop long avec Ctrl-C. En effet, l'exécution se faisant dans le thread, l'exception KeyboardInterrupt n'a pas d'action sur lui. Ici, un Ctrl-C ferme la fenêtre: c'est tout ce que j'ai trouvé pour se débarrasser d'un code trop long ou planté. Je reste à l'écoute d'une solution plus intelligente!
L'idée initiale de ce code m'a été soufflée sur un forum (http://www.developpez.net/forums/d1301688/autres-langages/python-zope/gui/pyside-pyqt/console-python-qtextedit/): merci à wiztricks!
Quelques modifications ont été faites depuis la toute 1ère publication (ce matin!). La plus importante est que l'on peut désormais fournir, dès le lancement, du code Python pour que l'interpréteur l'exécute au départ.
On peut ainsi:
Dans les 2 cas, les instructions seront exécutées comme si l'utilisateur les avaient tapées lui-même.
si l'argument transmis est une chaine mais que le fichier n'existe pas, ou est n'importe quoi d'autre qu'une liste ou un tuple, l'interpréteur n'en tiendra pas compte.
Voir les exemples fournis à la fin du code ci-dessous.
Cette fonctionnalité va bien avec l'objectif de départ: quelque soit le domaine dans lequel on est (scientifique, financier, construction, etc…), on peut initialiser automatiquement la console avec des modules de fonctions adaptées, fonctions qu'on pourra utiliser dans la console comme avec une calculatrice spécialisée!
Voilà ce que ça va donner:
Voilà le code.
#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import division # Python 2.7 import sys, os import code import StringIO from time import sleep from PyQt4 import QtCore, QtGui ############################################################################# # variables globales pour gérer les données communes avec le thread mutexokwrite = QtCore.QMutex() okwrite = False ############################################################################# class MonStringIO(StringIO.StringIO): def __init__(self, orig, buf = ''): StringIO.StringIO.__init__(self, buf) self.orig = orig def write(self, s): # envoie la réponse sous forme de message à la fenêtre self.orig.write(s) ############################################################################# class Interpy(QtCore.QThread, code.InteractiveConsole): #======================================================================== def __init__(self, proprio, locals=None, filename="<console>"): QtCore.QThread.__init__(self) code.InteractiveConsole.__init__(self, locals=None, filename="<console>") self.proprio = proprio # adresse de l'objet qui a lancé le thread self.texte = None # initialisation du texte disponible # se préparer à recevoir les instructions du QTextEdit personnalisé self.connect(self.proprio, QtCore.SIGNAL("pourlire(PyQt_PyObject)"), self.textedispo) #======================================================================== def run(self): """partie exécutée en asynchrone""" self.interact() #======================================================================== def runcode(self, code): """surcharge de runcode pour rediriger sys.stdout et sys.stderr""" # redirection de la sortie d'affichage std_sav = sys.stdout, sys.stderr sys.stdout = sys.stderr = sio = MonStringIO(self) # exécution de l'instruction Python try: exec code in self.locals except SystemExit: self.quit() except: self.showtraceback() finally: # remettre la sortie d'affichage initiale sys.stdout, sys.stderr = std_sav sio.close() #======================================================================== def raw_input(self, prompt): """lire la prochaine ligne d'instruction""" try: line = self.read(prompt) except Exception as err: log.exception (err) raise SystemExit() else: return line #======================================================================== def write(self, data): """affiche data en envoyant un message à la fenêtre""" global okwrite, mutexokwrite # drapeau pour le shake-hand d'affichage # pour être sûr que data est en unicode if not isinstance(data, unicode): data = unicode(data) # débarrasse data des éventuels fins de ligne des 2 côtés while data!=u"" and data[0] in ['\n', '\r']: data = data[1:] while data!=u"" and data[-1] in ['\n', '\r']: data = data[:-1] if data!=u"": # envoie la réponse sous forme de message (avec shake-hand) mutexokwrite.lock() okwrite = False mutexokwrite.unlock() self.emit(QtCore.SIGNAL("pourafficher(PyQt_PyObject)"), data) while not okwrite: pass #======================================================================== def textedispo(self, texte): """exécuté à chaque message reçu "pourlire" accompagné du texte """ self.texte = texte #======================================================================== def read(self, prompt): """lit la chaine à interpréter""" # envoi l'invite pour affichage self.write(prompt) # retourne la chaine dès qu'il y en a une de saisie while self.texte==None: sleep(0.1) texte = self.texte self.texte = None return texte #======================================================================== def quit(self): """appelé quand on veut fermer l'application""" # émet le signal de fin pour la fenêtre self.emit(QtCore.SIGNAL("finconsole()")) ############################################################################# class Visu(QtGui.QTextEdit): def __init__(self, parent=None, initpy=[]): super(Visu, self).__init__(parent) self.setAcceptRichText(False) self.setLineWrapMode(QtGui.QTextEdit.NoWrap) # prépa du lancement du thread qui porte l'interpréteur Python self.interpy = Interpy(self) # prépa pour recevoir de interpy un signal de fermeture de la fenêtre self.connect(self.interpy, QtCore.SIGNAL("finconsole()"), self.quitter) # prépa pour recevoir du thread 'interpy' du texte à afficher self.affichinit = False self.connect(self.interpy, QtCore.SIGNAL("pourafficher(PyQt_PyObject)"), self.affiche) # lancement du thread self.interpy.start() # initialisation de la position du curseur dans le texte self.pos1 = self.textCursor().position() # initialisation de l'historique des lignes d'instruction Python self.historique = [] self.ih = 0 # prépa de l'initialisation de l'interpréteur si demandé au lancement self.initpy = initpy if isinstance(self.initpy, (str, unicode)): # si c'est une chaine: elle doit représenter un fichier à charger nfc = self.initpy self.initpy = [] if os.path.exists(nfc): with open(nfc, 'r') as f: for ligne in f: self.initpy.append(ligne.rstrip()) else: if not isinstance(self.initpy, (list, tuple)): # mauvais type pour self.initpy: on n'en tient pas compte self.initpy = [] self.lginitpy = len(self.initpy) self.nbinvites = -1 #======================================================================== def affiche(self, texte): """Affiche la chaine 'texte' dans le widget QTextEdit""" global okwrite, mutextokwrite # drapeau pour le shake-hand d'affichage # ajoute la chaine unicode à la fin du QTextEdit self.append(texte) # déplace le curseur à la fin du texte self.moveCursor(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) # force le rafraichissement pour affichage en temps réel QtCore.QCoreApplication.processEvents() # met à jour la position du curseur dans le texte self.pos1 = self.textCursor().position() # renvoie un accusé de réception de fin d'affichage (shake-hand) mutexokwrite.lock() okwrite = True mutexokwrite.unlock() # envoie les lignes de code d'initialisation après la 1ère invite if self.nbinvites<self.lginitpy: if self.nbinvites>=0: self.emit(QtCore.SIGNAL("pourlire(PyQt_PyObject)"), self.initpy[self.nbinvites]) self.nbinvites += 1 # ======================================================================= def keyPressEvent(self, event): """traitement des évènements clavier du QTextEdit""" #-------------------------------------------------------------------- if event.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]: # touche "retour" # déplacer le curseur à la fin du texte si ce n'est pas le cas self.moveCursor(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) # texte situé entre la précédente position du curseur et l'actuelle pos1 = self.pos1 pos2 = self.textCursor().position() texte = unicode(self.toPlainText())[pos1:pos2] # débarrasse l'instruction 'texte' des éventuels fins de ligne while texte!=u"" and texte[0] in ['\n', '\r']: texte = texte[1:] texte = texte.rstrip() #envoyer la ligne d'instruction à l'interpréteur Python self.emit(QtCore.SIGNAL("pourlire(PyQt_PyObject)"), texte) # conserver la ligne d'instruction dans l'historique self.historique.append(texte) self.ih = len(self.historique)-1 # pointe sur le dernier élément # sauvegarde la position du curseur de début d'instruction self.pos1 = pos2 event.accept() #-------------------------------------------------------------------- elif event.key()==QtCore.Qt.Key_Z and \ (event.modifiers() & QtCore.Qt.ControlModifier): # ne fait rien, mais neutralise le Ctrl-Z event.accept() #-------------------------------------------------------------------- elif event.key()==QtCore.Qt.Key_C and \ (event.modifiers() & QtCore.Qt.ControlModifier): # Controle-C arrête l'interpréteur et ferme la fenêtre self.interpy.quit() event.accept() #-------------------------------------------------------------------- elif event.key()==QtCore.Qt.Key_Up: # traitement touche flèche en haut: instruction précédente if self.historique!=[] and self.ih>=0: pos1 = self.pos1 pos2 = self.textCursor().position() instruction = self.historique[self.ih] texte = unicode(self.toPlainText()) texte = texte[:pos1] + instruction + texte[pos2:] self.clear() self.affiche(texte) self.pos1 = pos1 if self.ih>0: self.ih -= 1 event.accept() #-------------------------------------------------------------------- elif event.key()==QtCore.Qt.Key_Down: # traitement touche flèche en bas, instruction suivante if self.historique!=[] and self.ih<len(self.historique)-1: self.ih += 1 pos1 = self.pos1 pos2 = self.textCursor().position() instruction = self.historique[self.ih] texte = unicode(self.toPlainText()) texte = texte[:pos1] + instruction + texte[pos2:] self.clear() self.affiche(texte) self.pos1 = pos1 event.accept() #-------------------------------------------------------------------- elif event.key()==QtCore.Qt.Key_Backspace: # empêche le backspace de revenir sur l'invite pos = self.textCursor().position() if pos>self.pos1: event.ignore() # évènement transmis à l'ancêtre QtGui.QTextEdit.keyPressEvent(self, event) else: event.accept() #-------------------------------------------------------------------- else: # n'importe quel autre caractère que ceux ci-dessus event.ignore() # évènement transmis à l'ancêtre QtGui.QTextEdit.keyPressEvent(self, event) #======================================================================== def quitter(self): """la fenêtre a reçu le signal de fermeture de l'interpréteur """ # on ré-émet le signal de fermeture pour la fenêtre self.emit(QtCore.SIGNAL("finfenetre()")) ############################################################################# class Consolepy(QtGui.QWidget): #======================================================================== def __init__(self, initpy=[], parent=None): super(Consolepy, self).__init__(parent) # instructions pour la fenêtre self.setWindowTitle(u"Interpréteur Python") self.resize(700, 700) # definit la taille de la fenetre QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Plastique')) #self.setWindowIcon(QtGui.QIcon('iconefenetre.ico')) # créer le QTextEdit personnalisé self.visu = Visu(self, initpy) # Changer la police de caractères et sa taille font = QtGui.QFont() font.setFamily(u"DejaVu Sans Mono") font.setPointSize(10) self.visu.setFont(font) # positionner dans la fenêtre posit = QtGui.QGridLayout() posit.addWidget(self.visu, 0, 0) self.setLayout(posit) # se préparer à recevoir de self.visu un signal de fermeture self.connect(self.visu, QtCore.SIGNAL("finfenetre()"), self.quitter) #======================================================================== def quitter(self): """la fenêtre a reçu le signal de fermeture""" self.close() ############################################################################# if __name__ == '__main__': app = QtGui.QApplication(sys.argv) # possibilité de donner comme argument une liste d'instructions Python initpy = [u"# -*- coding: utf-8 -*-", u"from __future__ import division", u"from math import *"] # possibilité de donner comme argument un nom de fichier Python à exécuter #initpy = "consolepy_init.py" # un argument d'un mauvais type ne sera pas considéré (ici un dictionnaire) #initpy = {} fen = Consolepy(initpy) fen.show() sys.exit(app.exec_())
Amusez-vous bien!