Change the Icon of the Selected Item of the QToolBox in PyQT - python

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])

Related

PyQt6 deleting custom widget with nested class causes the program to crash

I am using Python 3.9.5.
I have encountered some serious problem in my project and here is a minimum reproducible example code, along with some descriptions.
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class Editor(QTextEdit):
doubleClicked = pyqtSignal(QTextEdit)
def __init__(self):
super().__init__()
self.setReadOnly(True)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
self.doubleClicked.emit(self)
class textcell(QGroupBox):
def __init__(self, text):
super().__init__()
self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.label = QLabel(text)
self.apply = makebutton('Apply')
self.apply.hide()
self.editor = Editor()
self.editor.doubleClicked.connect(lambda: self.editor.setReadOnly(False))
self.editor.doubleClicked.connect(self.apply.show)
self.hbox = QHBoxLayout()
self.hbox.addSpacerItem(spacer)
self.hbox.addWidget(self.apply)
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.label)
self.vbox.addWidget(self.editor)
self.vbox.addLayout(self.hbox)
self.setLayout(self.vbox)
self.apply.clicked.connect(self.on_ApplyClick)
def on_ApplyClick(self):
self.editor.setReadOnly(True)
self.apply.hide()
def makebutton(text):
button = QPushButton()
button.setFixedSize(60, 20)
button.setText(text)
return button
class songpage(QGroupBox):
def __init__(self, texts):
super().__init__()
self.init(texts)
self.setCheckable(True)
self.setChecked(False)
def init(self, texts):
self.vbox = QVBoxLayout()
artist = textcell('Artist')
artist.editor.setText(texts[0])
album = textcell('Album')
album.editor.setText(texts[1])
title = textcell('Title')
title.editor.setText(texts[2])
self.vbox.addWidget(artist)
self.vbox.addWidget(album)
self.vbox.addWidget(title)
self.setLayout(self.vbox)
spacer = QSpacerItem(0, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
class Ui_MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(405, 720)
self.setWindowTitle('example')
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
self.centralwidget = QWidget(self)
vbox = QVBoxLayout(self.centralwidget)
hbox = QHBoxLayout()
add = makebutton('Add')
delete = makebutton('Delete')
hbox.addWidget(add)
hbox.addSpacerItem(spacer)
hbox.addWidget(delete)
vbox.addLayout(hbox)
self.scrollArea = QScrollArea(self.centralwidget)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
vbox.addWidget(self.scrollArea)
self.setCentralWidget(self.centralwidget)
add.clicked.connect(self.addObj)
delete.clicked.connect(self.deleteObj)
def addObj(self):
Obj = songpage(('AAA', 'BBB', 'CCC'))
self.verticalLayout.addWidget(Obj)
def deleteObj(self):
item = self.verticalLayout.itemAt(0)
widget = item.widget()
self.verticalLayout.removeItem(item)
self.verticalLayout.removeWidget(widget)
app = QApplication([])
window = Ui_MainWindow()
window.show()
app.exec()
The problem is very simple, if I click add button, the widget will be added and everything works fine, if I double click on a QTextEdit, its apply button will show and it will change from read only to editable.
After I click an apply button, the button will hide and the corresponding QTextEdit will be read only again.
I have finally managed to add a doubleClicked signal to QTextEdit.
And, the problem, is if I click delete button, instead of deleting the thing as expected, it crashes the whole application.
I am sorry the minimum reproducible example is so long, but I have only managed to reproduce the issue with all the code and I really don't know what went wrong.
So how to fix it?
Well, I have figured it out, it was caused by the spacer item that I add to every one of horizontal layouts where fixed-sized buttons are used.
All such layouts are using the same spacer item, exactly the same spacer item, not just identical.
From what I have observed, the spacer items are all references to the same object, they are all echos of the same object which lives at a fixed memory address.
I honestly don't understand how such thing works, that the same object can not only be added to multiple layouts and present in all the layouts concurrently, but also be added to the same layout multiple times, yet it always remains the original object, not duplicates of itself.
I thought when I added the same spacer item to multiple layouts, I didn't add the original spacer item, instead duplicates of the original item which are identical but are at different memory addresses, and clearly that isn't how Python works.
So when I remove a widget, everything inside it, everything inside its layout is deleted, and the spacer item is deleted.
Because all the spacer items in all the layouts are references to the original spacer item, when I delete one of the layouts, the original spacer item is deleted as well, and the spacer item is deleted from all other layouts, yet its shadows remain, and the item isn't properly removed from all the other layouts, the layouts contain references to an object that no longer exists, thus the application crashed.
By removing the definition of the spacer item and replacing adding the spacer item with .addStretch(), the bug is fixed.

PyQt "Class" object has no attribute "widget"

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.

Focus Highlighting issue with Combo box and Radio Button with Enter Key pressed event

I have been working on PyQt focus method for different widgets whenever Enter key pressed instead of conventional Tab key.
Furthermore, I am able to set focus to the widget which I intended upon Enter key event. However, whenever the focus is on QComboBox or QRadioButton, these two widgets don't seems to highlight like QLineEdit or QPushButton widgets.
I know that I have to set focus policy to StrongFocus and I have tried that and several other methods but unable to fix this issue.
Moreover, this behaviour is working perfectly fine and combo box or radio button seems to highlight as well with the Tab key.
Please find below my tried code until now and also snapshot for the actual results.
from PyQt5 import QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class Combo_and_Radio_Focus(QDialog):
def __init__(self):
super().__init__()
# setting title
self.setWindowTitle("Combo Box Focus Test")
# setting geometry
self.setGeometry(600, 200, 650, 400)
# Some widgets for testing
self.le1 = QLineEdit(self)
self.le1.setFixedSize(100, 25)
self.le2 = QLineEdit(self)
self.le2.setFixedSize(100, 25)
self.cbo1 = QComboBox(self)
self.cbo1.setFixedSize(100, 25)
self.cbo1.setStyleSheet('QComboBox {background-color: white;}')
self.cbo1.setFocusPolicy(Qt.StrongFocus)
self.cbo1.addItems(["", "Item1", "Item2", "Item3"])
self.cbo2 = QComboBox(self)
self.cbo2.setFixedSize(100, 25)
self.cbo2.setStyleSheet('QComboBox {background-color: white;}')
self.cbo2.setFocusPolicy(Qt.StrongFocus)
self.cbo2.addItems(["", "Item1", "Item2", "Item3"])
self.RB1 = QRadioButton("1")
self.RB1.setChecked(True)
self.RB1.setFixedSize(100, 20)
self.RB1.setFocusPolicy(Qt.StrongFocus)
self.RB2 = QRadioButton("2")
self.RB2.setFixedSize(100, 20)
self.RB2.setFocusPolicy(Qt.StrongFocus)
self.vbl = QVBoxLayout()
self.vbl.addWidget(self.le1)
self.vbl.addWidget(self.le2)
self.vbl.addWidget(self.cbo1)
self.vbl.addWidget(self.cbo2)
self.vbl.addWidget(self.RB1)
self.vbl.addWidget(self.RB2)
self.setLayout(self.vbl)
# showing all the widgets
self.show()
def keyPressEvent(self, qKeyEvent):
if int(qKeyEvent.modifiers()) == QtCore.Qt.AltModifier:
qKeyEvent.ignore()
return
if qKeyEvent.key() == QtCore.Qt.Key_Return or qKeyEvent.key() == QtCore.Qt.Key_Enter:
QWidget.focusNextChild(self)
else:
super().keyPressEvent(qKeyEvent)
App = QApplication(sys.argv)
App.setStyle(QStyleFactory.create('Fusion'))
# create the instance of our Window
combo_and_Radio_Focus = Combo_and_Radio_Focus()
# start the app
sys.exit(App.exec())
LineEdit1_Highlighted Below
ComboBox1_Dont_Highlight Below
When the Tab is pressed to make the focus change then the application sets the Qt.WA_KeyboardFocusChange attribute in the window and that is used by the QStyle to make the border painting but since this is not the case then that attribute has to be set directly:
def keyPressEvent(self, qKeyEvent):
if qKeyEvent.modifiers() & QtCore.Qt.AltModifier:
qKeyEvent.ignore()
elif qKeyEvent.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self.focusNextChild()
self.window().setAttribute(Qt.WA_KeyboardFocusChange)
else:
super().keyPressEvent(qKeyEvent)

Changing PyQT Table Item from QComboBox to QTableWidgetItem

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()

PyQt4 - add a text edit area animation example

I have realized a python simple application, without any animation on it.
Now I want to add a simple animation, triggered by a signal (a button click for example), which on trigger enlarges the width of the windows and shows a new text area with some text in it.
Honestly, I am quite new to python/pyqt4, and I do not know much about the animation framework.
I tried to add this to my class code, for example in a method called clicking on the about menu :) :
self.anim = QPropertyAnimation(self, "size")
self.anim.setDuration(2500)
self.anim.setStartValue(QSize(self.width(), self.height()))
self.anim.setEndValue(QSize(self.width()+100, self.height()))
self.anim.start()
and this enlarge my window as I want.
Unfortunately I have no idea how to insert a new text area, avoiding the widgets already present to fill the new space (actually, when the window enlarge, the widgets use
all the spaces, thus enlarging themselves)
Could someone help me knowing how to add the text area appearance animation?
Any help is appreciated...really...
One way to achieve this is to animate the maximumWidth property on both the window and the text-edit.
The main difficulty is doing it in a way that plays nicely with standard layouts whilst also allowing resizing of the window. Avoiding flicker during the animation is also quite tricky.
The following demo is almost there (the animation is slightly jerky at the beginning and end):
from PyQt4 import QtGui, QtCore
class Window(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self._offset = 200
self._closed = False
self._maxwidth = self.maximumWidth()
self.widget = QtGui.QWidget(self)
self.listbox = QtGui.QListWidget(self.widget)
self.button = QtGui.QPushButton('Slide', self.widget)
self.button.clicked.connect(self.handleButton)
self.editor = QtGui.QTextEdit(self)
self.editor.setMaximumWidth(self._offset)
vbox = QtGui.QVBoxLayout(self.widget)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addWidget(self.listbox)
vbox.addWidget(self.button)
layout = QtGui.QHBoxLayout(self)
layout.addWidget(self.widget)
layout.addWidget(self.editor)
layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
self.animator = QtCore.QParallelAnimationGroup(self)
for item in (self, self.editor):
animation = QtCore.QPropertyAnimation(item, 'maximumWidth')
animation.setDuration(800)
animation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
self.animator.addAnimation(animation)
self.animator.finished.connect(self.handleFinished)
def handleButton(self):
for index in range(self.animator.animationCount()):
animation = self.animator.animationAt(index)
width = animation.targetObject().width()
animation.setStartValue(width)
if self._closed:
self.editor.show()
animation.setEndValue(width + self._offset)
else:
animation.setEndValue(width - self._offset)
self._closed = not self._closed
self.widget.setMinimumSize(self.widget.size())
self.layout().setSizeConstraint(QtGui.QLayout.SetFixedSize)
self.animator.start()
def handleFinished(self):
if self._closed:
self.editor.hide()
self.layout().setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
self.widget.setMinimumSize(0, 0)
self.setMaximumWidth(self._maxwidth)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.move(500, 300)
window.show()
sys.exit(app.exec_())

Categories

Resources