PyQt: checkbox delegate inside tableview - pyqt

I have been able to make my checkbox editable in my tableview.
They appear in my tableview but I can't do anything with them.
It's like the flags are not properly set up.
class CheckBox(QtGui.QStyledItemDelegate):
def __init__(self, parent = None):
QtGui.QGraphicsWidget.__init__(self)
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable | Qt.ItemIsEnabled
def paint (self, painter, option, index):
item = QtGui.QTableWidgetItem()
item.setFlags(QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
# picks up data from database
checked = index.model().data(index, Qt.EditRole).toBool()
# Then draw the checkbox
box = QtGui.QStyleOptionButton()
box.palette = option.palette
box.rect = option.rect
box.state = QtGui.QStyle.State_Enabled
if checked:
box.state |= QtGui.QStyle.State_On
else:
box.state |= QtGui.QStyle.State_Off
style = QtGui.QApplication.instance().style()
style.drawControl(QtGui.QStyle.CE_CheckBox, box, painter)
painter.restore()
Hi this is a reviewed code based on Avaris comments to use checkrole instead of delegates. But I still can fix the problem. I cannot tick on the checkboxes, instead it shows me a spinbox. it is reading the value (0 or 1) from the database correctly but I would like to hide this value. Any help would be appreciated!
class EditableShoSqlModel(QtSql.QSqlQueryModel):
def __init__(self, parent=None):
super(EditableShoSqlModel, self).__init__(parent)
def flags(self, index):
flags = super(EditableShoSqlModel, self).flags(index)
if index.column() in (12):
flags |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable | Qt.ItemIsEnabled
return flags
def data(self, index, role=QtCore.Qt.DisplayRole):
value2 = super(EditableShoSqlModel, self).data(index)
if index.column() == 12 and role == QtCore.Qt.CheckStateRole:
if value2 != 0:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
return QtSql.QSqlQueryModel.data(self, index, role)
def setData(self, index, value, role):
if index.column() not in (12):
return False
primaryKeyIndex = self.index(index.row(), 0)
id = self.data(primaryKeyIndex)
#self.clear()
if index.column() == 12 and role == QtCore.Qt.CheckStateRole:
value2 = super(EditableShoSqlModel, self).data(index)
if value2 != 0:
ok = self.setAniStatus(id, value)
return QtCore.Qt.Unchecked
else:
ok = self.setAniStatus(id, value)
return QtCore.Qt.Checked
return True
self.refresh()
return ok
def refresh(self):
MainWindow.AllShotsFromProject ()
def setAniStatus(self, id, Number):
mon = Number
if mon != False:
mon = 1
else:
mon = 0
print "ANIS"
query = QtSql.QSqlQuery()
sql = "UPDATE Shot SET Ani= '{0}' WHERE idShot = {1}".format(int(mon), id.toString())
query.prepare(sql)
print 'lastQuery:', query.lastQuery()
return query.exec_()

if index.column() not in (12): will not work. (12) is same as 12 and you can't do foo in 12. That's a TypeError. You can do foo in (12,) but there is no need. That's simple foo == 12.
setData should return True or False that indicates success, but you are returning QtCore.Qt.Unchecked and QtCore.Qt.Checked. That won't fit well.
If you don't want to show 0/1, you should do that in data by simply returning None for DisplayRole.
This should do what you want:
class EditableShoSqlModel(QtSql.QSqlQueryModel):
def __init__(self, parent=None):
super(EditableShoSqlModel, self).__init__(parent)
def flags(self, index):
flags = super(EditableShoSqlModel, self).flags(index)
if index.column() == 12:
flags |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled
return flags
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.column() == 12:
value = super(EditableShoSqlModel, self).data(index, QtCore.Qt.DisplayRole)
if role == QtCore.Qt.CheckStateRole:
if value != 0:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
else:
return None
return super(EditableShoSqlModel, self).data(index, role)
def setData(self, index, value, role):
if index.column() == 12 and role == QtCore.Qt.CheckStateRole:
# `id` is a built-in function
id_ = self.data(self.index(index.row(), 0)).toString()
state, ok = value.toInt()
if state == QtCore.Qt.Checked:
value = 1
else:
value = 0
result = self.setAniStatus(id_, value)
if result:
# we modified the db, we need to query again
self.refresh()
return result
# I don't know what `refresh` does. But normally you don't need that
return False
def refresh(self):
# something that re-queries the model.
#self.setQuery('SELECT * FROM Shot')
def setAniStatus(self, id_, value):
query = QtSql.QSqlQuery()
sql = "UPDATE Shot SET Ani= {0} WHERE idShot = {1}".format(value, id_)
query.prepare(sql)
print 'lastQuery:', query.lastQuery()
return query.exec_()

Related

PyQt5 dragging and dropping in QTableview causes selected row to disappear

Following on from a previous question here, I looked to add delete row on key press functionality to my qtableview table in PyQt5 by adding the removeRows function to my model. However, since adding this function it has disrupted my drag and drop functionality, whereby the dragged row disappears when dropping elsewhere in the qtableview table.
Is there anyway I can prevent the dragged row from disappearing?
NB: Interestingly, when selecting the vertical header 'column' the drag/drop functionality works, but I'm keen to find a solution for dragging and dropping on row selection.
Here's my code below with the added removeRows function in the model, and also the keyPressEvent in the view
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
else:
return ''
return None
def removeRows(self, position, rows, QModelIndex):
self.beginRemoveRows(QModelIndex, position, position + rows - 1)
for i in range(rows):
del (self._data[position])
self.endRemoveRows()
self.layoutChanged.emit()
return True
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | Qt.CopyAction
class myTableView(QTableView):
def __init__(self, parent):
super().__init__(parent)
header = self.verticalHeader()
header.setSectionsMovable(True)
header.setSectionResizeMode(QHeaderView.Fixed)
header.setFixedWidth(10)
QShortcut('F7', self, self.getLogicalRows)
QShortcut('F6', self, self.toggleVerticalHeader)
QShortcut('Alt+Up', self, lambda: self.moveRow(True))
QShortcut('Alt+Down', self, lambda: self.moveRow(False))
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
to_index = self.indexAt(event.pos()).row()
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
header = self.verticalHeader()
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
super().dropEvent(event)
def toggleVerticalHeader(self):
self.verticalHeader().setHidden(self.verticalHeader().isVisible())
def moveRow(self, up=True):
selection = self.selectedIndexes()
if selection:
header = self.verticalHeader()
row = header.visualIndex(selection[0].row())
if up and row > 0:
header.moveSection(row, row - 1)
elif not up and row < header.count() - 1:
header.moveSection(row, row + 1)
def getLogicalRows(self):
header = self.verticalHeader()
for vrow in range(header.count()):
lrow = header.logicalIndex(vrow)
index = self.model().index(lrow, 0)
print(index.data())
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
index = self.currentIndex()
try:
self.model().removeRows(index.row(), 1, index)
except IndexError:
pass
else:
super().keyPressEvent(event)
class sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
if __name__ == '__main__':
app = QApplication(['Test'])
test = sample_data()
test.setGeometry(600, 100, 350, 185)
test.show()
app.exec_()
The main "problem" is that, by default, removeRows of QAbstractItemModel doesn't do anything (and returns False).
The technical problem is a bit more subtle.
A drag operation in an item view always begins with startDrag(), which creates a QDrag object and calls its exec(). When the user drops the data, that implementation also calls a private clearOrRemove function whenever the accepted drop action is MoveAction, which eventually overwrites the data or removes the row(s).
You've used setDragDropOverwriteMode(False), so it will call removeRows. Your previous code used to work because, as said, the default implementation does nothing, but now you've reimplemented it, and it actually deletes rows in that case.
The solution is to change the drop action whenever the drop event is a move (which is a bit unintuitive, but since the operation has been already performed, that shouldn't be an issue). Using IgnoreAction will avoid the unwanted behavior, as that clearOrRemove won't be called anymore in that case:
def dropEvent(self, event):
# ...
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
# ...
header.moveSection(from_index, to_index)
event.accept()
event.setDropAction(Qt.IgnoreAction)
super().dropEvent(event)
UPDATE
When the items don't fill the whole viewport, drop events can occur outside the items range, resulting in an invalid QModelIndex from indexAt() not only when dropping beyond the last row, but also the last column.
Since you're only interested in vertical movement, the solution is to get the to_index from the vertical header, and eventually set it to the last row whenever it's still invalid (-1):
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
globalPos = self.viewport().mapToGlobal(event.pos())
header = self.verticalHeader()
to_index = header.logicalIndexAt(header.mapFromGlobal(globalPos).y())
if to_index < 0:
to_index = header.logicalIndex(self.model().rowCount() - 1)
if from_index != to_index:
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
event.setDropAction(Qt.IgnoreAction)
super().dropEvent(event)

PyQt5, Can't use mouse wheel to scroll within a QTableView displaying a pandas dataframe

I use a QtableView similiar to this one. Everything works fine.
My problem is that, that I can't scroll with the mousewheel. Also scrolling with pressing left mouse button and move the scrollbar don't work.
I can only use the arrows to scroll up and down.
Here is the code:
class PandasModel(QAbstractTableModel):
def __init__(self, data, parent=None):
"""
:param data: a pandas dataframe
:param parent:
"""
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
current_column = index.column()
current_row = index.row()
gold_color = QColor(235, 201, 52)
lightred_color = QColor('#FF5858')
knallrot = QColor('#FF0000')
shine_yellow = QColor('#FFFF00')
greeny = QColor(92, 235, 52)
white_color = QColor(QtCore.Qt.white)
black_color = QColor(QtCore.Qt.black)
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
if role == Qt.BackgroundColorRole: # The role for backgrounbd color of a cell
if current_column == 7:
it = self._data.iloc[index.row(), current_column] # Finds the specific data (second column) to test and assigns it to the variable "it"
if 20 > it > 10:
return QBrush(gold_color)
if it >= 20:
return QBrush(shine_yellow)
if role == Qt.BackgroundColorRole: # The role for backgrounbd color of a cell
if current_column == 5:
it = self._data.iloc[index.row(), current_column] # Finds the specific data (second column) to test and assigns it to the variable "it"
if it > 100000:
return QBrush(greeny)
if 10000 > it >= 1000:
return QBrush(lightred_color)
if it < 1000:
return QBrush(knallrot)
if current_column == 2:
it = self._data.iloc[index.row(), current_column] # Finds the specific data (second column) to test and assigns it to the variable "it"
if it > 100000:
return QBrush(greeny)
if 10000 > it >= 1000:
return QBrush(lightred_color)
if it < 1000:
return QBrush(knallrot)
if role == Qt.TextColorRole:
return QColor(39, 68, 209)
if role == Qt.BackgroundColorRole and index == 7:
return QColor(235, 201, 52)
if role == Qt.FontRole and index == 7:
return QFont("Helvetica", 12, QFont.Bold, );
return None
def headerData(self, rowcol, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[rowcol]
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return self._data.index[rowcol]
if role == Qt.BackgroundColorRole and rowcol == 7:
return QColor(235, 201, 52) #gold
elif role == Qt.BackgroundColorRole and rowcol == 6:
return QColor(235, 67, 52) #red
elif role == Qt.BackgroundColorRole and rowcol == 5:
return QColor(235, 67, 52) #red
elif role == Qt.BackgroundColorRole and rowcol == 4:
return QColor(235, 67, 52) #red
elif role == Qt.BackgroundColorRole and rowcol == 3:
return QColor(92, 235, 52) #green
elif role == Qt.BackgroundColorRole and rowcol == 2:
return QColor(92, 235, 52) #green
elif role == Qt.BackgroundColorRole and rowcol == 1:
return QColor(92, 235, 52) #green
return None
def flags(self, index):
flags = super(self.__class__, self).flags(index)
#flags |= QtCore.Qt.ItemIsEditable
flags |= QtCore.Qt.ItemIsSelectable
flags |= QtCore.Qt.ItemIsEnabled
flags |= QtCore.Qt.ItemIsDragEnabled
flags |= QtCore.Qt.ItemIsDropEnabled
return flags
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
try:
self.layoutAboutToBeChanged.emit()
self._data = self._data.sort_values(self._data.columns[Ncol], ascending=not order)
self.layoutChanged.emit()
except Exception as e:
print(e)
From the main.py it's called like this:
self.tabCrawledresult = QTabWidget()
self.tabCrawledresult.layout = QGridLayout()
self.tabCrawledresult.setLayout(self.tabCrawledresult.layout)
self.tabs.addTab(self.tabCrawledresult, "Results")
self.view_minmax = QTableView(self.tabCrawledresult)
self.modelminmax = gui_pandasModel_sort.PandasModel(df)
self.view_minmax.setModel(self.modelminmax)
self.view_minmax.resize(1000,500) # is it possible to fit the size to width of headers?
self.view_minmax.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.view_minmax.setSortingEnabled(True)
self.view_minmax.sortByColumn(7, Qt.DescendingOrder)
self.view_minmax.setAlternatingRowColors(True)
self.view_minmax.setStyleSheet("alternate-background-color: rgb(209, 209, 209)"
"; background-color: rgb(244, 244, 244);")
self.view_minmax.show()
And how is it possible to fit the size of the QTableView to the width of headers?
First. The Class PandasModel is correct and make no troubles. What I have done in the main.py was wrong. I display the TableView in a QTabWidget and want to have an export button in the bottom of it. So my ugly solution is, to put a "layer" of empty QLabelWidgets on the Tab put then the TableView over it.
Here is my complete example with some Data.
import sys
from PyQt5 import QtCore
from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget, QFileDialog,
QGridLayout, QPushButton, QTableView, QLabel, QHeaderView
import numpy as np
import pandas as pd
from functools import partial
def createDF():
df = pd.DataFrame(np.random.randint(0,11,size=(50, 5)), columns=list(['A','B','C',
'D','Tree']))
print(df)
return df
trees = { 1: 'Arborvitae (Thuja occidentalis)',
2: 'Black Ash (Fraxinus nigra)',
3: 'White Ash (Fraxinus americana)',
4: 'Bigtooth Aspen (Populus grandidentata)',
5: 'Quaking Aspen (Populus tremuloides)',
6: 'Basswood (Tilia americana)',
7: 'American Beech (Fagus grandifolia)',
8: 'Black Birch (Betula lenta)',
9: 'Gray Birch (Betula populifolia)',
10: 'Paper Birch (Betula papyrifera)'}
class PandasModel(QAbstractTableModel):
def __init__(self, data, parent=None):
"""
:param data: a pandas dataframe
:param parent:
"""
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, rowcol, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[rowcol]
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return self._data.index[rowcol]
def flags(self, index):
flags = super(self.__class__, self).flags(index)
#flags |= QtCore.Qt.ItemIsEditable
flags |= QtCore.Qt.ItemIsSelectable
flags |= QtCore.Qt.ItemIsEnabled
flags |= QtCore.Qt.ItemIsDragEnabled
flags |= QtCore.Qt.ItemIsDropEnabled
return flags
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
try:
self.layoutAboutToBeChanged.emit()
self._data = self._data.sort_values(self._data.columns[Ncol],
ascending=not order)
self.layoutChanged.emit()
except Exception as e:
print(e)
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.resize(400, 600)
self.tabs = QTabWidget()
self.tabs.layout = QGridLayout()
self.tabs.setLayout(self.tabs.layout)
self.grid = QGridLayout()
self.grid.addWidget(self.tabs)
self.setLayout(self.grid)
self.setCentralWidget(self.tabs)
self.df = createDF()
self.displayDF()
def displayDF(self):
self.result = QTabWidget()
self.result.layout = QGridLayout()
self.result.setLayout(self.result.layout)
self.tabs.addTab(self.result, 'Dataframe')
#empty space to put the export button in the bottom of the TableView
positions = [(i,j) for i in range(20) for j in range(10)]
space = ' '
i = 0
for position, leer in zip(positions, space):
emptyLabels = QLabel(leer)
self.result.layout.addWidget(emptyLabels, *position)
#export button
self.buttonExport = QPushButton("Export", self)
self.buttonExport.clicked.connect(partial(self.writeToCSV, self.df))
self.result.layout.addWidget(self.buttonExport, 21, 0)
# QTableView
self.view_minmax = QTableView(self.result)
self.modelminmax = PandasModel(self.df)
self.view_minmax.setModel(self.modelminmax)
self.view_minmax.resize(360,500)
self.view_minmax.clicked.connect(self.onClickedRow)
self.view_minmax.sortByColumn(4, Qt.DescendingOrder)
self.view_minmax.show()
def onClickedRow(self, index=None):
print("Click !")
print(index.data())
def writeToCSV(self, df):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fileName, _ = QFileDialog.getSaveFileName(self,"Export results to a\
csv","","CSV (*.csv);;Text Files (*.txt)", options=options)
if fileName:
print(fileName)
df.to_csv (fileName, index = False, header=True)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('Fusion')
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
So when I create the #empty space QLabelWidget and export Button after the #QTableView section, like this, it comes to the strange behavior with the scrollbar because signals of mouse send to QLabelWidgets instead of the QTabelView
# QTableView
self.view_minmax = QTableView(self.result)
self.modelminmax = PandasModel(self.df)
self.view_minmax.setModel(self.modelminmax)
self.view_minmax.resize(360,500)
self.view_minmax.clicked.connect(self.onClickedRow)
self.view_minmax.sortByColumn(4, Qt.DescendingOrder)
self.view_minmax.show()
#empty space to put the export button in the bottom of the TableView
positions = [(i,j) for i in range(20) for j in range(10)]
space = ' '
i = 0
for position, leer in zip(positions, space):
emptyLabels = QLabel(leer)
self.result.layout.addWidget(emptyLabels, *position)
#export button
self.buttonExport = QPushButton("Export", self)
self.buttonExport.clicked.connect(partial(self.writeToCSV, self.df))
self.result.layout.addWidget(self.buttonExport, 21, 0)
Maybe it will help someone.

How to remove data from excel file displayed in PyQt5 and refresh it

My database application uses the Pandas library. I can display the excel file into my tableView but anytime I remove data from the mainframe and try to refresh the tableView. It gives me a keyError.
I'm trying to get it to display the refreshed table. I am attempting to drop the row that a user asks for. It works when it drops because I outputted the information but the tableView itself won't refresh and gives an error.
df = pd.read_excel("filename")
model = PandasModel(df)
self.tableView.setModel(model)
self.tableView.resizeColumnsToContents()
def DeletePlayer(self):
global df
choose = self.removePlayerEdit.text()
if(choose == '0'):
df = df.drop([0])
print("Player deleted")
print(df)
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df = pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if not index.isValid():
return QtCore.QVariant()
return QtCore.QVariant(str(self._df.ix[index.row(), index.column()]))
def setData(self, index, value, role):
row = self._df.index[index.row()]
col = self._df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df[col].dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
self._df.set_value(row, col, value)
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self._df.columns)
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
When implementing a model, you should not access the element that stores the data (dataframe) directly, because if you modify it, the model will not know what is going to generate problems, instead you should create methods that modify the internal data but use the methods as beginRemoveRows and endRemoveColumns that will notify the model of the change.
def removeColumn(self, col):
if 0 <= col < self.columnCount():
self.beginRemoveRows(QtCore.QModelIndex(), col, col)
self._df.drop(
self._df.columns[[col]], axis=1, inplace=True
)
self._df.reset_index(inplace=True, drop=True)
self.endRemoveColumns()
I have improved my initial model to the following:
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
import numpy as np
class FloatDelegate(QtWidgets.QStyledItemDelegate):
#property
def decimals(self):
if not hasattr(self, "_decimals"):
self._decimals = 2
return self._decimals
#decimals.setter
def decimals(self, decimals):
self._decimals = decimals
def createEditor(self, parent, option, index):
DBL_MAX = 1.7976931348623157e308
editor = QtWidgets.QDoubleSpinBox(
parent, minimum=-DBL_MAX, maximum=DBL_MAX, decimals=self.decimals
)
return editor
def setEditorData(self, editor, index):
editor.setValue(index.data())
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.DisplayRole)
def displayText(self, value, locale):
return "{}".format(value)
class DataFrameModel(QtCore.QAbstractTableModel):
DtypeRole = QtCore.Qt.UserRole + 1000
ValueRole = QtCore.Qt.UserRole + 1001
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = QtCore.pyqtProperty(
pd.DataFrame, fget=dataFrame, fset=setDataFrame
)
#QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role: int = QtCore.Qt.DisplayRole,
):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._dataframe.columns[section]
else:
return str(self._dataframe.index[section])
return QtCore.QVariant()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or not (
0 <= index.row() < self.rowCount()
and 0 <= index.column() < self.columnCount()
):
return QtCore.QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == QtCore.Qt.DisplayRole:
return val
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QtCore.QVariant()
def setData(self, index, value, role):
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
if hasattr(value, "toPyObject"):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._dataframe[col].dtype
if dtype != object:
value = None if value == "" else dtype.type(value)
self._dataframe.at[row, col] = value
return True
def flags(self, index):
flags = (
QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsDragEnabled
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
)
return flags
def roleNames(self):
roles = {
QtCore.Qt.DisplayRole: b"display",
DataFrameModel.DtypeRole: b"dtype",
DataFrameModel.ValueRole: b"value",
}
return roles
def removeRow(self, row):
if 0 <= row < self.rowCount():
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self._dataframe.drop([row], inplace=True)
self._dataframe.reset_index(inplace=True, drop=True)
self.endRemoveRows()
def removeColumn(self, col):
if 0 <= col < self.columnCount():
self.beginRemoveRows(QtCore.QModelIndex(), col, col)
self._dataframe.drop(
self._dataframe.columns[[col]], axis=1, inplace=True
)
self._dataframe.reset_index(inplace=True, drop=True)
self.endRemoveColumns()
def sort(self, column, order):
colname = self._dataframe.columns[column]
self.layoutAboutToBeChanged.emit()
self._dataframe.sort_values(
colname, ascending=order == QtCore.Qt.AscendingOrder, inplace=True
)
self._dataframe.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
tableview = QtWidgets.QTableView()
tableview.setSortingEnabled(True)
delegate = FloatDelegate(tableview)
tableview.setItemDelegate(delegate)
delegate.decimals = 4
self.spinbox_row = QtWidgets.QSpinBox()
self.button_row = QtWidgets.QPushButton(
"Delete Row", clicked=self.remove_row
)
self.spinbox_col = QtWidgets.QSpinBox()
self.button_col = QtWidgets.QPushButton(
"Delete Column", clicked=self.remove_col
)
df = pd.DataFrame(
np.random.uniform(0, 100, size=(100, 4)), columns=list("ABCD")
)
self._model = DataFrameModel(df)
tableview.setModel(self._model)
grid = QtWidgets.QGridLayout(self)
grid.addWidget(tableview, 0, 0, 1, 4)
grid.addWidget(self.spinbox_row, 1, 0)
grid.addWidget(self.button_row, 1, 1)
grid.addWidget(self.spinbox_col, 1, 2)
grid.addWidget(self.button_col, 1, 3)
self.on_rowChanged()
self.on_columnChanged()
self._model.rowsInserted.connect(self.on_rowChanged)
self._model.rowsRemoved.connect(self.on_rowChanged)
self._model.columnsInserted.connect(self.on_columnChanged)
self._model.columnsRemoved.connect(self.on_columnChanged)
#QtCore.pyqtSlot()
def on_rowChanged(self):
self.spinbox_row.setMaximum(self._model.rowCount() - 1)
#QtCore.pyqtSlot()
def on_columnChanged(self):
self.spinbox_col.setMaximum(self._model.columnCount() - 1)
#QtCore.pyqtSlot()
def remove_row(self):
row = self.spinbox_row.value()
self._model.removeRow(row)
#QtCore.pyqtSlot()
def remove_col(self):
col = self.spinbox_col.value()
self._model.removeColumn(col)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

