qsql tableview / qlsqlmodel mit checkboxen erweitern

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo,
vielleicht kann mir hier jemand weiterhelfen:
Ich habe einen qtableview, ein qsqldatamodel welche mit einer sqldatenbank (sqlite) verbunden sind. Die spalten werden eins zu eins abgebildet (total 5).

...

Code: Alles auswählen

self.db = get_database()
self.model = QSqlTableModel(self, self.db)
self.model.setTable("datatable")
self.model.select()

self.tableView.setModel(self.model)
Ich kann damit Zeilen loeschen und hinzufuegen.

Frage:
Wie kann ich in den tableView als letzte spalte nun eine Spalte mit Checkboxen einfuegen, damit sich hier einzelne Zeilen auswahleen lassen fuer weitere Operationen?

Einfach ein self.model.insertColumns(5,1) bringt nix.
Eine Idee, welche nicht funktionierte war:
Ein neues myQsqlTableModel(QSqlTableModel) erstellen/ableiten, dann in diesem Modell im __init__ ein self.insertColumns(5,1) fuer die neue Spalte durchfuehren, eine neue Methode data() schreiben
(aehnlich wie in http://www.mail-archive.com/pyqt@riverb ... 12970.html)


Oder woher weiss der view dass es eine neue spalte gibt, und wie sag ich dem Modell dass diese Spalte nichts mit der Datenbank dahinter zu tun hat? Oder wer weiss wo sowas steht?

Danke im Voraus fuer jeden Tip!
dirk
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Hi,

also eine Möglichkeit ist du leitest eine Klasse von QItemDelegate ab.
In der musst Du die Methode
createEditor(self, parent, option, index):
überschreiben in etwas so:

Code: Alles auswählen

def createEditor(self, parent, option, index):
        if index.column() == 0:
            spinbox = QSpinBox(parent)
            spinbox.setRange(0, 200000)
            spinbox.setSingleStep(1000)
            spinbox.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
            return spinbox
        elif index.column() == 1:
            combobox = QComboBox(parent)
            combobox.addItems(sorted(index.model().owners))
            combobox.setEditable(True)
            return combobox
        elif index.column() == 2:
            editor = QLineEdit(parent)
            self.connect(editor, SIGNAL("returnPressed()"),
                         self.commitAndCloseEditor)
            return editor
        elif index.column() == 3:
            editor = richtextlineedit.RichTextLineEdit(parent)
            self.connect(editor, SIGNAL("returnPressed()"),
                         self.commitAndCloseEditor)
            return editor
        else:
            return QItemDelegate.createEditor(self, parent, option,
                                              index)
Irgendwo musst Du halt Die QCheckBox zurückgeben.
Was nicht ganz schön ist an der Lösung ist das die CheckBox erst erscheint wenn Du das Feld editieren willst. Vermutlich kann man da aber auch noch was tun aber das weiß ich noch nicht. Ich steh gerade vor ähnlichen Problemen bin aber noch nicht soweit.
In der Klasse Itemdelegate sind noch mehr Methoden die überschrieben werden können:
Interessant ist die paint(self, painter, option, index) Methode und die
setEditorData(self, editor, index):

Code: Alles auswählen

 def setEditorData(self, editor, index):
        text = index.model().data(index, Qt.DisplayRole).toString()
        if index.column() == 0:
            value = text.replace(QRegExp("[., ]"), "").toInt()[0]
            editor.setValue(value)
        elif index.column() in (1, 2):
            i = editor.findText(text)
            if i == -1:
                i = 0
            editor.setCurrentIndex(i)
        elif index.column() == NAME:
            editor.setText(text)
        elif index.column() == 3:
            editor.setHtml(text)
        else:
            QItemDelegate.setEditorData(self, editor, index)
Die Beispiele sind aus dem PyQt-Buch Rapid GUI Programming von Mark Summerfiled
Hoffe sie helfen die weiter.

Gruß
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

http://www.qtrac.eu/pyqtbook.html
hier sind die Quelltexte von den Beispielen aus dem Buch zu haben.
Kaptitel 14.
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo ,
danke fuer die meldung! das buch hab ich vor mir liegen, aber es hilft mir an dieser stelle einfach nicht richtig weiter.
Ich habe das eaquivalente oder aehnlcihe (qt c++) probleme ungeloest auch noch hier
http://www.qtforum.org/article/26188/qs ... -icon.html
http://lists.trolltech.com/qt-interest/ ... 142-0.html
http://lists.trolltech.com/qt-interest/ ... 088-0.html
http://www.qtcentre.org/forum/archive/i ... -4147.html

gefunden. Model.setData()scheint dabei eine wichtige Rolle zu spielen.
Mir scheint es so, dass das ganze mit einem QTableModel einfacher funktionoiert als mit einem QSqlTableModel. Ich glaube aussderdem, dass das Modell der Knackpunkt an allem ist.
Das Delegate ist doch eher fuer die Darstellung eines items da, aber dazu muss es ja erstmal vorhanden sein.
Aber wie gesagt, alles ein "gefuehltes Halbwissen" :-)
Schoenen gruss
dirk
lunar

