I have an application which uses a QTableView/QAbstractTableModel combination. For the view, I've defined a Delegate which displays an image (a QPixmap, loaded from an image file) in one column of the table view.
Basically, the problem is that when a cell in the column with the Delegate is selected, sometimes the background shows and sometimes it doesn't.
Here is what I've discovered by experimentation so far, and I can't make much sense of it:
I have this relatively short test program:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsSelectable
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = QtWidgets.QTableView()
tableView.setItemDelegateForColumn(0, Delegate())
tableView.resize(550, 160)
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
When I specify app.setStyle('fusion') in __main__, I get what I would expect: When a cell in the column with the Delegate is selected, the cell background is blue and the image appears in front of it:
However, if I change to app.setStyle('windows'), even though in general it uses the same blue background for selected cells, when I move to a cell in the first column, the background disappears:
(You can't obviously see it, but the same cell is selected as in the first example).
That's just a piece of test code, which I don't completely understand.
In the actual application I'm writing, I am using Qt Designer to create the UI. Even though I specify app.setStyle('fusion'), the table has entirely different styling, with a different appearance to the background of a selected cell:
I can't for the life of me figure out where it is picking up the different style. It must come from Qt Designer somehow, but I've looked at the .py file Qt Designer creates, and I can't find it.
This style (wherever it comes from) seems to suffer from the same problem as the windows style. In the image above, there is no Delegate in use. The cell in row 2/column 2 is selected, and the background shows.
But if I add a Delegate to display a QPixmap in column 2, then the background does not show when the cell is selected:
(It's selected; take my word for it).
I thought maybe it was the case that once you use a Delegate to display an image, you could no longer get a background in the selected cell. But you obviously can. It works in one case, just not the others.
If anyone can shed light on this, I'd appreciate it. (I realize this is long; thanks for sticking with me).
I've been fiddling around with this issue more, and I've learned some things about my original question. In retrospect, I think it was not as clear as it could have been (or maybe I just understand it all a bit better).
For starters, I never should have referred to cells as being "selected". In fact, I don't even have the Qt.ItemIsSelectable flag set for any of the cells in the view. What I really have been trying to do is control the background of a cell when it is active (for lack of a better word) -- meaning it is the cell where the cursor is currently positioned.
This can be done by overriding initStyleOption() in the Delegate. My original test code is modified as shown below:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
if role == QtCore.Qt.BackgroundRole:
return QtGui.QBrush(QtGui.QColor(255, 255, 255))
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable
# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
# <Modification>
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
option.backgroundBrush = QtGui.QBrush(QtGui.QColor(232, 244, 252))
def paint(self, painter, option, index):
if (index.column() == 0):
# <Modification>
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
self.initStyleOption(option, index)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(option.backgroundBrush)
painter.drawRect(option.rect)
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
else:
super().paint(painter, option, index)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = TableView()
tableView.resize(550, 160)
tableView.setItemDelegate(Delegate())
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
initStyleOption() sets the background brush for a cell when it is active (current). But as I bemoaned before, this doesn't occur in the first column, which has a Delegate with a custom paint() method that displays a pixmap. So paint() must also take responsibility for setting the background for cells in that column when they are active. It uses the same backgroundBrush that initStyleOption() set.
The result is very nearly what I'm shooting for. The only fly in the ointment is that there is still clearly additional styling going on, that affects all the cells in the view except those in column 1 with the custom Delegate. So they don't look quite exactly alike when active:
(It's subtle, but there's a bit of a gradient to the background of the cell in column 2, which is absent in column 1).
I know now that there are style 'factories' that apply a widget-wide style. Since I'm using Fusion, that is evidently where the extra styling is coming from.
So now my question is -- where is that styling defined, and do I have any control over it? If I could see it, I could make my custom background style match it. Better yet, if I could modify it, I could make it match mine.
I had the same problem with my own tool today. I think your issue is the same as this other question. In short, you just need to call super in paint before doing any of your extra work. When I added super to my own code, selections worked again as expected in the delegate.
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
(FWIW I haven't tested the code above. But it should work).
Related
I have a custom QWidget that I have embedded into a QTableWidget.
When I toggle the QCheckBoxes and modify the text in the QLineEdit widgets, the program is not able to distinguish the widgets in rows 2 and 1 from the widgets in row 0. How can I change the program so that it prints the correct row and column of the QLineEdit widget that is being edited or the Checkbox that is being toggled?
Figure 1 shows a screenshot of the program with the output after selecting the third checkbox many times in Visual Studio Code. The output is expected to read “2 0” repeatedly but instead it reads “0 0”.
Figure 2 Similarly, when I modify the text in the QLineEdit in cell 2,0 from “My Custom Text” to “Text” the program prints “Handle Cell Edited 0,0”, although it is expected to print “Handle Cell Edited 2,0 Cell 2,0 was changed to Text”.
Code:
# Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
# Call the parent constructor
QTableWidget.__init__(self)
self.window = window
class myWidget(QWidget):
#This code is adapted paritally form a post by user sebastian at:
#https://stackoverflow.com/a/29764770/18914416
def __init__(self,parent=None):
super(myWidget,self).__init__()
self.Layout1 = QHBoxLayout()
self.item = QLineEdit("My custom text")
#https://stackabuse.com/working-with-pythons-pyqt-framework/
self.Checkbox = QCheckBox()
self.Checkbox.setCheckState(Qt.CheckState.Unchecked)
self.Layout1.addWidget(self.Checkbox)
self.Layout1.addWidget(self.item)
#https://stackoverflow.com/questions/29764395/adding-multiple-widgets-to-qtablewidget-cell-in-pyqt
self.item.home(True)
#https://www.qtcentre.org/threads/58387-Left-text-alignment-for-long-text-on-QLineEdit
self.setLayout(self.Layout1)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
myWidget1 = myWidget()
myWidget1.Checkbox.stateChanged.connect(self.handleButtonClicked)
myWidget1.item.editingFinished.connect(self.handle_cell_edited)
self.table_widget.setCellWidget(i,0,myWidget1)
myWidget1.Layout1.setContentsMargins(50*i+10,0,0,0)
self.show()
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
What I've Tried So Far:
I learned that QT has two types of widgets that can be embedded in a table; a QTableWigetItem which can be inserted into a table using setItem()(3) and Qwidgets, which can be placed into a table using setCellWidget().(4) Generally, I know that using a QTableWigetItem one can set the item.setFlags(Qt.ItemFlag.ItemIsUserCheckable)
flag to create a checkbox in the cell. (3) However, when using the QTableWigetItem, I wasn’t able to find a way to indent the checkboxes. Because giving each checkbox its own indentation level is important in the context of my program, I’ve decided to use Qwidgets instead of QTableWigetItems in the few select cells where indenting is important.
I’ve read that by creating a QItemDelegate(5)(6), you can do a lot more with setting QWidgets in boxes. However, creating a delegate seems complicated, so I’d prefer to avoid this if possible. If there is no other way to make the program register the correct cell number of the cell being edited, creating a delegate will be the next thing I look into.
For anyone who might want to experiment with QTableWigetItems in this application, here is an equivalent program that uses QTableWigetItems instead of QWidgets but doesn't permit separate indentation or editing of the text field in column 0. For either and both of these two reasons, a QTableWigetItem seems not to be usable for the checkboxes in column 0.
Less Successful Attempt using QTableWidgetItem:
#Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
# https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.table_widget = SimpleTable(window=self)
layout.addWidget(self.table_widget)
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
for i in range(len(items)):
c = QTableWidgetItem(items[i][0])
m = QTableWidgetItem(items[i][1])
self.table_widget.insertRow(self.table_widget.rowCount())
self.table_widget.setItem(i, 1, c)
self.table_widget.setItem(i, 2, m)
item = QTableWidgetItem("My Custom Text")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable| Qt.ItemFlag.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Unchecked)
self.table_widget.setItem(i,0,item)
#https://youtu.be/DM8Ryoot7MI?t=251
self.show()
#I added this line:
self.table_widget.itemChanged.connect(self.handle_cell_edited)
def handleButtonClicked(self):
#Adapted from a post by user: Andy at:
# https://stackoverflow.com/a/24149478/18914416
button = QApplication.focusWidget()
# or button = self.sender()
index = self.table_widget.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
# I added this fuction:
def handle_cell_edited(self):
if QApplication.focusWidget() != None:
index = self.table_widget.indexAt(QApplication.focusWidget().pos())
x,y = index.column(),index.row()
if index.isValid():
print("Handle Cell Edited",index.row(), index.column())
if self.table_widget.item(y,x)!= None:
print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")
def main():
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
main()
Bibliography:
1.https://i.stack.imgur.com/FudE3.png
2.https://i.stack.imgur.com/C2ypp.png
3.https://youtu.be/DM8Ryoot7MI?t=251
4.https://stackoverflow.com/questions/24148968/how-to-add-multiple-qpushbuttons-to-a-qtableview/24149478#24149478
5.Creating a QItemDelegate for QWidgets, https://stackoverflow.com/a/35418141/18914416
6.Need to create a QItemDelegate to add a stylesheet to QTableWidgetItems: https://forum.qt.io/topic/13124/solved-qtablewidgetitem-set-stylesheet
The geometry of a widget is always relative to its parent.
In your first example, the problem is that the pos() returned for the widget is relative to the myWidget container, and since the vertical position is always a few pixels below the top of the parent (the layout margin), you always get the same value.
The second example has another conceptual problem: the checkbox of a checkable item is not an actual widget, so the widget you get is the table itself.
def handle_cell_edited(self):
# this will print True
print(isinstance(QApplication.focusWidget(), QTableWidget))
As explained above, the geometry is always relative to the parent, so you will actually get the position of the table relative to the window.
The solution to the first case is quite simple, as soon as you understand the relativity of coordinate systems. Note that you shall not rely on the focusWidget() (the widget might not accept focus), but actually get the sender(), which is the object that emitted the signal:
def handleButtonClicked(self):
sender = self.sender()
if not self.table_widget.isAncestorOf(sender):
return
# the widget coordinates must *always* be mapped to the viewport
# of the table, as the headers add margins
pos = sender.mapTo(self.table_widget.viewport(), QPoint())
index = self.table_widget.indexAt(pos)
if index.isValid():
print(index.row(), index.column())
In reality, this might not be that necessary, as an item delegate will suffice if the indentation is the only requirement: the solution is to properly set the option.rect() within initStyleOption() and use a custom role for the indentation:
IndentRole = Qt.UserRole + 1
class IndentDelegate(QStyledItemDelegate):
def initStyleOption(self, opt, index):
super().initStyleOption(opt, index)
indent = index.data(IndentRole)
if indent is not None:
left = min(opt.rect.right(),
opt.rect.x() + indent)
opt.rect.setLeft(left)
class SimpleTable(QTableWidget):
def __init__(self,window):
QTableWidget.__init__(self)
self.window = window
self.setItemDelegateForColumn(0, IndentDelegate(self))
class Window(QWidget):
def __init__(self):
# ...
for i in range(len(items)):
# ...
item.setData(IndentRole, 20 * i)
I am adding a QGraphicTextItem to a scene using pyqt6.
I cannot resize the widget border when text is resized.
I have looked at a few way of resizing, but none work.
The text does change to a bigger font via the context menu.
The entire class is shown below.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super(FreeTextGraphicsItem, self).__init__(None)
self.x = x
self.y = y
self.text = text_
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(self.font)
self.setPlainText(self.text)
self.setPos(self.x, self.y)
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
menu.addAction(_("Large font"))
action = menu.exec(QtGui.QCursor.pos())
if action is None:
return
if action.text() == "Large font":
self.font = QtGui.QFont(self.settings['font'], 12, QtGui.QFont.Weight.Normal)
frame = self.document().documentLayout().frameBoundingRect(self.document().rootFrame())
self.boundingRect().setRect(0, 0, frame.width(), frame.height())
def paint(self, painter, option, widget):
color = QtCore.Qt.GlobalColor.white
painter.setBrush(QtGui.QBrush(color, style=QtCore.Qt.BrushStyle.SolidPattern))
painter.drawRect(self.boundingRect())
painter.setFont(self.font)
fm = painter.fontMetrics()
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
lines = self.text.split('\\n')
for row in range(0, len(lines)):
painter.drawText(5, fm.height() * (row + 1), lines[row])
You're not using the features of QGraphicsTextItem.
In fact, you're completely ignoring and overriding most of its aspects:
x and y are existing and dynamic properties of all QGraphicsItems and should never be overwritten;
the same for font of QGraphicsTextItem;
calling setRect() on the bounding rectangle is useless, as boundingRect() is a *property getter" and is returned internally by the item based on its contents (in this case, the text set with setPlainText());
the text drawing is completely overridden, and not reliable nor consistent with the text set for the item, considering that you're painting the text with split lines, while the original text has escaped new lines;
If your main purpose is to draw a border around the item, then you should only do that, and then rely on the existing capabilities of the item.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super().__init__(text_.replace('\\n', '\n'))
self.setPos(x, y)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
)
font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(font)
self.setDefaulTextColor(QtGui.QColor(QtCore.Qt.GlobalColor.white))
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
largeFontAction = menu.addAction(_("Large font"))
action = menu.exec(event.screenPos())
if action == largeFontAction:
font = QtGui.QFont(
self.settings['font'], 12, QtGui.QFont.Weight.Normal)
self.setFont(font)
def paint(self, painter, option, widget=None):
painter.save()
painter.setBrush(QtCore.Qt.GlobalColor.white)
painter.drawRect(self.boundingRect())
painter.restore()
super().paint(painter, option, widget)
Note: comparing actions with their text is pointless, other than conceptually wrong; not only you can have a more reliable object-based comparison using the action (as shown above), but that comparison can also become invalid: a menu could contain items that have the same names, and you're also probably using the _ for translations, so the text might not match at all.
I want to display a button in each cell of a QTableWidget's column. Each button, when clicked, must remove its corresponding row in the table.
To do so, I created a RemoveRowDelegate class with the button as editor and used the QAbstractItemView::openPersistentEditor method in a CustomTable class to display the button permanently.
class RemoveRowDelegate(QStyledItemDelegate):
def __init__(self, parent, cross_icon_path):
super().__init__(parent)
self.cross_icon_path = cross_icon_path
self.table = None
def createEditor(self, parent, option, index):
editor = QToolButton(parent)
editor.setStyleSheet("background-color: rgba(255, 255, 255, 0);") # Delete borders but maintain the click animation (as opposed to "border: none;")
pixmap = QPixmap(self.cross_icon_path)
button_icon = QIcon(pixmap)
editor.setIcon(button_icon)
editor.clicked.connect(self.remove_row)
return editor
# Delete the corresponding row
def remove_row(self):
sending_button = self.sender()
for i in range(self.table.rowCount()):
if self.table.cellWidget(i, 0) == sending_button:
self.table.removeRow(i)
break
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None):
super().__init__(parent)
self.columns = []
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
if df is not None:
self.fill(df)
# Build the table from a pandas df
def fill(self, df):
self.columns = [''] + list(df.columns)
nb_rows, _ = df.shape
nb_columns = len(self.columns)
self.setRowCount(nb_rows)
self.setColumnCount(nb_columns)
self.setHorizontalHeaderLabels(self.columns)
for i in range(nb_rows):
self.openPersistentEditor(self.model().index(i, 0))
for j in range(1, nb_columns):
item = df.iloc[i, j-1]
table_item = QTableWidgetItem(item)
self.setItem(i, j, table_item)
def add_row(self):
nb_rows = self.rowCount()
self.insertRow(nb_rows)
self.openPersistentEditor(self.model().index(nb_rows, 0))
def setItemDelegateForColumn(self, column_index, delegate):
super().setItemDelegateForColumn(column_index, delegate)
delegate.table = self
I set the delegate for the first column of the table and build the latter from a pandas dataframe:
self.table = CustomTable() # Here, self is my user interface
remove_row_delegate = RemoveRowDelegate(self, self.cross_icon_path)
self.table.setItemDelegateForColumn(0, remove_row_delegate)
self.table.fill(df)
For now, this solution does the job but I think of several other possibilities:
Using the QTableWidget::setCellWidget method
Overriding the paint method and catching the left click event
But:
I believe the first alternative is not very clean as I must create the buttons in a for loop and each time a row is added (but after all, I also call openPersistentEditor the same way here).
I am wondering if the second alternative is worth the effort. And if it does, how to do it?
Also:
I believe my remove_row method can be optimized as I iterate over all rows (that is one of the reasons why I thought about the second alternative). Would you have a better suggestion ?
I had to override the setItemDelegateForColumn method so that I can access the table from the RemoveRowDelegate class. Can it be avoided ?
Any other remark that you think might be of interest would be greatly appreciated!
As suggested by #ekhumoro, I finally used a context menu:
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None, add_icon_path=None, remove_icon_path=None):
super().__init__(parent)
self.add_icon_path = add_icon_path
self.remove_icon_path = remove_icon_path
# Activation of customContextMenuRequested signal and connecting it to a method that displays a context menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos))
def show_context_menu(self, pos):
idx = self.indexAt(pos)
if idx.isValid():
row_idx = idx.row()
# Creating context menu and personalized actions
context_menu = QMenu(parent=self)
if self.add_icon_path:
pixmap = QPixmap(self.add_icon_path)
add_icon = QIcon(pixmap)
add_row_action = QAction('Insert a line', icon=add_icon)
else:
add_row_action = QAction('Insert a line')
add_row_action.triggered.connect(lambda: self.insertRow(row_idx))
if self.remove_icon_path:
pixmap = QPixmap(self.remove_icon_path)
remove_icon = QIcon(pixmap)
remove_row_action = QAction('Delete the line', icon=remove_icon)
else:
remove_row_action = QAction('Delete the line')
remove_row_action.triggered.connect(lambda: self.removeRow(row_idx))
context_menu.addAction(add_row_action)
context_menu.addAction(remove_row_action)
# Displaying context menu
context_menu.exec_(self.mapToGlobal(pos))
Moreover, note that using QTableWidget::removeRow method is more optimized than my previous method. One just need to get the row index properly from the click position thanks to QTableWidget::indexAt method.
Using the code from Here and There, I made a GUI presenting my project on a smaller scale.
I have a qTableView,containing a large array of rows, and on each rows I have a delete and an edit button. On click, it should either edit or delete the current row. When using only the first source, it works exactly as intended, but as soon as I handle the click outside of the buttons class, it stops working.
Everytime I try to edit or delete, the button that either self.sender() or QtWidgets.qApp.focusWidget() sees as the sender has the coordinates [0,0], even if it's absolutely not it's coordinates.
I have searched on various websites and can't find this precise question.
What am I doing wrong, and what could I do to solve this problem?
My code :
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox
class EditButtonsWidget(QtWidgets.QWidget):
# Credit to : https://stackoverflow.com/a/29764914/13812144
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout()
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
self.buttonRow = 0
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(3)
self.setCentralWidget(self.table)
data1 = ['row1','row2','row3','row4']
data2 = ['1','2.0','3.00000001','3.9999999']
self.table.setRowCount(4)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.editButton.clicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self):
#button = QtWidgets.qApp.focusWidget()
button = self.sender()
index = self.table.indexAt(button.pos())
if index.isValid():
print(index.row(), index.column())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = MainWindow()
MainWindow.show()
sys.exit(app.exec_())
The position must be of the widget that is set in the QTableWidget, not of one of its children.
In this case it is better to consider the EditButtonsWidget as a black box and expose the clicked signals of the buttons as new signals so that the sender is EditButtonsWidget and no longer the buttons:
class EditButtonsWidget(QtWidgets.QWidget):
edit_clicked = QtCore.pyqtSignal()
delete_clicked = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtWidgets.QHBoxLayout(self)
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.editButton = QtWidgets.QPushButton('edit')
self.deleteButton = QtWidgets.QPushButton('del')
# add your buttons
layout.addWidget(self.editButton)
layout.addWidget(self.deleteButton)
self.editButton.clicked.connect(self.edit_clicked)
self.deleteButton.clicked.connect(self.delete_clicked)
for index in range(4):
item1 = QtWidgets.QTableWidgetItem(data1[index])
self.table.setItem(index,0,item1)
item2 = QtWidgets.QTableWidgetItem(data2[index])
self.table.setItem(index,1,item2)
self.btn_sell = EditButtonsWidget()
self.btn_sell.edit_clicked.connect(self.handleButtonClicked) # <---
self.table.setCellWidget(index,2,self.btn_sell)
Widget positions always use the parent's coordinate system as a reference.
In your case, the button is a child of EditButtonsWidget, and since it's also the first widget and the layout has no margins, the button is placed at 0, 0 in that coordinate reference system.
A theoretical solution to your problem would be to map the widget position to the actual widget you need a reference for, which is the viewport of the scroll area (the table):
def handleButtonClicked(self):
button = self.sender()
viewportPosition = button.mapTo(self.table.viewport(), QtCore.QPoint())
index = self.table.indexAt(viewportPosition)
if index.isValid():
print(index.row(), index.column())
The mapping is done using an empty QPoint, since the top-left corner of a widget is always 0, 0 in local coordinates.
While this works, it's not the most logic nor elegant or safest way to do so, as you should reference the actual index instaed.
A better solution would be to map the table index, use that as argument of the widget constructor, and send that index for a custom signal.
class EditButtonsWidget(QtWidgets.QWidget):
editClicked = QtCore.pyqtSignal(object)
def __init__(self, index):
super(EditButtonsWidget,self).__init__()
self.index = index
# ...
self.editButton.clicked.connect(lambda: self.editClicked.emit(index))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
for index in range(4):
# ...
persistenIndex = QtCore.QPersistentModelIndex(
self.table.indexFromItem(item2))
self.btn_sell = EditButtonsWidget(persistenIndex)
self.btn_sell.editClicked.connect(self.handleButtonClicked)
self.table.setCellWidget(index,2,self.btn_sell)
def handleButtonClicked(self, index):
if index.isValid():
print(index.row(), index.column())
Note that I used a QPersistentModelIndex, which ensures that the model index coordinates are always consistent even if the model changes (by deleting/inserting items or moving them).
Also note that you cannot directly use a QPersistentModelIndex for most functions that take a normal QModelIndex as parameter; in case you need that, you can recreate a QModelIndex like this:
modelIndex = self.table.model().index(
persistentIndex.row(), persistentIndex.column())
class RangeSelection(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QGridLayout(self)
self.setLayout(layout)
self._create_widgets()
layout.addWidget(self.select_combo, 1, 1)
layout.addWidget(self.stacked, 1, 2, 5, 1)
self.stacked.currentWidget().setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred)
self.stacked.currentChanged.connect(self.onCurrentChanged)
def onCurrentChanged(self):
currentw = self.stacked.currentWidget()
currentw.adjustSize()
if currentw == self.releasew:
currentw.sizeAdjustPolicy = QComboBox.AdjustToContentsOnFirstShow
self.adjustSize()
def _create_widgets(self):
self.stacked = QStackedWidget()
self.datew = QCalendarWidget()
self.datew.setVerticalHeaderFormat(QCalendarWidget.
NoVerticalHeader)
self.stacked.addWidget(self.datew)
self.buildidw = QLineEdit()
self.stacked.addWidget(self.buildidw)
self.releasew = QComboBox()
self.releasew.addItems([str(k) for k in sorted(releases())])
self.stacked.addWidget(self.releasew)
self.revw = QLineEdit()
self.stacked.addWidget(self.revw)
self.select_combo = QComboBox()
self.select_combo.addItems(['date', 'buildid', 'release', 'changeset'])
self.select_combo.activated.connect(self.stacked.setCurrentIndex)
I have this code where I am having four widgets in the QStackedWidget. When I run this code and change my selection in self.select_combo from date to release, the self.releasew combobox initially shows up as same size as that of the QCalendarWidget( which obviously looks horrible ). But, when I change my selection from release to any other value and then back to release, the self.releasew combobox shows up in the size it should. Why is this happening? What is the solution to this problem?
Note: I am using PyQt4. Also note that widgets for buildid and changeset do not show any abnormal behaviour.
I removed the setSizePolicy and sizeAdjustPolicy code. I also removed the call to self.adjustSize(). This worked. Though, I don't know why.