QTableView Update

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

Hallo Zusammen,

Ich mache ein Update im model, aber die TableView bekommt das nicht mit (Zeile 116). Auch die Spaltenbreiten werden nicht gesetzt (Zeile 96). Weiß jemand Rat?

Code: Alles auswählen

#!/usr/bin/env python3

from sqlalchemy import INTEGER, TEXT, Column, create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

import sys

from PyQt6.QtCore import QAbstractTableModel, QSize, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView, QVBoxLayout, QWidget, QToolBar, QStatusBar
from PyQt6.QtGui import QAction, QIcon

Base = declarative_base()
Session = sessionmaker()

class Project(Base):
    __tablename__ = "project"

    project_id = Column(INTEGER, primary_key=True)
    project_name = Column(TEXT, nullable=False, unique=True)
    project_netplan = Column(TEXT, nullable=True)
    project_description = Column(TEXT, nullable=True)

class TableModel(QAbstractTableModel):
    def __init__(self, session):
        super().__init__()
        self.session = session
        self.column_labels = ['Projekt', 'Netzplan', 'Beschreibung']
        self.column_attributes = ['project_name', 'project_netplan', 'project_description']
        self.projects = self.session.query(Project).all()

    def headerData(self, section, orientation, role):
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return self.column_labels[section]
            elif orientation == Qt.Orientation.Vertical:
                return f'Zeile-{section}  '
            else:
                return False

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
            return getattr(self.projects[index.row()], self.column_attributes[index.column()])

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            setattr(self.projects[index.row()], self.column_attributes[index.column()], value)
            self.session.commit()

            # Test
            # for pq in self.session.query(Project).all():
            #     print(f'{pq.project_name}    {pq.project_netplan}    {pq.project_description}    ID = {pq.project_id}')

            return True
        return False

    def flags(self, index):
        return Qt.ItemFlag.NoItemFlags | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable

    def rowCount(self, _index):
        return len(self.projects)

    def columnCount(self, _index):
        return len(self.column_labels)

    def update(self):
        self.projects = self.session.query(Project).all()    

class MainWindow(QMainWindow):
    def __init__(self, session):
        super().__init__(windowTitle = "Projekt-Daten")
        self.resize(QSize(1000, 500))
        self.session = session

        self.toolbar = QToolBar("Toolbar Test")
        self.toolbar.setIconSize(QSize(16,16))
        self.addToolBar(self.toolbar)

        self.button_new = QAction(QIcon("icons/new.png"), "Neu", self)
        self.button_new.setStatusTip("Neues Projekt hinzufügen")
        self.button_new.triggered.connect(self.buttonNewClick)
        self.toolbar.addAction(self.button_new)

        self.button_delete = QAction(QIcon("icons/cross.png"), "Löschen", self)
        self.button_delete.setStatusTip("Markiertes Projekt Löschen")
        self.button_delete.triggered.connect(self.buttonDeleteClick)
        self.toolbar.addAction(self.button_delete)

        self.button_exit = QAction(QIcon("icons/arrow-skip.png"), "Exit", self)
        self.button_exit.setStatusTip("Projekt-Daten verlassen")
        self.button_exit.triggered.connect(self.buttonExitClick)
        self.toolbar.addAction(self.button_exit)

        self.setStatusBar(QStatusBar(self))

        self.table = QTableView()
        self.table.resizeColumnsToContents()    # <------------------ Keine Änderung

        self.model = TableModel(self.session)
        self.table.setModel(self.model)

        self.layout = QVBoxLayout()
        self.layout.addWidget(self.table)

        self.widget = QWidget()
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

    def buttonNewClick(self, s):   
        self.project = Project(
            project_name = f'---',
            project_netplan = f'----',
            project_description = f'-----'
        )
        self.session.add(self.project)
        self.session.commit()
        self.model.update()         # <------------------------------- Kein Update

        # Testdaten ausgeben
        for pq in self.session.query(Project).all():
            print(f'{pq.project_name}    {pq.project_netplan}    {pq.project_description}    ID = {pq.project_id}')


    def buttonDeleteClick(self, s):   
        print("-")

    def buttonExitClick(self, s):   
        print("--")