Der View nutzt Optionsfeld aka Checkboxen für ein bestimmtes Feld, wenn das Modell beim Aufruf von ".data(index, Qt.CheckStateRole)" keinen ungültigen QVariant bzw. None (bei Version 2 der QVariant API ab PyQt 4.6) zurück gibt, sondern einen Wert aus Qt::CheckState.

Da man aus SQL-Typen nicht sinnvoll ableiten kann, ob ein Optionsfeld verwendet werden soll, kann man das natürlich nicht generisch im QSqlTableModel implementieren. Ebensowenig kann das Modell erahnen, ob eine weitere Spalte mit reinen Optionsfeldern angehängt werden soll.

Allerdings lässt sich das Modell ja entsprechend nachrüsten, indem man einfach eine neue Klasse ableitet, und die benötigten Methoden überschreibt. Das könnte in etwa so aussehen:

Code: Alles auswählen

class OptionSqlTableModel(QSqlTableModel):
    def columnCount(self, index):
        count = QSqlTableModel.columnCount(self, index):
        return count+1

    def data(self, index, role=Qt.DisplayRole):
        if index.column() = self.columnCount() - 1:  # letzte Spalte
            if role == Qt.CheckStateRole:
                return QVariant(Qt.Checked)
            else:
                return QVariant()
        else:
             return SqlTableModel.data(index, role)
Das Beispiel ist unvollständig, denn es fehlen noch einige Methoden, die ebenfalls überschrieben werden müssen (e.g. setData(), headerData(), etc., siehe Qt-Dokumentation). Zudem ist es nicht getestet.

Ich denke aber, es zeigt die Idee.
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo,
vielen dank! das sieht schon mal so aehnlcih aus wie das was ich meinte!
die checkboxen sind zwar nicht am ende sondern am anfahng, aber immerhin sind sie da und auch eine zusaetzliche spalte (habe PyQt4.version 4.5.4 Qt.version 4.5.2)!
Mit folgendem code gehts!

Code: Alles auswählen

class OptionSqlTableModel(QSqlTableModel):
    def columnCount(self, index):
        count = QSqlTableModel.columnCount(self, index)
        return count+1


    def data(self, index, role=Qt.DisplayRole):
        if index.column() == self.columnCount(index) - 1:  # letzte Spalte
            if role == Qt.CheckStateRole:
                return QVariant(Qt.Checked)
            else:
                return QVariant()
        else:
            return QSqlTableModel.data(self, index, role)
Die anderem methoden muessen ueberschrieben werden.
Danke nochmals,
dirk
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo,
hier der letzte stand der dinge (funktioniert leider immer noch nicht). Die Checkboxen stehen da, wo sie sollen, lassen sich aber nicht anklicken/aendern.
Folgende Methoden hab ich ueberschrieben:

