Structure d'un projet et du code avec PyQt

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour !

Il me faut créer un programme GUI sur une RPi pour effectuer de la modulation de dose de produits de traitement dans un verger. Le principe : à partir d'une carte et d'une position, déterminer la dose de produit à distribuer et la distribuer.

La carte sera grosso modo représentée par ce JSON :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "nom": "verger1",
    "rangs": [
        {
            "nom": "judeline",
            "zones": [
                {
                    "densite_de_floraison": 0.5
                },
                {
                    "densite_de_floraison": 0.8
                }
            ]
        }
    }
}

À partir de données externes, admettons que je parvienne à déterminer le rang et la zone devant laquelle se situe le tracteur : j'obtiens la dose de produit à distribuer. Bref, le programme consistera en l'affichage de la carte sous forme d'image, de diverses options - du genre ça -, en le positionnement du tracteur, en la lecture du JSON et en la commande des ports de la RPi. Et je me demandais comment l'organiser pour obtenir quelque chose de clair, propre, maintenable et extensible. Déjà, il me parait indispensable de séparer GUI et commande électrique.

Est-il judicieux de partir sur une QMainWindow ?

Sur la structure du projet, j'ai déniché cela.

Pour celle du code, j'avais déjà utilisé PyQt sur ce projet. J'ignore si c'est correct.

Merci !

Edit: je suis également tombé sur ça et ça.

Édité par Vayel

+0 -0
Auteur du sujet

Après quelques recherches, je pense adopter cette structure :

  • docs
  • projectname
    • controllers : classes héritant des vues et connectant des slots aux signaux
    • models : scripts interagissant avec les données - fichiers JSON, base de données…
    • resources
      • img
      • ui : fichiers générés par Qt Designer
      • css
    • tests : tests unitaires ?
    • views : widgets créés avec Qt Designer
    • main.py : initialise la QApplication
    • settings.py : constantes accessibles partout
  • README.md
  • TODO.md
  • LICENSE

La question est : est-il utile de générer des fichiers .py pour les vues via pyuic* ou bien est-il préférable d'employer setupUi() ?

Édité par Vayel

+0 -0
Staff

Cette réponse a aidé l'auteur du sujet

La question est : est-il utile de générer des fichiers .py pour les vues via pyuic* ou bien est-il préférable d'employer setupUi() ?

Ne t'embête pas à générer un py, sauf cas particulier ça n'apporte rien du tout. Donc autant pas t'embeter.

+0 -0
Auteur du sujet

Petite question supplémentaire : mes fonctions et variables globales - comme la loadUi d'ici -, faut-il les rassembler dans un même fichier à la racine - disons settings.py - ou bien faut-il les séparer selon le thème dans les __init__.py comme il est fait dans le lien précédent ?

Merci !

+0 -0
Auteur du sujet

Finalement, j'ai opté pour cette archi' :

  • README.md
  • TODO.md
  • LICENSE
  • projectname
    • config : fichiers .pro, .ts…
    • controllers : classes connectant des slots aux signaux des vues (entre autre)
    • models : classes permettant de représenter les données et d'interagir avec elles
    • resources : images, feuilles de style…
    • tests : tests unitaires
    • views : fichiers .ui
    • functions.py : fonctions globales
    • settings.py : constantes globales
    • main.py : point d'entrée de l'app (initialisation de la QApplication)

J'aurais par contre une question quant à la gestion des signaux. J'ai un controller nommé MainWindow qui hérite d'une QMainWindow et charge le fichier .ui associé. Dans un de ses docks, je place un autre widget, nommé OrchardOptions. Il s'agit en gros d'un formulaire. Comme précédemment, j'ai un controller au même nom qui charge le .ui correspondant.

Je me demandais alors où il fallait que je place mes slots - par exemple, quand un bouton de OrchardOptions est cliqué. Dans le fichier controllers/orchardoptions.py ou bien dans controllers/mainwindow.py puisque les actions sur le formulaire auront des répercussions dessus ?

En gros, faut-il faire ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# -*- coding: utf-8 -*-

from PyQt5 import QtWidgets

from functions import loadUi

