PyQt5 QTableView mit Resulat aus QStandardItemModel aktualisieren

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo zusammen,

bin seit Kurzem von PyQt4 auf PyQt5 umgestiegen.
Meinen bisherigen PyQt4-Code habe ich auf PyQt5 versucht umzustellen.
Das hat mit Googeln, meist gut funktioniert.

Leider stehe ich vor dem Problem, Änderungen eines Datensatzes in QTableView zu aktualisieren.
Folgendes vorweg genommen.
Ich wähle einen Datensatz aus der Tabelle von QTableView aus, dieser wird dann in einem weiteren Fenster als Dataset geöffnet. Nach dem Schließen des Dataset-Fensters, sollen die Änderungen in der Tabelle von QTableView übernommen werden, was leider bei mir nicht funktioniert.
Deshalb, würde ich mich über Eure Hilfe freuen!

Ich poste hier mal den Code des Dataset-Fensters:

Code: Alles auswählen

import os
import sys
import operator  # used for sorting
from PyQt5.QtCore import (Qt, QSize, QEvent, QObject,
    QSortFilterProxyModel)
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import (QMainWindow, QMdiArea, QWidget, QMessageBox, 
    QTableView, QGridLayout, QApplication)

title = 'Backlight management'
header = ['Pos', 'Supplier', 'Artikel', 'Benennung']
mylist = [['001', 'Meyer', '47110', 'Bratwurst weiss, 125 g'],
['002', 'Meyer', '47111', 'Bratwurst weiss, 425 g'],
['003', 'Meyer', '47112', 'Bratwurst weiss, 1225 g']]
columnWidths = [20, 15, 20, 40]


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("MDI demo")
        self.mdi_area = QMdiArea()
        self.setCentralWidget(self.mdi_area)
        self.mylist = mylist
        self.index = 0
        sub = Dataset(self, mylist[self.index], header, columnWidths)
        sub.setWindowTitle(title)
        self.mdi_area.addSubWindow(sub)
        sub.show()

class Dataset(QWidget):

    def __init__(self, parent, dataset, header, columnWidths):
        super(Dataset, self).__init__()
        self.parent = parent
        self.old_dataset = self.dataset = dataset
        self.old_list = self.parent.mylist
        self.header = header
        try:
            self.width = max(columnWidths)
        except TypeError:
            self.width = columnWidths
        self.setObjectName('DATASET')
        self.installEventFilter(self)
        self.setFocusPolicy(Qt.StrongFocus)
        self.view = QTableView(self)
        # GridLayout
        grid = QGridLayout()
        grid.addWidget(self.view, 0, 0)
        self.setLayout(grid)
        self.model = QStandardItemModel()
        # Load dataset
        [self.model.invisibleRootItem().appendRow(
            QStandardItem(column)) for column in self.dataset]
        # Vertical header
        [self.model.setHeaderData(i, Qt.Vertical, column)
            for i, column in enumerate(header)]
        self.proxy = QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)
        self.view.setModel(self.proxy)

    def closeEvent(self, event):
        self.exec_()
        event.accept()

    def exec_(self):
        self.dataset = [self.model.invisibleRootItem().child(i).text()
            for i in range(len(self.header))]
        if self.dataset != self.old_dataset:
            r = MessageBox(QMessageBox.Yes, 'Änderung speichern?')
            if r.result() != QMessageBox.Yes:
                self.dataset = self.old_dataset
            else:
                self.parent.mylist[self.parent.index] = self.dataset
        print('self.dataset', self.dataset)
        return self.parent.mylist
 
class MessageBox(QMessageBox): 

    def __init__(self, buttonReply, text):
        QMessageBox.__init__(self)
        self.setText(text)
        if buttonReply == self.Ok:
            self.setIcon(self.Warning)
            self.setWindowTitle('Info')
            self.setStandardButtons(self.Ok)
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        elif buttonReply == self.Yes:
            self.setIcon(self.Warning)
            self.setWindowTitle('Question')
            self.setStandardButtons(self.Yes | self.No)
            default = self.No
        elif buttonReply == self.Cancel:
            self.setIcon(self.Question)
            self.setWindowTitle('Question')
            self.setStandardButtons(self.Yes | self.No | self.Cancel)
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        elif buttonReply == self.Save:
            self.setIcon(self.Question)
            self.setWindowTitle('Save and Exit')
            self.setStandardButtons(self.Save | self.Discard | self.Cancel)
            self.setInformativeText('Änderung speichern?')
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        self.setDefaultButton(default)
        self.exec_()

