IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Afficher le contenu d'un dossier

Pour faire suite au reader CSV, ce programme affiche le contenu d'un dossier.
On lui donne un dossier et il affiche dans une zone de droite tous les fichiers du dossier. Et si on sélectionne un fichier, il affiche son contenu dans une zone de gauche.

On pourra y trouver une évolution intéressante apporté par papajoker et basée sur une délégation de style qui permet à tout fichier non lisible pour une raison ou une autre (problème de droit par exemple) d'être marqué en rouge dans la zone de droite (zone de listing). Et si on y revient alors qu'il est redevenu lisible (problème résolu) il est réaffiché en noir.
Cet exemple est disponible dans les versions PyQt5, PyQt6 et PySide6.
Avatar de papajoker
Expert confirmé https://www.developpez.com
Le 18/02/2024 à 14:20
bonjour

Je n'aime pas l'utilisation systématique d'un attribut self.__ihm : code pas classique et énorme dépendance entre chaque objet :

Par exemple, pour moi, il est beaucoup plus intéressant d'avoir un widget (type myWork) réutilisable tel quel dans tous mes projets. Il suffit juste de supprimer cette dépendance "dure".
On utilise un code QT plus classique : c'est au "maitre" d'écouter le widget (si il le désire).

Code : Sélectionner tout
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
class myMainWindow(QMainWindow):
        def __init__(self, ihm, *args, **kwargs):
                # La zone qui va afficher le fichier
		self.__affich=myWork(parent=self.centralWidget())
		self.__affich.dossierChanged.connect(self.update_status_bar)
		# self.__affich.fichierChanged.connect(...

	def update_status_bar(self, text):
		self.statusBar().showMessage(text)

class myWork(QWidget):
	dossierChanged = pyqtSignal(object)
	fichierChanged = pyqtSignal(object)

        def __init__(self, *args, **kwargs):
             """" on ne passe plus le dico ihm """

        def getDir(self, dossier):
            ...   
                if not dossier.is_dir():
                        self.dossierChanged.emit(f"{dossier} n'est pas un dossier")
			#self.__ihm["mainWid"].statusBar().showMessage("{0} n'est pas dossier".format(dossier))
                ...
                self.dossierChanged.emit(f"Dossier: {dossier}")

        def __work(self, fic):
                ...
                self.fichierChanged.emit(f"Fichier: {fic}")
Pour myWork.__titre, Perso, je ne l'insère pas dans ce widget et émets aussi un signal (intercepté par myMainWindow qui a ça propre zone d'affichage), par conséquent, ce widget est encore plus réutilisable/souple.
Avatar de fred1599
Expert éminent https://www.developpez.com
Le 18/02/2024 à 11:36
Hello,

Merci pour le partage,

Tout fonctionne jusqu'au moment où je souhaite sélectionner un nouveau dossier,

Code : Sélectionner tout
1
2
3
Traceback (most recent call last):  File "/home/fred1599/.../.../test.py", line 296, in __slotLoadFile
    fic=current.data(Qt.ItemDataRole.UserRole)
AttributeError: 'NoneType' object has no attribute 'data'
Il faudrait aussi préciser que le module chardet doit être installé avant utilisation.

EDIT : J'utilise la version PyQt6
Avatar de papajoker
Expert confirmé https://www.developpez.com
Le 18/02/2024 à 15:40
Afficher en rouge les fichiers binaires

Il suffit de passer un paramètre au datas dans le widget : remplacer fichier par (fichier, status).
Ensuite, on utilise un QStyledItemDelegate que l'on attache à notre widget (valable pour tout type de widget) qui lui va interpréter ce nouveau paramètre.
Code : Sélectionner tout
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
class myWork(QWidget):
	# Chargement dossier
	def getDir(self, dossier):
		...
			item=QListWidgetItem(str(p), parent=self.__listFile)
			item.setData(Qt.ItemDataRole.UserRole, (p, 1))  # on passe un (des) parametre supplémentaire
			self.__listFile.addItem(item)

# si on ne désire pas attendre le clic utilisateur (plus logique?)
# on peut déjà passer un paramètre en fonction de ... (type fichier, dossier ou non, erreur, ....)

	# Slot demandant le chargement d'un fichier
	@pyqtSlot(QListWidgetItem, QListWidgetItem)
	def __slotLoadFile(self, current, prev):
		fic, status =current.data(Qt.ItemDataRole.UserRole) # changement, ici on récupère un tuple
		...
		except (PermissionError, UnicodeDecodeError) as e:
			current.setData(Qt.ItemDataRole.UserRole, (fic, -1))  # On change l'état : pour démo : -1 == erreur

	# Constructeur
	def __init__(self, *args, **kwargs):
		# La zone d'affichage des fichiers du dossier
		self.__listFile=QListWidget(parent=self)
		try:
			self.__listFile.setItemDelegate(FileDelegate(self.__listFile))   # on delège une partie de l'affichage
		except NameError:
			pass   # indépendance !!! c'est une option si on réutilise ce widget
La classe optionnelle qui va gérer une couleur de notre widget
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
class FileDelegate(QStyledItemDelegate):
	ERRORVALUE = QColor(240, 80, 60, 220)    # va être calculée qu'une fois
	def initStyleOption(self, option, index):
		if not index.isValid():
			return None
		super().initStyleOption(option, index)
		file, status = index.data(Qt.ItemDataRole.UserRole)
		if status < 0:
			option.palette.setBrush(QPalette.ColorRole.Text, self.ERRORVALUE)  # QPalette.ColorRole.Text ce n'est que pour qt6 ?
		# if file.isdir(): ... ATTENTION appel au "délégé" fait a chaque affichage, donc pas bon
Note: fichier original avec des tabulations et non espaces, résultat : pas top comme affichage dans ce forum
Avatar de fred1599
Expert éminent https://www.developpez.com
Le 18/02/2024 à 18:48
Citation Envoyé par Sve@r
Je le croyais installé par défaut ??? PS: je bosse sous Linux Xubuntu...
Moi aussi, mais je ne pense pas que le problème soit lié au système d'exploitation

lien PyPi chardet

Par contre je ne vois pas où tu l'utilises...

Citation Envoyé par Sve@r
Ce sera corrigé après la pub
C'est l'essentiel, la manière dont tu gères le bug te revient...


J'ai un peu plus de temps pour visionner ton code, quelques observations (ce qui est dit par @papajoker est raccord avec ce que je pense aussi).

Citation Envoyé par Sve@r
from PyQt6.QtCore import *
from PyQt6.QtGui import *

from PyQt6.QtWidgets import *
ça prend du temps, mais c'est essentiel, l'utilisateur s'en fou, moi, beaucoup moins en tant que développeur, j'aime savoir quels sont les widgets utilisés. Comme c'est dédié aux deux types sur ce forum, je trouve dommage de s'en priver...

Code : Sélectionner tout
self.__ihm=dict(ihm)
C'est inutile, lors de ton instanciation de myMainWindow dans myAppli, tu utilises en paramètre un dictionnaire écrit en brut...


Dans ton code j'ai trouvé difficile de faire le lien entre le nom de tes méthodes et le nom de chaque classe, il y a de l'anglais et du français, perso, la syntaxe devrait toujours être écrite en anglais et évidemment le texte se décide par le développeur selon le besoin de l'application (français ou international).