def main():
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    session = Session(bind=engine)

    # Testdaten erzeugen
    for i in range(12):
        project = Project(
            project_name = f'P-{i}-{i*10}',
            project_netplan = f'Netplan-{i / 5}',
            project_description = f'Description-{i * 22}'
        )
        session.add(project)

    # Eintragen in Datenbank
    session.commit()

    # Auslesen aller Datensätze aus Datenbank
    for pq in session.query(Project).all():
        print(f'{pq.project_name}    {pq.project_netplan}    {pq.project_description}    ID = {pq.project_id}')

    
    app = QApplication(sys.argv)
    window = MainWindow(session)
    window.show()
    app.exec()


if __name__ == "__main__":
    main()
Danke und Gruß
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ist gerade aus dem Kopf, aber nach meiner Erinnerung muss man Signale abschicken, die den View über beginn und Ende der Bearbeitung informieren.
Benutzeravatar
__blackjack__
User
Beiträge: 13937
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Also das `resizeColumnsToContents()` wird sicher korrekt ausgeführt. Zu dem Zeitpunkt gibt es halt keinen Inhalt. Es gibt ja noch nicht einmal ein Model. Was erwartest Du an der Stelle?
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

@__deets__: dazu müsste es einen slot geben zu refresh (o.ä.), beides findet sich nicht.

@__blackjack__: danke, sobald ich das hinter die model-Zuweisung setze, funktioniert es einmalig. Das Update hängt dann auch wieder mit dem signal/slot thema zusammen. Nur muss ich in dem Fall dann im model einen connect zu MainWindow herstellen. Stehe irgendwie auf dem Schlauch, überblicke die Zusammenhänge nicht. Muss mich wohl erst mal eine Weile mit den Eigenschaften von Qt beschäftigen...

Danke und Gruß
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wieso muss es einen slot geben? Die Signale reichen aus, der *view* (den du nicht geschrieben hast) verbindet sich (und damit auch irgendwelche internen slots) damit.

Ich hab's ja schon gesagt, die Qt-Dokumentation ist hier massgebend: https://doc.qt.io/qt-6/qabstractitemmod ... ubclassing

"""
The dataChanged() and headerDataChanged() signals must be emitted explicitly when reimplementing the setData() and setHeaderData() functions, respectively.
"""

Also viel klarer kann man das doch nicht schreiben.
Benutzeravatar
__blackjack__
User
Beiträge: 13937
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Nicht Slot sondern wenn dann Signal. Das Model muss ja signalisieren das sich etwas geändert hat. Und da fängt das Problem an, dass es nicht ein einfaches „es hat sich irgend was“ geändert gibt, sondern man da mindestens `beginModelReset()` und `endModelReset()` verwenden muss.

Bei der Abfrage der Daten fehlt eine explizite Sortierung. Kann sein, dass das im Moment in immer der gleichen Reihenfolge geliefert wird und dass das die ist, die Du haben möchtest, aber SQL garantiert da nichts solange nicht explizit ein ORDER BY … beziehungsweise hier `order_by(…)` angibt.

`Session.query()` ist übrigens veraltet, das sollte man nicht mehr verwenden. Das wird irgendwann verschwinden, falls das in SQLAlchemy 2 nicht bereits weg ist.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Donnerstag 31. August 2023, 15:14 @mechanicalStore: Nicht Slot sondern wenn dann Signal. Das Model muss ja signalisieren das sich etwas geändert hat.
Ich stehe komplett auf dem Schlauch. Das model ändert sich ja nicht von selbst, sondern es wird ja über die View (durch Editieren der Zellen) geändert. Dann müsste doch eher die View signalisieren?!
Und da fängt das Problem an, dass es nicht ein einfaches „es hat sich irgend was“ geändert gibt, sondern man da mindestens `beginModelReset()` und `endModelReset()` verwenden muss.
Werde mich mehr damit beschäftigen müssen, komme irgendwie nicht so richtig mit.

Trotzdem danke und Gruß...
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

https://doc.qt.io/qt-6/qabstractitemmodel.html#setData

"""
The dataChanged() signal should be emitted if the data was successfully set.

The base class implementation returns false. This function and data() must be reimplemented for editable models.
"""
Benutzeravatar
__blackjack__
User
Beiträge: 13937
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: ”Den” View? Dir ist schon klar das es da mehr als einen geben kann‽ Und nicht nur ein View kann Änderungen auslösen. Das Model muss die Views, oder auch jedweden anderen Listener informieren wenn es Änderungen gab. Wenn Du das Model selbst schreibst, bist Du auch dafür verantwortlich das die entsprechenden Signale ausgelöst werden. Die sind soweit ich das sehe alle private, also Du kannst sie nicht direkt aussenden, aber es gibt dafür vorgesehene Methoden. Die meisten davon senden den Indexbereich der sich geändert hat. Das heisst Du müsstest Dich intern um eine Index→Daten Zuordnung kümmern, oder aber die grobe Keule “Reset“ bemühen, also den Listenern sagen das *alles* was sie bisher glauben gewusst zu haben über das Model, nicht mehr gültig ist.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

Immer noch am intensiven arbeiten mit der QT Doku (um überhaupt Eure Antworten richtig zu interpretieren), habe ich trotzdem eine Zwischenfrage; dass ein Model der/den View(s) signalisieren muss, dass es sich geändert hat, ist schon klar. Solange das "im Hintergrund" geschieht (also nicht durch Benutzereingaben), ist das auch noch zu verstehen. Jedoch, wenn ich innerhalb einer TableView (oder sonstigem) im Editmodus eine Zelle ändere, dann wird doch das model erst aufgrund dessen geändert. Gleichzeitig soll das model aber die view informieren, dass es sich geändert hat. Mir kommt das wie eine Henne-Ei-Beziehung daher.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Es gibt eine Vielzahl von Möglichkeiten, sowas zu implementieren. Und augenscheinlich hat Qt eine gewählt, bei der das eben so ist. Muss dich das kümmern, wenn’s funktioniert?
Benutzeravatar
__blackjack__
User
Beiträge: 13937
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Noch mal: Es gibt nicht den *einen* View. Ein Model kann mit mehreren Views verbunden sein. Und wenn man über *einen* dieser Views die Daten im Model verändert, dann weiss dieser eine View das, aber woher sollen die anderen das wissen wenn sie nicht vom Model informiert werden‽

Simples Beispiel ein Tabellen-Model und ein View der die Tabelle als 2D-Tabelle mit bearbeitbaren Zellen zeigt, und einer, der die Daten grafisch als Diagramm anzeigt. Jetzt ändert der Benutzer eine Zahl in einer Zelle. Klar weiss *der* View dann, das sich da etwas geändert hat, aber wie sollte die Diagrammanzeige das mitbekommen wenn das Model nicht sagt es hat sich was geändert‽ Zudem kann sich durch eine Bearbeitung über einen View ja mehr im Model ändern als der View weiss. Zum Beispiel könnte das Model in der letzten Zeile eine Summe der jeweiligen Spalte aktualisieren wenn man den Inhalt einer Zelle ändert. Dann würde das Model die Views nicht nur über die Änderung in der manuell geänderten Zelle informieren, sondern auch über die daraus folgende Änderung in der Summenzeile/-zelle.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Es kann auch sein, dass man die Code-Pfade gleich halten will, und den Fall eigene Änderung und Fremdänderung nicht unterscheiden will. Für die Bearbeitung zb einer Tabelle kommt auch ein extra Widget zum Einsatz, der eigentliche View zur Darstellung ist ein Cell-View. Der ist nicht geändert worden. Und es kann auch sein, dass das Modell die Änderung ablehnt, aufgrund interner Invarianten.
Benutzeravatar
snafu
User
Beiträge: 6833
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Und außerdem sind es ja Signale, die ausgelöst werden. Wer darauf lauscht und dann evtl. eine Reaktion von welcher Art auch immer durchführt, sollte aus Sicht des Models völlig egal sein. Und genau diese Entkopplung sollte man sich auch bewusst machen. Das Model löst das Signal nicht aus, um auf komplizierte Weise den View zu informieren, sondern weil es die Qt-Schnittstelle so festgelegt hat, dass nach Abschluss einer Änderung das Signal getriggert werden muss.
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

Hallo Zusammen,