def main():
    app = QApplication(sys.argv)
    ex = MainWindow()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich sehe nicht, wo da eine Auswahl zur Bearbeitung uebernommen wird.

Die prinzipielle Loesung ist allerdings klar: du benutzt QStandardItemModel. Das enthaelst deine Daten, als Text. Alles was du tun musst ist das den geaenderten Text mit setText setzen. Hast du das schon probiert? Funktioniert super fuer mich.

Code: Alles auswählen

import sys
for p in [
        "/usr/local/Cellar/pyqt5/5.10.1_1/lib/python3.7/site-packages",
        "/usr/local/Cellar/sip/4.19.8_6/lib/python3.7/site-packages",
        ]:
    sys.path.append(p)

import os
import random
import sys
import operator  # used for sorting
from PyQt5.QtCore import (Qt, QSize, QEvent, QObject,
                          QSortFilterProxyModel, QTimer)
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import (QMainWindow, QMdiArea, QWidget, QMessageBox,
    QTableView, QGridLayout, QApplication)

title = 'Backlight management'
header = ['Pos', 'Supplier', 'Artikel', 'Benennung']
mylist = [['001', 'Meyer', '47110', 'Bratwurst weiss, 125 g'],
['002', 'Meyer', '47111', 'Bratwurst weiss, 425 g'],
['003', 'Meyer', '47112', 'Bratwurst weiss, 1225 g']]
columnWidths = [20, 15, 20, 40]


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("MDI demo")
        self.mdi_area = QMdiArea()
        self.setCentralWidget(self.mdi_area)
        self.mylist = mylist
        self.index = 0
        sub = Dataset(self, mylist[self.index], header, columnWidths)
        sub.setWindowTitle(title)
        self.mdi_area.addSubWindow(sub)
        sub.show()

class Dataset(QWidget):

    def __init__(self, parent, dataset, header, columnWidths):
        super(Dataset, self).__init__()
        self.parent = parent
        self.old_dataset = self.dataset = dataset
        self.old_list = self.parent.mylist
        self.header = header
        try:
            self.width = max(columnWidths)
        except TypeError:
            self.width = columnWidths
        self.setObjectName('DATASET')
        self.installEventFilter(self)
        self.setFocusPolicy(Qt.StrongFocus)
        self.view = QTableView(self)
        # GridLayout
        grid = QGridLayout()
        grid.addWidget(self.view, 0, 0)
        self.setLayout(grid)
        self.model = QStandardItemModel()
        # Load dataset
        [self.model.invisibleRootItem().appendRow(
            QStandardItem(column)) for column in self.dataset]
        # Vertical header
        [self.model.setHeaderData(i, Qt.Vertical, column)
            for i, column in enumerate(header)]
        self.proxy = QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)
        self.view.setModel(self.proxy)
        self.timer = QTimer()
        self.timer.timeout.connect(self._update_random_item)
        self.timer.start(3000)


    def _update_random_item(self):
        thing_to_change = self.model.item(
            random.randint(
                0,
                self.model.rowCount() - 1,
            )
        )
        thing_to_change.setText("foobar")


    def closeEvent(self, event):
        self.exec_()
        event.accept()

    def exec_(self):
        self.dataset = [self.model.invisibleRootItem().child(i).text()
            for i in range(len(self.header))]
        if self.dataset != self.old_dataset:
            r = MessageBox(QMessageBox.Yes, 'Änderung speichern?')
            if r.result() != QMessageBox.Yes:
                self.dataset = self.old_dataset
            else:
                self.parent.mylist[self.parent.index] = self.dataset
        print('self.dataset', self.dataset)
        return self.parent.mylist

class MessageBox(QMessageBox):

    def __init__(self, buttonReply, text):
        QMessageBox.__init__(self)
        self.setText(text)
        if buttonReply == self.Ok:
            self.setIcon(self.Warning)
            self.setWindowTitle('Info')
            self.setStandardButtons(self.Ok)
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        elif buttonReply == self.Yes:
            self.setIcon(self.Warning)
            self.setWindowTitle('Question')
            self.setStandardButtons(self.Yes | self.No)
            default = self.No
        elif buttonReply == self.Cancel:
            self.setIcon(self.Question)
            self.setWindowTitle('Question')
            self.setStandardButtons(self.Yes | self.No | self.Cancel)
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        elif buttonReply == self.Save:
            self.setIcon(self.Question)
            self.setWindowTitle('Save and Exit')
            self.setStandardButtons(self.Save | self.Discard | self.Cancel)
            self.setInformativeText('Änderung speichern?')
            self.setEscapeButton(self.Cancel)
            default = self.Cancel
        self.setDefaultButton(default)
        self.exec_()

