Base visuelle de la programmation graphique
Avant-propos
Description
QLineEdit
au dessus des boutons : "graph_name".QLabel
au dessus : "label" → "Nom du graphe".Vertical Spacer
) au dessus.QLineEdit
et les boutons.Dialog_new
et graph_name
.Main
./pc/dialog_new_graph.py
a bien été créé automatiquement.clic droit > Run dialog_new_graph
⇒ La boîte de dialogue s'affiche :
Si votre Qt Designer est en français, vous remarquerez une différence dans le libellé des boutons, ci-dessus :
Utils
.Main
.utils.py
et main.py
→ implémenter les codes suivants :utils.py
:
Ajout de la méthode translate()
QTranslator
, QLocale
et QLibraryInfo
.Le fichier utils.py
devient (code complet) :
from PyQt5.QtCore import QSettings, QTimer, QRect, QTranslator, QLocale, QLibraryInfo
import subprocess
import inspect
import os
import sys
class DateTime:
"""
Classe dédiée à la gestion du temps :
- Dates, heures, différents formats (Unix, Windows, ...)
- Temporisateurs, horloges, ...
"""
def __init__(self):
self.d_tempo = dict() # Dictionnaire de temporisateurs.
def delay(self, client_function, delay=20, params=None):
""" Asynchrone (ne prend pas la main).
:param client_function: Cette fonction sera exécutée après un délai.
:param delay: Délai en ms (20ms par défaut).
:param params: Optionnel. Arguments dans l'appel de la fonction.
:return: NA
"""
key = client_function.__name__
if key in self.d_tempo:
if self.d_tempo[key].isActive():
self.d_tempo[key].disconnect()
else:
self.d_tempo[key] = QTimer()
self.d_tempo[key].setSingleShot(True)
if params is None:
self.d_tempo[key].timeout.connect(client_function)
else:
self.d_tempo[key].timeout.connect(lambda: client_function(params))
self.d_tempo[key].start(delay)
class Utils:
def __init__(self):
self.o_dt = DateTime()
self.caller_dir = os.getcwd() # dossier du code appelant.
self.settings = QSettings(f"{self.caller_dir}{os.sep}params.conf", QSettings.IniFormat)
def save_state(self, win):
def memo_state():
self.settings.setValue('Geometrie', win.geometry())
self.settings.setValue('Etat', win.saveState())
self.o_dt.delay(memo_state, 100)
def restore_state(self, win):
""" Lecture et application de la géométrie à la fenêtre principale. """
win.setGeometry(self.settings.value('Geometrie', QRect(200, 200, 400, 200)))
""" Lecture et application de la géométrie aux dockables. """
q_state = self.settings.value('Etat')
if q_state is not None:
win.restoreState(q_state)
@staticmethod
def translate(app):
""" Exemple d'utilisation : ut.translate(app)
:param app: objet QApplication
:return: NA
"""
translator = QTranslator(app)
locale = QLocale.system().name().split('_')[0] # Paramètre de Windows. Exemple : 'fr'
path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
translator.load(f'{path}/qtbase_{locale}')
app.installTranslator(translator)
@staticmethod
def ui2py(action=None):
"""
Cette méthode est chargée de compiler tous les fichiers .ui et .qrc du dossier /design du code appelant.
- Le code appelant manipule des fenêtres ou des boîtes de dialogue créées par Qt Designer.
- Le code appelant est dans un dossier : /pc, /affichage, /tests, ... et d'autres à venir.
:param action: False, True ou None
- Si False, ne compile pas.
- Si True, compile systématiquement.
- si None (par défaut), compile si nécessaire (automatique).
|_ automatique = seuls les fichiers-source qui ont été modifiés.
:return: True si réussite, False si échec.
"""
if action is False:
return True
""" Création de listes des fichiers concernés. """
# client_dir = os.getcwd() # Le test_main.py ne passe plus : sa classe Main (qui hérite de Ui_Main)
client_dir = os.path.abspath(os.path.dirname(inspect.stack()[1][1])) # dossier du code client.
design_dir = os.path.abspath(f"{client_dir}{os.sep}design")
l_files_ui = list() # Liste des fichiers-source '.ui' (interfaces utilisateur).
l_files_rc = list() # Liste des fichiers-source '.qrc' (ressources).
l_targets = list() # Liste des fichiers-cible à obtenir '.py'.
for file_name in os.listdir(design_dir):
if file_name.lower().endswith('.ui'):
l_files_ui.append(file_name[:-3]) # Nom du fichier sans l'extension '.ui'
l_targets.append(f"{client_dir}{os.sep}{file_name[:-3]}.py") # chemin complet, avec l'extension '.py'
elif file_name.lower().endswith('.qrc'):
l_files_rc.append(file_name[:-4]) # Nom du fichier sans l'extension '.qrc'
l_targets.append(f"{client_dir}{os.sep}{file_name[:-4]}_rc.py") # complet, avec l'extension '.py'
settings = QSettings(f"{client_dir}{os.sep}params.conf", QSettings.IniFormat)
if action is True:
""" action == True => Compilation forcée (pas de filtrage). """
filtered_files_ui = l_files_ui
filtered_files_rc = l_files_rc
else:
""" Compilation automatique => Filtrage (seulement les fichiers modifiés).
On compare les date-heure (de modification) de chaque fichier à compiler avec celles qui ont été mémorisées.
Si égalité => compilation inutile.
"""
filtered_files_ui = list()
for file_ui in l_files_ui:
file_source = f"{design_dir}{os.sep}{file_ui}.ui"
file_target = f"{client_dir}{os.sep}{file_ui}.py"
dh_now = os.path.getmtime(file_source)
dh_memo = settings.value(f"dh_{file_source}", 0.)
if not os.path.isfile(file_target) or float(dh_now) != float(dh_memo):
filtered_files_ui.append(file_ui)
filtered_files_rc = list()
for file_rc in l_files_rc:
file_source = f"{design_dir}{os.sep}{file_rc}.qrc"
file_target = f"{client_dir}{os.sep}{file_rc}.py"
dh_now = os.path.getmtime(file_source)
dh_memo = settings.value(f"dh_{file_source}", 0.)
if not os.path.isfile(file_target) or float(dh_now) != float(dh_memo):
filtered_files_rc.append(file_rc)
""" Compilation """
scripts_path = f"{os.path.dirname(sys.executable)}{os.sep}Scripts{os.sep}"
pyuic_exe = f"{scripts_path}pyuic5.exe"
for file_ui in filtered_files_ui:
f_source = f"{design_dir}{os.sep}{file_ui}.ui"
f_target = f"{client_dir}{os.sep}{file_ui}.py"
if subprocess.run([pyuic_exe, f_source, '-o', f_target, '-x']).returncode != 0:
return False
settings.setValue(f"dh_{f_source}", os.path.getmtime(f_source))
pyrcc_exe = f"{scripts_path}pyrcc5.exe"
for file_rc in filtered_files_rc:
f_source = f"{design_dir}{os.sep}{file_rc}.qrc"
f_target = f"{client_dir}{os.sep}{file_rc}_rc.py"
if subprocess.run([pyrcc_exe, f_source, '-o', f_target]).returncode != 0:
return False
settings.setValue(f"dh_{f_source}", os.path.getmtime(f_source))
return True
main.py
→ Ajouter l'appel de la méthode. remplacer le code final de if __name__ == '__main__':
par celui-ci :
if __name__ == '__main__':
def start():
app = QApplication(sys.argv)
ut.translate(app)
fen = UiMain()
fen.show()
sys.exit(app.exec())
""" Affichage de la fenêtre. """
start()
Ok
est cliqué.Ui_Dialog_new
dans main.py
.NewGraph
au fichier main.py
.new_graph()
à la classe UiMain
.actionNew
('Nouveau graphe')main.py
devient :
from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog
from PyQt5.QtCore import QEvent
import sys
import os
from functions.utils import Utils
ut = Utils()
if ut.ui2py():
""" Si la compilation a réussi. """
from fen_mere import Ui_MainWindow
from dialog_new_graph import Ui_Dialog_new
else:
""" Si la compilation a échoué. """
raise SystemExit("La compilation des fichiers de Qt Designer a échoué.")
class UiMain(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
ut.restore_state(self)
""" Événements. """
self.dockNodes.installEventFilter(self)
self.dockParams.installEventFilter(self)
self.actionNodes.triggered.connect(self.visibility_nodes)
self.actionParams.triggered.connect(self.visibility_params)
self.actionNew.triggered.connect(self.new_graph)
""" Nouveau graphe. """
def new_graph(self):
""" - Affichage de la boîte de dialogue, centrée dans la fenêtre-parent.
- Saisie du nom du nouveau graphe.
- Clic sur 'Annuler' -> fermeture.
- Clic sur 'Ok' :
- Création d'un dossier
"""
NewGraph(self).exec()
""" Visibilité dockable des noeuds. """
def visibility_nodes(self):
""" Si visible => devient invisible et vice-versa. """
self.dockNodes.setVisible(not self.dockNodes.isVisible())
""" Visibilité dockable des paramètres. """
def visibility_params(self):
self.dockParams.setVisible(not self.dockParams.isVisible())
def eventFilter(self, obj, ev):
if ev.type() in [QEvent.Move, QEvent.Resize, QEvent.Hide, QEvent.Show]:
ut.save_state(self)
return super().eventFilter(obj, ev)
def moveEvent(self, ev):
""" Mémorisation de la géométrie. """
ut.save_state(self)
def resizeEvent(self, ev):
""" Mémorisation de la géométrie. """
ut.save_state(self)
class NewGraph(QDialog, Ui_Dialog_new):
""" Clic sur le bouton 'Annuler' : sortie sans exécuter de code.
Clic sur le bouton 'Ok' :
- Si le nom du nouveau graphe existe déjà, on ne crée rien, on ouvre le graphe existant.
- Sinon, on crée le graphe puis on l'ouvre.
"""
def __init__(self, o_main):
super().__init__(o_main) # Attribut o_main pour centrer la boîte de dialogue dans la fenêtre-parent.
self.o_main = o_main
self.setupUi(self)
if __name__ == '__main__':
def start():
app = QApplication(sys.argv)
ut.translate(app)
fen = UiMain()
fen.show()
sys.exit(app.exec())
""" Affichage de la fenêtre. """
start()
Nouveau graphe
et vérifier que le libellé des boutons est en français :Snippets
Bonjour les codeurs !