I want to make some data hidden in QTableWidget until a specific cell is clicked. Right now I only manage to display "*" instead of actual value. I think I could connect somehow with action when the specific cell is clicked and replaced the value of the clicked cell. I also know that there is a function setData() that can be invoked on QTableWidgetItem and give me wanted behavior. But I cannot find any useful example for Python implementation of qt. What is the best solution to this problem?
def setTableValues(self):
self.table.setRowCount(len(self.tableList))
x = 0
for acc in self.tableList:
y = 0
for val in acc.getValuesAsList():
if isinstance(val,Cipher):
val = "*"*len(val.getDecrypted())
item = QTableWidgetItem(val)
self.table.setItem(x,y,item)
y += 1
x += 1
self.table.resizeRowsToContents()
self.table.resizeColumnsToContents()
You can associate a flag with a role that indicates the visibility of the text and then use a delegate to hide the text. That flag will change when the items are pressed:
from PyQt5 import QtCore, QtGui, QtWidgets
VisibilityRole = QtCore.Qt.UserRole + 1000
class VisibilityDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if not index.data(VisibilityRole):
option.text = "*" * len(option.text)
class TableWidget(QtWidgets.QTableWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
delegate = VisibilityDelegate(self)
self.setItemDelegate(delegate)
self.visibility_index = QtCore.QModelIndex()
self.pressed.connect(self.on_pressed)
#QtCore.pyqtSlot(QtCore.QModelIndex)
def on_pressed(self, index):
if self.visibility_index.isValid():
self.model().setData(self.visibility_index, False, VisibilityRole)
self.visibility_index = index
self.model().setData(self.visibility_index, True, VisibilityRole)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = TableWidget(10, 4)
for i in range(w.rowCount()):
for j in range(w.columnCount()):
it = QtWidgets.QTableWidgetItem("{}-{}".format(i, j))
w.setItem(i, j, it)
w.show()
w.resize(640, 480)
sys.exit(app.exec_())
Related
I want to make that when i click on particular cell on the QTableWidget it will block the corresponding rows and I want to return the value of each row selected into the QLineEdit.
I couldnt seem to find the solution my code only return when i click it will block the rows but not getting the value.
def click_inventorytable(self):
self.tableInventory.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
index = (self.tableInventory.selectionModel().currentIndex())
value = index.row()
list = [value]
if(len(list)==6):
self.lineproductnameinv.setText((list[1]))
self.linedescinv.setText((list[2]))
self.combocateinv.setText((list[3]))
self.linepriceinv.setText((list[4]))
self.linecurrentstock.setText((list[5]))
self.addstock.setText('')
Since the OP does not provide an MRE then I will create a simple demo of how you can implement the functionality of mapping the elements of the selected row of a table in various widgets.
The appropriate widgets must be chosen so that the user does not enter incorrect values, for example in the case of stock if a QLineEdit is used the user could enter a word which does not make sense since a number is expected so it is better to use a QSpinBox.
Also when the data is saved in the table it is not good to convert it to a string since it loses the way to differentiate them, it is better to save the value through setData() associated with the Qt::DisplayRole role.
Finally, the key to the solution is to use a QDataWidgetMapper that allows mapping parts of a model in widgets, so each time a row is selected the currentIndex of the mapper is updated and it sends the information to the editors.
from functools import cached_property
import random
import sys
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
LABELS = (
"Product ID",
"Product Name",
"Description",
"Category",
"Price",
"Stock",
)
CATEGORY_OPTIONS = (
"OPTION1",
"OPTION2",
"OPTION3",
"OPTION4",
)
def __init__(self, parent=None):
super().__init__(parent)
self.mapper.setModel(self.tableWidget.model())
self.tableWidget.selectionModel().currentChanged.connect(
self.mapper.setCurrentModelIndex
)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
lay = QtWidgets.QHBoxLayout(central_widget)
flay = QtWidgets.QFormLayout()
lay.addLayout(flay)
lay.addWidget(self.tableWidget, stretch=1)
editors = (
self.name_edit,
self.description_edit,
self.category_combo,
self.price_edit,
self.stock_edit,
)
for i, (label, widget) in enumerate(zip(self.LABELS[1:], editors)):
flay.addRow(label, widget)
self.mapper.addMapping(widget, i)
self.fillTable()
self.resize(960, 480)
#cached_property
def tableWidget(self):
table = QtWidgets.QTableWidget(
0,
len(self.LABELS),
selectionBehavior=QtWidgets.QAbstractItemView.SelectRows,
selectionMode=QtWidgets.QAbstractItemView.SingleSelection,
)
table.setHorizontalHeaderLabels(self.LABELS)
return table
#cached_property
def name_edit(self):
return QtWidgets.QLineEdit()
#cached_property
def description_edit(self):
return QtWidgets.QLineEdit()
#cached_property
def category_combo(self):
combo = QtWidgets.QComboBox()
combo.addItems(["--Null--"] + list(self.CATEGORY_OPTIONS))
combo.setCurrentIndex(0)
return combo
#cached_property
def price_edit(self):
return QtWidgets.QDoubleSpinBox(maximum=2147483647)
#cached_property
def stock_edit(self):
return QtWidgets.QSpinBox(maximum=2147483647)
#cached_property
def mapper(self):
return QtWidgets.QDataWidgetMapper()
def fillTable(self):
self.tableWidget.setRowCount(0)
for i in range(30):
self.tableWidget.insertRow(self.tableWidget.rowCount())
values = (
i,
f"name-{i}",
f"Description-{i}",
random.choice(self.CATEGORY_OPTIONS),
random.uniform(100, 2000),
random.randint(0, 100),
)
for j, value in enumerate(values):
it = QtWidgets.QTableWidgetItem()
it.setData(QtCore.Qt.DisplayRole, value)
it.setFlags(it.flags() & ~QtCore.Qt.ItemIsEditable)
self.tableWidget.setItem(i, j, it)
def main():
app = QtWidgets.QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
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.
I'm trying to create a layout where the user can click a combination of buttons, each button's click will add a 1 or a 0 to a certain position in a list which is the actual input I'd like to get out of it.
However, I don't know how to manage a cluster of buttons, there are 48 buttons and managing them all individually is the antithesis of DRY.
Here's an example attempt:
num_buttons = 48
press_list = [None]*len(num_buttons)
button_list = list()
for button in range(num_buttons):
some_btn = QtWidgets.QPushButton(SomeDialog)
some_btn.setGeometry(QtCore.QRect(70, 90, 141, 28))
some_btn.setObjectName("button_%s" % (button,))
some_btn.clicked.connect(self.button_pressed(button))
def button_pressed(self, button_num):
if press_list[button_num] == 1:
press_list[button_num] = 0
else:
press_list[button_num] = 1
(clicks turn buttons blue), is it possible to have a set geometry through the Qt designer and still do something like this, or will I have to calculate the setGeometry positions and add the buttons through the code?
If you want to pass an additional argument to the slots you can use partial as shown below:
import sys
from functools import partial
from PyQt5 import QtWidgets
QSS = """
QToolButton::checked{
background-color: blue
}
"""
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.listA = [0 for _ in range(24)]
self.listB = [0 for _ in range(24)]
lay = QtWidgets.QVBoxLayout(self)
hlay1 = QtWidgets.QHBoxLayout()
hlay2 = QtWidgets.QHBoxLayout()
lay.addLayout(hlay1)
lay.addLayout(hlay2)
for i, val in enumerate(self.listA):
button = QtWidgets.QToolButton()
button.setCheckable(True)
hlay1.addWidget(button)
button.clicked.connect(partial(self.callbackA, i))
button.setStyleSheet(QSS)
for i, val in enumerate(self.listB):
button = QtWidgets.QToolButton()
button.setCheckable(True)
hlay2.addWidget(button)
button.clicked.connect(partial(self.callbackB, i))
button.setStyleSheet(QSS)
def callbackA(self, index, state):
self.listA[index] = 1 if state else 0
print("listA: ")
print(self.listA)
def callbackB(self, index, state):
self.listB[index] = 1 if state else 0
print("listB: ")
print(self.listB)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I have a QTableView in my GUI in which I want to have some table cells in which I can insert line breaks using something like \n or <br>.
So far I have tried to set an QLabel as the IndexWidget:
l = QLabel(val[2])
self.setRowHeight(i, int(l.height() / 8))
l.setAutoFillBackground(True)
self.setIndexWidget(QAbstractItemModel.createIndex(self.results_model, i, 2), l)
The Problem with this approach is the code is not very clean and cannot just be done in the AbstractTableModel without this code to replace the cell with a widget. The second problem is, that when selecting a row with a widget in it the blue highlighting doesn't apply to the cell. Another problem is that the resizeRowsToContents() method doesn't take the height of this widget in to consideration.
Any Ideas would be very appreciated, thanks!
One way to implement this task is to use an HtmlDelegate, in this case the line break will be given by <br>:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class HTMLDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
painter.save()
doc = QTextDocument()
doc.setHtml(opt.text)
opt.text = "";
style = opt.widget.style() if opt.widget else QApplication.style()
style.drawControl(QStyle.CE_ItemViewItem, opt, painter)
painter.translate(opt.rect.left(), opt.rect.top())
clip = QRectF(0, 0, opt.rect.width(), opt.rect.height())
doc.drawContents(painter, clip)
painter.restore()
def sizeHint(self, option, index ):
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
doc = QTextDocument()
doc.setHtml(opt.text);
doc.setTextWidth(opt.rect.width())
return QSize(doc.idealWidth(), doc.size().height())
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QTableView()
model = QStandardItemModel(4, 6)
delegate = HTMLDelegate()
w.setItemDelegate(delegate)
w.setModel(model)
w.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
w.show()
sys.exit(app.exec_())
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]