def main():
    app = QApplication(sys.argv)
    ex = MainWindow()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo __deets__,

Danke für Deine schnelle Antwort, ich werde das ausprobieren.

Grüße Nobuddy
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Ist der von mir editierte Code richtig ?

Code: Alles auswählen

    def _update_random_item(self):
        for i in range(len(self.header)):
            thing_to_change = self.model.item(random.randint(i, i,))
            thing_to_change.setText(self.model.invisibleRootItem().child(i).text())
random.randint(i, self.model.rowCount() - 1,) habe ich durch random.randint(i, i,) ersetzt.
Bin mir aber nicht sicher, da ich nicht weiß, was der zweite Wert aussagt bzw. beeinflusst.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hast du mal nachgeschaut, was random.randint macht? Welchen Sinn koennte das denn haben, das DU das benutzt? Und wenn du ein Item an Stelle i rausholst aus dem Modell (wie ich das mit 'self.model.item(stelle_des_items)' mache), welchen Sinn kann das haben, sich ueber das root-Item an die genau gleiche Stelle zu hangeln, und dem gerade gewaehlten Item den Text zu setzten, den es *selbst* beinhaltet?

Es sieht desweiteren so aus, als ob du vertikale Eintraege missbrauchst, um einen Datensatz ("Dataset") zu modellieren. Nirgendwo sonst gibt es das, und das aus einem guten Grund: wenn man klassisch einen Datensatz pro Zeile hat, kann man eine klare Zuordnung machen, was alles dazu gehoert. Einfach in eine der Zellen einer Zeile klicken, und alle Nachbarn links und rechts gehoeren dazu.

Bei einer losen Liste von untereinander stehenden Eigenschaften zu wissen, wo da Anfang und Ende ist - das ist doch total muehselig. Sowohl zu benutzen, als auch zu programmieren. Warum benutzt du nicht einfach wie jeder andere verschiedene Spalten, mit Ueberschriften, um einen Datensatz darzustellen? Das Model beherrscht doch Spalten.

Ich glaube du verrennst dich hier gerade ziemlich dolle. Ich probiere mal ein anderes Beispiel zu bauen, kann aber etwas dauern.
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Ich habe darüber auch schon nachgedacht, dass ich die random-Methode hier falsch anwende.
Eigentlich gibt ja dies die geänderten Werte aus:

Code: Alles auswählen

dataset = [self.model.invisibleRootItem().child(i).text() for i in range(len(self.header))]
Das mit dem Dataset, wo die Columns untereinander, statt neben einander angebracht sind, hat folgenden Grund. Manche Listen, enthalten viele Spalten, darunter auch mit langen Texten.
Verwende ich den Datensatz dann horizontal statt vertikal, muss ich umständlich horizontal scrollen, dabei ist das Ganze wesentlich unübersichtlicher, als wenn ich das Ganze vertikal verwende.

Wenn ich Dich richtig verstanden habe, stellt dies ein Problem dar, das Tabellenfenster zu aktualisieren.

Mit PyQt4 hatte ich das Problem nicht, hoffe es findet sich für PyQt5 auch eine Lösung das umzusetzen.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du hast mich nicht richtig verstanden. Das hat auch ueberhaupt nichts mit Qt4 vs Qt5 zu tun. Sondern mit der Frage, wie deine Benutzeroberflaeche eben aussieht. Das sind DEINE Entscheidungen.

Dein Uebersichtlichkeitsargument ist natuerlich eine Geschmackssache. Bekannte Programme wie Excel und Co loesen dieses Problem, indem sie eine Zelle extra darstellen, wenn man sie betritt, oder ueber ihr schwebt mit dem Cursor, etc.

Eine weitere Moeglichkeit besteht darin, einen Tree-View zu verwenden. Denn das ist eigentlich die Art, wie man dein Problem loest: es gibt das root-Item, und N Items darunter, und jedes von diesen hat wiederum eine Liste von Kindern, die ein Schluessel/Wert-Paar bilden. Wichtig aber daran: fuer die Bearbeitung gibt es genau EIN Item. Das ist relevant fuer die weitere Programmierung.

