I have a interface that launches FME scripts (FME is an Extract-Transform-Load software) and I would like to have a progress bar that informs the user on the progress of the script execution.
After going through some documentation and forums, I understand that a progress bar requires a value to run (ie values from a loop). The thing is, I don't really have values, I only have a script that runs and I would like to know how far it is.
So I have a signal: each time a button is clicked it emits a signal to change the value of the progress bar. But when I write ui.progressBar.setValue() i need a value and I don't know what this value should be.
I don't know if i'm using the right tool or if there is something easier/better.
Here is a snippet I often use in my different scripts when they execute long operations (for example HTTP requests). This is just a basic spinning wheel that disapear after a few seconds. But you can also use signals & slots to hide it.
import math, sys
from PyQt4.QtCore import Qt, QTimer
from PyQt4.QtGui import *
class Overlay(QWidget):
def __init__(self, parent = None):
QWidget.__init__(self, parent)
palette = QPalette(self.palette())
palette.setColor(palette.Background, Qt.transparent)
self.setPalette(palette)
def paintEvent(self, event):
painter = QPainter()
painter.begin(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(event.rect(), QBrush(QColor(255, 255, 255, 127)))
painter.setPen(QPen(Qt.NoPen))
for i in range(6):
if (self.counter / 5) % 6 == i:
painter.setBrush(QBrush(QColor(127 + (self.counter % 5)*32, 127, 127)))
else:
painter.setBrush(QBrush(QColor(127, 127, 127)))
painter.drawEllipse(
self.width()/2 + 30 * math.cos(2 * math.pi * i / 6.0) - 10,
self.height()/2 + 30 * math.sin(2 * math.pi * i / 6.0) - 10,
20, 20)
painter.end()
def showEvent(self, event):
self.timer = self.startTimer(50)
self.counter = 0
def timerEvent(self, event):
self.counter += 1
self.update()
if self.counter == 60:
self.killTimer(self.timer)
self.hide()
class MainWindow(QMainWindow):
def __init__(self, parent = None):
QMainWindow.__init__(self, parent)
widget = QWidget(self)
self.editor = QTextEdit()
self.editor.setPlainText("0123456789"*100)
layout = QGridLayout(widget)
layout.addWidget(self.editor, 0, 0, 1, 3)
button = QPushButton("Wait")
layout.addWidget(button, 1, 1, 1, 1)
self.setCentralWidget(widget)
self.overlay = Overlay(self.centralWidget())
self.overlay.hide()
button.clicked.connect(self.overlay.show)
def resizeEvent(self, event):
self.overlay.resize(event.size())
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
As you are not providing any code, I can't adapt it to fit your needs.
Also, here is the source of the snippet.
There are a lot of interesting snippets on this wiki: Python.org wiki
Related
Both of the scale and scroll for the content of a QGraphicsView applied to the wheelEvent.
The problem is scroll only works if there is no scale on the wheelEvent.
No success with applying self.update() between scale and scroll call.
Extra points and guesses
If comment out the scroll part, scale works exact and on its place without displacement of view with scrolling up and down multiple times but with scroll part not commented out, with multiple scrolling (multiple zoom in and out) a little displacement of view has been obsereved.
It seems there is a conflict of fireing methods that call ui elements.
Appreciate your share of knowledge
# importing libraries
from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *
import sys
class Scene(QGraphicsScene):
def __init__(self, _scene_width, _scene_height):
super(Scene, self).__init__()
self.setSceneRect(0, 0, _scene_width, _scene_height)
map_center = _scene_width / 2
te = QTextEdit()
te.insertPlainText("Test Text")
te.setGeometry(map_center, map_center, 100, 100)
self.addWidget(te)
class View(QGraphicsView):
def __init__(self, _scene_width, _scene_height):
super(View, self).__init__()
self.scale_factor = 1
self.scene_width = _scene_width
self.scene_height = _scene_height
scene = Scene(_scene_width, _scene_height)
self.setScene(scene)
self.showMaximized()
def wheelEvent(self, event):
if self.underMouse():
# scale part which doesn't let the scroll part to work
if event.angleDelta().y() > 0:
self.scale_factor = self.scale_factor * 1.2
self.scale(1.2, 1.2)
else:
self.scale_factor = self.scale_factor / 1.2
self.scale(1 / 1.2, 1 / 1.2)
# scroll part which works fine if the scale part is commented out
self.scrollContentsBy(10, 0) # values are just for testing
def main():
app = QApplication(sys.argv)
view = View(1000000, 1000000)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).
I want something like this
QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...
What do I have to use?
The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.
The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).
This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class Loader(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.gradient = QtGui.QConicalGradient(.5, .5, 0)
self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
self.gradient.setColorAt(.25, QtCore.Qt.transparent)
self.gradient.setColorAt(.75, QtCore.Qt.transparent)
self.animation = QtCore.QVariantAnimation(
startValue=0., endValue=1.,
duration=1000, loopCount=-1,
valueChanged=self.updateGradient
)
self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)
self.focusWidget = None
self.hide()
parent.installEventFilter(self)
def start(self, timeout=None):
self.show()
self.raise_()
self.focusWidget = QtWidgets.QApplication.focusWidget()
self.setFocus()
if timeout:
self.stopTimer.start(timeout)
else:
self.stopTimer.setInterval(0)
def stop(self):
self.hide()
self.stopTimer.stop()
if self.focusWidget:
self.focusWidget.setFocus()
self.focusWidget = None
def updateGradient(self, value):
self.gradient.setAngle(-value * 360)
self.update()
def eventFilter(self, source, event):
# ensure that we always cover the whole parent area
if event.type() == QtCore.QEvent.Resize:
self.setGeometry(source.rect())
return super().eventFilter(source, event)
def showEvent(self, event):
self.setGeometry(self.parent().rect())
self.animation.start()
def hideEvent(self, event):
# stop the animation when hidden, just for performance
self.animation.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
color = self.palette().window().color()
color.setAlpha(max(color.alpha() * .5, 128))
qp.fillRect(self.rect(), color)
text = 'Loading...'
interval = self.stopTimer.interval()
if interval:
remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
textWidth = self.fontMetrics().width(text + ' 000%')
text += ' {}%'.format(remaining)
else:
textWidth = self.fontMetrics().width(text)
textHeight = self.fontMetrics().height()
# ensure that there's enough space for the text
if textWidth > self.width() or textHeight * 3 > self.height():
drawText = False
size = max(0, min(self.width(), self.height()) - textHeight * 2)
else:
size = size = min(self.height() / 3, max(textWidth, textHeight))
drawText = True
circleRect = QtCore.QRect(0, 0, size, size)
circleRect.moveCenter(self.rect().center())
if drawText:
# text is going to be drawn, move the circle rect higher
circleRect.moveTop(circleRect.top() - textHeight)
middle = circleRect.center().x()
qp.drawText(
middle - textWidth / 2, circleRect.bottom() + textHeight,
textWidth, textHeight,
QtCore.Qt.AlignCenter, text)
self.gradient.setColorAt(.5, self.palette().windowText().color())
qp.setPen(QtGui.QPen(self.gradient, textHeight))
qp.drawEllipse(circleRect)
class LoadingExtension(object):
# a base class to extend any QWidget subclass's top level window with a loader
def startLoading(self, timeout=0):
window = self.window()
if not hasattr(window, '_loader'):
window._loader = Loader(window)
window._loader.start(timeout)
# this is just for testing purposes
if not timeout:
QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)
def loadingFinished(self):
if hasattr(self.window(), '_loader'):
self.window()._loader.stop()
class Test(QtWidgets.QWidget, LoadingExtension):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
# just a test widget
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 0, 0, 1, 2)
textEdit.setMinimumHeight(20)
layout.addWidget(QtWidgets.QLabel('Timeout:'))
self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
layout.addWidget(self.timeoutSpin, 1, 1)
self.timeoutSpin.setValue(2000)
btn = QtWidgets.QPushButton('Start loading...')
layout.addWidget(btn, 2, 0, 1, 2)
btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag.
For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example:
Main window:
moveEvent -> signal moved
Dialog:
signal move -> re-center window.
Its look as not hard.
By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?
You can use this slot: void QWidget::raise().
Raises this widget to the top of the parent widget's stack.
After this call the widget will be visually in front of any overlapping sibling widgets.
I want to do board with square widgets. When I run code it creates nice board but after resize it become looks ugly. I am trying resize it with resize Event but it exists (probably some errors). I have no idea how to resize children after resize of parent.
Children widgets must be squares so it is also problem since I can not use auto expand. Maybe it is simple problem but I can not find solution. I spend hours testing different ideas but it now works as it should.
This what I want resize (click maximize):
After maximize it looks ugly (I should change children widget but on what event (I think on resizeEvent but it is not works) and how (set from parent or children cause program exit).
This is my minimize code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
self.setLayout(grid)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nize but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I do resize of square children to avoid holes?
2nd try - improved code but still I have not idea how to resize children
Some new idea with centering it works better (no gaps now) but still I do not know how to resize children (without crash).
After show():
Too wide (it keeps proportions):
Too tall (it keeps proportions):
Larger (it keeps proportions but children is not scaled to free space - I do not know how to resize children still?):
Improved code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
vertical = QVBoxLayout()
horizontal = QHBoxLayout()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
horizontal.addStretch()
horizontal.addLayout(grid)
horizontal.addStretch()
vertical.addStretch()
vertical.addLayout(horizontal)
vertical.addStretch()
self.setLayout(vertical)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nice but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I resize square children without crash?
There are two possible solution.
You can use the Graphics View framework, which is intended exactly for this kind of applications where custom/specific graphics and positioning have to be taken into account, otherwise create a layout subclass.
While reimplementing a layout is slightly simple in this case, you might face some issues as soon as the application becomes more complex. On the other hand, the Graphics View framework has a steep learning curve, as you'll need to understand how it works and how object interaction behaves.
Subclass the layout
Assuming that the square count is always the same, you can reimplement your own layout that will set the correct geometry based on its contents.
In this example I also created a "container" with other widgets to show the resizing in action.
When the window width is very high, it will use the height as a reference and center it horizontally:
On the contrary, when the height is bigger, it will be centered vertically:
Keep in mind that you should not add other widgets to the board, otherwise you'll get into serious issues.
This would not be impossible, but its implementation might be much more complex, as the layout would need to take into account the other widgets positions, size hints and possible expanding directions in order to correctly compute the new geometry.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
self.setMinimumSize(50, 50)
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), self.color)
class EvenLayout(QtWidgets.QGridLayout):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSpacing(0)
def setGeometry(self, oldRect):
# assuming that the minimum size is 50 pixel, find the minimum possible
# "extent" based on the geometry provided
minSize = max(50 * 8, min(oldRect.width(), oldRect.height()))
# create a new squared rectangle based on that size
newRect = QtCore.QRect(0, 0, minSize, minSize)
# move it to the center of the old one
newRect.moveCenter(oldRect.center())
super().setGeometry(newRect)
class Board(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
layout = EvenLayout(self)
self.squares = []
for row in range(8):
for column in range(8):
square = Square(self, not (row + column) & 1)
self.squares.append(square)
layout.addWidget(square, row, column)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())
Using the Graphics View
The result will be visually identical to the previous one, but while the overall positioning, drawing and interaction would be conceptually a bit easier, understanding how Graphics Views, Scenes and objects work might require you some time to get the hang of it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QGraphicsWidget):
def __init__(self, color):
super().__init__()
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def paint(self, qp, option, widget):
qp.fillRect(option.rect, self.color)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self):
super().__init__()
self.container = QtWidgets.QGraphicsWidget()
layout = QtWidgets.QGraphicsGridLayout(self.container)
layout.setSpacing(0)
self.container.setContentsMargins(0, 0, 0, 0)
layout.setContentsMargins(0, 0, 0, 0)
self.addItem(self.container)
for row in range(8):
for column in range(8):
square = Square(not (row + column) & 1)
layout.addItem(square, row, column, 1, 1)
class Board(QtWidgets.QGraphicsView):
def __init__(self):
super().__init__()
scene = Scene()
self.setScene(scene)
self.setAlignment(QtCore.Qt.AlignCenter)
# by default a graphics view has a border frame, disable it
self.setFrameShape(0)
# make it transparent
self.setStyleSheet('QGraphicsView {background: transparent;}')
def resizeEvent(self, event):
super().resizeEvent(event)
# zoom the contents keeping the ratio
self.fitInView(self.scene().container, QtCore.Qt.KeepAspectRatio)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())
I am trying to make a GUI with PyQt5. It will have a notification button with an icon. I want to add a small bubble with the number of notifications on the icon.
If a number is not possible, I would like to use a red dot as a backup method.
But how should I keep track of the new notifications (like a listener for notification) and change the icon while the window is running?
I have been googling about this problem, but only mobile development stuff and non-PyQt5 related results come up.
Expected result: Let's say we have a list. And the icon of the button will automatically change when a new item is added to the list. Then when the button is clicked, the icon will change back.
A possible solution is to create a widget that has a layout where you place a QToolButton and at the top right a QLabel with a QPixmap that has the number
from PyQt5 import QtCore, QtGui, QtWidgets
def create_pixmap(point, radius=64):
rect = QtCore.QRect(QtCore.QPoint(), 2 * radius * QtCore.QSize(1, 1))
pixmap = QtGui.QPixmap(rect.size())
rect.adjust(1, 1, -1, -1)
pixmap.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pixmap)
painter.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing
)
pen = painter.pen()
painter.setPen(QtCore.Qt.NoPen)
gradient = QtGui.QLinearGradient()
gradient.setColorAt(1, QtGui.QColor("#FD6684"))
gradient.setColorAt(0, QtGui.QColor("#E0253F"))
gradient.setStart(0, rect.height())
gradient.setFinalStop(0, 0)
painter.setBrush(QtGui.QBrush(gradient))
painter.drawEllipse(rect)
painter.setPen(pen)
painter.drawText(rect, QtCore.Qt.AlignCenter, str(point))
painter.end()
return pixmap
class NotificationButton(QtWidgets.QWidget):
scoreChanged = QtCore.pyqtSignal(int)
def __init__(self, score=0, icon=QtGui.QIcon(), radius=12, parent=None):
super(NotificationButton, self).__init__(parent)
self.m_score = score
self.m_radius = radius
self.setContentsMargins(0, self.m_radius, self.m_radius, 0)
self.m_button = QtWidgets.QToolButton(clicked=self.clear)
self.m_button.setContentsMargins(0, 0, 0, 0)
self.m_button.setIcon(icon)
self.m_button.setIconSize(QtCore.QSize(18, 18))
lay = QtWidgets.QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.m_button)
self.m_label = QtWidgets.QLabel(self)
self.m_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.m_label.raise_()
self.setSizePolicy(self.m_button.sizePolicy())
self.update_notification()
#QtCore.pyqtProperty(int, notify=scoreChanged)
def score(self):
return self.m_score
#score.setter
def score(self, score):
if self.m_score != score:
self.m_score = score
self.update_notification()
self.scoreChanged.emit(score)
#QtCore.pyqtSlot()
def clear(self):
self.score = 0
#QtCore.pyqtProperty(int)
def radius(self):
return self.m_radius
#radius.setter
def radius(self, radius):
self.m_radius = radius
self.update_notification()
def update_notification(self):
self.setContentsMargins(0, self.m_radius, self.m_radius, 0)
self.m_label.setPixmap(create_pixmap(self.m_score, self.m_radius))
self.m_label.adjustSize()
def resizeEvent(self, event):
self.m_label.move(self.width() - self.m_label.width(), 0)
super(NotificationButton, self).resizeEvent(event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.m_item_le = QtWidgets.QLineEdit("Stack Overflow")
add_button = QtWidgets.QPushButton("Add", clicked=self.add_item)
self.m_notification_button = NotificationButton(
icon=QtGui.QIcon("image.png")
)
self.m_list_widget = QtWidgets.QListWidget()
vlay = QtWidgets.QVBoxLayout(self)
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(self.m_item_le)
hlay.addWidget(add_button)
vlay.addLayout(hlay)
vlay.addWidget(
self.m_notification_button, alignment=QtCore.Qt.AlignRight
)
vlay.addWidget(self.m_list_widget)
#QtCore.pyqtSlot()
def add_item(self):
text = self.m_item_le.text()
self.m_list_widget.addItem(
"%s: %s" % (self.m_list_widget.count(), text)
)
self.m_notification_button.score += 1
self.m_list_widget.scrollToBottom()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
It would be nice if you show your code so far. Anyhow, these may help you solve your question:
You'll need two different icons: one to represent a dirty (just loaded) list and the other for the "clean" list
class YourClass(Dialog):
def __init__(self)
super().__init__()
self.lst = []
# ...
def setUI(self):
# ...
self.notButton = QPushButton(icon_off, '0')
self.notButton.clicked.connect(self.clearButton)
# ...
#pyqtSlot()
def clearButton(self):
self.notButton.setIcon(icon_clean)
def addToList(self, item):
self.lst.append(item)
self.notButton.setIcon(icon_dirty)
self.notButton.setText(str(len(self.lst)
A possible solution to updating the icon would be to have a separate image file for each icon and its associated notification number. You can keep track of the number of current notifications in a counter variable. Use that number to call the corresponding icon.
I am trying to make a GUI that will display (and eventually let the user build) circuits. Below is a rough sketch of what the application is supposed to look like.
The bottom panel (currently a simple QToolBar) should be of constant height but span the width of the application and the side panels (IOPanels in the below code) should have a constant width and span the height of the application.
The main part of the application (Canvas, which is currently a QWidget with an overriden paintEvent method, but might eventually become a QGraphicsScene with a QGraphicsView or at least something scrollable) should then fill the remaining space.
This is my current code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, QSize
class MainWindow(QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self._wire_ys = None
self._init_ui()
self.update_wire_ys()
def update_wire_ys(self):
self._wire_ys = [(i + 0.5) * self.panel.height() / 4 for i in range(4)]
self.input.update_field_positions()
self.output.update_field_positions()
def wire_ys(self):
return self._wire_ys
def _init_ui(self):
self.panel = QWidget(self)
self.canvas = Canvas(self, self.panel)
self.input = IOPanel(self, self.panel)
self.output = IOPanel(self, self.panel)
hbox = QHBoxLayout(self.panel)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.output, 0, Qt.AlignRight)
self.setCentralWidget(self.panel)
self.addToolBar(Qt.BottomToolBarArea, self._create_run_panel())
self.reset_placement()
def _create_run_panel(self):
# some other code to create the toolbar
return QToolBar(self)
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def resizeEvent(self, *args, **kwargs):
super().resizeEvent(*args, **kwargs)
self.update_wire_ys()
class IOPanel(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
self.io = [Field(self) for _ in range(4)]
def update_field_positions(self):
wire_ys = self.main.wire_ys()
for i in range(len(wire_ys)):
field = self.io[i]
field.move(self.width() - field.width() - 10, wire_ys[i] - field.height() / 2)
def sizeHint(self):
return QSize(40, self.main.height())
class Field(QLabel):
def __init__(self, *args):
super().__init__(*args)
self.setAlignment(Qt.AlignCenter)
self.setText(str(0))
self.resize(20, 20)
# This class is actually defined in another module and imported
class Canvas(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
def paintEvent(self, e):
print("ASFD")
qp = QPainter()
qp.begin(self)
self._draw(qp)
qp.end()
def _draw(self, qp):
# Draw stuff
qp.drawLine(0, 0, 1, 1)
# __main__.py
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Running that code gives me the following:
Here I have coloured the components to better see them using code like this in their construction:
p = self.palette()
p.setColor(self.backgroundRole(), Qt.blue)
self.setPalette(p)
self.setAutoFillBackground(True)
Green is the central panel (MainWindow.panel), blue are the IOPanels, the Fields are supposed to be red, and the Canvas is supposed to be white.
Ignore the bottom toolbar, it's some extra code I didn't include above (to keep it as minimal and relevant as possible), but it does no resizing of anything and no layout management except for its own child QWidget. In fact, including the painting code in my above minimal example gave a similar result with thinner bottom toolbar without the Run button. I'm just including the toolbar here to show its expected behaviour (as the toolbar is working correctly) in the general layout.
This result has several problems.
Problem 1
The Fields do not show up, initially. However, they do show up (and are appropriately placed within their respective panels) once I resize the main window. Why is this? The only thing the main window's resizeEvent does is update_wire_ys and update_field_positions, and those are performed by the main window's __init__ as well.
Problem 2
The IOPanels are not properly aligned. The first one should be on the left side of the central panel. Changing the order of adding them fixes this, as so:
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.output, 0, Qt.AlignRight)
However, shouldn't the Qt.AlignX already do this, regardless of the order they're added in? What if I later on wanted to add another panel to the left side, would I have to remove all the components, add the new panel and then re-add them?
Problem 3
The IOPanels are not properly sized. They need to span the entire height of the central panel and touch the left/right edge of the central panel. I'm not sure if this is an issue with the layout or my colouring of the panels. What am I doing wrong?
Problem 4
The Canvas does not show up at all and in fact its paintEvent is never called ("ASFD" never gets printed to the console). I have not overridden its sizeHint, because I want the central panel's layout to appropriately size the Canvas by itself. I was hoping the stretch factor of 1 when adding the component would accomplish that.
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
How do I get the canvas to actually show up and fill all the remaining space on the central panel?
This is the typical spaghetti code, where many elements are tangled, which is usually difficult to test, I have found many problems such as sizeEvent is only called when the layout containing the widget is called, another example is when you use the Function update_field_positions and update_wire_ys that handle each other object.
In this answer I will propose a simpler implementation:
IOPanel clas must contain a QVBoxLayout that handles the changes of image size.
In the MainWindow class we will use the layouts with the alignments but you must add them in order.
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
To place a minimum width for IOPanel we use QSizePolicy() and setMinimumSize()
Complete code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Field(QLabel):
def __init__(self, text="0", parent=None):
super(Field, self).__init__(parent=parent)
self.setAlignment(Qt.AlignCenter)
self.setText(text)
class IOPanel(QWidget):
numbers_of_fields = 4
def __init__(self, parent=None):
super(IOPanel, self).__init__(parent=None)
lay = QVBoxLayout(self)
for _ in range(self.numbers_of_fields):
w = Field()
lay.addWidget(w)
self.setMinimumSize(QSize(40, 0))
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.setSizePolicy(sizePolicy)
class Panel(QWidget):
def __init__(self, parent=None):
super(Panel, self).__init__(parent=None)
lay = QHBoxLayout(self)
self.input = IOPanel()
self.output = IOPanel()
self.canvas = QWidget()
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
self.initUi()
self.reset_placement()
def initUi(self):
panel = Panel(self)
self.setCentralWidget(panel)
self.addToolBar(Qt.BottomToolBarArea, QToolBar(self))
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Screenshot: