Automatische Layoutanpassung eines externen QTableWidget

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
McProgger
User
Beiträge: 9
Registriert: Mittwoch 30. September 2020, 14:13

Ich mal wieder :),

momentan wundere ich mich, warum sich MainWindow und Layout meiner GUI.py nicht automatisch anpassen, sobald ich ein QTableWidget aus einer externen Table.py-Datei einbinde. Erstelle ich die Tabelle direkt in der grafischen Oberfläche, wird auch das Layout angepasst. Habe ich hier etwas vergessen oder ist das ein Bug?

Zum Quellcode:
Table.py:

Code: Alles auswählen

import csv, sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QColor
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QApplication, QWidget, QHeaderView
from os.path import isfile

class Table(QWidget):
    def __init__(self, fileName):
        super(Table, self).__init__()

        self.lst = []
        self.tmp = []
        self.column = 0
        self.row = 0
        self.cnt = 1

        # Files exists?
        if isfile(fileName):
            # Get all columns
            self.column = csv.reader(open(fileName), delimiter=";").__next__().__len__() - 1 # The last sign is \n            

            for element in csv.reader(open(fileName, "r")):
                self.lst.append(element[0])
            
            # Get all rows
            self.row = self.lst.__len__()
        #

            self.header = self.lst[0].split(";")

        # Create the table
        self.table = QTableWidget(self.row, self.column, self)
        self.fontBold = QFont("Open Sans", 12)
        self.fontNormal = QFont("Open Sans", 12)

        self.fontBold.setBold(True)

        # Hide horizonal and vertical header
        self.table.horizontalHeader().hide()
        self.table.verticalHeader().hide()
        #

        if self.column == 5:
            self.head1 = QTableWidgetItem(self.header[0])
            self.head2 = QTableWidgetItem(self.header[1])
            self.head3 = QTableWidgetItem(self.header[2])
            self.head4 = QTableWidgetItem(self.header[3])
            self.head5 = QTableWidgetItem(self.header[4])

            self.head1.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head1.setBackground(QColor("#FFDC00"))
            self.head1.setForeground(QColor("Black"))
            self.head1.setFont(self.fontBold)
            self.head2.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head2.setBackground(QColor("#FFDC00"))
            self.head2.setForeground(QColor("Black"))
            self.head2.setFont(self.fontBold)
            self.head3.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head3.setBackground(QColor("#FFDC00"))
            self.head3.setForeground(QColor("Black"))
            self.head3.setFont(self.fontBold)
            self.head4.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head4.setBackground(QColor("#FFDC00"))
            self.head4.setForeground(QColor("Black"))
            self.head4.setFont(self.fontBold)
            self.head5.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head5.setBackground(QColor("#FFDC00"))
            self.head5.setForeground(QColor("Black"))
            self.head5.setFont(self.fontBold)
            self.table.setItem(0, 0, self.head1)
            self.table.setItem(0, 1, self.head2)
            self.table.setItem(0, 2, self.head3)
            self.table.setItem(0, 3, self.head4)
            self.table.setItem(0, 4, self.head5)
        elif self.column == 4:
            self.head1 = QTableWidgetItem(self.header[0])
            self.head2 = QTableWidgetItem(self.header[1])
            self.head3 = QTableWidgetItem(self.header[2])
            self.head4 = QTableWidgetItem(self.header[3])

            self.head1.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head1.setBackground(QColor("#FFDC00"))
            self.head1.setForeground(QColor("Black"))
            self.head1.setFont(self.fontBold)
            self.head2.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head2.setBackground(QColor("#FFDC00"))
            self.head2.setForeground(QColor("Black"))
            self.head2.setFont(self.fontBold)
            self.head3.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head3.setBackground(QColor("#FFDC00"))
            self.head3.setForeground(QColor("Black"))
            self.head3.setFont(self.fontBold)
            self.head4.setFlags(Qt.ItemFlag.ItemIsEditable)
            self.head4.setBackground(QColor("#FFDC00"))
            self.head4.setForeground(QColor("Black"))
            self.head4.setFont(self.fontBold)
            self.table.setItem(0, 0, self.head1)
            self.table.setItem(0, 1, self.head2)
            self.table.setItem(0, 2, self.head3)
            self.table.setItem(0, 3, self.head4)

        self.lst.pop(0) # Delete header

        # Insert content in the table
        for element in self.lst:
            self.tmp = element.split(";")

            if self.column == 4:
                self.content1 = QTableWidgetItem(self.tmp[0])
                self.content2 = QTableWidgetItem(self.tmp[1])
                self.content3 = QTableWidgetItem(self.tmp[2])
                self.content4 = QTableWidgetItem(self.tmp[3])

                self.content1.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content1.setForeground(QColor("Black"))
                self.content1.setFont(self.fontNormal)
                self.content2.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content2.setForeground(QColor("Black"))
                self.content2.setFont(self.fontNormal)
                self.content3.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content3.setForeground(QColor("Black"))
                self.content3.setFont(self.fontNormal)
                self.content4.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content4.setForeground(QColor("Black"))
                self.content4.setFont(self.fontNormal)
                self.table.setItem(self.cnt, 0, self.content1)
                self.table.setItem(self.cnt, 1, self.content2)
                self.table.setItem(self.cnt, 2, self.content3)
                self.table.setItem(self.cnt, 3, self.content4)
            elif self.column == 5:
                self.content1 = QTableWidgetItem(self.tmp[0])
                self.content2 = QTableWidgetItem(self.tmp[1])
                self.content3 = QTableWidgetItem(self.tmp[2])
                self.content4 = QTableWidgetItem(self.tmp[3])
                self.content5 = QTableWidgetItem(self.tmp[4])

                self.content1.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content1.setForeground(QColor("Black"))
                self.content1.setFont(self.fontNormal)
                self.content2.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content2.setForeground(QColor("Black"))
                self.content2.setFont(self.fontNormal)
                self.content3.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content3.setForeground(QColor("Black"))
                self.content3.setFont(self.fontNormal)
                self.content4.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content4.setForeground(QColor("Black"))
                self.content4.setFont(self.fontNormal)
                self.content5.setFlags(Qt.ItemFlag.ItemIsEditable)
                self.content5.setForeground(QColor("Black"))
                self.content5.setFont(self.fontNormal)
                self.table.setItem(self.cnt, 0, self.content1)
                self.table.setItem(self.cnt, 1, self.content2)
                self.table.setItem(self.cnt, 2, self.content3)
                self.table.setItem(self.cnt, 3, self.content4)
                self.table.setItem(self.cnt, 4, self.content5)

            self.tmp.clear()

            self.cnt += 1
        #

        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) # Auto resize table
GUI.py:

Code: Alles auswählen

import sys
from helper.miscellaneous import getPrinter
from PyQt6.QtGui import QIcon, QCursor
from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QMainWindow, QHBoxLayout, QVBoxLayout, QRadioButton, QGroupBox, QLabel, QComboBox, QCheckBox, QPushButton, QTableWidget
from PyQt6.QtCore import Qt
from helper.Table import Table

class MainWindow(QMainWindow):
    def __init__(self, title = ""):
        super(MainWindow, self).__init__()
        
        self.widget = QWidget()
        self.grpTable = QGroupBox()
        self.lytTable = QHBoxLayout()
        self.lytFrame = QVBoxLayout()
        self.tblDevices = Table("files/Tablets.csv")
        ...
        self.lytTable.addWidget(self.tblDevices)
        ...
        self.grpTable.setLayout(self.lytTable)
        ...
        self.lytFrame.addWidget(self.grpTable)
        self.widget.setLayout(self.lytFrame)
        self.setCentralWidget(self.widget) # Set the default layout
        ...
if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow("Test")

    # Only set the close button and a title bar icon
    win.setWindowFlags(Qt.WindowType(Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowCloseButtonHint))
    win.setWindowIcon(QIcon("Logo.ico"))
    #

    win.show() # Make the main window visible
    sys.exit(app.exec()) # Execute the event loop
Wo liegt mein Denkfehler?

LG McProgger
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ohne das geprueft zu haben: deine Table ist im QWidget nicht wiederum in einem Layout, das propagiert dann also nicht. Und das Vorgehen ist suboptimal: statt abzuleiten und die Items zu erstellen, benutz den Designer, und setz stattdessen ein sauber implementiertes Model. Das macht alles einfacher und schicker.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@McProgger: sys und QApplication werden importiert aber nicht benutzt.
Aus os.path importiert man nicht direkt. os.path ist auch veraltet, statt dessen benutzt man pathlib.Path.
Variablennamen schreibt man generell komplett klein. Man benutzt keine Abkürzungen, lst ist auch zu generisch und tmp für ein Attribut auch eher ungeeignet.
Wenn man Dateien öffnet, dann muß man sie auch wieder schließen. Zweimal die selbe Datei zu öffnen ist auch nicht so toll. Zumal Du ja wahrscheinlich den Header nicht in Deiner Liste haben möchtest.
Die __-Methoden benutzt man nicht direkt, statt dessen also len(next(...)).
Du benutzt einen csv-Reader mit einem falschen Delimiter um später Händisch die Spalten aufzuspalten??? Warum???
Wenn self.row immer die Länger der Liste ist, dann speichert man das nicht extra, weil man ja auch einfach über len(self.lst) drauf zugreifen kann.

Wenn man Namen durchnummeriert macht man etwas falsch, weil man eigentlich eine Liste benutzen möchte. Bei Dir kommt noch vielfach kopierter Code statt Schleifen und eine Fallunterscheidung die mit Schleifen nicht nötig wäre dazu.
Wenn man innerhalb einer for-Schleife etwas an Attribute bindet, ist das ein Zeichen dafür, dass das gar keine Attribute sind, weil man die ja bei jedem Schleifendurchgang überschreibt und letztlich gar nicht über das Ende der Methode hinaus braucht.
tmp.clear auf einer Liste aufzurufen, die man nicht mehr braucht, ist quatsch.

Das ganze sollte also ungefähr so aussehen:

Code: Alles auswählen

import csv
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QColor
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QWidget, QHeaderView
from pathlib import Path

class Table(QWidget):
    def __init__(self, filename):
        super(Table, self).__init__()

        filename = Path(filename)
        if filename.is_file():
            # Get all columns
            with filename.open(encoding="UTF8") as file:
                reader = csv.reader(file, delimiter=";")
                self.header = next(reader)
                self.rows = list(reader)
        else:
            self.header = []
            self.rows = []

        font_bold = QFont("Open Sans", 12)
        font_bold.setBold(True)
        font_normal = QFont("Open Sans", 12)
        foreground = QColor("Black")
        background_header = QColor("#FFDC00")

        self.table = QTableWidget(self.row, len(self.header), self)
        self.table.horizontalHeader().hide()
        self.table.verticalHeader().hide()

        for index, column_name in enumerate(self.header):
            header_item = QTableWidgetItem(column_name)
            header_item.setFlags(Qt.ItemFlag.ItemIsEditable)
            header_item.setBackground(background_header)
            header_item.setForeground(foreground)
            header_item.setFont(font_bold)
            self.table.setItem(0, index, header_item)

        for row_index, row in enumerate(self.rows, 1):
            for column_index, content in enumerate(row):
                item = QTableWidgetItem(content)
                item.setFlags(Qt.ItemFlag.ItemIsEditable)
                item.setForeground(foreground)
                item.setFont(font_normal)
                self.table.setItem(row_index, column_index, item)

        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
McProgger
User
Beiträge: 9
Registriert: Mittwoch 30. September 2020, 14:13

Hey ihr Beiden,

@Sirius3: Vielen Dank. Hat funktioniert. Du hast natürlich recht: Die Zeilen einzeln anzusprechen ist kontraproduktiv und sieht auch nicht sehr schick aus. Nach deiner Erklärung sind bei mir noch Fragen aufgekommen:

1. Weshalb ist os.path deprecated? In der Doku habe ich dazu nichts gefunden. Der einzige auszumachende Unterscheid für mich ist, dass pathlib objektorientiert, os.path aber prozedural funktioniert.
2. Ich komme aus der C/C++/C#-Ecke, weshalb ich die Camel-Schreibweise für Variablen verwende (gut, das war jetzt keine Frage)
3. Kannst du mir den Unterschied zwischen __next()__ und next() erklären? Warum werden Funktionen mit __ nicht direkt genutzt?

@__deets__: Ist die Erstellung mittels Qt Creator unproblematisch? Ich habe damit zuletzt vor 14 Jahren gearbeitet. Wie funktioniert das mit dem Einbinden in meine Skripte?
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich finde der Designer (heute wohl Teil des Creator) funktioniert super, und man laedt die .ui-Dateien einfach zur Laufzeit. Findest du hier im Forum haufenweise Beispiele, ein Anti-Pattern ist naemlich, den Code generieren zu lassen, und dann zu modifizieren. Womit man vor einem Scherbenhaufen steht, wenn man etwas am Layout aendert.

Die __-Funktionen sind dazu gedacht, Implementierungsdetails zu verstecken. Ausser __init__ im Fall von Ueberladungen ruft man die eigentlich nie direkt auf. Und auch so zu handhaben, und zB next oder len zu benutzen, macht den Code idiomatischer, lesbarer, und stellt sicher, dass die sich auch verhalten, wie es sein soll. Weil zB StopIteration geschmissen werden muss, etc.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@McProgger: Ad 1.: Naja genau deshalb. Die führen ja kein neues Modul ein für die gleiche(n) Aufgabe(n) damit dann die alten Module (das ist ja teilweise noch über mehr als nur `os` und `os.path` verstreut) weiter verwendet werden. Über die `pathlib`-API wurde nachgedacht wie man das sinnvoll, verständlich in einer objektorientierten Programmiersprache gestalten kann. Das ganze andere Zeugs ist in der Regel einfach nur eine ganz dünne Schicht über die gleichnamigen C-Bibliotheksfunktionen. Wenn Du aus der C++-Ecke kommst, sieh's so wie die Header aus der C-Standardbibliothek. Sind in C++ alle vorhanden, aber sollte man eher nicht verwenden wenn es auch einen C++-Weg dafür gibt.

Ad 3.: Die ”magischen” Methoden sind manchmal auch nicht eindeutig, sprich es kann mehrere geben die für ein Sprachfeature zuständig sind. Und das hat sich in der Vergangenheit auch mal geändert. Die offizielle API über dafür vorgesehenen Funktionen dagegen bleibt gleich. Beispiel für mehrere Methoden wäre `bool()` das nicht nur `__bool__()` verwendet, sondern gegebenenfalls auch `len()` (und damit `__len__()`) betrachtet für Objekte die keine `__bool__()`-Methode haben. Und umgekehrt ist halt nicht garantiert ob in künftigen Versionen mehr Varianten als die `__next__()`-Methode geben kann, die man berücksichtigen muss. Das ist dann aber Wissen was in so einer Version in der `next()`-Funktion stecken wird.

Guido hat diese Funktionen wie `len()` und `abs()` und so weiter als Operatoren gesehen, und die ”magischen” Methoden somit als Operatorüberladung. Man schreibt ja auch nicht ``a.__add__(b)`` statt ``a + b``, auch wenn man das natürlich machen könnte.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten