Structure d'un projet et du code avec PyQt

a marqué ce sujet comme résolu.

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.

+0 -0

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() ?

+0 -0

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.

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

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 !

+0 -0

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).

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).

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. ^^

+0 -0

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

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 ?

+0 -0

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
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

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