In my PyQT window, I have a table containing QComboBox in one column. How can the QComboBox later be changed to the regular QTableWidgetItem to display some text?
I tried the following but the QComboBox was not replaced by text from QTableWidgetItem.
myTable= QTableWidget()
myTable.setRowCount(6)
myTable.setColumnCount(2)
myTable.setHorizontalHeaderLabels(QString("Name;Age;").split(";"))
myTable.horizontalHeader().setResizeMode(QHeaderView.Stretch)
# Populate with QComboBox in column 1
for i, name in enumerate(nameList):
myTable.setItem(i, 0, QTableWidgetItem(name ))
ageCombo = QComboBox()
for option in ageComboOptions:
ageCombo.addItem(option)
myTable.setCellWidget(i, 1, ageCombo)
# Change column 1 to QTableWidgetItem
for i, name in enumerate(nameList):
myTable.setItem(i, 1, QTableWidgetItem(name))
The short answer is that if you just removeCellWidget you'll get what you want. Example code below.
But in more detail:
The "Item" as set by setItem and the "Widget" as set by setCellWidget are different - they play different roles. The item carries the data for the cell: in the model view architecture it's in the model. The widget is doing the display: it's in the view. So, when you set the cell widget you might still expect it to use an item in the model behind it. However, the QTableWidget provides a simplified API to the full model view architecture as used in QT (e.g. see the QTableView and QAbstractitemModel). It provides its own default model which you access via an item for each cell. Then, when you replace the widget on a cell it dispenses with any item at all and just allows you to control the widget directly. Remove the widget and it goes back to using the item.
Here's a working example:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
self.myTable= QtGui.QTableWidget()
self.myTable.setRowCount(1)
self.myTable.setColumnCount(2)
item1 = QtGui.QTableWidgetItem("a")
self.myTable.setItem(0, 0, item1)
item2 = QtGui.QTableWidgetItem("b")
self.myTable.setItem(0, 1, item2)
self.setCentralWidget(self.myTable)
menubar = QtGui.QMenuBar(self)
self.setMenuBar(menubar)
menu = QtGui.QMenu(menubar)
menu.setTitle("Test")
action = QtGui.QAction(self)
action.setText("Test 1")
action.triggered.connect(self.test1)
menu.addAction(action)
action = QtGui.QAction(self)
action.setText("Test 2")
action.triggered.connect(self.test2)
menu.addAction(action)
menubar.addAction(menu.menuAction())
self.show()
def test1(self):
self.myTable.removeCellWidget(0, 1)
def test2(self):
combo = QtGui.QComboBox()
combo.addItem("c")
combo.addItem("d")
self.myTable.setCellWidget(0, 1, combo)
def main():
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Related
Following is the PyQT code,
from PyQt5.QtWidgets import *
import sys
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QGridLayout()
self.setLayout(layout)
# Add toolbar and items
toolbox = QToolBox()
layout.addWidget(toolbox, 0, 0)
label = QLabel()
toolbox.addItem(label, "Students")
label = QLabel()
toolbox.addItem(label, "Teachers")
label = QLabel()
toolbox.addItem(label, "Directors")
app = QApplication(sys.argv)
screen = Window()
screen.show()
sys.exit(app.exec_())
What I want is whenever an Item in that ToolBox is selected, its icon should change from "arrow-straight" to "arrow-down" to represent that this item is currently opened and others are closed. Now, if another item is clicked, then the first item's arrow should again be changed back to arrow-straight and the item that is clicked gets its arrow changed now.
How can I accomplish this in PyQT? Be it from the designer or from the Code Logic.
EDIT: For example, look at this designer below,
Since the "Registration Details" is selected, so I want its arrow to be replaced with another Icon (Say "arrow down" icon). And once I select some other item in the toolbox (like View Clashes), then Registration Details' arrow should be replaced with the old arrow and View Clashes arrow should get changed to another Icon.
The code for this in Pyuic file is this,
icon = QIcon()
icon.addFile(u":/icons/icons/arrow-right.svg", QSize(), QIcon.Normal, QIcon.Off)
self.toolBox.addItem(self.page_2, icon, u"Registration Details")
You can set a default icon when adding items, and then connect the currentChanged signal in order to set the other one.
If you create a basic list with both icons, setting the proper icon is even simpler, as you only need to cycle through all items and set the icon based on the index match.
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.arrowIcons = []
for direction in ('right', 'down'):
self.arrowIcons.append(QtGui.QIcon(
':/icons/icons/arrow-{}.svg'.format(direction)))
layout = QtWidgets.QVBoxLayout(self)
self.toolBox = QtWidgets.QToolBox()
layout.addWidget(self.toolBox)
self.toolBox.currentChanged.connect(self.updateIcons)
for i in range(5):
self.toolBox.addItem(
QtWidgets.QLabel(),
self.arrowIcons[0],
'Item {}'.format(i + 1))
def updateIcons(self, index):
for i in range(self.toolBox.count()):
self.toolBox.setItemIcon(i, self.arrowIcons[index == i])
In PyQt, I have a basic program. It consists of 2 combo boxes, 1 line edit and 3 checkboxes. What I want to do is, depending on the item of the first combo box, hide / show specific widgets. However, I keep getting an error: 'ExportDialog' object has no attribute 'exportSetDelimiter_lbl'. I have defined this widget above in initUI, and I run initUIininit`, so I'm not sure why I am getting this error. Here is my code:
from PyQt5 import QtGui, QtCore, QtWidgets
import sys
class ExportDialog(QtWidgets.QMainWindow):
def __init__(self,imagePath):
super(ExportDialog, self).__init__()
self.initUI(imagePath)
#Set The GUI Position And Size
self.setGeometry(500, 500, 600, 450)
#Set The GUI Title
self.setWindowTitle("Export Deck")
#Set The GUI Icon
self.setWindowIcon(QtGui.QIcon('MainFlashcardAppIcon.png'))
def initUI(self, PATH):
#Create The New Deck Label
self.exportFormat_lbl = QtWidgets.QLabel(self)
self.exportFormat_lbl.setText("Export Format: ")
exportFormat_font = QtGui.QFont()
exportFormat_font.setPointSize(8)
self.exportFormat_lbl.setFont(exportFormat_font)
self.exportFormat_lbl.adjustSize()
self.exportFormat_combo = QtWidgets.QComboBox()
self.exportFormat_combo.setMinimumHeight(35)
self.exportFormat_combo.setFixedWidth(380)
self.exportFormat_combo.currentTextChanged.connect(self.on_combobox_changed)
self.exportDeckName_lbl = QtWidgets.QLabel(self)
self.exportDeckName_lbl.setText("Include: ")
self.exportDeckName_lbl.setFont(exportFormat_font)
self.exportDeckName_lbl.adjustSize()
self.exportDeckName_combo = QtWidgets.QComboBox()
self.exportDeckName_combo.setMinimumHeight(35)
self.exportDeckName_combo.setFixedWidth(380)
self.exportFormat_combo.addItem(".TXT")
self.exportFormat_combo.addItem(".CSV")
self.exportFormat_combo.addItem(".DB")
self.exportSetDelimiter_lbl = QtWidgets.QLabel()
self.exportSetDelimiter_lbl.setText("Set Delimiter (Leave blank for standard delimited):")
self.exportSetDelimiter_txt = QtWidgets.QLineEdit()
self.exportSetDelimiter_txt.setMaxLength(1)
self.exportSetDelimiter = QtWidgets.QLineEdit()
vboxExport_setDelimiter = QtWidgets.QVBoxLayout()
vboxExport_setDelimiter.addWidget(self.exportSetDelimiter_lbl)
vboxExport_setDelimiter.addWidget(self.exportSetDelimiter_txt)
self.includeMedia_check = QtWidgets.QCheckBox("Include HTML and Media References")
self.includeTags_check = QtWidgets.QCheckBox("Include Tags")
self.includeAllSQL_check = QtWidgets.QCheckBox("Include All SQL Tables")
self.exportFormat_combo.addItem("B3 Biology")
self.exportFormat_combo.addItem("B2 Biology")
self.exportFormat_combo.addItem("B1 Biology")
self.allComboList = ["B3 Biology", "B2 Biology", "B1 Biology"]
self.exportDeckName_combo.setCurrentIndex(self.allComboList.index(PATH))
#Create Confirm Button
self.confirmButton = QtWidgets.QPushButton(self)
self.confirmButton.setText("OK")
self.confirmButton.clicked.connect(self.createDeck)
#Create Cancel Button
self.cancelButton = QtWidgets.QPushButton(self)
self.cancelButton.setText("Cancel")
self.cancelButton.clicked.connect(self.close)
hboxExportFormat = QtWidgets.QHBoxLayout()
hboxExportFormat.addWidget(self.exportFormat_lbl)
hboxExportFormat.addStretch()
hboxExportFormat.addWidget(self.exportFormat_combo)
hboxExportName = QtWidgets.QHBoxLayout()
hboxExportName.addWidget(self.exportDeckName_lbl)
hboxExportName.addStretch()
hboxExportName.addWidget(self.exportDeckName_combo)
hboxButtonsBottom = QtWidgets.QHBoxLayout()
hboxButtonsBottom.addStretch()
hboxButtonsBottom.addWidget(self.confirmButton)
hboxButtonsBottom.addWidget(self.cancelButton)
#Create The VBoxLayout
mainLayout = QtWidgets.QVBoxLayout(self)
mainLayout.addLayout(hboxExportFormat)
mainLayout.addLayout(hboxExportName)
mainLayout.addLayout(vboxExport_setDelimiter)
mainLayout.addWidget(self.includeMedia_check)
mainLayout.addWidget(self.includeTags_check)
mainLayout.addWidget(self.includeAllSQL_check)
mainLayout.addStretch()
mainLayout.addLayout(hboxButtonsBottom)
def on_combobox_changed(self, i):
if i == ".TXT":
self.exportSetDelimiter_lbl.show()
self.exportSetDelimiter_txt.show()
self.includeMedia_check.show()
self.includeTags_check.show()
self.includeAllSQL_check.hide()
elif i == ".CSV":
self.exportSetDelimiter_lbl.hide()
self.exportSetDelimiter_txt.hide()
self.includeMedia_check.show()
self.includeTags_check.show()
self.includeAllSQL_check.hide()
elif i == ".DB":
self.exportSetDelimiter_lbl.hide()
self.exportSetDelimiter_txt.hide()
self.includeMedia_check.show()
self.includeTags_check.show()
self.includeAllSQL_check.show()
def createDeck(self):
print("Exported Sucessfully")
self.close()
#Create A Windows
app = QtWidgets.QApplication(sys.argv)
window = ExportDialog("B1 Biology")
window.show()
sys.exit(app.exec_())
This is my first question, so if you need any additional information, I will add it in. Any help would be much appreciated. Thank you!
When a combobox is newly created, it has an invalid current index (-1) and no current text set. As soon as the first item is added, the index is automatically updated to 0 and the current text changes to that of the item.
You've connected to the currentTextChanged signal before adding new items, and since the function currentTextChanged assumes that the whole ui has been already created (including exportSetDelimiter_lbl), you get the attribute error.
While there's no rule for the placing of signal connections, it's usually a good habit to group all connections at the end of the function that creates them, or anyway, ensure that everything required by their connection has already been created.
So, just move the signal connection at the end of initUI and everything will work fine.
Well... No. Because you didn't set a central widget for the main window and tried to set the layout on it (which is not allowed, since a QMainWindow has a private and unaccessible layout).
Add a QWidget, call self.setCentralWidget(someWidget) and create the layout for that widget.
Situation
Below is a Qt example that contains an AbstractListModel and two display widgets that are linked to this model (a ListView and a LineEdit):
from PyQt5 import QtCore, QtWidgets
class ListModel(QtCore.QAbstractListModel):
def __init__(self, data_values, tooltips, parent=None):
super().__init__(parent)
self.data_values = data_values
self.tooltips = tooltips
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.data_values)
def data(self, index, role=QtCore.Qt.DisplayRole):
if (role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole):
return self.data_values[index.row()]
elif role == QtCore.Qt.ToolTipRole:
return self.tooltips[index.row()]
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
data_values = ['apple', 'pumpkin', 'orange']
tooltips = [
"Don't accept when offered by evil queen in disguise.",
"Excellent halloween decoration.",
"Good source of Vitamin C.",
]
self.list_model = ListModel(data_values, tooltips)
self.line_edit = QtWidgets.QLineEdit(parent=self)
self.line_edit.setReadOnly(True)
self.list_view = QtWidgets.QListView(parent=self)
self.list_view.setModel(self.list_model)
self.list_view.setCurrentIndex(self.list_model.index(0))
self.mapper = QtWidgets.QDataWidgetMapper(parent=self)
self.mapper.setModel(self.list_model)
self.mapper.addMapping(self.line_edit, 0)
self.mapper.toFirst()
self.list_view.selectionModel().currentRowChanged.connect(self.mapper.setCurrentModelIndex)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.list_view, 0)
layout.insertSpacing(1, 40)
layout.addWidget(self.line_edit, 2)
self.setLayout(layout)
qt_application = QtWidgets.QApplication([])
window = Window()
window.show()
qt_application.exec_()
I have configured the data method of the AbstractListModel to supply tooltip texts to linked widgets. A tooltip indeed appears when the mouse cursor is placed over an item in the ListView. However no tooltip appears when the mouse cursor is placed over the LineEdit.
Problem
I would like for the LineEdit to display a tooltip with the text provided by the linked AbstractListModel. Is there any way at all to achieve this?
It is not possible to achieve this with QDataWidgetMapper. QDataWidgetMapper always uses the Qt::EditRole value of the model. One could suggest to use the overloaded version of addMapping and a TableModel with one column (section) for displaying and one for the tooltip, but this is not possible, because QDataWidgetMapper only allows you to implement one-to-one mapping:
If the widget is already mapped to a section, the old mapping will be
replaced by the new one.
Solution
The easiest solution is to create a slot yourself which you connect to the currentRowChanged signal and which sets the tooltip (QWidget::setToolTip) and text (QLineEdit::setText) manually.
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_())
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()