How do I make a pixmap's alignment centered to its column in a QTreeView? I have 2 columns with icons that are aligned to left, but I want one of them centered, so this needs to work on a single column and not force the whole table to one alignment.
I'm using a QTreeView with QAbstractItemModel as its model. On one column I flagged it as QtCore.Qt.DecorationRole and return a pixmap in the model's data() method so that it displays images along that column.
All works well, except that the images all align left, and for the life of me I can't get them centered horizontally.
In the data() method, I tried returning QtCore.Qt.AlignCenter if the role was QtCore.Qt.TextAlignmentRole, but that seems to only effect text (duh!).
Is there another way to achieve this? I'm not interested in taking the route of delegates if possible.
A possible solution is to overwrite the delegate's initStyleOption() method:
from PySide2 import QtCore, QtGui, QtWidgets
class IconCenterDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(IconCenterDelegate, self).initStyleOption(option, index)
option.decorationAlignment = (
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignCenter
)
option.decorationPosition = QtWidgets.QStyleOptionViewItem.Top
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QTreeView()
model = QtGui.QStandardItemModel(w)
w.setModel(model)
delegate = IconCenterDelegate(w)
w.setItemDelegateForColumn(1, delegate)
icons = [
"SP_TitleBarMinButton",
"SP_TitleBarMenuButton",
"SP_TitleBarMaxButton",
"SP_TitleBarCloseButton",
"SP_TitleBarNormalButton",
"SP_TitleBarShadeButton",
"SP_TitleBarUnshadeButton",
"SP_TitleBarContextHelpButton",
"SP_MessageBoxInformation",
"SP_MessageBoxWarning",
"SP_MessageBoxCritical",
"SP_MessageBoxQuestion",
"SP_DesktopIcon",
]
parent = model.invisibleRootItem()
for icon_name in icons:
icon = QtWidgets.QApplication.style().standardIcon(
getattr(QtWidgets.QStyle, icon_name)
)
its = []
for _ in range(3):
it = QtGui.QStandardItem()
it.setIcon(icon)
its.append(it)
parent.appendRow(its)
model.appendRow(it)
w.resize(640, 480)
w.expandAll()
w.show()
sys.exit(app.exec_())
If you want the icons of all the columns to be aligned centrally then you could overwrite the viewOptions() method of the view:
class TreeView(QtWidgets.QTreeView):
def viewOptions(self):
option = super().viewOptions()
option.decorationAlignment = (
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignCenter
)
option.decorationPosition = QtWidgets.QStyleOptionViewItem.Top
return option
Related
I am working on PyQt5 where I have a QTableWidget. It has a header column which I want to word wrap. Below is how the table looks like:
As we can see that the header label like Maximum Variation Coefficient has 3 words, thus its taking too much column width. How can wrap the words in the header.
Below is the code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
# Main Window
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 - QTableWidget'
self.left = 0
self.top = 0
self.width = 300
self.height = 200
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createTable()
self.layout = QVBoxLayout()
self.layout.addWidget(self.tableWidget)
self.setLayout(self.layout)
# Show window
self.show()
# Create table
def createTable(self):
self.tableWidget = QTableWidget()
# Row count
self.tableWidget.setRowCount(3)
# Column count
self.tableWidget.setColumnCount(2)
self.tableWidget.setHorizontalHeaderLabels(["Maximum Variation Coefficient", "Maximum Variation Coefficient"])
self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.tableWidget.setItem(0, 0, QTableWidgetItem("3.44"))
self.tableWidget.setItem(0, 1, QTableWidgetItem("5.3"))
self.tableWidget.setItem(1, 0, QTableWidgetItem("4.6"))
self.tableWidget.setItem(1, 1, QTableWidgetItem("1.2"))
self.tableWidget.setItem(2, 0, QTableWidgetItem("2.2"))
self.tableWidget.setItem(2, 1, QTableWidgetItem("4.4"))
# Table will fit the screen horizontally
self.tableWidget.horizontalHeader().setStretchLastSection(True)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I tried adding this self.tableWidget.setWordWrap(True) but this doesnt make any change. Can anyone give some good solution. Please help. Thanks
EDIT:
Also tried this :
self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignHCenter | Qt.Alignment(QtCore.Qt.TextWordWrap))
But it didnt worked
In order to achieve what you need, you must set your own header and proceed with the following two assumptions:
the header must provide the correct size hint height according to the section contents in case the width of the column is not sufficient;
the text alignment must include the QtCore.Qt.TextWordWrap flag, so that the painter knows that it can wrap text;
Do note that, while the second aspect might be enough in some situations (as headers are normally tall enough to fit at least two lines), the first point is mandatory as the text might require more vertical space, otherwise some text would be cut out.
The first point requires to subclass QHeaderView and reimplement sectionSizeFromContents():
class WrapHeader(QtWidgets.QHeaderView):
def sectionSizeFromContents(self, logicalIndex):
# get the size returned by the default implementation
size = super().sectionSizeFromContents(logicalIndex)
if self.model():
if size.width() > self.sectionSize(logicalIndex):
text = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.DisplayRole)
if not text:
return size
# in case the display role is numeric (for example, when header
# labels are not defined yet), convert it to a string;
text = str(text)
option = QtWidgets.QStyleOptionHeader()
self.initStyleOption(option)
alignment = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.TextAlignmentRole)
if alignment is None:
alignment = option.textAlignment
# get the default style margin for header text and create a
# possible rectangle using the current section size, then use
# QFontMetrics to get the required rectangle for the wrapped text
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_HeaderMargin, option, self)
maxWidth = self.sectionSize(logicalIndex) - margin * 2
rect = option.fontMetrics.boundingRect(
QtCore.QRect(0, 0, maxWidth, 10000),
alignment | QtCore.Qt.TextWordWrap,
text)
# add vertical margins to the resulting height
height = rect.height() + margin * 2
if height >= size.height():
# if the height is bigger than the one provided by the base
# implementation, return a new size based on the text rect
return QtCore.QSize(rect.width(), height)
return size
class App(QWidget):
# ...
def createTable(self):
self.tableWidget = QTableWidget()
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
# ...
Then, to set the word wrap flag, there are two options:
set the alignment flag on the underlying model with setHeaderData() for each existing column:
# ...
model = self.tableWidget.model()
default = self.tableWidget.horizontalHeader().defaultAlignment()
default |= QtCore.Qt.TextWordWrap
for col in range(self.tableWidget.columnCount()):
alignment = model.headerData(
col, QtCore.Qt.Horizontal, QtCore.Qt.TextAlignmentRole)
if alignment:
alignment |= QtCore.Qt.TextWordWrap
else:
alignment = default
model.setHeaderData(
col, QtCore.Qt.Horizontal, alignment, QtCore.Qt.TextAlignmentRole)
Use a QProxyStyle to override the painting of the header, by applying the flag on the option:
# ...
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
super().drawControl(control, option, painter, widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle(ProxyStyle())
ex = App()
sys.exit(app.exec_())
Finally, consider that:
using setSectionResizeMode with ResizeToContents or Stretch, along with setStretchLastSection, will always cause the table trying to use as much space as required by the headers upon showing the first time;
by default, QHeaderView sections are not clickable (which is a mandatory requirement for sorting) and the highlightSections property is also False; both QTableView and QTableWidget create their headers with those values as True, so when a new header is set you must explicitly change those aspects if sorting and highlighting are required:
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
self.tableWidget.horizontalHeader().setSectionsClickable(True)
self.tableWidget.horizontalHeader().setHighlightSections(True)
both sorting and section highlighting can create some issues, as the sort indicator requires further horizontal space and highlighted sections are normally shown with a bold font (but are shown normally while the mouse is pressed); all this might create some flickering and odd behavior; unfortunately, there's no obvious solution for these problems, but when using the QProxyStyle it's possible to avoid some flickering by overriding the font style:
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
if option.state & self.State_Sunken:
option.state |= self.State_On
super().drawControl(control, option, painter, widget)
I'm trying to have a + button added to a QTabBar. There's a great solution from years ago, with a slight issue that it doesn't work with PySide2. The problem is caused by the tabs auto resizing to fill the sizeHint, which in this case isn't wanted as the extra space is needed. Is there a way I can disable this behaviour?
I've tried QTabBar.setExpanding(False), but according to this answer, the property is mostly ignored:
The bad news is that QTabWidget effectively ignores that property, because it always forces its tabs to be the minimum size (even if you set your own tab-bar).
The difference being in PySide2, it forces the tabs to be the preferred size, where I'd like the old behaviour of minimum size.
Edit: Example with minimal code. The sizeHint width stretches the tab across the full width, whereas in older Qt versions it doesn't do that. I can't really override tabSizeHint since I don't know what the original tab size should be.
import sys
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def sizeHint(self):
return QtCore.QSize(100000, super().sizeHint().height())
class Test(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
tabWidget = QtWidgets.QTabWidget()
tabWidget.setTabBar(TabBar())
layout.addWidget(tabWidget)
tabWidget.addTab(QtWidgets.QWidget(), 'this shouldnt be stretched')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
I think there may be an easy solution to your problem (see below). Where the linked partial solution calculated absolute positioning for the '+' button, the real intent with Qt is always to let the layout engine do it's thing rather than trying to tell it specific sizes and positions. QTabWidget is basically a pre-built amalgamation of layouts and widgets, and sometimes you just have to skip the pre-built and build your own.
example of building a custom TabWidget with extra things across the TabBar:
import sys
from PySide2 import QtWidgets
from random import randint
class TabWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
#layout for entire widget
vbox = QtWidgets.QVBoxLayout(self)
#top bar:
hbox = QtWidgets.QHBoxLayout()
vbox.addLayout(hbox)
self.tab_bar = QtWidgets.QTabBar()
self.tab_bar.setMovable(True)
hbox.addWidget(self.tab_bar)
spacer = QtWidgets.QSpacerItem(0,0,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
hbox.addSpacerItem(spacer)
add_tab = QtWidgets.QPushButton('+')
hbox.addWidget(add_tab)
#tab content area:
self.widget_stack = QtWidgets.QStackedLayout()
vbox.addLayout(self.widget_stack)
self.widgets = {}
#connect events
add_tab.clicked.connect(self.add_tab)
self.tab_bar.currentChanged.connect(self.currentChanged)
def add_tab(self):
tab_text = 'tab' + str(randint(0,100))
tab_index = self.tab_bar.addTab(tab_text)
widget = QtWidgets.QLabel(tab_text)
self.tab_bar.setTabData(tab_index, widget)
self.widget_stack.addWidget(widget)
self.tab_bar.setCurrentIndex(tab_index)
def currentChanged(self, i):
if i >= 0:
self.widget_stack.setCurrentWidget(self.tab_bar.tabData(i))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = TabWidget()
test.show()
sys.exit(app.exec_())
All that said, I think the pre-built QTabWidget.setCornerWidget may be exactly what you're looking for (set a QPushButton to the upper-right widget). The example I wrote should much easier to customize, but also much more effort to re-implement all the same functionality. You will have to re-implement some of the signal logic to create / delete / select / rearrange tabs on your own. I only demonstrated simple implementation, which probably isn't bulletproof to all situations.
Using the code from Aaron as a base to start on, I managed to implement all the functionality required to work with my existing script:
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def minimumSizeHint(self):
"""Allow the tab bar to shrink as much as needed."""
minimumSizeHint = super(TabBar, self).minimumSizeHint()
return QtCore.QSize(0, minimumSizeHint.height())
class TabWidgetPlus(QtWidgets.QWidget):
tabOpenRequested = QtCore.Signal()
tabCountChanged = QtCore.Signal(int)
def __init__(self, parent=None):
self._addingTab = False
super(TabWidgetPlus, self).__init__(parent=parent)
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Bar layout
self._tabBarLayout = QtWidgets.QHBoxLayout()
self._tabBarLayout.setContentsMargins(0, 0, 0, 0)
self._tabBarLayout.setSpacing(0)
layout.addLayout(self._tabBarLayout)
self._tabBar = TabBar()
self._tabBarLayout.addWidget(self._tabBar)
for method in (
'isMovable', 'setMovable',
'tabsClosable', 'setTabsClosable',
'tabIcon', 'setTabIcon',
'tabText', 'setTabText',
'currentIndex', 'setCurrentIndex',
'currentChanged', 'tabCloseRequested',
):
setattr(self, method, getattr(self._tabBar, method))
self._plusButton = QtWidgets.QPushButton('+')
self._tabBarLayout.addWidget(self._plusButton) # TODO: Find location to insert
self._plusButton.setFixedWidth(20)
self._tabBarLayout.addStretch()
# Content area
self._contentArea = QtWidgets.QStackedLayout()
layout.addLayout(self._contentArea)
# Signals
self.currentChanged.connect(self._currentChanged)
self._plusButton.clicked.connect(self.tabOpenRequested.emit)
# Final setup
self.installEventFilter(self)
#QtCore.Slot(int)
def _currentChanged(self, i):
"""Update the widget."""
if i >= 0 and not self._addingTab:
self._contentArea.setCurrentWidget(self.tabBar().tabData(i))
def eventFilter(self, obj, event):
"""Intercept events until the correct height is set."""
if event.type() == QtCore.QEvent.Show:
self.plusButton().setFixedHeight(self._tabBar.geometry().height())
self.removeEventFilter(self)
return False
def tabBarLayout(self):
return self._tabBarLayout
def tabBar(self):
return self._tabBar
def plusButton(self):
return self._plusButton
def tabAt(self, point):
"""Get the tab at a given point.
This takes any layout margins into account.
"""
offset = self.layout().contentsMargins().top() + self.tabBarLayout().contentsMargins().top()
return self.tabBar().tabAt(point - QtCore.QPoint(0, offset))
def addTab(self, widget, name=''):
"""Add a new tab.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.addTab(name)
tabBar.setTabData(index, widget)
self._contentArea.addWidget(widget)
finally:
self._addingTab = False
return index
def insertTab(self, index, widget, name=''):
"""Inserts a new tab.
If index is out of range, a new tab is appended.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.insertTab(index, name)
tabBar.setTabData(index, widget)
self._contentArea.insertWidget(index, widget)
finally:
self._addingTab = False
return index
def removeTab(self, index):
"""Remove a tab."""
tabBar = self.tabBar()
self._contentArea.removeWidget(tabBar.tabData(index))
tabBar.removeTab(index)
if __name__ == '__main__':
import sys
import random
app = QtWidgets.QApplication(sys.argv)
test = TabWidgetPlus()
test.addTab(QtWidgets.QPushButton(), 'yeah')
test.insertTab(0, QtWidgets.QCheckBox(), 'what')
test.insertTab(1, QtWidgets.QRadioButton(), 'no')
test.removeTab(1)
test.setMovable(True)
test.setTabsClosable(True)
def tabTest():
name = 'Tab ' + str(random.randint(0, 100))
index = test.addTab(QtWidgets.QLabel(name), name)
test.setCurrentIndex(index)
test.tabOpenRequested.connect(tabTest)
test.tabCloseRequested.connect(lambda index: test.removeTab(index))
test.show()
sys.exit(app.exec_())
The one difference is if you're using tabWidget.tabBar().tabAt(point), this is no longer guaranteed to be correct as it doesn't take any margins into account. I set the margins to 0 so this shouldn't be an issue, but I also included those corrections in TabWidgetPlus.tabAt.
I only copied a few methods from QTabBar to QTabWidget as some may need extra testing.
This question is similar to the one in this this topic Preserve QStandardItem subclasses in drag and drop but with issue that I cant find a good solution for. That topic partially helps but fail on more complex task.
When I create an item in QTreeView I put that item in my array but when I use drag&Drop the item gets deleted and I no longer have access to it. I know that its because drag and drop copies the item and not moves it so I should use setData. I cant setData to be an object because even then the object gets copied and I lose reference to it.
Here is an example
itemsArray = self.addNewRow
def addNewRow(self)
'''some code with more items'''
itemHolder = QStandardItem("ProgressBarItem")
widget = QProgressBar()
itemHolder.setData(widget)
inx = self.model.rowCount()
self.model.setItem(inx, 0, itemIcon)
self.model.setItem(inx, 1, itemName)
self.model.setItem(inx, 2, itemHolder)
ix = self.model.index(inx,2,QModelIndex())
self.treeView.setIndexWidget(ix, widget)
return [itemHolder, itemA, itemB, itemC]
#Simplified functionality
data = [xxx,xxx,xxx]
for items in itemsArray:
items[0].data().setPercentage(data[0])
items[1].data().setText(data[1])
items[2].data().setChecked(data[2])
The code above works if I won't move the widget. The second I drag/drop I lose reference I lose updates on all my items and I get crash.
RuntimeError: wrapped C/C++ object of type QProgressBar has been deleted
The way I can think of of fixing this problem is to loop over entire treeview recursively over each row/child and on name match update item.... Problem is that I will be refreshing treeview every 0.5 second and have 500+ rows with 5-15 items each. Meaning... I don't think that will be very fast/efficient... if I want to loop over 5 000 items every 0.5 second...
Can some one suggest how I could solve this problem? Perhaps I can edit dropEvent so it does not copy/paste item but rather move item.... This way I would not lose my object in array
Qt can only serialize objects that can be stored in a QVariant, so it's no surprise that this won't work with a QWidget. But even if it could serialize widgets, I still don't think it would work, because index-widgets belong to the view, not the model.
Anyway, I think you will have to keep references to the widgets separately, and only store a simple key in the model items. Then once the items are dropped, you can retrieve the widgets and reset them in the view.
Here's a working demo script:
from PyQt4 import QtCore, QtGui
class TreeView(QtGui.QTreeView):
def __init__(self, *args, **kwargs):
super(TreeView, self).__init__(*args, **kwargs)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setAllColumnsShowFocus(True)
self.setModel(QtGui.QStandardItemModel(self))
self._widgets = {}
self._dropping = False
self._droprange = range(0)
def dropEvent(self, event):
self._dropping = True
super(TreeView, self).dropEvent(event)
for row in self._droprange:
item = self.model().item(row, 2)
self.setIndexWidget(item.index(), self._widgets[item.data()])
self._droprange = range(0)
self._dropping = False
def rowsInserted(self, parent, start, end):
super(TreeView, self).rowsInserted(parent, start, end)
if self._dropping:
self._droprange = range(start, end + 1)
def addNewRow(self, name):
model = self.model()
itemIcon = QtGui.QStandardItem()
pixmap = QtGui.QPixmap(16, 16)
pixmap.fill(QtGui.QColor(name))
itemIcon.setIcon(QtGui.QIcon(pixmap))
itemName = QtGui.QStandardItem(name.title())
itemHolder = QtGui.QStandardItem('ProgressBarItem')
widget = QtGui.QProgressBar()
widget.setValue(5 * (model.rowCount() + 1))
key = id(widget)
self._widgets[key] = widget
itemHolder.setData(key)
model.appendRow([itemIcon, itemName, itemHolder])
self.setIndexWidget(model.indexFromItem(itemHolder), widget)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.treeView = TreeView()
for name in 'red yellow green purple blue orange'.split():
self.treeView.addNewRow(name)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.treeView)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 600, 400)
window.show()
sys.exit(app.exec_())
Using Python 2.7.3 and Qt Designer 4.8.2: I'm new to Qt, how may I create a simple grid area that is clickable to generate a map? The image below illustrates what I intend.
In essence my main issue is the grid area, I'm unable to see anything like 'off the shelf' within Qt.
The nearest equivalent would seem to be a QTableWidget.
Here is a crude demo that should give you a start in the right direction:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = QtGui.QTableWidget(rows, columns, self)
self.table.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
header = self.table.horizontalHeader()
header.setResizeMode(QtGui.QHeaderView.Fixed)
header.setDefaultSectionSize(25)
header.hide()
header = self.table.verticalHeader()
header.setResizeMode(QtGui.QHeaderView.Fixed)
header.setDefaultSectionSize(25)
for row in range(rows):
item = QtGui.QTableWidgetItem('0x00')
self.table.setVerticalHeaderItem(row, item)
for column in range(columns):
item = QtGui.QTableWidgetItem()
item.setBackground(QtCore.Qt.white)
self.table.setItem(row, column, item)
self.table.itemPressed.connect(self.handleItemPressed)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
def handleItemPressed(self, item):
if item.background().color() == QtCore.Qt.black:
item.setBackground(QtCore.Qt.white)
else:
item.setBackground(QtCore.Qt.black)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(4, 8)
window.resize(300, 150)
window.show()
sys.exit(app.exec_())
One (clunky?) solution would be to draw your map image using a label widget with a pixmap set. You can achieve the click-ability by listening for mousePressEvent on that widget, upon which you can get a QMouseEvent object that contains mouse x, y position (both global and relative to the clicked widget). This can then be used to determine where on the image was clicked.
what I want to do is to change the color of a QTableWidget item, when I hover with the mouse over the item of my QTableWidget.
Firstly, the table widget needs to have mouse-tracking switched on to get the hover events.
Secondly, we need to find some signals that tell us when the mouse enters and leaves the table cells, so that the background colours can be changed at the right times.
The QTableWidget class has the cellEntered / itemEntered signals, but there is nothing for when the mouse leaves a cell. So, we will need to create some custom signals to do that.
The TableWidget class in the demo script below sets up the necessary cellExited / itemExited signals, and then shows how everything can be hooked up to change the item background when hovering with the mouse:
from PyQt4 import QtGui, QtCore
class TableWidget(QtGui.QTableWidget):
cellExited = QtCore.pyqtSignal(int, int)
itemExited = QtCore.pyqtSignal(QtGui.QTableWidgetItem)
def __init__(self, rows, columns, parent=None):
QtGui.QTableWidget.__init__(self, rows, columns, parent)
self._last_index = QtCore.QPersistentModelIndex()
self.viewport().installEventFilter(self)
def eventFilter(self, widget, event):
if widget is self.viewport():
index = self._last_index
if event.type() == QtCore.QEvent.MouseMove:
index = self.indexAt(event.pos())
elif event.type() == QtCore.QEvent.Leave:
index = QtCore.QModelIndex()
if index != self._last_index:
row = self._last_index.row()
column = self._last_index.column()
item = self.item(row, column)
if item is not None:
self.itemExited.emit(item)
self.cellExited.emit(row, column)
self._last_index = QtCore.QPersistentModelIndex(index)
return QtGui.QTableWidget.eventFilter(self, widget, event)
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = TableWidget(rows, columns, self)
for column in range(columns):
for row in range(rows):
item = QtGui.QTableWidgetItem('Text%d' % row)
self.table.setItem(row, column, item)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
self.table.setMouseTracking(True)
self.table.itemEntered.connect(self.handleItemEntered)
self.table.itemExited.connect(self.handleItemExited)
def handleItemEntered(self, item):
item.setBackground(QtGui.QColor('moccasin'))
def handleItemExited(self, item):
item.setBackground(QtGui.QTableWidgetItem().background())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(6, 3)
window.setGeometry(500, 300, 350, 250)
window.show()
sys.exit(app.exec_())
You can achieve your goal pretty easily using the proper signals as proved by the following simple code:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class TableViewer(QMainWindow):
def __init__(self, parent=None):
super(TableViewer, self).__init__(parent)
self.table = QTableWidget(3, 3)
for row in range (0,3):
for column in range(0,3):
item = QTableWidgetItem("This is cell {} {}".format(row+1, column+1))
self.table.setItem(row, column, item)
self.setCentralWidget(self.table)
self.table.setMouseTracking(True)
self.current_hover = [0, 0]
self.table.cellEntered.connect(self.cellHover)
def cellHover(self, row, column):
item = self.table.item(row, column)
old_item = self.table.item(self.current_hover[0], self.current_hover[1])
if self.current_hover != [row,column]:
old_item.setBackground(QBrush(QColor('white')))
item.setBackground(QBrush(QColor('yellow')))
self.current_hover = [row, column]
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
tv = TableViewer()
tv.show()
sys.exit(app.exec_())
You may be interested in other signals too, especially itemEntered. However, if you want total control over the editing and display of items then using delegates (via the QTableWidget.setItemDelegate method) is strongly recommended.
UPDATE:
sorry, I had forgotten the second part of the problem i.e. what happens when the mouse exits a cell. Even then the problem can be solved easily without using events. See the updated code, please.
There are no events based on QTableWidgetItem, but you can do this:
reimplement the mouseMoveEvent() of QTableWidget, you can get the mouse position;
use itemAt() method to get the item under your mouse cursor;
customize your item;
This may simalute what you want.
I know this is old but wanted to update a couple of parts to it as I came across this page looking for a similar solution. This has a couple of parts to it, one is similar to the above but avoids the NoneType error if the cell is empty. Additionally, it will change the color of the highlighted cell, but also update a tooltip for the cell to display the contents of the cell in a tooltip. Nice if you have cells with runoffs 123...
Sure it could be cleaned up a bit, but works for PyQt5. Cheers!
def cellHover(self, row, column):
item = self.My_Table1.item(row, column)
old_item = self.My_Table1.item(self.current_hover[0], self.current_hover[1])
if item is not None:
if self.current_hover != [row,column]:
text = item.text()
if text is not None:
self.My_Table1.setToolTip(text)
item.setBackground(QBrush(QColor('#bbd9f7')))
old_item.setBackground(QBrush(QColor('white')))
self.current_hover = [row, column]