class OrchardOptions(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(OrchardOptions, self).__init__(parent)
        loadUi(self)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# -*- coding: utf-8 -*-

from PyQt5 import QtWidgets

from functions import loadUi

from models.orchard import Orchard

from controllers.orchardoptions import OrchardOptions

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        loadUi(self)

        self.setCurrentOrchard(None)

        self.initControllers()
        self.connectActions()
        self.display()

    def initControllers(self):
        self._orchard_options = OrchardOptions(self) # Intégré dans un dock

    ###
    ## Display
    #
    def display(self):
        self.showMaximized()

    ###
    ## Actions
    #
    def connectActions(self):
        self._orchard_options.ui_create_row_btn.clicked.connect(some slot) # Par ici !

ou ça ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding: utf-8 -*-

from PyQt5 import QtCore, QtWidgets

from functions import loadUi

class OrchardOptions(QtWidgets.QWidget):
    create_row = QtCore.pyqtSignal()

    def __init__(self, parent=None):
        super(OrchardOptions, self).__init__(parent)
        loadUi(self)

        self.connectActions()

    def connectActions(self):
        self.ui_create_row_btn.clicked.connect(self.slot)

    def slot(self):
        # [Actions intermédiaires]
        self.create_row.emit([params])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# -*- coding: utf-8 -*-

from PyQt5 import QtWidgets

from functions import loadUi

from models.orchard import Orchard

from controllers.orchardoptions import OrchardOptions

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        loadUi(self)

        self.setCurrentOrchard(None)

        self.initControllers()
        self.connectActions()
        self.display()

    def initControllers(self):
        self._orchard_options = OrchardOptions(self) # Intégré dans un dock

    ###
    ## Display
    #
    def display(self):
        self.showMaximized()

    ###
    ## Actions
    #
    def connectActions(self):
        self._orchard_options.create_row.connect(some slot) # Par ici !

Avec la première méthode, je n'ai pas de couple signal/slot en plus mais avec la seconde, je peux faire des actions intermédiaires, comme récupérer le contenu d'un widget et le passer en argument au signal. Certes, je pourrais aussi le faire via self._orchard_options.ui_example_line_edit.text() mais ça permettrait de mettre tout ce qui concerne le widget OrchardOptions dans le même fichier et de ne communiquer qu'avec les signaux.

Merci !

Édité par Vayel

+0 -0

Cette réponse a aidé l'auteur du sujet

Il faut que la connexion des signaux et des slots se fassent au plus près des objets à connecter, quitte à renvoyer un autre signal. La seconde méthode est donc à privilégier.

Ça te permet, entre autre, de respecter la philosophie objet (un objet n'a pas à connaître les détails d'implémentation d'un autre) et d'obtenir un code plus souple (si tu décides un jour de ne plus utiliser un bouton mais une checkbox, tu n'auras pas à retoucher à ta mainwindow).

Shave the whales! | Thistle

+0 -0
Auteur du sujet

J'aurais une petite question quant à l'organisation du code que j'ai adoptée. J'ai cette succession d'inclusions :

  • MainWindow
    • OrchardOptionsTabs
      • OrchardOptionsTab
      • RowOptionsTab
    • OrchardTree

Quand je clique sur un QPushButton de OrchardOptionsTab - celui pour supprimer le verger courant par exemple -, j'envoie un signal à OrchardOptionsTabs avec le verger en paramètre et il est transmis à MainWindow, laquelle supprime le verger de la liste des vergers - _orchardGroup - et envoie un signal comme quoi le travail est fait, notamment pour avertir OrchardTree qu'il doit enlever le verger de son QTreeWidget.

Je voulais savoir s'il s'agissait de la bonne méthode.

Le dépôt est ici.

Merci !

+0 -0

De ce que je comprends, tu as une redondance inutile d'informations : tes vergers sont stockés à la fois dans _orchardGroup et dans le modèle de ton QTreeWidget. A ta place, je me contenterai uniquement du modèle (une modification du modèle entraîne la mise à jour des vues).

Shave the whales! | Thistle

+0 -0
Auteur du sujet

Mais dans le modèle, je ne peux stocker que du texte, non ? Pour l'instant il n'y a pas beaucoup d'attributs mais je vais devoir stocker des dimensions, des positions… pour chaque verger/rang… que je ne voudrai pas nécessairement afficher.

Merci. ^^

Édité par Vayel

+0 -0

Tu peux stocker n'importe quel type compatible avec QVariant (dont QString, QRect et QPoint), par défaut. Les item roles permettront de stocker et de gérer plusieurs données différentes pour un même item.

Shave the whales! | Thistle

+0 -0
Auteur du sujet

Pour l'instant, j'ai quelque chose de cet acabit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from PyQt5 import QtCore

class OrchardGroup(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)

        self._orchards = []

class Orchard(QtCore.QObject):
    def __init__(self, group, name):
        QtCore.QObject.__init__(self)

        self.setGroup(group)
        self.setName(name)
        self._rows = []

class Row(QtCore.QObject):
    def __init__(self, orchard, name, x, y, length):
        QtCore.QObject.__init__(self)

        self.setOrchard(orchard)
        self.setName(name)
        self.setX(x)
        self.setY(y)
        self.setLength(length)

Dans ma MainWindow, j'ai une instance de OrchardGroup. Au clique sur un bouton, j'en crée une de Orchard et dis au QTreeWidget de créer un nouvel item. Pareil avec les Row - en spécifiant l'item du verger parent.

Comment puis-je faire pour intégrer ça à un modèle ?

Désolé, je peine un peu. ^^'

+0 -0
Auteur du sujet

Ouais, pourquoi ? Comme les données sont séparées du reste, il n'y a pas besoin.

Edit : je pensais stocker les objets via le QtCore.Qt.UserRole mais ça revient au même que ce que j'ai actuellement. Faut-il que j'intègre tous les attributs de chaque objet dans mon modèle et que je filtre à l'affichage ?

Édité par Vayel

+0 -0

Tu peux stocker directement tes instances de Orchard dans ton modèle avec le rôle Qt.UserRole (ce rôle n'est pas utilisé par les vues et peut donc servir à stocker ce que tu veux). Tu n'auras donc plus besoin de OrchardGroup.

Shave the whales! | Thistle

+0 -0
Auteur du sujet

La représentation sous forme d'arbre semble la plus adaptée à mes données :

  • Verger 1
    • Rang 1
      • Côté 1
        • Zone horizontale 1
      • Côté 2
    • Rang 2
  • Verger 2

Du coup, j'ai cru comprendre qu'il fallait que j'hérite de QAbstractItemModel. De toute manière, ce n'est pas un QListModel, ni un QTableModel vu l'agencement de mes données.

+0 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte