Coloring Row in QTableView instead of Cell - python-3.x

Backstory: Using an imported UI, I place my table onto a QTableView. I also make use of alternating row colors to better differentiate rows.
Problem: I'm looking to color the row of a table that contains a True value in one of the columns. I am able to color the cell, but have not found a way to color the entire row. I use a PandasModel class to format the tables:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
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.values[index.row()][index.column()])
if role == QtCore.Qt.BackgroundRole:
row = index.row()
col = index.column()
if self._data.iloc[row,col] == True:
return QtGui.QColor('yellow')
return None
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[col]
return None
I've look through numerous examples, and I am aware there may be multiple ways to color a table using QBrush or QColor, but so far the best I am able to do is simply color the cell that contains the True value. Splicing in code from other examples, I thought it was possible that the col = index.column() was getting in the way, as maybe it was limiting it to the cell, however, when I remove this it becomes ambiguous.
Important: I am wanting to keep the alternating row colors that I set elsewhere in the script, so please keep that in mind! I am only looking to color the specifics rows that contain any True value.

If the column of the boolean values is known, you just have to check for the value of that column at the given row of the index.
Supposing that the column index is 2:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
self.boolColumn = 2
# ...
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.values[index.row()][index.column()])
if role == QtCore.Qt.BackgroundRole:
row = index.row()
if self._data.iloc[row, self.boolColumn] == True:
return QtGui.QColor('yellow')
Note: return None is implicit at the end of a function block, you don't need to specify it.

Related

Removing rows of a QSortFilterProxyModel behaves badly

I have an application which contains a QTableView for which I would like to have the possibility to sort its contents but also to remove one or more rows. Below is an example code that implements this.
from PyQt5 import QtCore, QtWidgets
class NeXuSFilesModel(QtCore.QAbstractTableModel):
fields = ['col1','col2']
def __init__(self,parent):
super(NeXuSFilesModel,self).__init__(parent)
self._nexus_contents = [['aaa','111'],['bbb','222'],['ccc','333']]
def columnCount(self, parent=None):
return 2
def data(self,index,role):
if not index.isValid():
return QtCore.QVariant()
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return str(self._nexus_contents[row][col])
else:
return QtCore.QVariant()
def headerData(self, index, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return NeXuSFilesModel.fields[index]
else:
return index + 1
return None
def removeRow(self, row, parent):
self.beginRemoveRows(QtCore.QModelIndex(),row,row+1)
del self._nexus_contents[row]
self.endRemoveRows()
return True
def rowCount(self, parent=None):
return len(self._nexus_contents)
class NeXuSDataTableView(QtWidgets.QTableView):
def __init__(self,parent):
super(NeXuSDataTableView,self).__init__(parent)
self.horizontalHeader().setStretchLastSection(True)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Delete:
model = self.model()
selected_indexes = self.selectionModel().selectedRows()
source_indexes_rows = sorted([model.mapToSource(index).row() for index in selected_indexes],reverse=True)
for row in source_indexes_rows:
model.sourceModel().removeRow(row,QtCore.QModelIndex())
super(NeXuSDataTableView, self).keyPressEvent(event)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow,self).__init__()
self._table = NeXuSDataTableView(self)
model = NeXuSFilesModel(self)
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(model)
self._table.setModel(proxy_model)
self._table.setSortingEnabled(True)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
_ = MainWindow()
app.exec_()
When I run that code, I fall into several problems for which I could not find the solution or could not understand the explanations given by the various sources I could find.
When the program starts, the data is showed initially in the wrong order. Indeed, it is displayed in descending order whereas I would like to display it in ascending order
When I remove one item, it removes actually two items !
Would you have any idea about what is wrong with my implementation ?
About both your questions:
proxy_model.sort(0, QtCore.Qt.AscendingOrder) after self._table.sortSortingEnabled(True) results in an ascending order:
self._table.setSortingEnabled(True)
proxy_model.sort(0, QtCore.Qt.AscendingOrder)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
Using self.beginRemoveRows(QtCore.QModelIndex(),row,row) will remove only one row.

PYQT QTableView Delegate can not show createEditor when applied with Proxy

