I'm trying to make a native looking sidebar that can expand and collapse in PyQt5. I've managed to create this:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtProperty
from PyQt5.QtGui import *
class Sidebar(QListWidget):
def __init__(self, buttons: list = None):
super().__init__()
font = QFont()
font.setPointSize(12)
self.setFont(font)
self.setIconSize(QSize(35, 35))
for button in buttons:
self.addItem(button)
self.maxWidth = self.sizeHintForColumn(0) + 10
iconWidth = self.iconSize().width()
self.minWidth = iconWidth + 16
self.awidth = self.minWidth
self.expandAnimation = QPropertyAnimation(self, b'awidth')
self.expandAnimation.setEndValue(self.maxWidth)
self.expandAnimation.setDuration(200)
self.expandAnimation.setEasingCurve(QEasingCurve.InOutCubic)
self.contractAnimation = QPropertyAnimation(self, b'awidth')
self.contractAnimation.setEndValue(self.minWidth)
self.contractAnimation.setDuration(200)
self.contractAnimation.setEasingCurve(QEasingCurve.InOutCubic)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setTextElideMode(Qt.TextElideMode.ElideNone)
def leaveEvent(self, e) -> None:
self.contractAnimation.start()
def enterEvent(self, e) -> None:
self.expandAnimation.start()
#pyqtProperty(int)
def awidth(self) -> int:
return self._awidth
#awidth.setter
def awidth(self, value) -> None:
self._awidth = value
super().setFixedWidth(self._awidth)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.buttonState = False
self.setMinimumSize(500, 300)
button1 = QListWidgetItem(QIcon.fromTheme('battery'), 'Button 1')
button2 = QListWidgetItem(QIcon.fromTheme('computer'), 'Button 2')
button3 = QListWidgetItem(QIcon.fromTheme('camera-web'), 'Button 3')
self.sidebar = Sidebar([button1, button2, button3])
self.sidebar.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Expanding)
self.mainWidget = QPushButton()
self.mainWidget.setText('0')
font = QFont()
font.setPointSize(20)
font.setBold(True)
self.mainWidget.setFont(font)
self.mainWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.mainWidget.pressed.connect(self.buttonPressed)
layout = QHBoxLayout()
layout.addWidget(self.sidebar)
layout.addWidget(self.mainWidget)
centralWidget = QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
self.sidebar.itemSelectionChanged.connect(self.sidebarChanged)
def sidebarChanged(self):
selection = self.sidebar.selectedIndexes()[0].row()
print(selection)
self.mainWidget.setText(str(selection))
def buttonPressed(self):
if self.buttonState:
self.sidebar.contractAnimation.start()
else:
self.sidebar.expandAnimation.start()
self.buttonState = not self.buttonState
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
This works well with my theme that I use (WhiteSur on KDE), but I couldn't just use self.iconSize().width() for the minimum width, because it would cut off the icon, I had to use self.iconSize().width() + 16, but this means that when I use another theme, like Breeze, it's too wide, I have to use a number like 10. And also with the Breeze theme, the sidebar itself can be selected and have a blue border around it, which I don't want. Is there a way to get the proper icon size that respects the padding and all that, and also a way to make the sidebar itself not selectable? Or is there a better way to make a native-looking sidebar that I can animate expanding and collapsing?
Related
In the list_widget I have added a add button I also want to add a remove button which asks which item you wants to remove and remove the chosen item. I was trying it to do but I didn't had any idea to do so .Also, please explain the solution I am a beginner with pyqt5 or I'd like to say absolute beginner.
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication,QMainWindow,
QListWidget, QListWidgetItem
import sys
class MyWindow(QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.x = 200
self.y = 200
self.width = 500
self.length = 500
self.setGeometry(self.x, self.y, self.width,
self.length)
self.setWindowTitle("Stock managment")
self.iniTUI()
def iniTUI(self):
# buttons
self.b1 = QtWidgets.QPushButton(self)
self.b1.setText("+")
self.b1.move(450, 100)
self.b1.resize(50, 25)
self.b1.clicked.connect(self.take_inputs)
# This is the button I want to define.
self.btn_minus = QtWidgets.QPushButton(self)
self.btn_minus.setText("-")
self.btn_minus.move(0, 100)
self.btn_minus.resize(50, 25)
# list
self.list_widget = QListWidget(self)
self.list_widget.setGeometry(120, 100, 250, 300)
self.item1 = QListWidgetItem("A")
self.item2 = QListWidgetItem("B")
self.item3 = QListWidgetItem("C")
self.list_widget.addItem(self.item1)
self.list_widget.addItem(self.item2)
self.list_widget.addItem(self.item3)
self.list_widget.setCurrentItem(self.item2)
def take_inputs(self):
self.name, self.done1 =
QtWidgets.QInputDialog.getText(
self, 'Add Item to List', 'Enter The Item you want
in
the list:')
self.roll, self.done2 = QtWidgets.QInputDialog.getInt(
self, f'Quantity of {str(self.name)}', f'Enter
Quantity of {str(self.name)}:')
if self.done1 and self.done2:
self.item4 = QListWidgetItem(f"{str(self.name)}
Quantity{self.roll}")
self.list_widget.addItem(self.item4)
self.list_widget.setCurrentItem(self.item4)
def clicked(self):
self.label.setText("You clicked the button")
self.update()
def update(self):
self.label.adjustSize()
def clicked():
print("meow")
def window():
apk = QApplication(sys.argv)
win = MyWindow()
win.show()
sys.exit(apk.exec_())
window()
The core issue here is the lack of separation of the view and the data. This makes it very hard to reason about how to work with graphical elements. You will almost certainly want to follow the Model View Controller design paradigm https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
which offers a systematic way to handle this separation.
Once you do so, it immediately becomes very straight forward how to proceed with the question: You essentially just have a list, and you either want to add a thing to this list, or remove one based on a selection.
I include an example here which happens to use the built-in classes QStringListModel and QListView in Qt5, but it is simple to write your own more specialized widgets and models. They all just use a simple signal to emit to the view that it needs to refresh the active information.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class StuffViewer(QMainWindow):
def __init__(self, model):
super().__init__()
self.setWindowTitle("Stock managment")
# 1: Use layouts.
hbox = QHBoxLayout()
widget = QWidget()
widget.setLayout(hbox)
self.setCentralWidget(widget)
# 2: Don't needlessly store things in "self"
vbox = QVBoxLayout()
add = QPushButton("+")
add.clicked.connect(self.add_new_stuff)
vbox.addWidget(add)
sub = QPushButton("-")
sub.clicked.connect(self.remove_selected_stuff)
vbox.addWidget(sub)
vbox.addStretch(1)
hbox.addLayout(vbox)
# 3: Separate the view of the data from the data itself. Use Model-View-Controller design to achieve this.
self.model = model
self.stuffview = QListView()
self.stuffview.setModel(self.model)
hbox.addWidget(self.stuffview)
def add_new_stuff(self):
new_stuff, success = QInputDialog.getText(self, 'Add stuff', 'Enter new stuff you want')
if success:
self.stuff.setStringList(self.stuff.stringList() + [new_stuff])
def remove_selected_stuff(self):
index = self.stuffview.currentIndex()
all_stuff = self.stuff.stringList()
del all_stuff[index.column()]
self.stuff.setStringList(all_stuff)
def window():
apk = QApplication(sys.argv)
# Data is clearly separated:
# 4: Never enumerate variables! Use lists!
stuff = QStringListModel(["Foo", "Bar", "Baz"])
# The graphical components is just how you interface with the data with the user!
win = StuffViewer(stuff)
win.show()
sys.exit(apk.exec_())
window()
Starting the program, the QIcon is aligned on the left (it's standard i guess) with the text right to it.
Instead I want the icon to be centered on top with the text below it.
I tried using setStyleSheet with show_all.setStyleSheet("QIcon { vertical-align: top }") and show_all.setStyleSheet("QPushButton { text-align: bottom }").
How can I achieve this?
QPushButton doesn't allow to choose the layout of its icon and label. Also, remember that while Qt features style sheets to style widgets, not all CSS known properties and selectors are available. Furthermore, style sheets only work on widgets, so using the QIcon selector isn't supported, since QIcon is not a QWidget subclass.
The most simple solution is to use a QToolButton and set the toolButtonStyle correctly:
self.someButton = QtWidgets.QToolButton()
# ...
self.someButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
The alternative is to subclass the button, provide a customized paint method and reimplement both sizeHint() and paintEvent(); the first is to ensure that the button is able to resize itself whenever required, while the second is to paint the button control (without text!) and then paint both the icon and the text.
Here's a possible implementation:
from PyQt5 import QtCore, QtGui, QtWidgets
class CustomButton(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._icon = self.icon()
if not self._icon.isNull():
super().setIcon(QtGui.QIcon())
def sizeHint(self):
hint = super().sizeHint()
if not self.text() or self._icon.isNull():
return hint
style = self.style()
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
# get the possible rect required for the current label
labelRect = self.fontMetrics().boundingRect(
0, 0, 5000, 5000, QtCore.Qt.TextShowMnemonic, self.text())
iconHeight = self.iconSize().height()
height = iconHeight + spacing + labelRect.height() + margin * 2
if height > hint.height():
hint.setHeight(height)
return hint
def setIcon(self, icon):
# setting an icon might change the horizontal hint, so we need to use a
# "local" reference for the actual icon and go on by letting Qt to *think*
# that it doesn't have an icon;
if icon == self._icon:
return
self._icon = icon
self.updateGeometry()
def paintEvent(self, event):
if self._icon.isNull() or not self.text():
super().paintEvent(event)
return
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
opt.text = ''
qp = QtWidgets.QStylePainter(self)
# draw the button without any text or icon
qp.drawControl(QtWidgets.QStyle.CE_PushButton, opt)
rect = self.rect()
style = self.style()
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
iconSize = self.iconSize()
iconRect = QtCore.QRect((rect.width() - iconSize.width()) / 2, margin,
iconSize.width(), iconSize.height())
if self.underMouse():
state = QtGui.QIcon.Active
elif self.isEnabled():
state = QtGui.QIcon.Normal
else:
state = QtGui.QIcon.Disabled
qp.drawPixmap(iconRect, self._icon.pixmap(iconSize, state))
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
labelRect = QtCore.QRect(rect)
labelRect.setTop(iconRect.bottom() + spacing)
qp.drawText(labelRect,
QtCore.Qt.TextShowMnemonic|QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop,
self.text())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = CustomButton('Alles anzeigen', icon=QtGui.QIcon.fromTheme('document-new'))
w.setIconSize(QtCore.QSize(32, 32))
w.show()
sys.exit(app.exec_())
Alternatively, try it:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout,
QToolBar, QAction)
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
add_action = QAction(QIcon("img/add.png"), "Add", self)
add_action.triggered.connect(self.addValue)
sub_action = QAction(QIcon("img/min.png"), "Sub", self)
sub_action.triggered.connect(self.subValue)
toolbar = QToolBar()
toolbar.setContentsMargins(0, 0, 0, 0)
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon | Qt.AlignLeading)
toolbar.setIconSize(QSize(50, 50))
toolbar.addAction(add_action)
toolbar.addAction(sub_action)
rootGrid = QGridLayout(self)
rootGrid.addWidget(toolbar)
def addValue(self):
print("def addValue:")
def subValue(self):
print("def subValue:")
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Widget()
main.show()
sys.exit(app.exec_())
I'm trying to set a background image on a QWidget and have some QlineEdit on top of it.
So for know I have this code
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class Model_GUI(QMainWindow):
def __init__(self, parent= None ):
super(Model_GUI, self).__init__()
self.left = 0
self.top = 0
self.width = 800
self.height = 800
self.resize(self.width,self.height)
GB = QGroupBox(" Gain ")
GB_layout = QHBoxLayout()
label = QLabel('A')
edit = QLineEdit('1')
GB_layout.addWidget(label)
GB_layout.addWidget(edit)
GB.setLayout(GB_layout)
GB2 = QGroupBox(" Gain ")
GB_layout2 = QHBoxLayout()
label2 = QLabel('A')
edit2 = QLineEdit('1')
GB_layout2.addWidget(label2)
GB_layout2.addWidget(edit2)
GB2.setLayout(GB_layout2)
#Graph
Graph = graph()
self.CentralWidget = QWidget()
self.globallayout = QHBoxLayout()
self.globallayout.addWidget(GB)
self.globallayout.addWidget(Graph)
self.globallayout.addWidget(GB2)
self.CentralWidget.setLayout(self.globallayout)
self.setCentralWidget(self.CentralWidget)
class graph(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(600,600)
oImage = QImage("img.png")
sImage = oImage.scaled(QSize(self.width(), self.height())) # resize Image to widgets size
palette = QPalette()
palette.setBrush(10, QBrush(sImage)) # 10 = Windowrole
self.setPalette(palette)
self.label1 = QLabel('Param1', self)
self.edit1 = QLineEdit('1', self)
self.label2 = QLabel('Param2', self)
self.edit2 = QLineEdit('10', self)
self.label1.move(50, 50)
self.edit1.move(500, 50)
self.label2.move(50, 500)
self.edit2.move(500, 500)
def main():
app = QApplication(sys.argv)
ex = Model_GUI(app)
ex.setWindowTitle('window')
ex.show()
sys.exit(app.exec_( ))
if __name__ == '__main__':
main()
but when I execute it I don't have the image in the QWidget (in the middle).
If I replace ex = Model_GUI(app)with ex = graph(), I have the correct expectation :
I don't understand why the image is correctly set when I'm using the QWidget alone but it isn't set right when I embedded it in a QMainWindow?
QWidgets use their QPalette.Window role only if they are top level widgets (as in "windows"), otherwise the parent background is used instead unless the autoFillBackground property (which is false by default) is true.
Just set the property in the widget initialization:
self.setAutoFillBackground(True)
When I decrease the window size all the widgets disappear.I want the widgets to move along when size is decreased.How do I solve this problem?
I have a drop-down menu from which a value is selected.When an "Add cmd" button is pressed the value is added to edit box.
Thanks in advance.
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class tabdemo(QTabWidget):
def __init__(self, parent = None):
super(tabdemo, self).__init__(parent)
self.setGeometry(50, 50, 400,400)
QShortcut(QKeySequence("Esc"), self, self.close)
self.tab1 = QWidget()
self.tab2 = QWidget()
self.addTab(self.tab1,"Tab 1")
self.tab1UI()
self.setWindowTitle("Main Window")
def tab1UI(self):
self.comboBox = QComboBox(self.tab1)
self.comboBox.addItem('ABC')
self.comboBox.addItem('BCD')
self.comboBox.addItem('CDE')
self.comboBox.move(5,20)
self.comboBox.resize(180,30)
self.button = QPushButton('Add Cmd', self.tab1)
self.button.move(190,20)
self.button.resize(80,30)
self.button.clicked.connect(self.handleTest)
self.b = QTextEdit(self.tab1)
self.b.move(20,75)
self.b.resize(290,200)
self.button = QPushButton('Send Batch', self.tab1)
self.button.move(40,300)
self.button.resize(150,30)
self.button = QPushButton('Clear', self.tab1)
self.button.move(200,300)
self.button.resize(80,30)
self.button.clicked.connect(self.deletevalue)
layout = QFormLayout()
self.setTabText(4,"BatchCMDS")
self.tab1.setLayout(layout)
def handleTest(self):
self.b.append(str(self.comboBox.currentText()))
def deletevalue(self):
self.b.clear()
def main():
app = QApplication(sys.argv)
ex = tabdemo()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
If you want the widgets to adapt to the size of the window you should use layouts, For this, the application must be designed, for this an image is used of how you want your application to be:
As we see the widgets that are inside a tab are divided into 3 groups, the first is made up of the QComboBox with the QPushButton, the second the QTextEdit, and the third the 2 remaining buttons. Each group is horizontally distributed, so in that case we should use QHBoxLayout except the QTextEdit that is alone, and each group should be in QVBoxLayout. I do not understand why you use the QFormLayout, also if you use the layouts the positions are not necessary.
Another error that I see in your code is that several buttons have the same name, this causes errors like for example that the Add CMD button does not work, you must give a different name to each widget.
class tabdemo(QTabWidget):
def __init__(self, parent = None):
super(tabdemo, self).__init__(parent)
self.setGeometry(50, 50, 400,400)
QShortcut(QKeySequence("Esc"), self, self.close)
self.tab1 = QWidget()
self.tab2 = QWidget()
self.addTab(self.tab1,"Tab 1")
self.addTab(self.tab2,"Tab 2")
self.tab1UI()
def tab1UI(self):
vlayout = QVBoxLayout(self.tab1)
hlayout1 = QHBoxLayout()
self.comboBox = QComboBox(self.tab1)
self.comboBox.addItems(['ABC', 'BCD', 'CDE'])
self.button = QPushButton('Add Cmd', self.tab1)
self.button.clicked.connect(self.handleTest)
hlayout1.addWidget(self.comboBox)
hlayout1.addWidget(self.button)
hlayout1.addItem(QSpacerItem(100, 10, QSizePolicy.Expanding, QSizePolicy.Preferred))
vlayout.addLayout(hlayout1)
self.b = QTextEdit(self.tab1)
vlayout.addWidget(self.b)
hlayout2 = QHBoxLayout()
self.buttonSend = QPushButton('Send Batch', self.tab1)
self.buttonClear = QPushButton('Clear', self.tab1)
self.buttonClear.clicked.connect(self.deletevalue)
hlayout2.addItem(QSpacerItem(100, 10, QSizePolicy.Expanding, QSizePolicy.Preferred))
hlayout2.addWidget(self.buttonSend)
hlayout2.addWidget(self.buttonClear)
hlayout2.addItem(QSpacerItem(100, 10, QSizePolicy.Expanding, QSizePolicy.Preferred))
vlayout.addLayout(hlayout2)
self.setTabText(4,"BatchCMDS")
def handleTest(self):
self.b.append(self.comboBox.currentText())
def deletevalue(self):
self.b.clear()
I have a list which is generated based on user-input.
I am trying to display this list in a QMessageBox. But, I have no way of knowing the length of this list. The list could be long.
Thus, I need to add a scrollbar to the QMessageBox.
Interestingly, I looked everywhere, but I haven’t found any solutions for this.
Below is, what I hope to be a “Minimal, Complete and Verifiable Example”, of course without the user input; I just created a list as an example.
I appreciate any advice.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class W(QWidget):
def __init__(self):
super().__init__()
self.initUi()
def initUi(self):
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.lst = list(range(2000))
self.show()
def buttonClicked(self):
result = QMessageBox(self)
result.setText('%s' % self.lst)
result.exec_()
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())
You can not add a scrollbar directly since the widget in charge of displaying the text is a QLabel. The solution is to add a QScrollArea. The size may be inadequate so a stylesheet has to be used to set minimum values.
class ScrollMessageBox(QMessageBox):
def __init__(self, l, *args, **kwargs):
QMessageBox.__init__(self, *args, **kwargs)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
self.content = QWidget()
scroll.setWidget(self.content)
lay = QVBoxLayout(self.content)
for item in l:
lay.addWidget(QLabel(item, self))
self.layout().addWidget(scroll, 0, 0, 1, self.layout().columnCount())
self.setStyleSheet("QScrollArea{min-width:300 px; min-height: 400px}")
class W(QWidget):
def __init__(self):
super().__init__()
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.lst = [str(i) for i in range(2000)]
self.show()
def buttonClicked(self):
result = ScrollMessageBox(self.lst, None)
result.exec_()
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())
Output:
Here is another way to override the widgets behavior.
You can get references to the children of the widget by using 'children()'.
Then you can manipulate them like any other widget.
Here we add a QScrollArea and QLabel to the original widget's QGridLayout. We get the text from the original widget's label and copy it to our new label, finally we clear the text from the original label so it is not shown (because it is beside our new label).
Our new label is scrollable. We must set the minimum size of the scrollArea or it will be hard to read.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ScrollMessageBox(QMessageBox):
def __init__(self, *args, **kwargs):
QMessageBox.__init__(self, *args, **kwargs)
chldn = self.children()
scrll = QScrollArea(self)
scrll.setWidgetResizable(True)
grd = self.findChild(QGridLayout)
lbl = QLabel(chldn[1].text(), self)
lbl.setWordWrap(True)
scrll.setWidget(lbl)
scrll.setMinimumSize (400,200)
grd.addWidget(scrll,0,1)
chldn[1].setText('')
self.exec_()
class W(QWidget):
def __init__(self):
super(W,self).__init__()
self.btn = QPushButton('Show Message', self)
self.btn.setGeometry(10, 10, 100, 100)
self.btn.clicked.connect(self.buttonClicked)
self.message = ("""We have encountered an error.
The following information may be useful in troubleshooting:
1
2
3
4
5
6
7
8
9
10
Here is the bottom.
""")
self.show()
def buttonClicked(self):
result = ScrollMessageBox(QMessageBox.Critical,"Error!",self.message)
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = W()
sys.exit(app.exec_())