Code: Alles auswählen

    def headerData(self, section, orientation, role = Qt.DisplayRole):
        if section == CHECKSTAT and orientation == Qt.Horizontal:
            return QVariant("use this")
        else:
            return QSqlTableModel.headerData(self, section, orientation, role)

def flags(self, index):
        if not index.isValid():
            print "not valid!"
            return Qt.ItemIsEnabled
        return  Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsUserCheckable  | Qt.ItemIsSelectable

def setData(self, index, value, role = Qt.EditRole):
        if index.isValid():
            if (index.column() == CHECKSTAT  and role  == Qt.CheckStateRole):
                if value == Qt.Checked:
UND HIER MUESSTE WAS KOMMEN, zB QSqlTableModel.setData(index,  Qt.Unchecked)


                self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
            else:
                QSqlTableModel.setData(self, index, value, role)
            return True
        return False
komischerweise wird aber der zustand value ==Qt.Checked nie erreicht, auch wenn im Tableview alle Haken gesetzt erscheinen. Wenn ich auf einen Haken klicken, kann ich mir an dieser Stelle auch eine Textmeldung ausgeben lassen, nur aendert sich der zustand halt nicht.
Weiss jemand woran es nun hakt?
Vielen Dank dirk
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Hi,
ich bin gespannt wie es geht, da ich vor dem selben Problem stand.

Meine Lösung war dass ich eine Methode in setData aufrufe die dann mein Flag toggeld.

setData wird nur aufgerufen wenn in die CheckBox geklickt wird und bei leertaste wenn das Feld ausgewählt ist.
Ich habs beim besten Willen auch nicht hinbekommen an den Wert aus der schei.. CheckBox zukomme ;-)

Gruß
lunar

Lies die Dokumentation und denke ein bisschen mehr über die Datentypen nach, die Dein Programm verwendet!

"value" ist vom Typ "QVariant". Dieser Typ lässt sich nicht sinnvoll mit int vergleichen. Lies die Dokumentation zur Klasse QVariant, um herauszufinden, wie Du einen QVariant in eine Zahl umwandeln kannst.
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo,
QVariant value hat je nach zustand verschiedene Werte.

Code: Alles auswählen

print "CHECKSTAT:value int ", value.toInt(), "CHECKSTAT:value string: ",  value.toString(), CHECKSTAT:value bool  False value is  0
ergibt fuer haken gesetzt :
CHECKSTAT:value int (0, True) CHECKSTAT:value string: 0 CHECKSTAT:value bool False

und fuer haken nicht gesetzt
CHECKSTAT:value int (2, True) CHECKSTAT:value string: 2 CHECKSTAT:value bool True

Gehe ich also hinein via

Code: Alles auswählen

if value.toInt()[0]== 0:  # (is checked)
Nun wuerde ich hier den value (= QVariant)gerne veraendern, aber wie?
value.setValue(1) (laut doku http://qt.nokia.com/doc/4.5/qvariant.html) -> geht nicht,
('QVariant' object has no attribute 'setValue')
QSqlTableModel.setData(self, index, QVariant(Qt.Unchecked), role) -> passiert nichts.
Ist

Code: Alles auswählen

QSqlTableModel.setData(self, index, QVariant(Qt.Unchecked), role) 
, also weitereichen an die Basisklasse hier falsch?
Benoetige ich hier vielleicht einen custom delegate(glaube ich zwar nicht) ?
Oder muss das setzen ueber (pseudo)

Code: Alles auswählen

item = model.getItem(index)
item.setData()
bin fuer jeden tip dankbar!
gruss
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Naja jetzt musst Du halt zwischen deinem Eintrag in der DB und 0 und 2 übersetzten.
Was steht in der DB für Checked ?
Wenn der Wert -->
in data():
return QVariant(Qt.Checked)
else
return QVariant(Qt.Unchecked)

in Setdata()
if.value.toBool():
QSqlTableModel.setData(index, DEIN WERT DER IN DEINER DB FÜR CHECKED STEHT)
else:
QSqlTableModel.setData(index, DER WERT FÜR UNCHECKED)

Trotz der Lösung finde ich das sehr verwirrend wie das in Qt an der Stelle gelöst ist.

"Normales" True und False ist 1 bzw 0 und hier ist es halt 2 und 0


@Lunar
Danke für die Hinweise.
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

ja, gut- aber der witz an der ganzen sache ist ja, das der wert eben nicht in der Datenbank steht, sondern nur in dem tableview existiert (qsqlmodel wurde ja erweitert), und

Code: Alles auswählen

QSqlTableModel.setData(self, index, QVariant(Qt.Unchecked))

wird in der Ansicht nicht erneuert.
Entweder funktioniert dieses QSqlTableModel.setData() so nicht, oder der Tableview wird nicht richtig erneuert, obwohl

Code: Alles auswählen

self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index) 
unmittelbar folgt. Hat jemand noch eine Idee?
danke
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Also per Default soll der Wert auf Unchecked sein nehm ich an.
Dann wählt der User die Zeile in dem er die CheckBox checked. Richtig ?