Je pense que tu as voulu raccourcir le code au maximum, je peux me tromper... (n'hésite pas à le dire) mais en ce qui me concerne, il serait bien plus long, car j'aurai préféré plus séparé les responsabilités et leurs actions. C'est un choix technique qui ne regarde que moi . Il n'y aurait pas qu'un seul module et ce nombre de classes.
Avatar de MPython Alaplancha
Membre expérimenté https://www.developpez.com
Le 19/02/2024 à 9:43
Bonjour,
Par curiosité j'ai voulu voir le code, mais bon lorsque j'essaie de télécharger :
Vous devez vous connecter pour accéder à cette partie du site
. Je suis pourtant bien connecté et avec noscript mis en veilleuse
Comme c'était juste pour voir je ne vais pas contacter un administrateur...
Ce message juste pour te remercier, car tu m'inspires l'envie de faire de même avec kivy.
Avatar de papajoker
Expert confirmé https://www.developpez.com
Le 19/02/2024 à 12:56
Ce sont 2 sites différents developpez.net et developpez.com, il suffit de se re-connecter avec les mêmes identifiants.

---------------------

Note :
Existe déjà le modèle QT QFileSystemModel qui permet l'affichage d'un répertoire dans un treeview, listview ou tableview.

Exemple (prévisualise fichiers texte et certaines images):

Code : Sélectionner tout
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env python

from pathlib import Path
import sys
try:
    from PySide6 import QtCore, QtGui, QtWidgets
except ImportError:
    print("ERREUR: installez pyside 6 !")
    exit(13)


class WinMain(QtWidgets.QMainWindow):
    """ Application simple """

    def __init__(self, directory):
        super(WinMain, self).__init__()

        self.model = QtWidgets.QFileSystemModel()
        self.model.setFilter(QtCore.QDir.Files | QtCore.QDir.NoDotAndDotDot)

        self.resize(600, 520)
        if layout := QtWidgets.QVBoxLayout():  # décalage juste pour mon visuel
            splitter = QtWidgets.QSplitter()
            tmp = QtWidgets.QHBoxLayout(splitter)
            self.content = QtWidgets.QLabel()
            self.content.setText("Voir image")
            self.content.setGeometry(QtCore.QRect(0, 0, 300, 300))
            tmp.addWidget(self.content)
            self.content.setMaximumWidth(600)
            self.list = QtWidgets.QListView(splitter)
            self.list.setModel(self.model)
            self.list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
            self.list.customContextMenuRequested.connect(self.onContextMenu)
            self.list.doubleClicked.connect(self.afficher)

            layout.addWidget(splitter)
            widget = QtWidgets.QWidget()
            widget.setLayout(layout)
            self.setCentralWidget(widget)
        self.statusBar()
        self.directory = directory

    @property
    def directory(self):
        return self._directory
    @directory.setter
    def directory(self, value):
        """on change de répertoire"""
        self._directory = value
        self.content.clear()
        self.content.setText("Voir image/fichier texte")
        self.content.setWordWrap(True)
        self.model.setRootPath(self.directory)
        self.list.setRootIndex(self.model.index(self.directory))
        self.setWindowTitle(self.directory)
        self.statusBar().showMessage(self.directory)
        print("root dir:", self.model.rootDirectory())

    def changeDir(self):
        choix=QtWidgets.QFileDialog.getExistingDirectory(
            self,
            "Choisissez votre dossier",
            self.directory,
            QtWidgets.QFileDialog.Option.ShowDirsOnly | QtWidgets.QFileDialog.Option.ReadOnly,
        )
        if choix:
            self.directory = choix

    def afficher(self, item):
        #file_ = self.model.filePath(item) # même chose que ligne suivante
        file_ =item.data(QtWidgets.QFileSystemModel.Roles.FilePathRole)
        print(file_)
        pix = QtGui.QPixmap(file_)
        # ("png", "jpeg", "jpg", "svg", "bmp", "gif", 'ico', "pdf")
        if not pix.isNull():
            self.content.setPixmap(pix)
            sizem = min(self.content.width(), self.content.height())
            size = QtCore.QSize(sizem, sizem)
            myScaledPixmap = pix.scaled(size, QtCore.Qt.KeepAspectRatio)
            self.content.setPixmap(myScaledPixmap)
        else:
            try:
                # que appercu car pas de scrolling
                self.content.setText(Path(file_).read_text()[0:412])
            except UnicodeDecodeError:
                self.content.setText("fichier incompatible  •`_´•")
                self.statusBar().showMessage("")
                return
        self.statusBar().showMessage(file_)

    def onContextMenu(self, point):
        menu = QtWidgets.QMenu()
        notUsed = menu.addAction('-')
        notUsed.setEnabled(False)
        menu.addSeparator()
        aInfo = menu.addAction("Changer de dossier...")
        aRefresh = menu.addAction("Actualiser")
        action = menu.exec(self.list.mapToGlobal(point))
        if action == aInfo:
            self.changeDir()
        if action == aRefresh:
            self.directory = self.directory


def run(directory):
    app = QtWidgets.QApplication(sys.argv)
    ico = QtGui.QIcon.fromTheme("folder")
    app.setWindowIcon(ico)

    trans = QtCore.QTranslator()
    trans.load('qt_fr', QtCore.QLibraryInfo.path(QtCore.QLibraryInfo.LibraryPath.TranslationsPath))
    QtCore.QCoreApplication.installTranslator(trans)    # pour avoir les dialogues en fr

    win = WinMain(directory)
    win.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    run(QtCore.QDir.currentPath())
Pour changer de répertoire : clic droit

pySyde/pyQt sont légèrement différents !
Par exemple, PyQt6.QtGui.QFileSystemModel, PySide6.QtWidgets.QFileSystemModel
Avatar de Sve@r
Expert éminent sénior https://www.developpez.com
Le 19/02/2024 à 14:25
Super extra, j'ai de quoi m'amuser durant quelques jours.
Maintenant quelques réponses plus précises...
Citation Envoyé par fred1599 Voir le message
Moi aussi, mais je ne pense pas que le problème soit lié au système d'exploitation... Par contre je ne vois pas où tu l'utilises... (à propos de chardet)
Zut je n'arrive pas à retrouver l'idée qui m'a fait penser que c'était installé d'office. Ceci dit effectivement il n'est pas utile. J'avais un moment pensé à afficher l'encoding du fichier traité (comme je fais pour le readerCSV) et là il aurait servi puis ai abandonné l'idée et ai oublié de l'enlever (history code)...

Citation Envoyé par fred1599 Voir le message
C'est inutile, lors de ton instanciation de myMainWindow dans myAppli, tu utilises en paramètre un dictionnaire écrit en brut...(à propos self.__ihm=dict(ihm))
En fait si la copie est nécessaire (enfin dans ma tête) car la fenêtre qui récupère ce dictionnaire modifie certaines clefs ("self" et "parent" notamment). Or il ne faut pas modifier le dico de l'appelant.
C'était le truc que j'avais trouvé à mes débuts pour que les widgets Qt puissent connaître leur hiérarchie (chaque widget connait ainsi son parent). Mais suite à vos très sympatiques remarques et conseils j'ai revu le concept (et ai corrigé).

Citation Envoyé par fred1599 Voir le message
ça prend du temps, mais c'est essentiel, l'utilisateur s'en fou, moi, beaucoup moins en tant que développeur, j'aime savoir quels sont les widgets utilisés. Comme c'est dédié aux deux types sur ce forum, je trouve dommage de s'en priver...
Là je pense que tu préfèrerais que je remplace from PyQt5.QtWidgets import * par from PyQt5.QtWidgets import QAppli, QMainWindow, QWidget, QMenuBar, QListWidget, QListWidgetItem, etc. c'est ça ?
Ca doit être énormément de boulot à faire et à maintenir (regarde ce pauvre chardet que j'ai même pas pensé à enlever). Est-ce vraiment nécessaire ? Par exemple pour les autres import tu le fais aussi ???

Citation Envoyé par fred1599 Voir le message
Dans ton code j'ai trouvé difficile de faire le lien entre le nom de tes méthodes et le nom de chaque classe, il y a de l'anglais et du français, perso, la syntaxe devrait toujours être écrite en anglais
Oui je suis d'accord. L'anglais (ex "open" est plus pratique que le français (ex "ouvre", "ouvrir" ???) mais bon il y a les bonnes résolutions et la motivation de s'y tenir...

Citation Envoyé par fred1599 Voir le message
je pense que tu as voulu raccourcir le code au maximum, je peux me tromper... (n'hésite pas à le dire) mais en ce qui me concerne, il serait bien plus long, car j'aurai préféré plus séparé les responsabilités et leurs actions. C'est un choix technique qui ne regarde que moi . Il n'y aurait pas qu'un seul module et ce nombre de classes.
Oui là aussi je suis d'accord. Dans un vrai projet chaque objet a son propre source. Mais là je n'avais pas envie de demander à télécharger un zip donc j'ai tout mis dans un seul code unique un peu long mais qui se télécharge en une fois. A regarder comme un "proof of concept" ou "comment utiliser telle ou telle techno" (à l'image de mes autres exemples plus simples).

Avatar de tyrtamos
Expert éminent https://www.developpez.com
Le 20/02/2024 à 5:24
Bonjour Sve@r

Curieusement, quand j'ai vu le titre de ton post, j'ai pensé que tu utilisais un QTreeView avec le modèle QFileSystemModel, et j'ai été surpris que ce ne soit pas le cas.

C'est pourtant ce qu'il y a de plus pratique. Voilà un petit code de principe en PyQt5:

Code : Sélectionner tout
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# -*- coding: utf-8 -*-

import sys
import os

from PyQt5.QtCore import QLocale, QTranslator, QLibraryInfo
from PyQt5.QtGui import QFont#, QIcon
from PyQt5.QtWidgets import (QApplication, QFileSystemModel, QTreeView,
                             QFrame, QGridLayout, QMainWindow, QStyleFactory)

##############################################################################
class NavigFichiers(QMainWindow):

    #=========================================================================
    def __init__(self, repertoire, parent=None):
        super().__init__(parent)

        self.resize(800, 600)
        self.setWindowTitle("Navigateur de fichiers")

        # crée le modèle
        model = QFileSystemModel()
        model.setRootPath(repertoire)

        #Crée le QTreeView et intégre le modèle
        self.view = QTreeView()
        self.view.setModel(model)
        self.view.setRootIndex(model.index(repertoire))

        # Police de caractères à utiliser
        font = QFont()
        font.setStyleHint(QFont.Monospace)
        self.view.setFont(font)

        # largeur de la colonne 0
        self.view.setColumnWidth(0, 350)

        # place le QTreeView dans la fenêtre
        self.setCentralWidget(QFrame())
        layout = QGridLayout()
        layout.addWidget(self.view, 0, 0)
        self.centralWidget().setLayout(layout)

        # Etablit le lien entre signal et méthode
        self.view.clicked.connect(self.clicligne)

        # initialise la ligne de status pour les messages
        self.statusbar = self.statusBar()
        self.statusbar.showMessage("")

    #=========================================================================
    def clicligne(self, qindex):
        """affiche en bas de page le chemin cliqué
        """
        chemin =  self.view.model().filePath(qindex)
        if os.path.isdir(chemin):
            message = "Répertoire: " + chemin
        else:
            message = "Fichier: " + chemin
        self.statusbar.showMessage(message)

##############################################################################
if __name__ == '__main__':

    #========================================================================
    # Répertoire d'exécution avec ou sans pyinstaller (onedir ou onefile)
    if getattr(sys, 'frozen', False):
        REPEXE = sys._MEIPASS # programme traité par pyinstaller
    else:
        REPEXE = os.path.dirname(os.path.abspath(__file__)) # prog. normal py

    #========================================================================
    # initialise la bibliothèque graphique
    app = QApplication(sys.argv)

    #========================================================================
    #met la même icône pour toutes les fenêtres du programme
    #app.setWindowIcon(QIcon(os.path.join(REPEXE, "navigfichiers.png")))

    #========================================================================
    # met les tooltips à fond jaune clair dans toute l'application
    #app.setStyleSheet("QToolTip {background-color: #ffff99; border: 1px solid black}")

    #========================================================================
    # style pour toute l'application
    if "Fusion" in [st for st in QStyleFactory.keys()]:
        app.setStyle(QStyleFactory.create("Fusion"))
    elif sys.platform=="win32":
        app.setStyle(QStyleFactory.create("WindowsVista"))
    elif sys.platform=="linux":
        app.setStyle(QStyleFactory.create("gtk"))
    elif sys.platform=="darwin":
        app.setStyle(QStyleFactory.create("macintosh"))
    # pour trouver les styles disponibles:
    # print([st for st in QStyleFactory.keys()])
    # Windows 11: ['WindowsVista', 'Windows', 'Fusion']

    app.setPalette(QApplication.style().standardPalette())

    #========================================================================
    # assure la traduction automatique du conversationnel à la locale
    locale = QLocale.system().name()
    translator = QTranslator()
    reptrad = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
    translator.load("qtbase_" + locale, reptrad) # qtbase_fr.qm
    app.installTranslator(translator)

    #========================================================================

    repertoire = "C:\\"

    navigfichiers = NavigFichiers(repertoire)
    navigfichiers.show()
    sys.exit(app.exec_())
La navigation est facile, et chaque clic sur une ligne, lance la méthode "clicligne" (j'ai été très inspiré pour son nom... ), qui se contente d'afficher le nom cliqué en bas de fenêtre avec son chemin. Mais cette méthode peut faire n'importe quoi d'autre, y compris afficher le contenu du fichier cliqué dans une autre fenêtre avec un QTextEdit si c'est un fichier texte, ou même lancer VLC si c'est une vidéo.
Avatar de fred1599
Expert éminent https://www.developpez.com
Le 23/02/2024 à 14:01
Hello,

Tant qu'on est à montrer certaines techniques, voici un exemple simple de ce à quoi peut ressembler la clean architecture avec ton projet.

Repo GitHub

L'objectif est que quelque soit le module que vous souhaitez utiliser pour votre interface graphique, vous ayez un minimum de changement ou ajout à faire. L'autre objectif est de ne rien supprimer et de rendre modulable graphiquement le projet.

Chaque interface graphique nouvellement créée est dépendante des méthodes obligatoires de la classe Abstraite UIPort.
On sépare la partie métier de l'interface, même si quelque fois cela peut paraître inutile, sa modification sera simple et sa recherche aussi (évite d'aller dans les méandres d'un code mélangé avec Qt par ex.)

L'ajout d'une interface tkinter peut se faire dans infrastructure/tkadapter.py avec stricto les mêmes méthodes d'affichage
Developpez.com décline toute responsabilité quant à l'utilisation des différents éléments téléchargés.