PyQt Custom QAbstractTableModel for combobox

In the following sample code I want to use a QDataWidgetMapper together with a Combobox to display data retrieved from a database.
Unfortunately I can't e.g. preselect the choice of the combobox and I can't retrieve the database id on selecting a value in the combobox. Therefore I think I am really missing something fundamental here.
I reduced my program to this sample code which I hope illustrates my problem.
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class ComboModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, *args):
super(ComboModel, self).__init__()
# The data would normally be read from a database (not using the Qt SQL classes)
self.dbIds = [ 13, 49, 81 ]
self.dbValues = [ 'Value 1', 'Value 2', 'Value 3' ]
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self.dbIds)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return 2
def data(self, index, role=QtCore.Qt.DisplayRole):
print 'ComboModel data'
if not index.isValid() or ( role != QtCore.Qt.DisplayRole and role != QtCore.Qt.EditRole ):
print 'ComboModel return invalid QVariant'
return QtCore.QVariant()
if index.column() == 0:
return QtCore.QVariant(self.dbIds[index.row()])
if index.column() == 1:
return QtCore.QVariant(self.dbValues[index.row()])
print 'ComboModel return invalid QVariant'
return QtCore.QVariant()
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, *args):
super(TableModel, self).__init__()
self.editValue = 'Value to edit'
self.comboValue = 49 # This is the database id of the selected entry
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return 1
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return 2
def data(self, index, role=QtCore.Qt.DisplayRole):
print 'TableModel data'
if not index.isValid() or ( role != QtCore.Qt.DisplayRole and role != QtCore.Qt.EditRole ):
print 'TableModel return invalid QVariant'
return QtCore.QVariant()
if index.column() == 0:
return QtCore.QVariant(self.editValue)
if index.column() == 1:
return QtCore.QVariant(self.comboValue)
print 'TableModel return invalid QVariant'
return QtCore.QVariant()
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
print 'TableModel setData'
if not index.isValid():
return False
if index.column() == 0:
print 'new Value: ' + value.toString()
self.editValue = value.toString()
if index.column() == 1:
print 'new combobox value'
print 'new Value: ' + value.toString()
self.comboValue = value.toString() # how do I get the dbId?
return True
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Test')
self.edit = QtGui.QLineEdit(self)
self.edit.setGeometry(10, 40, 80, 30)
self.combo = QtGui.QComboBox(self)
self.combo.setGeometry(10, 80, 80, 30)
self.combo_model = ComboModel()
self.combo.setModel(self.combo_model)
self.combo.setModelColumn(1)
self.model = TableModel()
self.mapper = QtGui.QDataWidgetMapper()
self.mapper.setModel(self.model)
self.mapper.addMapping(self.edit, 0)
self.mapper.addMapping(self.combo, 1)
self.mapper.toFirst();
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

qtreeview root colored

I have a QTreeView with a colored background but I need to have also the root colored..
I'm not able to do so.
Someone can help me?
This is the code..
Thanks
from PyQt4 import QtGui, QtCore
HORIZONTAL_HEADERS = ("Surname", "Given Name")
class person_class(object):
def init(self, sname, fname, isMale):
self.sname = sname
self.fname = fname
self.isMale = isMale
class TreeItem(object):
def init(self, person, header, parentItem):
self.person = person
self.parentItem = parentItem
self.header = header
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return 2
def data(self, column):
if self.person == None:
if column == 0:
return QtCore.QVariant(self.header)
if column == 1:
return QtCore.QVariant("")
else:
if column == 0:
return QtCore.QVariant(self.person.sname)
if column == 1:
return QtCore.QVariant(self.person.fname)
return QtCore.QVariant()
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class treeModel(QtCore.QAbstractItemModel):
def init(self, parent=None):
super(treeModel, self).init(parent)
self.people = []
for fname, sname, isMale in (("John","Brown", 1),
("Fred", "Bloggs", 1), ("Sue", "Smith", 0)):
person = person_class(sname, fname, isMale)
self.people.append(person)
self.rootItem = TreeItem(None, "ALL", None)
self.parents = {0 : self.rootItem}
self.setupModelData()
def columnCount(self, parent=None):
if parent and parent.isValid():
return parent.internalPointer().columnCount()
else:
return len(HORIZONTAL_HEADERS)
def data(self, index, role):
if not index.isValid():
return QtCore.QVariant()
item = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
return item.data(index.column())
if role == QtCore.Qt.UserRole:
if item:
return item.person
if role == QtCore.Qt.BackgroundRole :
return QtGui.QColor('pink')
return QtCore.QVariant()
#def headerData(self, column, orientation, role):
#if (orientation == QtCore.Qt.Horizontal and
#role == QtCore.Qt.DisplayRole):
#try:
#return QtCore.QVariant(HORIZONTAL_HEADERS[column])
#except IndexError:
#pass
#return QtCore.QVariant()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
if not childItem:
return QtCore.QModelIndex()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.column() > 0:
return 0
if not parent.isValid():
p_Item = self.rootItem
else:
p_Item = parent.internalPointer()
return p_Item.childCount()
def setupModelData(self):
for person in self.people:
if person.isMale:
sex = "MALES"
else:
sex = "FEMALES"
if not self.parents.has_key(sex):
newparent = TreeItem(None, sex, self.rootItem)
self.rootItem.appendChild(newparent)
self.parents[sex] = newparent
parentItem = self.parents[sex]
newItem = TreeItem(person, "", parentItem)
parentItem.appendChild(newItem)
if name == "main":
app = QtGui.QApplication([])
model = treeModel()
dialog = QtGui.QDialog()
dialog.setMinimumSize(300,150)
layout = QtGui.QVBoxLayout(dialog)
tv = QtGui.QTreeView(dialog)
tv.setModel(model)
layout.addWidget(tv)
dialog.exec_()
app.closeAllWindows()

Resources