Von irgendwo her muss dein Model ja die Daten bekommen und sie darin auch wieder speichern. Sonst seh ich da ein Henne-Ei Problem. Woher soll data() wissen welchen Zustand es anzeigen soll !?
Und wohin soll setData() sein neues wissen speichern wenn sich was ändert ?

Findige Idee wäre für sowas evt eine Variable :roll:
Eine die Zeilenindex und True/False speichert und das am End für jede Zeile. Gesetzen Fall Du möchtest mehrere Zeilen Wählen können.

self.RowSelectedOrNot = [[indexRow[0], True], [indexRow[1], False],[indexRow[2], True]]
oder so ähnlich.

Kein Plan ob das dein Problem erschlägt oder ich total auf em Holzweg bin. Kenn ja deinen genauen Zweck nicht. Aber ein Methode wie data() und setData() speichet keine Werte sondern holt sie und speichert sie von irgnedwoher nach irgendwohin.
Woher und Wohin ist teilweise deine Datenbank und teilweise eben ein Tupel oder Liste oder Variable oder oder oder.

Gruß
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

allmaehlich dämmert da was ... danke!
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

prinzipiell geht das so.
Du hast (brauchst) einen Datensatz (Egal ob DB oder woher auch immer)
Dieser wird vom Model verwaltet. D.h. er ließt daraus und schreibt darein.
Weiter übergibt das Model die Werte an die View(s) und nimmt sie von denen wieder zurück. Dazu stellt es sicher das sämtliche Views mitbekommen wenn sich was geändert hat. Aber ein Datenspeicher ansich ist das Model nicht.

Der/Das Delgate kommt ins Spiel wenn Du die in der View die Daten verändern willst in dem dier bsp-weise ein Editor bereitgestellt wird (QLineEdit oder QComboBox etc)
Weiter macht der noch das ein oder andere beim Darstellen der Daten, aber das ist im Kapitel 14 vom Buch ganz gut beschrieben.

Der Text ist o.Gewähr, so hab ich es bissher zumindest verstanden.

Gruß
zmanid
User
Beiträge: 11
Registriert: Donnerstag 23. Juli 2009, 14:18
Wohnort: Berlin

hallo,
nun gehts , es tut was es tun sollte.
hier noch mal die Zusammenfassung.

Ich habe eine Datenbank mit den Feldern

Code: Alles auswählen

datafields = ["id", "name", "height", "owner", "date"]
, id is der primaerschluessel und ein autoincrement.
Die Darstelllung und das Control erfolgt ueber einen QSqltableView.
dieser View soll aber eine Zusatzspalte Checkstat erhalten, ueber die sich einzelne zeilen auswaehlen (und ggf weiterverarbeiten) lassen.

Der Tableview is hat die Spaltenzuordnung
ID, NAME,HEIGHT, OWNER, DATE, CHECKSTAT = range(6)
is editable und nach Spalten sortierbar.

Der Basisdialog dazu enthaelt im init()

Code: Alles auswählen