Das man so wie du im Grunde eine Liste dazu missbraucht, eine Reihe von solchen Eintraegen darzustellen, inklusive dem Header der dann sehr, sehr lang wird, das wird halt problematisch. Das faengt damit an, dass du dann irgendwie Trenn-Zeilen einbauen musst. Denn sonst wird das ja total unuebersichtlich. Dann kann man darin auch nicht sortieren. Denn da muessen ja immer ganze Bloecke von Items bewegt werden, die zusammen gehoeren. Und dann muss man das zuordnen, etc etc.

Ich glaube wirklich, du solltest das lassen. Schau dir TreeView an. Damit modelliert sich das fuer dich deutlich leichter.
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Ich glaube Du mißverstehst mich, Meine Tabellen sind ganz normal aufgebaut.
Eine Datensatz ist eine Datenzeile. Das mit der vertikalen Ausgabe, betrifft nur meine Dataset-Ausgabe.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Aber dafuer missbrauchst du doch dann immer noch einen Listview. Wozu? Wenn das eine Bearbeitungsoberflaeche fuer nur EINEN Datensatz sein soll, dann bau dir die doch aus Laben und Texteingabefeldern (die ja auch mehrzeilig sein koennen, Formatierung haben, etc)?
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Daran habe ich jetzt noch nicht gedacht. Bin davon ausgegangen, dass es auch unter PyQt5 funktionieren sollte, wenn es unter PyQt4 funktionierte.
Hast Du evtl. ein Beispiel parat?
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hoer doch mal auf so zu tun, als ob unter Qt4 alles anders waere als unter Qt5. Das ist es nicht, und das hat mit dieser Frage doch ueberhaupt nichts zu tun.

Es geht doch nur um die Frage, welcher Art deine Daten sind, und wie die bearbeitet werden sollten. Wenn du ein Programm schreiben willst, dessen einzige Aufgabe es ist, den Namen deines einzigen Haustieres zu verwalten - dann baust du doch auch keinen riesen Apparat aus Listviews zusammen, um einen einzigen Text einzugeben. Du benutzt ein QTextEdit.

Und wenn deine Datensaetze immer gleich sind, und sogar mit speziellen Typen daherkommen und eigenen Regeln, wie Postleitzahlen oder Farbwerten oder so - dann baut man dafur ein Bearbeitungsformular, aus diskreten Felder.

So wie das hier: http://doc.qt.io/archives/qt-4.8/sql-forms.html

Nur zum suchen und filtern und auswaehlen benutzt man die Liste. So wie in dem Beispiel bei dem Link. Da kann man augenscheinlich sogar ganz viel automatisieren - damit habe ich noch nichtgearbeitet.

Der einzige sinnvolle Grund, so etwas wie du es hier vorschlaegst zu machen, waeren Daten, wo jeder Datensatz beliebige Felder enthalten kann. Und man sich darum eines ListViews als Kruecke bedient, so wie man in Excel ja auch alles in Zellen in einer Spalte kippen KANN.

Sind deine Daten so? Ich jedenfalls sehe immer nur Daten mit gleichen Feldern.
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Nein das mit PyQt4 vs. PyQt5, möchte ich damit nicht ausdrücken.
Ich bin ja froh, dass Du mir dazu geraten hast, auch wenn manches anderst ist, aber das lässt sich ja erarbeiten.

Meine Daten zur jeweiligen Liste, haben immer die gleichen Anzahl von Columns, wäre ja auch etwas schräg, das anderst handzuhaben.

Du kennst ja meinen ganzen zugehörigen Code, vom erstellen des Tabellenfenster bis zum erstellen des Datasets nicht. Das wäre zuviel Code und das wollte ich Dir ja auch nicht aufhalsen ....

Ich habe jetzt aber für mich die Lösung gefunden!

Code: Alles auswählen

    def exec_(self):
        self.dataset = [self.model.invisibleRootItem().child(i).text()
            for i in range(len(self.header))]
        if self.dataset != self.old_dataset:
            r = MessageBox(QMessageBox.Yes, 'Änderung speichern?')
            if r.result() != QMessageBox.Yes:
                self.dataset = self.old_dataset
            else:
		for i in range(len(self.header)):
		    text = self.model.invisibleRootItem().child(i).text()
		    self.parent.model.mylist[self.parent.index][i] = text
Die letzten 3 Zeilen, aktualisieren das Tabellen-Fenster.

Deinen Link, werde ich mir anschauen!
Antworten