Ci-dessous, les différences entre deux révisions de la page.
Les deux révisions précédentes Révision précédente | |||
pyqt4_console_python [2013/01/28 17:42] tyrtamos |
pyqt4_console_python [2013/02/07 13:11] (Version actuelle) tyrtamos |
||
---|---|---|---|
Ligne 4: | Ligne 4: | ||
[modification le 28/1/2013 16h] | [modification le 28/1/2013 16h] | ||
+ | |||
+ | [modification le 7/2/2013] | ||
===== Objectif ===== | ===== Objectif ===== | ||
Ligne 12: | Ligne 14: | ||
Et ça marchera, même dans un programme autonome traité par cx_freeze, et exécuté sur un PC qui n'a pas Python! | Et ça marchera, même dans un programme autonome traité par cx_freeze, et exécuté sur un PC qui n'a pas Python! | ||
+ | |||
+ | Autre variante: utiliser cette fenêtre lancée par une icone située dans la zone de notification! Il y a une solution sur ce site dans le chapitre concernant PyQt4. | ||
Ligne 18: | Ligne 22: | ||
On utilise le module " | On utilise le module " | ||
- | La fenêtre est toute simple: | + | La fenêtre est toute simple: |
- | l' | + | L' |
- | Mais cela posera un problème particulier: | + | Mais cela posera un problème particulier: |
- | La partie interpréteur utilisée est la classe " | + | La partie interpréteur utilisée est la classe " |
- | Pour traiter les 2 points précédents, | + | Concernant |
- | + | ||
- | Dernier point à résoudre, | + | |
Voilà pour les principes. le reste est largement commenté dans le code lui-même. | 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 " | Ce code fonctionne bien, mais il n'est pas encore parfait: il faut le considérer en version " | ||
- | |||
- | La seule chose que je n'ai pas réussi à faire, c'est d' | ||
L' | L' | ||
- | 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' | + | On peut ajouter des lignes |
- | + | ||
- | On peut ainsi: | + | |
- | + | ||
- | * fournir une chaine | + | |
- | + | ||
- | * fournir une liste (ou un tuple) | + | |
- | + | ||
- | Dans les 2 cas, les instructions | + | |
- | + | ||
- | si l' | + | |
- | Voir les exemples fournis à la fin du code ci-dessous. | + | Le menu " |
- | Cette fonctionnalité va bien avec l'objectif de départ: quelque soit le domaine dans lequel on est (scientifique, | + | Il faudra aussi lui ajouter |
Voilà ce que ça va donner: | Voilà ce que ça va donner: | ||
Ligne 58: | Ligne 48: | ||
{{: | {{: | ||
- | Voilà le code. | + | Voilà le code. Il est en Python 2.7, mais il est facile à convertir en Python 3.2 grâce à 2to3.py (+ quelques modifications manuelles supplémentaires). |
===== Code ===== | ===== Code ===== | ||
Ligne 67: | Ligne 57: | ||
from __future__ import division | from __future__ import division | ||
# Python 2.7 | # Python 2.7 | ||
+ | |||
+ | __programme__ = u" | ||
+ | __logiciel__ = " | ||
+ | __version__ = " | ||
+ | __date__ = " | ||
import sys, os | import sys, os | ||
import code | import code | ||
- | import StringIO | + | import |
- | from time import | + | from io import BytesIO as StringIO |
+ | from Queue import | ||
from PyQt4 import QtCore, QtGui | from PyQt4 import QtCore, QtGui | ||
############################################################################# | ############################################################################# | ||
- | # variables globales pour gérer les données communes avec le thread | + | class Interpy(code.InteractiveConsole): |
- | + | """ | |
- | mutexokwrite = QtCore.QMutex() | + | |
- | okwrite = False | + | |
- | + | ||
- | ############################################################################# | + | |
- | class MonStringIO(StringIO.StringIO): | + | |
- | + | ||
- | def __init__(self, | + | |
- | StringIO.StringIO.__init__(self, | + | |
- | 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, | + | |
| | ||
# | # | ||
- | def __init__(self, | + | def __init__(self, |
- | | + | |
code.InteractiveConsole.__init__(self, | code.InteractiveConsole.__init__(self, | ||
- | self.proprio | + | self.tlanceur |
- | self.texte = None # initialisation du texte disponible | + | |
- | # se préparer à recevoir les instructions | + | # prépa de l' |
- | self.connect(self.proprio, QtCore.SIGNAL(" | + | self.stopexecflag |
+ | | ||
+ | # rediriger sur le thread lanceur pour gagner | ||
+ | self.write = self.tlanceur.write | ||
+ | | ||
+ | self.quit = self.tlanceur.quit | ||
| | ||
- | # | ||
- | def run(self): | ||
- | """ | ||
- | self.interact() | ||
- | | ||
# | # | ||
def runcode(self, | def runcode(self, | ||
""" | """ | ||
# redirection de la sortie d' | # redirection de la sortie d' | ||
- | std_sav = sys.stdout, sys.stderr | + | |
- | sys.stdout = sys.stderr = sio = MonStringIO(self) | + | sys.stdout = sys.stderr = sio = StringIO() |
- | # exécution de l' | + | sio.write = self.write #2 rediriger l' |
+ | | ||
+ | # exécution de l' | ||
try: | try: | ||
- | exec code in self.locals | + | |
+ | | ||
+ | sys.settrace(None) # arrêt du traçage | ||
except SystemExit: | except SystemExit: | ||
- | self.quit() | + | |
+ | | ||
except: | except: | ||
- | self.showtraceback() | + | |
+ | | ||
finally: | finally: | ||
# remettre la sortie d' | # remettre la sortie d' | ||
- | sys.stdout, sys.stderr = std_sav | + | sys.stdout, sys.stderr = self.std_sav |
sio.close() | sio.close() | ||
| | ||
+ | # | ||
+ | def trace(self, frame, event, arg): | ||
+ | """ | ||
+ | if event == ' | ||
+ | if self.stopexecflag: | ||
+ | self.stopexecflag = False | ||
+ | raise KeyboardInterrupt (" | ||
+ | return self.trace | ||
+ | |||
+ | # | ||
+ | def stopexec(self): | ||
+ | """ | ||
+ | self.stopexecflag = True | ||
+ | |||
# | # | ||
def raw_input(self, | def raw_input(self, | ||
- | """ | + | """ |
- | | + | |
- | line = self.read(prompt) | + | |
- | except Exception as err: | + | |
- | log.exception (err) | + | |
- | raise SystemExit() | + | |
- | else: | + | |
- | return line | + | |
# | # | ||
def write(self, data): | def write(self, data): | ||
- | """ | + | """ |
- | | + | pass |
+ | |||
+ | # | ||
+ | def read(self, prompt): | ||
+ | """ | ||
+ | | ||
+ | |||
+ | # | ||
+ | def quit(self): | ||
+ | """ | ||
+ | pass | ||
+ | |||
+ | ############################################################################# | ||
+ | class Tipy(QtCore.QThread): | ||
+ | """ | ||
+ | |||
+ | finconsole = QtCore.pyqtSignal() | ||
+ | pourafficher = QtCore.pyqtSignal(unicode) | ||
+ | |||
+ | # | ||
+ | def __init__(self, parent=None): | ||
+ | super(Tipy, self).__init__(parent) | ||
+ | # lancement de l' | ||
+ | self.interpy = Interpy(self) | ||
+ | # initialisation de la pile qui contiendra l' | ||
+ | self.instruction = Queue(maxsize=1) | ||
+ | | ||
+ | self.okwrite = False | ||
+ | self.mutexokwrite = QtCore.QMutex() | ||
| | ||
+ | # | ||
+ | def run(self): | ||
+ | """ | ||
+ | self.interpy.interact() | ||
+ | | ||
+ | # | ||
+ | def write(self, data): | ||
+ | """ | ||
# pour être sûr que data est en unicode | # pour être sûr que data est en unicode | ||
if not isinstance(data, | if not isinstance(data, | ||
Ligne 151: | Ligne 181: | ||
if data!=u"": | if data!=u"": | ||
# envoie la réponse sous forme de message (avec shake-hand) | # envoie la réponse sous forme de message (avec shake-hand) | ||
- | mutexokwrite.lock() | + | |
- | okwrite = False | + | |
- | mutexokwrite.unlock() | + | |
- | self.emit(QtCore.SIGNAL(" | + | # envoie data avec le signal ' |
- | while not okwrite: | + | self.pourafficher.emit(data) |
- | pass | + | # attend jusqu' |
- | + | while not self.okwrite: | |
- | # | + | pass |
- | def textedispo(self, | + | |
- | """ | + | |
- | self.texte = texte | + | |
- | | + | |
# | # | ||
def read(self, prompt): | def read(self, prompt): | ||
- | """ | + | """ |
- | | + | |
# envoi l' | # envoi l' | ||
self.write(prompt) | self.write(prompt) | ||
- | + | | |
- | | + | |
- | | + | |
- | sleep(0.1) | + | |
- | texte = self.texte | + | |
- | self.texte = None | + | |
- | return texte | + | |
+ | # | ||
+ | def stop(self): | ||
+ | """ | ||
+ | self.interpy.stopexec() | ||
+ | | ||
# | # | ||
def quit(self): | def quit(self): | ||
- | """ | + | """ |
+ | # arrête l' | ||
+ | self.stop() | ||
# émet le signal de fin pour la fenêtre | # émet le signal de fin pour la fenêtre | ||
- | self.emit(QtCore.SIGNAL(" | + | self.finconsole.emit() |
- | + | ||
############################################################################# | ############################################################################# | ||
class Visu(QtGui.QTextEdit): | class Visu(QtGui.QTextEdit): | ||
+ | """ | ||
+ | | ||
+ | """ | ||
| | ||
- | def __init__(self, | + | |
- | | + | pourafficher = QtCore.pyqtSignal(unicode) |
+ | # signal pour recevoir une demande d' | ||
+ | finconsole = QtCore.pyqtSignal() | ||
+ | # signal pour émettre une demande de fermeture de la fenêtre | ||
+ | finfenetre = QtCore.pyqtSignal() | ||
+ | |||
+ | # | ||
+ | | ||
super(Visu, self).__init__(parent) | super(Visu, self).__init__(parent) | ||
- | | + | # stockage |
- | self.setLineWrapMode(QtGui.QTextEdit.NoWrap) | + | self.initpy |
- | + | ||
- | | + | |
- | self.interpy = Interpy(self) | + | |
- | # prépa pour recevoir de interpy un signal de fermeture de la fenêtre | + | |
- | self.connect(self.interpy, | + | |
- | # prépa pour recevoir du thread ' | + | |
- | self.affichinit = False | + | |
- | self.connect(self.interpy, | + | |
- | # lancement du thread | + | |
- | self.interpy.start() | + | |
- | + | ||
- | # initialisation de la position du curseur dans le texte | + | |
- | self.pos1 = self.textCursor().position() | + | |
- | + | ||
- | # initialisation | + | |
- | self.historique = [] | + | |
- | self.ih = 0 | + | |
| | ||
# prépa de l' | # prépa de l' | ||
- | self.initpy = initpy | ||
if isinstance(self.initpy, | if isinstance(self.initpy, | ||
# si c'est une chaine: elle doit représenter un fichier à charger | # si c'est une chaine: elle doit représenter un fichier à charger | ||
Ligne 219: | Ligne 239: | ||
for ligne in f: | for ligne in f: | ||
self.initpy.append(ligne.rstrip()) | self.initpy.append(ligne.rstrip()) | ||
- | | ||
else: | else: | ||
if not isinstance(self.initpy, | if not isinstance(self.initpy, | ||
Ligne 225: | Ligne 244: | ||
self.initpy = [] | self.initpy = [] | ||
self.lginitpy = len(self.initpy) | self.lginitpy = len(self.initpy) | ||
- | self.nbinvites = -1 | ||
| | ||
+ | # configuration du QTextEdit | ||
+ | self.setAcceptRichText(False) | ||
+ | self.setLineWrapMode(QtGui.QTextEdit.NoWrap) | ||
+ | # Changer la police de caractères et sa taille | ||
+ | font = QtGui.QFont() | ||
+ | font.setFamily(u" | ||
+ | font.setPointSize(10) | ||
+ | self.setFont(font) | ||
+ | |||
+ | # lancement du thread qui porte l' | ||
+ | self.tipy = Tipy() | ||
+ | # prépa pour recevoir de tipy un signal de fermeture de la fenêtre | ||
+ | self.tipy.finconsole.connect(self.quitter) | ||
+ | # prépa pour recevoir de tipy du texte à afficher | ||
+ | self.tipy.pourafficher.connect(self.affiche) | ||
+ | # démarrage du thread | ||
+ | self.tipy.start() | ||
+ | |||
+ | # initialisation de la position courante du curseur après invite | ||
+ | self.pos1 = self.textCursor().position() | ||
+ | # portera la position du curseur à la 1ère invite | ||
+ | self.pos0 = self.pos1 | ||
+ | |||
+ | # initialisation de l' | ||
+ | self.historique = [] | ||
+ | self.ih = 0 | ||
+ | |||
+ | # compteur du nombre d' | ||
+ | self.nbinvites = -1 | ||
| | ||
# | # | ||
+ | @QtCore.pyqtSlot(unicode) | ||
def affiche(self, | def affiche(self, | ||
""" | """ | ||
- | global okwrite, mutextokwrite # drapeau pour le shake-hand d' | ||
| | ||
# ajoute la chaine unicode à la fin du QTextEdit | # ajoute la chaine unicode à la fin du QTextEdit | ||
Ligne 244: | Ligne 290: | ||
| | ||
# renvoie un accusé de réception de fin d' | # renvoie un accusé de réception de fin d' | ||
- | mutexokwrite.lock() | + | |
- | okwrite = True | + | |
- | mutexokwrite.unlock() | + | |
| | ||
# envoie les lignes de code d' | # envoie les lignes de code d' | ||
- | if self.nbinvites< | + | if self.nbinvites < self.lginitpy: |
+ | if self.nbinvites==0: | ||
+ | # 1ère invite | ||
+ | self.pos0 = self.textCursor().position() | ||
if self.nbinvites> | if self.nbinvites> | ||
- | self.emit(QtCore.SIGNAL(" | + | |
+ | | ||
self.nbinvites += 1 | self.nbinvites += 1 | ||
Ligne 269: | Ligne 319: | ||
# débarrasse l' | # débarrasse l' | ||
while texte!=u"" | while texte!=u"" | ||
- | texte = texte.rstrip() | + | texte = texte.rstrip() |
- | #envoyer la ligne d' | + | # empile l' |
- | self.emit(QtCore.SIGNAL(" | + | self.tipy.instruction.put(texte) |
# conserver la ligne d' | # conserver la ligne d' | ||
- | self.historique.append(texte) | + | |
- | self.ih = len(self.historique)-1 # pointe sur le dernier élément | + | |
+ | self.ih = len(self.historique)-1 # pointe sur le dernier élément | ||
# sauvegarde la position du curseur de début d' | # sauvegarde la position du curseur de début d' | ||
self.pos1 = pos2 | self.pos1 = pos2 | ||
Ligne 280: | Ligne 331: | ||
| | ||
# | # | ||
- | elif event.key()==QtCore.Qt.Key_Z and \ | + | elif event.key()==QtCore.Qt.Key_Z and (event.modifiers() & QtCore.Qt.ControlModifier): |
- | (event.modifiers() & QtCore.Qt.ControlModifier): | + | # Ctrl-Z: annule ce qui vient d' |
- | # ne fait rien, mais neutralise le Ctrl-Z | + | |
event.accept() | event.accept() | ||
- | | + | # exécute le Ctrl-Z normal du QTextEdit |
+ | QtGui.QTextEdit.keyPressEvent(self, | ||
+ | # déplace le curseur à la fin du texte | ||
+ | self.moveCursor(QtGui.QTextCursor.End, | ||
+ | # 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() | ||
+ | | ||
# | # | ||
- | elif event.key()==QtCore.Qt.Key_C and \ | + | elif event.key()==QtCore.Qt.Key_S and \ |
| | ||
- | # Controle-C arrête l' | + | # Ctrl-S arrête l' |
- | self.interpy.quit() | + | self.tipy.stop() |
event.accept() | event.accept() | ||
- | | + | |
+ | # | ||
+ | elif event.key()==QtCore.Qt.Key_E and \ | ||
+ | | ||
+ | # Alt-E remet l' | ||
+ | texte = unicode(self.toPlainText()) | ||
+ | texte = texte[: | ||
+ | self.clear() | ||
+ | self.affiche(texte) | ||
+ | self.pos1 = self.pos0 | ||
+ | event.accept() | ||
# | # | ||
elif event.key()==QtCore.Qt.Key_Up: | elif event.key()==QtCore.Qt.Key_Up: | ||
Ligne 334: | Ligne 403: | ||
event.accept() | event.accept() | ||
| | ||
+ | # | ||
+ | elif event.key()==QtCore.Qt.Key_Tab: | ||
+ | """ | ||
+ | self.insertPlainText(u" | ||
+ | | ||
+ | # | ||
+ | elif event.key() in [QtCore.Qt.Key_Up, | ||
+ | | ||
+ | | ||
+ | | ||
+ | """ | ||
+ | | ||
+ | """ | ||
+ | event.accept() | ||
+ | |||
# | # | ||
else: | else: | ||
# n' | # n' | ||
event.ignore() | event.ignore() | ||
- | # évènement transmis à l' | + | # évènement transmis à l' |
QtGui.QTextEdit.keyPressEvent(self, | QtGui.QTextEdit.keyPressEvent(self, | ||
- | | + | |
# | # | ||
+ | @QtCore.pyqtSlot() | ||
def quitter(self): | def quitter(self): | ||
""" | """ | ||
# on ré-émet le signal de fermeture pour la fenêtre | # on ré-émet le signal de fermeture pour la fenêtre | ||
- | self.emit(QtCore.SIGNAL(" | + | |
+ | self.finfenetre.emit() | ||
############################################################################# | ############################################################################# | ||
- | class Consolepy(QtGui.QWidget): | + | class Consolepy(QtGui.QMainWindow): |
| | ||
# | # | ||
- | def __init__(self, | + | def __init__(self, |
super(Consolepy, | super(Consolepy, | ||
| | ||
# instructions pour la fenêtre | # instructions pour la fenêtre | ||
- | self.setWindowTitle(u" | + | self.setWindowTitle(__programme__) |
self.resize(700, | self.resize(700, | ||
- | QtGui.QApplication.setStyle(QtGui.QStyleFactory.create(' | + | |
- | | + | self.setWindowIcon(icone) |
# créer le QTextEdit personnalisé | # créer le QTextEdit personnalisé | ||
self.visu = Visu(self, initpy) | self.visu = Visu(self, initpy) | ||
| | ||
- | | + | |
- | font = QtGui.QFont() | + | |
- | font.setFamily(u" | + | |
- | font.setPointSize(10) | + | |
- | self.visu.setFont(font) | + | |
- | + | ||
- | # positionner dans la fenêtre | + | |
posit = QtGui.QGridLayout() | posit = QtGui.QGridLayout() | ||
posit.addWidget(self.visu, | posit.addWidget(self.visu, | ||
- | self.setLayout(posit) | + | self.centralWidget().setLayout(posit) |
+ | |||
+ | # | ||
+ | # créer le menu principal de la fenêtre | ||
+ | menubar = self.menuBar() | ||
+ | |||
+ | aideMenu = menubar.addMenu(' | ||
+ | |||
+ | aideAction = QtGui.QAction('& | ||
+ | aideAction.setShortcut(QtCore.Qt.Key_F1) | ||
+ | aideAction.setStatusTip(u" | ||
+ | aideAction.triggered.connect(self.manuel_m) | ||
+ | aideMenu.addAction(aideAction) | ||
+ | |||
+ | aproposAction = QtGui.QAction('& | ||
+ | aproposAction.setStatusTip(u" | ||
+ | aproposAction.triggered.connect(self.apropos) | ||
+ | aideMenu.addAction(aproposAction) | ||
+ | |||
+ | # | ||
+ | # initialisation de la barre de status (affiche le nom de page html) | ||
+ | self.statusBar().showMessage(u"" | ||
# se préparer à recevoir de self.visu un signal de fermeture | # se préparer à recevoir de self.visu un signal de fermeture | ||
self.connect(self.visu, | self.connect(self.visu, | ||
+ | |||
+ | # mettre le focus sur le QTextEdit | ||
+ | self.visu.setFocus() | ||
+ | | ||
+ | # | ||
+ | def manuel_m(self): | ||
+ | # affiche le manuel dans le navigateur web par défaut | ||
+ | manuel = os.path.abspath(" | ||
+ | if os.path.exists(manuel): | ||
+ | webbrowser.open(manuel) | ||
+ | | ||
+ | # | ||
+ | def apropos(self): | ||
+ | """ | ||
+ | pf = sys.platform | ||
+ | if pf==' | ||
+ | elif pf==' | ||
+ | elif pf==' | ||
+ | else: pass | ||
+ | QtGui.QMessageBox.about(self, | ||
+ | u""" | ||
+ | Copyright Jean-Paul Vidal 2013 | ||
+ | Licence GPL3 | ||
+ | Source sur demande ici: http:// | ||
+ | | ||
+ | Contexte d' | ||
+ | Système d' | ||
+ | Python version: %s | ||
+ | Qt version: %s | ||
+ | PyQt4 version: %s""" | ||
+ | __logiciel__, | ||
+ | __version__, | ||
+ | __date__, | ||
+ | pf, | ||
+ | sys.version, | ||
+ | QtCore.QT_VERSION_STR, | ||
+ | QtCore.PYQT_VERSION_STR) | ||
+ | ) | ||
# | # | ||
def quitter(self): | def quitter(self): | ||
- | """ | + | """ |
self.close() | self.close() | ||
+ | | ||
+ | # | ||
+ | def closeEvent(self, | ||
+ | """ | ||
+ | event.accept() | ||
| | ||
############################################################################# | ############################################################################# | ||
if __name__ == ' | if __name__ == ' | ||
+ | # | ||
app = QtGui.QApplication(sys.argv) | app = QtGui.QApplication(sys.argv) | ||
+ | # définition du style | ||
+ | app.setStyle(QtGui.QStyleFactory.create(u" | ||
+ | app.setPalette(QtGui.QApplication.style().standardPalette()) | ||
| | ||
- | # possibilité de donner comme argument une liste d' | + | #======================================================================== |
+ | # pour assurer la traduction automatique du conversationnel à la locale | ||
+ | locale = QtCore.QLocale.system().name() | ||
+ | translator = QtCore.QTranslator () | ||
+ | |||
+ | if os.path.splitext(sys.argv[0])[1] in ['.py', ' | ||
+ | # exécution par l' | ||
+ | reptrad = unicode(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)) | ||
+ | else: | ||
+ | # exécution de la version exécutable après cx_freeze (ou équivalent): | ||
+ | #=>les fichiers de traduction doivent se trouver dans " | ||
+ | reptrad = unicode(" | ||
+ | |||
+ | translator.load(QtCore.QString(" | ||
+ | app.installTranslator(translator) | ||
+ | |||
+ | # | ||
+ | # paramétrage: | ||
+ | """ | ||
initpy = [u"# -*- coding: utf-8 -*-", | initpy = [u"# -*- coding: utf-8 -*-", | ||
u"from __future__ import division", | u"from __future__ import division", | ||
u"from math import *"] | u"from math import *"] | ||
- | | + | |
- | | + | initpy = " |
- | # un argument d'un mauvais type ne sera pas considéré (ici un dictionnaire) | + | |
- | #initpy = {} | + | |
+ | # | ||
fen = Consolepy(initpy) | fen = Consolepy(initpy) | ||
fen.show() | fen.show() | ||
+ | |||
+ | # | ||
sys.exit(app.exec_()) | sys.exit(app.exec_()) | ||
</ | </ |