...
self.model = OptionSqlTableModel(self, self.db)
self.model.setTable("datatable")
self.model.select()
self.model.initCheckstat()
self.tableView.setModel(self.model)
...
Das dazu noetige eigene OptionSqlTabelmodel sieht so aus:

Code: Alles auswählen

class OptionSqlTableModel(QSqlTableModel):
    def __init__(self,  parent = None , db = QSqlDatabase):
        print "OptionSqlTableModel::init()"
        super(OptionSqlTableModel, self).__init__(parent,db)
        self.checkstat = {}
und verwaltet den checkstat ueber ein Dictionary. Damit der checkstat auch bei allen sortierungen funktioniert, wird er ueber die ID der jeweiligen zeile "geschluesselt". Dazu gibts im Model die Funktion:

Code: Alles auswählen

def initCheckstat(self):
#in diesem dict wird der status der zeile gespeiert, im init ist die tablelle noch leer
    for row in range(self.rowCount(self)):
        index =  QSqlTableModel.index(self, row, ID )
        thisID = self.data(index).toString()
        self.checkstat[thisID] = QVariant(Qt.Checked)
Die data() Methode muss angepasst werden und fuer diese Spalte den Zustand aus dem checkstat-dictionary abholen:

Code: Alles auswählen

def data(self, index, role=Qt.DisplayRole):
    if index.column() == CHECKSTAT: 
        if role == Qt.CheckStateRole:
             thisrow = index.row()
             idIndex = QSqlTableModel.index(self, thisrow, ID)
             thisId = QSqlTableModel.data(self, idIndex).toString()
             return self.checkstat.get(thisId)
                
        else:
            return QSqlTableModel.data(self, index, role)
setData() braucht ebenfalls eine Spezialbehandlung fuer die checkstat spalte und sieht nun so aus:

Code: Alles auswählen

def setData(self, index, value, role = Qt.EditRole):
        if index.isValid():
            if (index.column() == CHECKSTAT  and role  == Qt.CheckStateRole):
                print "CHECKSTAT:value int ", value.toInt(), "CHECKSTAT:value string: ",  value.toString(),
                print "CHECKSTAT:value bool ", value.toBool()

                thisrow = index.row()
                idIndex = QSqlTableModel.index(self, thisrow, ID)   #index zur id-spalte
                thisId = QSqlTableModel.data(self, idIndex).toString()   #  gefundene ID

                if value.toInt()[0]== 0:  # (is checked)
                    print "value is  0"
                    self.checkstat[thisId] = QVariant(Qt.Unchecked)
                    self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)

                elif value.toInt()[0]== 2:  # (is unchecked)
                    self.checkstat[thisId] = QVariant(Qt.Checked)
                    self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)

            else:
                QSqlTableModel.setData(self, index, value, role)
            return True
        return False

Die Zusatzsspalte im tableview heisst "use this" kommt zustande ueber

Code: Alles auswählen

def headerData(self, section, orientation, role = Qt.DisplayRole):
      if section == CHECKSTAT and orientation == Qt.Horizontal:
            return QVariant("use this")
      else:
            return QSqlTableModel.headerData(self, section, orientation, role)
Das neue model will auch noch wissen wie die Dimensionen sind, dazu

Code: Alles auswählen

def columnCount(self, index):
        colcount = QSqlTableModel.columnCount(self)
        return colcount + 1
und

Code: Alles auswählen

def rowCount(self, index):
        rowcount = QSqlTableModel.rowCount(self)
        return rowcount
Vielen Dank fuer die Hilfe von ischisch und lunar!
gruss dirk
INFACT
User
Beiträge: 385
Registriert: Freitag 5. Dezember 2008, 16:08

Code: Alles auswählen

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import Qt

app = QApplication(sys.argv)

listWidget = QListWidget()

for i in xrange(10):
    item = QListWidgetItem("Checkbox")
    item.setCheckState(Qt.Checked)
    listWidget.addItem(item)
    
listWidget.show()
app.exec_()
print item.checkState() # die letzte checkbox!
[b][i]ein kleines game für die die lust haben http://konaminut.mybrute.com[/i][/b]
;-)
Antworten