I'm having problem to show the Editor Widget when Delegate is applied with the Proxy situation.
-> self.table.setModel(self.proxy)
If the Delegate is applied to the View/Model structure, then there is no problem at all.
-> #self.table.setModel(self.model)
Refer to: https://www.pythonfixing.com/2021/10/fixed-adding-row-to-qtableview-with.html
See the code below:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
if index.column() == 0:
comboBox = QComboBox(parent)
comboBox.addItems(self.type_items)
return comboBox
# no need to check for the other columns, as Qt automatically creates a
# QLineEdit for string values and QTimeEdit for QTime values;
return super().createEditor(parent, option, index)
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._data.append(data)
self.endInsertRows()
def data(self, index, role=Qt.DisplayRole):
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def flags(self, index):
# allow editing of the index
return super().flags(index) | Qt.ItemIsEditable
class CustomProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filters = dict()
#property
def filters(self):
return self._filters
def setFilter(self, expresion, column):
if expresion:
self.filters[column] = expresion
elif column in self.filters:
del self.filters[column]
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
for column, expresion in self.filters.items():
text = self.sourceModel().index(source_row, column, source_parent).data()
regex = QRegExp(
expresion, Qt.CaseInsensitive, QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
return True
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
localWidget = QWidget()
self.table = QTableView(localWidget)
data = [["1", "Hi", QTime(2, 1)], ["2", "Hello", QTime(3, 0)]]
self.model = TableModel(data)
self.proxy = CustomProxyModel() # Customized Filter
self.proxy.setSourceModel(self.model)
#self.table.setModel(self.model) # Original code, for View/Model
self.table.setModel(self.proxy) # Revised code, for View/Proxy/Model
self.table.setItemDelegate(Delegate())
self.add_row = QPushButton("Add Row", localWidget)
self.add_row.clicked.connect(self.addRow)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
layout_v = QVBoxLayout()
layout_v.addWidget(self.table)
layout_v.addWidget(self.add_row)
localWidget.setLayout(layout_v)
self.setCentralWidget(localWidget)
self.show()
def addRow(self):
row = self.model.rowCount()
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.appendRowData(new_row_data)
for i in range(self.model.columnCount()):
index = self.model.index(row, i)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Test with View/Model, Widget Editor display.
Test with View/Proxy/Model, Widget Editor not display.
Any attempt to access the view indexes must use the view's model.
Your code doesn't work because the index you are providing belongs to another model, so the editor cannot be created because the view doesn't recognize the model of the index as its own: the view uses the proxy model, while you're trying to open an editor for the source model.
While in this case the simplest solution would be to use self.proxy.index(), the proper solution is to always refer to the view's model.
Change both self.model.index(...) to self.table.model().index(...).
Yes, thanks you very much.
Revised the code as below, now the Widget Editor display accordingly.
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
#index = self.model.index(row, column) # original code, which get the wrong index from the model
index = self.proxy.index(row, column) # revised code, get the correct index from the proxy
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor

wxPython - grid.AutoSizeColumns() doesn't apply to the index column?

I've got a grid of analysis results that shows up in my results frame, and after creating the grid object I call grid.AutoSizeColumns() to fit the columns to the data contents. This works fine for the actual columns containing data, but the leftmost column (the 'index' column?) containing just the row indices remains fairly wide -- much wider than needed, since there's only four rows.
What I'd like to do:
-Remove the leftmost column (the gray one containing only row indices), and/or
-Remove the actual "Index" column (with the header "Index") as it's redundant, and narrow the leftmost gray column, OR
-Remove both the leftmost column AND the column labeled "index".
Note: I am aware of the grid.DeleteCols(pos=0, numCols=1) method, but this always throws an error:
wx._core.wxAssertionError: C++ assertion ""Assert failure"" failed at ..\..\src\generic\grid.cpp(1471) in wxGridTableBase::DeleteCols(): Called grid table class function DeleteCols
but your derived table class does not override this function
and I'm not sure how to "override this function". If someone could explain the error to me/how to fix it so that grid.DeleteCols() works, that would be halfway to solving my issue (the other half being narrowing or deleting that gray far-left column in the grid)
Minimal code example below, along with a screenshot of what it currently looks like (taken from the actual program, not the minimal code)
On a related note, is there an easy way to right-justify the cell values in the grid? I format the results to all have the same number of decimal places, and I think it would look neater if the values were right-justfied (or even centered).
Minimal example:
import wx
import wx.grid
import numpy as np
import pandas as pd
class StartFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent=parent,
id=wx.ID_ANY, title="title", size=wx.Size(900,600),
style=wx.CAPTION|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SYSTEM_MENU|wx.RESIZE_BORDER)
self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
self.numarray=[[1,0.0000,0.071,3.535,3.313,0.22],
[10,1.000,0.071,3.535,3.888,-0.353],
[20,1.301,0.357,4.634,4.526,0.108],
[50,1.699,0.929,6.456,6.442,0.023]]
self.df = pd.DataFrame(self.numarray, columns=['Parasites/ml','log_qty',
'probability','probit','est. probit','residual'])
self.table = DataTable(self.df)
grid = wx.grid.Grid(self, -1)
grid.SetTable(self.table, takeOwnership=True)
grid.AutoSizeColumns()
self.pnl = wx.Panel(self)
self.vsizer = wx.BoxSizer(wx.VERTICAL)
self.hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.hsizer.AddStretchSpacer()
self.hsizer.Add(grid, 0, wx.CENTER)
self.hsizer.AddStretchSpacer()
self.vsizer.AddStretchSpacer()
self.vsizer.Add(self.hsizer, wx.SizerFlags().Expand().Border(wx.ALL, 5))
self.vsizer.AddStretchSpacer()
self.pnl.SetSizerAndFit(self.vsizer)
class DataTable(wx.grid.GridTableBase):
def __init__(self, printdata=None):
wx.grid.GridTableBase.__init__(self)
self.headerRows = 1
if printdata is None: data = pd.DataFrame()
self.data = printdata
def GetNumberRows(self):
return len(self.data)
def GetNumberCols(self):
return len(self.data.columns) + 1
def GetValue(self, row, col):
if col == 0: return self.data.index[row]
return self.data.iloc[row, col - 1]
def SetValue(self, row, col, value):
self.data.iloc[row, col - 1] = value
def GetColLabelValue(self, col):
if col == 0:
if self.data.index.name is None: return 'Index'
else: return self.data.index.name
return str(self.data.columns[col - 1])
def main():
app = wx.App()
frm = StartFrame(None)
frm.Show()
app.MainLoop()
if __name__ == "__main__":
main()
You can Hide Columns and/or Rows, so that is an easy method of dealing with the errant left-hand column.
Hide comes in useful for other things as well e.g. grid.HideRowLabels(), will hide those pesky row numbers of the left.
The alignment issue could probably be handled by using a wx.grid.GridCellAttrProvider or you can simply iterate over the grid setting each cell, as I have done below for simplicity.
import wx
import wx.grid
import numpy as np
import pandas as pd
class StartFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent=parent,
id=wx.ID_ANY, title="title", size=wx.Size(900,600),
style=wx.CAPTION|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SYSTEM_MENU|wx.RESIZE_BORDER)
self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
self.numarray=[[1,0.0000,0.071,3.535,3.313,0.22],
[10,1.000,0.071,3.535,3.888,-0.353],
[20,1.301,0.357,4.634,4.526,0.108],
[50,1.699,0.929,6.456,6.442,0.023]]
self.df = pd.DataFrame(self.numarray, columns=['Parasites/ml','log_qty',
'probability','probit','est. probit','residual'])
self.table = DataTable(self.df)
grid = wx.grid.Grid(self, -1)
grid.SetTable(self.table, takeOwnership=True)
# Hide Index column
grid.HideCol(0)
grid.HideRowLabels()
# Alignment of cells
r = grid.GetNumberRows()
c = grid.GetNumberCols()
for row in range(r):
for cell in range(c):
grid.SetCellAlignment(row, cell, wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
grid.AutoSizeColumns()
self.pnl = wx.Panel(self)
self.vsizer = wx.BoxSizer(wx.VERTICAL)
self.hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.hsizer.AddStretchSpacer()
self.hsizer.Add(grid, 0, wx.CENTER)
self.hsizer.AddStretchSpacer()
self.vsizer.AddStretchSpacer()
self.vsizer.Add(self.hsizer, wx.SizerFlags().Expand().Border(wx.ALL, 5))
self.vsizer.AddStretchSpacer()
self.pnl.SetSizerAndFit(self.vsizer)
class DataTable(wx.grid.GridTableBase):
def __init__(self, printdata=None):
wx.grid.GridTableBase.__init__(self)
self.headerRows = 1
if printdata is None: data = pd.DataFrame()
self.data = printdata
def GetNumberRows(self):
return len(self.data)
def GetNumberCols(self):
return len(self.data.columns) + 1
def GetValue(self, row, col):
if col == 0: return self.data.index[row]
return self.data.iloc[row, col - 1]
def SetValue(self, row, col, value):
self.data.iloc[row, col - 1] = value
def GetColLabelValue(self, col):
if col == 0:
if self.data.index.name is None: return 'Index'
else: return self.data.index.name
return str(self.data.columns[col - 1])
def main():
app = wx.App()
frm = StartFrame(None)
frm.Show()
app.MainLoop()
if __name__ == "__main__":
main()

PyQt QTreeView Disable child selection when parent selected

Does anyone know how one can disable a child from being selected once its corresponding parent is already selected and vice versa. Consider the following example:
A-1
-2
where A is a parent node in the treeview and 1 and 2 are two children under that parent. If the user selects A he/she should not be able to select either 1 or 2. In the same way if the user selects either 1 or 2 he/she should not be able to select A.
Here is the code I have used. This class defines the TreeView model:
class TreeModel(QtCore.QAbstractItemModel):
'''
This class handles the treeview model.
'''
def __init__(self, top, *args, **kwargs):
super(TreeModel, self).__init__(*args, **kwargs)
self.__top = top
def index(self, row, column, parent=QtCore.QModelIndex()):
if parent.isValid():
parent_node = parent.internalPointer()
node = parent_node.children[row]
index = self.createIndex(row, column, node)
else:
index = self.createIndex(row, column, self.__top)
return index
def parent(self, index):
if index.isValid():
node = index.internalPointer()
parent = node.parent
if parent is None:
parent_index = QtCore.QModelIndex()
else:
parent_index = self.createIndex(parent.row(), 0, parent)
else:
parent_index = QtCore.QModelIndex()
return parent_index
def rowCount(self, index=QtCore.QModelIndex()):
node = index.internalPointer()
if node is None:
count = 1
else:
count = len(node.children)
return count
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
node = index.internalPointer()
data = str(node.number)
else:
data = None
return data
def addChild(self, index, child):
self.beginInsertRows(index, self.rowCount(index), self.rowCount(index) + 1)
parent = index.internalPointer()
parent.addChild(child)
self.endInsertRows()
self.layoutChanged.emit()
This function adds nodes to the tree based on user input:
for key, value in treeViewModelDict.items():
temp = [key, value]
treeViewDictList.append(temp)
print(self.treedict)
def recurse(parent, children_data):
for child_data in children_data:
if isinstance(child_data, list):
recurse(child, child_data)
else:
child = MyData(child_data, parent=parent)
top = MyData("Tree View of Selected Meters")
for i, next in enumerate(treeViewDictList):
recurse(top, next)
self.model = TreeModel(top)
self.treeView.setModel(self.model)
self.treeView.expandAll()
Is there a pythonic way of doing this?
You need to record somewhere what the current selection is. I normally store that in the model somewhere. The selectionChanged signal on the tree view's selection model is good for this.
Then give your model a flags method (it should really have this already) which checks if the current node's parent is selected, if it isn't then you return whatever flags you want or'd with Qt.ItemIsSelectable, if it is, return flags without Qt.ItemIsSelectable

PyQt4.9.1 view never calls model.data

I'm trying to figure out how to work with the models and views in PyQt4.9.1 and I've run into a bit of a problem.
Here's the code that matters:
class TestModel(QtGui.QStandardItemModel):
def __init__(self,parent=None):
QtGui.QStandardItemModel.__init__(self,parent)
self.initData()
self.headings = ['name','value','']
def initData(self):
self.rows = [['name {0}'.format(i), i] for i in range(20)]
def data(self, index, value, role):
print ("foo")
if not index.isValid():
return
if (role == QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if (col == 3):
return "BUTTON GOES HERE"
return self.rows[row][col]
def headerData(self,section,orientation,role):
if (role == QtCore.Qt.DisplayRole):
if (orientation == QtCore.Qt.Horizontal):
return self.headings[section]
def rowCount(self,parent):
return len(self.rows)
def columnCount(self,parent):
return 3
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.m = TestModel()
self.initUi()
def initUi(self):
layout = QtGui.QVBoxLayout()
widget = QtGui.QTableView()
widget.setModel(self.m)
layout.addWidget(widget)
self.setLayout(layout)
self.show()
Here's what happens when I launch my application's MainWindow: There are no error messages, the table is drawn with the right number of rows and columns and the correct headings, but the table is empty. You might notice that the model's draw method starts off with a print statement. That statement is never reached. Is there something I'm missing? I cant find any tutorials at all for PyQt4.9.1.
data() doesn't have any value parameter, but removing it doesn't solve the problem.
The virtual method index(row, column, parent), which is called whenever a QModelIndex needs to be created, always returns an invalid index for QStandardItemModel, unless a QStandardItem was explicitly created for the requested index. The view probably doesn't try to display cells when the index is invalid, so data() is never called.
If you kept inheriting from QStandardItemModel, you would need to reimplement index() to create valid indexes, but since you are using your own structure to store the data instead of using QStandardItem, you could simply inherit from QtCore.QAbstractTableModel:
class TestModel(QtCore.QAbstractTableModel):
def __init__(self,parent=None):
super(TestModel, self).__init__(parent)
self.initData()
self.headings = ['name','value','']
def initData(self):
self.rows = [['name {0}'.format(i), i] for i in range(20)]
def data(self, index, role):
if index.parent().isValid():
return None
if (role == QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
# 3rd column index is 2 not 3
if (col == 2):
return "BUTTON GOES HERE"
# that return should also be "inside" the role DisplayRole
return self.rows[row][col]
return None
def headerData(self,section,orientation,role):
if (role == QtCore.Qt.DisplayRole):
if (orientation == QtCore.Qt.Horizontal):
return self.headings[section]
Also, you should only return a non-zero row/column count for top-level items (the one without parent), if you are not representing a tree model :
def rowCount(self,parent):
if not parent.isValid():
return len(self.rows)
return 0
def columnCount(self,parent):
if not parent.isValid():
return 3
return 0

Resources