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]
Related
The code below creates a single QTableView and QPushButton. When the button is clicked I would like to toggle the current selection (inverse it): what used to be selected is now deselected and what used to be deselected is selected.
Finally I would like to remove (delete) the rows that are now selected leaving only those that are deselected.
Question: How to achieve it?
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
app = QApplication([])
class Dialog(QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setLayout(QVBoxLayout())
self.view = QTableView(self)
self.view.setSelectionBehavior(QTableWidget.SelectRows)
self.view.setSortingEnabled(True)
self.view.sortByColumn(0, Qt.DescendingOrder)
self.view.setModel(QStandardItemModel(4, 4))
for each in [(row, col, QStandardItem('item %s_%s' % (row, col))) for row in range(4) for col in range(4)]:
self.view.model().setItem(*each)
self.layout().addWidget(self.view)
btn1 = QPushButton('Invert selection then remove what selected')
btn1.clicked.connect(self.invertSelectionRemoveSelected)
self.layout().addWidget(btn1)
self.resize(500, 250)
self.show()
def invertSelectionRemoveSelected(self):
print 'invertSelectionRemoveSelected'
dialog = Dialog()
app.exec_()
You have to iterate to get the QModelIndex associated with each cell, and use the QItemSelection to invert the selection of each cell.
def invertSelectionRemoveSelected(self):
model = self.view.model()
for i in range(model.rowCount()):
for j in range(model.columnCount()):
ix = model.index(i, j)
self.view.selectionModel().select(ix, QItemSelectionModel.Toggle)
# delete rows
for ix in reversed(self.view.selectionModel().selectedRows()):
model.removeRow(ix.row())
Another Solution:
From your request I understand that you want to eliminate the unselected rows, and deselect all the others afterwards. So the next solution does it directly.
def invertSelectionRemoveSelected(self):
model = self.view.model()
rows_selected =[ix.row() for ix in self.view.selectionModel().selectedRows()]
[model.removeRow(i) for i in reversed(range(model.rowCount())) if i not in rows_selected]
self.view.clearSelection()
Note: #eyllanesc's answer is shorter, here
Before deleting selected lines we should know the indexes of them. As you may guess, deleting an item changes others indexes.
def invertSelectionRemoveSelected(self):
#from #eyllanesc's answer, inverse selected items
model = self.view.model()
for i in range(model.rowCount()):
for j in range(model.columnCount()):
ix = model.index(i, j)
self.view.selectionModel().select(ix, QItemSelectionModel.Toggle)
#delete selected items
index_list = []
for model_index in self.view.selectionModel().selectedRows():
index = QPersistentModelIndex(model_index)
index_list.append(index)
for index in index_list:
model.removeRow(index.row())
I have a QTableWidget with floats or complex entries that need a lot of horizontal space. Displaying the values with reduced number of digits via string formatting works fine, but obviously I loose precision when editing and storing entries in the table.
I have found a solution for QLineEdit widgets by using an eventFilter: A FocusIn event copies the stored value with full precision to the QLineEdit textfield, a FocusOut event or a Return_Key stores the changed value and overwrites the text field with reduced number of digits.
Using the same approach with a QTableWidgets gives me the following (possibly related) problems:
FocusIn and FocusOut events are not generated as expected: When I double-click on an item, I get a FocusOut event, clicking on another item produces a FocusIn event
I can't copy the content of my edited, selected item, I always get the unedited value.
Selecting an item by clicking on it doesn't produce an event.
I've tried evaluating QTableWidgetItem events, but I don't receive any - do I need to setup an event filter on every QTableWidgetItem? If so, do I need to disconnect the QTableWidgetItem eventFilters every time I resize the table (which do frequently in my application)? Would it make sense to populate my table with QLineEdit widgets instead?
The attached MWE is not exactly small, but I could shrink it any further.
# -*- coding: utf-8 -*-
#from PyQt5.QWidgets import ( ...)
from PyQt4.QtGui import (QApplication, QWidget, QTableWidget, QTableWidgetItem,
QLabel, QVBoxLayout)
import PyQt4.QtCore as QtCore
from PyQt4.QtCore import QEvent
from numpy.random import randn
class EventTable (QWidget):
def __init__(self, parent = None):
super(EventTable, self).__init__(parent)
self.myTable = QTableWidget(self)
self.myTable.installEventFilter(self) # route all events to self.eventFilter()
myQVBoxLayout = QVBoxLayout()
myQVBoxLayout.addWidget(self.myTable)
self.setLayout(myQVBoxLayout)
self.rows = 3; self.columns = 4 # table + data dimensions
self.data = randn(self.rows, self.columns) # initial data
self._update_table() # create table
def eventFilter(self, source, event):
if isinstance(source, (QTableWidget, QTableWidgetItem)):
# print(type(source).__name__, event.type()) #too much noise
if event.type() == QEvent.FocusIn: # 8: enter widget
print(type(source).__name__, "focus in")
self.item_edited = False
self._update_table_item() # focus: display data with full precision
return True # event processing stops here
elif event.type() == QEvent.KeyPress:
print(type(source).__name__, "key pressed")
self.item_edited = True # table item has been changed
key = event.key() # key press: 6, key release: 7
if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: # store entry
self._store_item() # store edited data in self.data
return True
elif key == QtCore.Qt.Key_Escape: # revert changes
self.item_edited = False
self._update_table() # update table from self.data
return True
elif event.type() == QEvent.FocusOut: # 9: leave widget
print(type(source).__name__, "focus out")
self._store_item()
self._update_table_item() # no focus: use reduced precision
return True
return super(EventTable, self).eventFilter(source, event)
def _update_table(self):
"""(Re-)Create the table with rounded data from self.data """
self.myTable.setRowCount(self.rows)
self.myTable.setColumnCount(self.columns)
for col in range(self.columns):
for row in range(self.rows):
self.myTable.setItem(row,col,
QTableWidgetItem(str("{:.3g}".format(self.data[row][col]))))
self.myTable.resizeColumnsToContents()
self.myTable.resizeRowsToContents()
def _update_table_item(self, source = None):
""" Re-)Create the current table item with full or reduced precision. """
row = self.myTable.currentRow()
col = self.myTable.currentColumn()
item = self.myTable.item(row, col)
if item: # is something selected?
if not item.isSelected(): # no focus, show self.data[row][col] with red. precision
print("\n_update_item (not selected):", row, col)
item.setText(str("{:.3g}".format(self.data[row][col])))
else: # in focus, show self.data[row][col] with full precision
item.setText(str(self.data[row][col]))
print("\n_update_item (selected):", row, col)
def _store_item(self):
""" Store the content of item in self.data """
if self.item_edited:
row = self.myTable.currentRow()
col = self.myTable.currentColumn()
item_txt = self.myTable.item(row, col).text()
self.data[row][col] = float(str(item_txt))
print("\n_store_entry - current item/data:", item_txt, self.data[row][col])
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
mainw = EventTable()
app.setActiveWindow(mainw)
mainw.show()
sys.exit(app.exec_())
You're going about this in completely the wrong way. These kinds of use-cases are already catered for by the existing APIs, so there are several solutions available that are much simpler than what you currently have.
Probably the simplest of all would be to use a QStyledItemDelegate and reimplement its dispalyText method. This will allow you to store the full values in the table, but format them differently for display. When editing, the full value will always be shown (as a string):
from PyQt4.QtGui import (QApplication, QWidget, QTableWidget, QTableWidgetItem,
QLabel, QVBoxLayout,QStyledItemDelegate)
import PyQt4.QtCore as QtCore
from PyQt4.QtCore import QEvent
from numpy.random import randn
class ItemDelegate(QStyledItemDelegate):
def displayText(self, text, locale):
return "{:.3g}".format(float(text))
class EventTable (QWidget):
def __init__(self, parent = None):
super(EventTable, self).__init__(parent)
self.myTable = QTableWidget(self)
self.myTable.setItemDelegate(ItemDelegate(self))
myQVBoxLayout = QVBoxLayout()
myQVBoxLayout.addWidget(self.myTable)
self.setLayout(myQVBoxLayout)
self.rows = 3; self.columns = 4 # table + data dimensions
self.data = randn(self.rows, self.columns) # initial data
self._update_table() # create table
def _update_table(self):
self.myTable.setRowCount(self.rows)
self.myTable.setColumnCount(self.columns)
for col in range(self.columns):
for row in range(self.rows):
item = QTableWidgetItem(str(self.data[row][col]))
self.myTable.setItem(row, col, item)
self.myTable.resizeColumnsToContents()
self.myTable.resizeRowsToContents()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
mainw = EventTable()
app.setActiveWindow(mainw)
mainw.show()
sys.exit(app.exec_())
NB: it's tempting to use item roles to solve this issue. However, both QTableWidgetItem and QStandardItem treat the DisplayRole and EditRole as one role, which means it would be necessary to reimplement their data and setData methods to get the required functionality.
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.
After every click on a "Add Item..." button, I want a row(label, button) to be appended to the layout (below that same button).
So, it should add one row per click.
Problem is it adds the following:
1st click: 1 row added (total item rows = 1) (correct)
2nd click: 2 rows added (total item rows = 3) (should be 2)
3rd click: 3 rows added (total item rows = 6) (should be 3)
Here's the relevant code:
from PySide import QtCore
from PySide import QtGui
import sys
class Form(QtGui.QDialog):
items = []
def __init__(self, parent = None):
super(Form, self).__init__(parent)
self.btn = QtGui.QPushButton("Add Item...")
self.btn.clicked.connect(self.item_toggle)
self.layout = self.initial_view()
self.setLayout(self.layout)
def item_toggle(self, add = True):
layout = self.layout
if add:
string = ("25468 5263.35 54246") #####random text
self.items.append(string)
for item in self.items:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(item, rem_btn)
self.setLayout(layout)
def remove_item(self, ):
#self.items.pop() #something to delete that item
self.add_item("False") #redraw items part
def initial_view(self, ):
layout = QtGui.QFormLayout()
#adding to layout
layout.addRow(self.btn)
return layout
app = QtGui.QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
I figure its not erasing the previous widgets, but I can't quiet figure it out. Also, a way to to remove the items(remove_item function), would also help me out.
I hope I explained well and you get what I'm trying to do...
Any help will be appreciated. Thanks in advance
To prevent adding additional items to your list just remove the for loop and just do the following:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(string, rem_btn)
What you have to know about the addRow call, is that this add your QPushButton in the second column, and auto-creates a QLabel for the first column. So when you want to remove the row, you will have to remove both the button and the label.
Now about the remove. I guess the easiest way to start would be to find out which button is asking to be removed.
sending_button = self.sender()
At this point you will need to get access to the QLabel. Luckily there is a call on the layout called labelForField which will return the QLabel associated with your QPushButton
labelWidget = self.layout.labelForField(sending_button)
Then to remove the actual widgets
sending_button.deleteLater()
if labelWidget:
labelWidget.deleteLater()