Disable user to click over QTableWidget - qt

I have QTableWidget with CheckBoxes in some cells. I want to disable user to perform mouse click over the table cells (so he can't change checkBox state) for some time while I am using data from the table. I've tried table.setDisabled(1) but that disables whole table and I need scroll to be enabled.
Any help would be appreciated.
EDIT
To be more precise: there could be up to 15x3000 cells in table, filled with text(editable), checkbox(checkable), svg graphic(opens other window when double click on it) or some custom widgets(which also have clickable or editable parts). I need to disable user to click or double click over cells(so he can't change any of them) for 1sec - 10sec time interval (solution must be something fast, not iterating through all items), but I need scroll-bar to be enabled and normal table visibility.

One way to achieve this is to subclass QTableWidgetItem and re-implement the setData method. That way, you can control whether items accept values for certain roles.
To control the "checkability" for all items, you could add a class attribute to the subclass which could be tested whenever a value for the check-state role was passed to setData.
Here's what the subclass might look like:
class TableWidgetItem(QtGui.QTableWidgetItem):
_blocked = True
#classmethod
def blocked(cls):
return cls._checkable
#classmethod
def setBlocked(cls, checkable):
cls._checkable = bool(checkable)
def setData(self, role, value):
if role != QtCore.Qt.CheckStateRole or self.checkable():
QtGui.QTableWidgetItem.setData(self, role, value)
And the "checkability" of all items would be disabled like this:
TableWidgetItem.setCheckable(False)
UPDATE:
The above idea can be extended by adding a generic wrapper class for cell widgets.
The classes below will block changes to text and check-state for table-widget items, and also a range of keyboard and mouse events for cell widgets via an event-filter (other events can be blocked as required).
The cell-widgets would need to be created like this:
widget = CellWidget(self.table, QtGui.QLineEdit())
self.table.setCellWidget(row, column, widget)
and then accessed like this:
widget = self.table.cellWidget().widget()
Blocking for the whole table would be switched on like this:
TableWidgetItem.setBlocked(True)
CellWidget.setBlocked(True)
# or Blockable.setBlocked(True)
Here are the classes:
class Blockable(object):
_blocked = False
#classmethod
def blocked(cls):
return cls._blocked
#classmethod
def setBlocked(cls, blocked):
cls._blocked = bool(blocked)
class TableWidgetItem(Blockable, QtGui.QTableWidgetItem):
def setData(self, role, value):
if (not self.blocked() or (
role != QtCore.Qt.EditRole and
role != QtCore.Qt.CheckStateRole)):
QtGui.QTableWidgetItem.setData(self, role, value)
class CellWidget(Blockable, QtGui.QWidget):
def __init__(self, parent, widget):
QtGui.QWidget.__init__(self, parent)
self._widget = widget
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widget)
widget.setParent(self)
widget.installEventFilter(self)
if hasattr(widget, 'viewport'):
widget.viewport().installEventFilter(self)
widget.show()
def widget(self):
return self._widget
def eventFilter(self, widget, event):
if self.blocked():
etype = event.type()
if (etype == QtCore.QEvent.KeyPress or
etype == QtCore.QEvent.KeyRelease or
etype == QtCore.QEvent.MouseButtonPress or
etype == QtCore.QEvent.MouseButtonRelease or
etype == QtCore.QEvent.MouseButtonDblClick or
etype == QtCore.QEvent.ContextMenu or
etype == QtCore.QEvent.Wheel):
return True
return QtGui.QWidget.eventFilter(self, widget, event)

Just iterate through all QStandardItems and change flags values for items which should not be changeable.
You can use flag: Qt::ItemIsEditable or/and Qt::ItemIsEnabled.

You would need to disable the items themselves as opposed to the whole table if you have other items than QCheckBoxes that you would not like to disable. See the python code below for details:
'''
Iterate through all the check boxes in the standard items
and make sure the enabled flag is cleared so that the items are disabled
'''
for standardItem in standardItems:
standardItem.setFlags(standardItem.flags() & ~Qt.ItemIsEnabled)
Here you can find the corresponding documentation:
void QTableWidgetItem::setFlags(Qt::ItemFlags flags)
Sets the flags for the item to the given flags. These determine whether the item can be selected or modified.

Related

Qt, value on Spinbox are not setted correctly on first time of TreeView

I have implemented a delegate and model on a TreeView, which contains an spinbox on the column number one, but the number format is not the correct one. But, after I click the spinbox, it is setted correctly. I am using PyQt5.
Here I attached two images which explain it much better.
Value setted on a first time
The value is seen correctly after click the spinbox
Here is the code where spinbox are initialized on a first time:
def createEditor(self, parent, option, index):
item = index.internalPointer()
if index.column() == 0:
editor = super().createEditor(parent, option, index)
elif index.column() == 1:
name = item.get_data()[0]
data = item.get_data()[1]
editor = QtWidgets.QSpinBox(parent, minimum=-9999999999, maximum=9999999999)
# editor.setEditTriggers(QAbstractItemView.SelectedClicked)
editor.setObjectName(name)
editor.setValue(data)
editor.setStyleSheet("""
border-radius: 0px;
""")
editor.installEventFilter(self)
else:
editor = super().createEditor(parent, option, index)
return editor
I think that I missed a comitData somewhere, but I don't know where is the better place to emit it, or if it is the correct way to do that.
Thanks you,
Marcel
The problem was resolved.
It was not in the delegate, and it has not got any relationship about QSpinBox, but the model and str format.
On QtCore.Qt.DisplayRole role on function data, I did the correct conversion, as I show on the following code:
def data(self, in_index, role):
if not in_index.isValid():
return QVariant()
node = in_index.internalPointer()
if role == QtCore.Qt.DisplayRole:
if node.data(in_index.column()) is None:
return ''
if in_index.column() == 1:
return str(float(node.data(in_index.column())))
return str(node.data(in_index.column()))
Thanks you all.
Regards,
Marcel

QAbstractItemModel set check state automatically

I am trying to check a node in a QTreeView automatically (eg when user loads some data). Manual checkbox ticking functionality works fine. I search the tree for the relevant item as per http://rowinggolfer.blogspot.com.au/2010/05/qtreeview-and-qabractitemmodel-example.html ie:
In the model:
def searchModel(self, person):
def searchNode(node):
for child in node.childItems:
if person == child.person:
index = self.createIndex(child.row(), 0, child)
return index
if child.childCount() > 0:
result = searchNode(child)
if result:
return result
node_index = searchNode(self.parents[0])
return node_index
def find_GivenName(self, fname):
app = None
for person in self.people:
if person.fname == fname:
app = person
break
if app != None:
index = self.searchModel(app)
return (True, index)
return (False, None)
Then I pass the relevant node into the model to set its check state eg
model.setData(node_index, 2, QtCore.Qt.CheckStateRole)
In the model:
def setData(self, index, value, role):
if role == Qt.CheckStateRole:
row = index.row()
self.args[row].checked = value
return True
But the checkbox for the relevant node does not get checked. Any ideas?
The checkbox was being checked, but only when the mouse was moved to hover over the relevant node. As per the pyqt docs - 'when reimplementing the setData() function, the dataChanged() signal must be emitted explicitly' http://pyqt.sourceforge.net/Docs/PyQt4/qabstractitemmodel.html#dataChanged. I changed the setData method in the model to:
def setData(self, index, value, role):
if role == Qt.CheckStateRole:
row = index.row()
self.args[row].checked = value
self.dataChanged.emit(index, index)
return True
There is some good information of the dataChanged() signal here: When to emit dataChanged from a QAbstractItemModel

Having A Working QSqlRelationalDelegate With QSortFilterProxyModel

I am using QSortFilterProxyModels all the time. However, if a QSqlRelation is setup on the source model, along with a QSqlRelationalDelegate on the view, whenever the view is switched to the proxy model, the QSqlRelationalDelegate disappears, leaving the basic QLineEdit or QSpinBox.
How can I make columns in a view work with both a QSortFilterProxyModel and QSqlRelationalDelegate, giving the expected QCombobox drop down?
This is a better methodology: QSqlRelationalTableModel with QSqlRelationalDelegate not working behind QAbstractProxyModel.
One needs to use mapToSource because the views index can be different then the models index.
class Delegate(QtSql.QSqlRelationalDelegate):
"""
Delegate handles custom editing. This allows the user to have good
editing experience.
Because the join table uses a proxy model a subclass QSqlRelationalDelegate
is required. This is to support the foreign key combobox.
"""
def __init__(self, parent = None):
"""
Class constructor.
"""
# Python super lets you avoid referring to the base class explicitly.
super(Delegate, self).__init__(parent)
def createEditor(self, parent, option, index):
"""
This creates the editors in the delegate.
Reimplemented from QAbstractItemDelegate::createEditor().
Returns the widget used to edit the item specified by
index for editing.
The parent widget and style option are used to control how the
editor widget appears.
1. Get the model associated with the view. In this case it is the
QSortFilterProxyModel.
2. Because with a proxy model the views index does not have to be the
same as the models index. If one sorts,
then the index are not the same.
3. mapToSource.
This is why mapToSource is being used.
mapToSouce Reimplement this function to return the
model index in the proxy model that corresponds to the
sourceIndex from the source model.
4. Return the createEditor with the base index being set to the source
model and not the proxy model.
"""
if index.column() == 2:
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(Delegate, self).createEditor(parent, option, base_index)
else:
return super(Delegate, self).createEditor(parent, option, index)
def setEditorData(self, editor, index):
"""
Once the editor has been created and given to the view
the view calls setEditorData().
This gives the delegate the opportunity to populate the editor
with the current data, ready for the user to edit.
Sets the contents of the given editor to the data for the item
at the given index.
Note that the index contains information about the model being used.
The base implementation does nothing.
If you want custom editing you will need to reimplement this function.
1. Get the model which is a QSortFilterProxyModel.
2. Call mapToSource().
Because with a proxy model the views index does not have to be the
same as the models index. If one sorts,
then the index are not the same.
This is why mapToSource is being used. MapToSouce Reimplement this
function to return the model index in the proxy model
that corresponds to the sourceIndex from the source model.
3. Return setEditorData with the editor and the mapToSource index.
4. Else for all other columns return the base method.
"""
if index.column() == 2:
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(JoinDelegate, self).setEditorData(editor, base_index)
else:
return super(Delegate, self).setEditorData(editor, index)
def setModelData(self, editor, model, index):
if index.column() == 2:
base_model = model.sourceModel()
base_index = model.mapToSource(index)
return super(JoinDelegate, self).setModelData(editor, base_model, base_index)
else:
super(Delegate, self).setModelData(editor, model, index)
def sizeHint(self, option, index):
"""
This pure abstract function must be reimplemented if you want to
provide custom rendering. The options are specified by option and
the model item by index.
"""
if index.isValid():
column = index.column()
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
# change cell Width, height (One can add or subtract to change the relative dimension)
return QtCore.QSize(QtSql.QSqlRelationalDelegate.sizeHint(self, option, index).width() - 200,
QtSql.QSqlRelationalDelegate.sizeHint(self, option, index).height() + 40)
else:
return super(Delegate, self).sizeHint(option, index)
By default, QSqlRelationalDelegate can't handle proxy models, so you have to subclass it. The below is probably far from perfect, so comments/tweaks are welcome, but has been working well on views that have a mixture of QSqlRelations/straight data, without glitches.
class ProxyDelegate(QSqlRelationalDelegate):
def __init__(self):
QSqlRelationalDelegate.__init__(self)
def createEditor(self, p, o, i): # parent, option, index
if i.model().sourceModel().relation(i.column()).isValid(): # if the column has a QSqlRelation, then make the expected QComboBox
e = QComboBox(p)
return e
else:
return QStyledItemDelegate(p).createEditor(p, o, i)
def setEditorData(self, e, i):
m = i.model()
sM = m.sourceModel()
relation = sM.relation(i.column())
if relation.isValid():
m = i.model()
sM = m.sourceModel()
relation = sM.relation(i.column())
pModel = QSqlTableModel() # pModel means populate model. Because I've aimed for generic use, it makes a new QSqlTableModel, even if one already exists elsewhere for that SQL table
pModel.setTable(relation.tableName())
pModel.select()
e.setModel(pModel)
pModel.sort(pModel.fieldIndex(relation.displayColumn()), Qt.AscendingOrder) # default sorting. A custom attribute would need adding to each source model class, in order for this line to know the desired sorting order for this QComboBox delegate
e.setModelColumn(pModel.fieldIndex(relation.displayColumn()))
e.setCurrentIndex(e.findText(m.data(i).toString()))
else:
return QStyledItemDelegate().setEditorData(e, i)
def setModelData(self, e, m, i):
m = i.model() # this could probably be written more elegantly so you don't need to create another SqlModel
sM = m.sourceModel()
relation = sM.relation(i.column())
table = relation.tableName()
indexColumn = relation.indexColumn()
indexColumnId = sM.fieldIndex(indexColumn)
displayColumn = relation.displayColumn()
if relation.isValid():
pModel = QSqlTableModel()
pModel.setTable(relation.tableName())
pModel.select()
displayColumnId = pModel.fieldIndex(displayColumn)
chosenRowInPModel = pModel.match(pModel.index(0, displayColumnId), Qt.DisplayRole, e.currentText())[0].row()
chosenIdInPModel = pModel.data(pModel.index(chosenRowInPModel, indexColumnId)).toString()
m.setData(i, chosenIdInPModel)
self.closeEditor.emit(e, QAbstractItemDelegate.NoHint)
else:
QStyledItemDelegate().setModelData(e, m, i)

QTableView signal or event for "row height changed"

I' m looking for an Event or Signal like "row height changed" that is called if the user changes the height of a row in my QTableView. I want to use this signal to resize all other rows in the QTableView to the new height. I didn' t find such an Event or Signal so I reckon there must be some kind of handy workaround.
Any suggestions will be appreciated.
Row resizing is performed by the vertical QHeaderView. The object emmiting it is QTableView::verticalHeader()
the signal you are interested in is
void QHeaderView::sectionResized ( int logicalIndex, int oldSize, int newSize )
This signal is emitted when a section is resized. The section's logical number is specified by logicalIndex, the old size by oldSize, and the new size by newSize.
Use QHeaderView (You can get an instance by calling QTableView::horizontalHeader()/QTableView::verticalHeader() ) and connect to geometriesChanged() or sectionResized()
The solution with QHeaderView and sectionResized works very well for me. So for resizing all
rows to the same height as the edited row I use this code now:
class MyWidget(QWidget):
def __init__(self, parent=None):
#Do some other init things
self.myTable.verticalHeader().sectionResized.connect(
self.row_resized)
def row_resized(self, index, old_size, new_size):
for i in range(self.myTable.verticalHeader().count()):
self.myTable.setRowHeight(i, new_size)
I wanted to save data after a section was resized, but found sectionResized fired a signal continuously while adjusting, rather than once at the end.
The simplest solution I could think of was to subclass the QHeaderView.
class CustomHeader(QtWidgets.QHeaderView):
"""
Custom header class to return when a section has been resized
Only after mouse released, opposed to continuously while resizing
"""
on_section_resized = QtCore.Signal(int)
def __init__(self, *args, **kwargs):
super(CustomHeader, self).__init__(*args, **kwargs)
self._has_resized = False
self._index = 0
self.sectionResized.connect(self.section_resized)
def section_resized(self, index, old_size, new_size):
self._has_resized = True
self._index = index
return
def mouseReleaseEvent(self, event):
super(CustomHeader, self).mouseReleaseEvent(event)
if self._has_resized:
self._has_resized = False
self.on_section_resized.emit(self._index)
return
In my main class I assigned the header to the table widget:
self.table_widget.setVerticalHeader(CustomHeader(QtCore.Qt.Vertical, self.table_widget))
Then connected the custom signal to my function within the original class:
self.table_widget.verticalHeader().on_section_resized.connect(self.save_section_height)
Part of the Save function:
def save_section_height(self, row):
"""
Updates the edited row's section height
:param row: int, row that has been changed
:return: None
"""
new_section_height = self.table_widget.verticalHeader().sectionSize(row)
I expect some things could be optimised better, but this at least fires one save signal opposed to ten plus, or something!

Clickable elements or child widgets inside custom-painted delegate

I have a QListView, where I display items using a custom delegate with custom painting. Within each item (i.e. each list row) I want to be able to show a couple of "hyperlinks" which the user could click on and which would then call on some functions.
I have already tried to check the official documentation (e.g. Model/View Programming) as well as quite a lot of googling, but haven't been able to figure out how to accomplish this.
I have two ideas, each with their own problems:
I could draw them using child widgets, like a flat QPushButton. How do I then position and display these widgets?
I could also draw them as text strings. How do I then make them clickable? Or can I capture click events on the parent QListView and somehow determine coordinates from those? I could then match coordinates to these clickable elements and act accordingly.
My initial approach was to use QListWidget with .setItemWidget(), where I had a proper widget with a layout and child widgets. Unfortunately this was too slow when my list grew to hundreds or thousands of items. That's why I changed to QListView with a delegate.
I seem to be closing in on a solution.
I can receive clicks on the elements by overriding the delegate's .editorEvent(event, model, option, index). I can then find out the event.type(), the clicked row from index.row() and the actual coordinates from event.x() and event.y() (since, if the event type is MouseButtonRelease, the event is a QMouseEvent).
From these, I think I can correlate the coordinates to my elements on screen and act accordingly.
I will update this answer once I have working code.
EDIT
A simple working example, using PySide:
class MyModel(QtGui.QStandardItemModel):
def __init__(self):
super(MyModel, self).__init__()
for i in range(10): self.appendRow(QtGui.QStandardItem("Row %d" % i))
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
self.links = {}
def makeLinkFunc(self, row, text):
def linkFunc(): print("Clicked on %s in row %d" % (text, row))
return linkFunc
def paint(self, painter, option, index):
painter.save()
textHeight = QtGui.QFontMetrics(painter.font()).height()
painter.drawText(option.rect.x()+2, option.rect.y()+2+textHeight, index.data())
rowLinks = {}
for i in range(3):
text = "Link %d" % (3-i)
linkWidth = QtGui.QFontMetrics(font).width(text)
x = option.rect.right() - (i+1) * (linkWidth + 10)
painter.drawText(x, y, text)
rect = QtCore.QRect(x, y - textHeight, linkWidth, textHeight)
rowLinks[rect] = self.makeLinkFunc(index.row(), text)
self.links[index.row()] = rowLinks
painter.restore()
def sizeHint(self, option, index):
hint = super().sizeHint(option, index)
hint.setHeight(30)
return hint
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
for rect, link in self.links[index.row()].items():
if rect.contains(event.pos()):
link()
return True
return False
listmodel = MyModel()
listview = QtGui.QListView()
listview.setModel(listmodel)
listview.setItemDelegate(MyDelegate(parent=listview))
listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)

Resources