nachdem ich mich Qt nun etwas mehr angehähert habe, hoffe ich, zumindest diesen Teil einigermaßen verstanden zu haben. Wesentlich waren diese Worte von __deets__, die ich vorher falsch interpretiert hatte:
__deets__ hat geschrieben: Donnerstag 31. August 2023, 14:58 Wieso muss es einen slot geben? Die Signale reichen aus, der *view* (den du nicht geschrieben hast) verbindet sich (und damit auch irgendwelche internen slots) damit.
(da ich ständig nach einem entsprechenden Slot gesucht habe, ich dachte, es müsste in jedem Fall explizit ein

Code: Alles auswählen

.connect
aufgerufen werden. Die wesentliche Änderung ist "nur" diese (jedoch bin ich froh, die Komplexität dahinter ansatzweise verstanden zu haben):

Code: Alles auswählen

   def update(self):
        self.beginResetModel()
        self.projects = self.session.query(Project).all()
        self.endResetModel()
Dazu die erste Frage;

Code: Alles auswählen

beginResetModel() 
tut offenbar nicht mehr, als das Signal

Code: Alles auswählen

modelAboutToBeReset()
zu emitten (oder doch?). Könnte man hier ansonsten ersatzweise

Code: Alles auswählen

modelAboutToBeReset.emit()
mit entsprechenden Parametern aufrufen?

Zudem habe ich hier ebenfalls diese Funktionen eingebaut:

Code: Alles auswählen

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            self.beginResetModel()
            setattr(self.projects[index.row()], self.column_attributes[index.column()], value)
            self.session.commit()
            self.endResetModel()
            return True
Sobald ich dann aber in der Table von Hand was ändere, kommt folgende Meldung:
QAbstractItemView::closeEditor called with an editor that does not belong to this view
QAbstractItemView::commitData called with an editor that does not belong to this view
QAbstractItemView::closeEditor called with an editor that does not belong to this view
Was hat es damit auf sich (sieht aus, wie das Henne-Ei-Problem)? Da ich nur eine View habe, muss ich beginResetModel() an der Stelle nicht unbedingt aufrufen, aber wenn ich mehrere hätte, müsste ich das ja ?!

@__blackjack__: Du hattest erwähnt, dass session.query() deprecated ist. In https://docs.sqlalchemy.org/en/20/chang ... ew_20.html wir da zwar was erwähnt,
Legacy Query gains tuple typing as well.
aber es ist (für mich) nicht ersichtglich, durch was das ersetzt wird?!
Benutzeravatar
__blackjack__
User
Beiträge: 13937
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: `modelAboutToBeReset` kann man nicht selber aussenden weil das private ist. Darum gibt es ja die Methode.

`beginResetModel()`/`endResetModel()` ist die ganz grobe Keule wenn man nicht weiss, oder sich nicht darum kümmern will was genau sich geändert hat. Wenn Du das Datum an einem einzelnen Index veränderst, dann gibt es dafür andere Methoden bei denen man mitteilt, dass sich das Datum an diesem Index verändert hat.

Die Stelle in der Dokumentation hat was mit Typannotationen zu tun, die auch das veraltete `Query`-Objekt betreffen. Das die Migration von der `Session.query()`-Methode wird im Migrations-Kapitel beschrieben: https://docs.sqlalchemy.org/en/20/chang ... -orm-usage
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
mechanicalStore
User
Beiträge: 166
Registriert: Dienstag 29. Dezember 2009, 00:09

Vielen Dank an Alle für die hilfreichen Erklärungen.
__blackjack__ hat geschrieben: Montag 4. September 2023, 13:53 `beginResetModel()`/`endResetModel()` ist die ganz grobe Keule wenn man nicht weiss, oder sich nicht darum kümmern will was genau sich geändert hat.
So ist es. Bei ein paar Einträgen (ca. 40-50) macht das kaum Sinn, sich darüber Gedanken zu machen. In einem anderen Fall wird es dann mehr, da wäre es eine Überlegung wert. Dazu eine Frage; z.B. mit

Code: Alles auswählen

.beginInsertRows()
definiere ich doch "nur", dass ich das vorhabe, d.h. ich muss selbst dafür sorgen, dass die Änderung im model (was aus z.B. aus einer Liste besteht) haargenau dazu passt. Ist das so richtig, oder ganz anders gedacht?
Antworten