Does pyqt support a stacked progress bar with two values? - python

Similar to this question, but for pyqt. I have an application that has two threads, one of which processes some data (time consuming), and the second thread that presents the results and asks for verification on the results. I want to show the number of objects processed in a progress bar. However, I also want to show the number of objects verified by user. Number processed will always be equal or greater than the number of objects verified (since you can't verify what hasn't been verified). In essence, it's kind of like the loading bar of a youtube video or something, showing a grey part that is "loaded" and red part that is "watched." Is this something that can be supported in pyqt? The documentation for QProgressBar does not seem to hint that there's any support. Using PyQt5 and Python 3.6.
It should look similar to this:
Here's a minimal viable code that has TWO separate progress bars, one for the number of objects processed and the other for the number verified, but I want them overlapped...
import sys
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar, QPushButton)
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Progress Bar')
self.objectsToProcess = 100
self.objectsProcessed = 0
self.objectsVerified = 0
self.processProgress = QProgressBar(self)
self.processProgress.setGeometry(5, 5, 300, 25)
self.processProgress.setMaximum(self.objectsToProcess)
self.verifyProgress = QProgressBar(self)
self.verifyProgress.setGeometry(5, 35, 300, 25)
self.verifyProgress.setMaximum(self.objectsToProcess)
self.processButton = QPushButton('Process', self)
self.processButton.move(5, 75)
self.verifyButton = QPushButton('Verify', self)
self.verifyButton.move(90, 75)
self.show()
self.processButton.clicked.connect(self.process)
self.verifyButton.clicked.connect(self.verify)
def process(self):
if self.objectsProcessed + 1 < self.objectsToProcess:
self.objectsProcessed += 1
self.processProgress.setValue(self.objectsProcessed)
def verify(self):
if self.objectsVerified < self.objectsProcessed:
self.objectsVerified += 1
self.verifyProgress.setValue(self.objectsVerified)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
sys.exit(app.exec_())
Result from above code:

A possible solution is to create a new attribute in QProgressBar that shows the alternative advance, and to do the painting we can use a QProxyStyle:
from PyQt5 import QtCore, QtGui, QtWidgets
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, element, option, painter, widget):
if element == QtWidgets.QStyle.CE_ProgressBar:
super(ProxyStyle, self).drawControl(element, option, painter, widget)
if hasattr(option, 'alternative'):
alternative = option.alternative
last_value = option.progress
last_pal = option.palette
last_rect = option.rect
option.progress = alternative
pal = QtGui.QPalette()
# alternative color
pal.setColor(QtGui.QPalette.Highlight, QtCore.Qt.red)
option.palette = pal
option.rect = self.subElementRect(QtWidgets.QStyle.SE_ProgressBarContents, option, widget)
self.proxy().drawControl(QtWidgets.QStyle.CE_ProgressBarContents, option, painter, widget)
option.progress = last_value
option.palette = last_pal
option.rect = last_rect
return
super(ProxyStyle, self).drawControl(element, option, painter, widget)
class ProgressBar(QtWidgets.QProgressBar):
def paintEvent(self, event):
painter = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOptionProgressBar()
if hasattr(self, 'alternative'):
opt.alternative = self.alternative()
self.initStyleOption(opt)
painter.drawControl(QtWidgets.QStyle.CE_ProgressBar, opt)
#QtCore.pyqtSlot(int)
def setAlternative(self, value):
self._alternative = value
self.update()
def alternative(self):
if not hasattr(self, '_alternative'):
self._alternative = 0
return self._alternative
class Actions(QtWidgets.QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Progress Bar')
self.objectsToProcess = 100
self.objectsProcessed = 0
self.objectsVerified = 0
self.progress_bar = ProgressBar(maximum=self.objectsToProcess)
self.process_btn = QtWidgets.QPushButton('Process')
self.verify_btn = QtWidgets.QPushButton('Verify')
self.process_btn.clicked.connect(self.process)
self.verify_btn.clicked.connect(self.verify)
lay = QtWidgets.QGridLayout(self)
lay.addWidget(self.progress_bar, 0, 0, 1, 2)
lay.addWidget(self.process_btn, 1, 0)
lay.addWidget(self.verify_btn, 1, 1)
def process(self):
if self.objectsProcessed + 1 < self.objectsToProcess:
self.objectsProcessed += 1
self.progress_bar.setValue(self.objectsProcessed)
def verify(self):
if self.objectsVerified < self.objectsProcessed:
self.objectsVerified += 1
self.progress_bar.setAlternative(self.objectsVerified)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle(ProxyStyle(app.style()))
w = Actions()
w.show()
sys.exit(app.exec_())

Thanks for #eyllanesc for providing a robust solution. I chose to go with a lighter (admittedly hackish) solution of overlapping two progress bars and making the top bar slightly transparent using QGraphicsOpacityEffect.
# Opaque prog bar
self.verifyProgress = QProgressBar(self)
self.verifyProgress.setGeometry(5, 5, 300, 25)
self.verifyProgress.setMaximum(self.objectsToProcess)
self.verifyProgress.setFormat('%p% / ')
self.verifyProgress.setAlignment(Qt.AlignCenter)
# Must set the transparent prog bar second to overlay on top of opaque prog bar
self.processProgress = QProgressBar(self)
self.processProgress.setGeometry(5, 5, 300, 25)
self.processProgress.setMaximum(self.objectsToProcess)
self.processProgress.setFormat(' %p%')
self.processProgress.setAlignment(Qt.AlignCenter)
op = QGraphicsOpacityEffect(self.processProgress)
op.setOpacity(0.5)
self.processProgress.setGraphicsEffect(op)
Result:

I needed to do something similar recently and choose to use a gradient color for the progressbar chunk since I needed to use stylesheets too.
def set_pb_value(self, pb, value_1, value_2):
if value_2 > value_1:
pb.setValue(value_2)
pb.setFormat("{} / {}".format(value_1, value_2))
pb.setStyleSheet('QProgressBar::chunk {' +
'background-color: qlineargradient(spread:pad, x1:' + str(value_1/pb.maximum()) + ', y1:0, x2:' +
str(value_1/value_2) + ', y2:0, stop:' + str(value_1/value_2) + ' rgba(0, 255, 0, 255), stop:1 '
'rgba(255, 0, 0, 255)); width: -1px; margin: -1px;}')
else:
pb.setValue(value_1)
pb.setFormat("%v")
My values were whole numbers so value_1/pb.maximum() was necessary for me, but change it per your needs.
I also had some issues with the other stylesheets and the progressbar margins which is why they are set to -1 right now, you may not need to include that.

Related

QScrollArea: Scroll from item to item

Please consider the following code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Gallery(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Set widget and layout
self._scroll_widget = QWidget()
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(25)
self._scroll_widget.setLayout(self._layout)
self.setWidget(self._scroll_widget)
self.setWidgetResizable(True)
# Stretch
self._layout.addStretch(1) # Stretch above widgets
self._layout.addStretch(1) # Stretch below widgets
# Initialize ---------------------------------|
for _ in range(10):
self.add_item()
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
# Calculate Margins --------------------|
children = self._scroll_widget.findChildren(QLabel)
first_widget = children[0]
last_widget = children[-1]
self._layout.setContentsMargins(
0,
int(event.size().height() / 2 - first_widget.size().height() / 2),
0,
int(event.size().height() / 2 - last_widget.size().height() / 2)
)
def add_item(self) -> None:
widget = QLabel()
widget.setStyleSheet('background: #22FF88')
widget.setFixedSize(90, 125)
child_count = len(
self._scroll_widget.findChildren(QLabel)
)
self._layout.insertWidget(1 + child_count, widget,
alignment=Qt.AlignCenter)
if __name__ == '__main__':
app = QApplication([])
window = Gallery()
window.show()
app.exec()
Currently, the margins of the layout are dynamically set, so that, no matter the size of the window, the first and last item are always vertically centered:
What I want to achieve now is that whenever I scroll (either by mousewheel or with the arrow-keys, as the scrollbars are disabled) the next widget should take the position in the vertical center, i.e. I want to switch the scrolling mode from a per pixel to a per-widget-basis, so that no matter how far I scroll, I will never land between two widgets.
How can this be done?
I found that QAbstractItemView provides the option the switch the ScrollMode to ScrollPerItem, though I'm not sure if that's what I need because I was a bit overwhelmed when trying to subclass QAbstractItemView.
Edit:
This shows the delay I noticed after adapting #musicamante's answer:
It's not really disrupting, but I don't see it in larger projects, so I suppose something is not working as it should.
Since most of the features QScrollArea provides are actually going to be ignored, subclassing from it doesn't give lots of benefits. On the contrary, it could make things much more complex.
Also, using a layout isn't very useful: the "container" of the widget is not constrained by the scroll area, and all features for size hinting and resizing are almost useless in this case.
A solution could be to just set all items as children of the "scroll area", that could be even a basic QWidget or QFrame, but for better styling support I chose to use a QAbstractScrollArea.
The trick is to compute the correct position of each child widget, based on its geometry. Note that I'm assuming all widgets have a fixed size, otherwise you might need to use their sizeHint, minimumSizeHint or minimum sizes, and check for their size policies.
Here's a possible implementation (I changed the item creation in order to correctly show the result):
from random import randrange
class Gallery(QAbstractScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(175)
self.items = []
self.currentIndex = -1
for _ in range(10):
widget = QLabel(str(len(self.items) + 1),
self, alignment=Qt.AlignCenter)
widget.setStyleSheet('background: #{:02x}{:02x}{:02x}'.format(
randrange(255), randrange(255), randrange(255)))
widget.setFixedSize(randrange(60, 100), randrange(50, 200))
self.addItem(widget)
def addItem(self, widget):
self.insertItem(len(self.items), widget)
def insertItem(self, index, widget):
widget.setParent(self.viewport())
widget.show()
self.items.insert(index, widget)
if len(self.items) == 1:
self.currentIndex = 0
self.updateGeometry()
def setCurrentIndex(self, index):
if not self.items:
self.currentIndex = -1
return
self.currentIndex = max(0, min(index, len(self.items) - 1))
self.updateGeometry()
def stepBy(self, step):
self.setCurrentIndex(self.currentIndex + step)
def updateGeometry(self):
super().updateGeometry()
if not self.items:
return
rects = []
y = 0
for i, widget in enumerate(self.items):
rect = widget.rect()
rect.moveTop(y)
rects.append(rect)
if i == self.currentIndex:
centerY = rect.center().y()
y = rect.bottom() + 25
centerY -= self.height() / 2
centerX = self.width() / 2
for widget, rect in zip(self.items, rects):
widget.setGeometry(rect.translated(centerX - rect.width() / 2, -centerY))
def sizeHint(self):
return QSize(175, 400)
def resizeEvent(self, event: QResizeEvent):
self.updateGeometry()
def wheelEvent(self, event):
if event.angleDelta().y() < 0:
self.stepBy(1)
else:
self.stepBy(-1)

Increasing addAction icon size PyQt5

I have QLineEdit in which I wanted to add a clear button at the end of it. I enabled clear button in QLineEdit, it was working fine. I need to add a custom clear button at the end of the QLineEdit, so I used addAction() of QLineEdit and added my custom icon. The problem is that I can't find a solution to increase the size, I tried increasing the image size and it's not working.
class TextBox(QFrame):
def __init__(self, parent):
super(TextBox, self).__init__(parent=parent)
self.setObjectName("textBox")
self.isActive = False
self.lineEdit = QLineEdit()
self.lineEdit.addAction(QIcon("assets/icons/clear#3x.png"), QLineEdit.TrailingPosition)
A QIcon does not have a specific size, as it's only "decided" by the widget that uses it. While most widgets that use icons have a iconSize property, the icons of actions in a QLineEdit are shown in a different way.
Up until Qt 5.11 (excluded), the size was hardcoded to 16 pixels if the line edit was smaller than 34 pixels or 32 if it was taller.
Starting from Qt 5.11 the size is retrieved using the style (through pixelMetric()), and this can be overridden using a proxy style:
class Proxy(QtWidgets.QProxyStyle):
def pixelMetric(self, metric, opt=None, widget=None):
if (metric == self.PM_SmallIconSize and
isinstance(widget, QtWidgets.QLineEdit)):
size = widget.property('iconSize')
if size is not None:
return size
return widget.fontMetrics().height()
return super().pixelMetric(metric, opt, widget)
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setProperty('iconSize', 64)
# ...
For previous versions of Qt, though, things are a bit tricky. The only solution I came up with is to install event filters on all QToolButton that are children of the line edit (every action uses an internal QToolButton, including the clear action), manually set their geometry (required for correct click actions) and paint it in the event filter.
The following includes the proxystyle implementation in case the current version correctly supports it as explained before:
from PyQt5 import QtWidgets, QtCore, QtGui
if int(QtCore.QT_VERSION_STR.split('.')[1]) > 11:
IconSizeFix = False
else:
IconSizeFix = True
class Proxy(QtWidgets.QProxyStyle):
def pixelMetric(self, metric, opt=None, widget=None):
if (metric == self.PM_SmallIconSize and
isinstance(widget, QtWidgets.QLineEdit)):
size = widget.property('iconSize')
if size is not None:
return size
return widget.fontMetrics().height()
return super().pixelMetric(metric, opt, widget)
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setProperty('iconSize', 64)
self.setClearButtonEnabled(True)
self.addAction(QtGui.QIcon("icon.png"), self.TrailingPosition)
font = self.font()
font.setPointSize(48)
self.setFont(font)
def checkButtons(self):
for button in self.findChildren(QtWidgets.QToolButton):
button.installEventFilter(self)
def actionEvent(self, event):
super().actionEvent(event)
if IconSizeFix:
self.checkButtons()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Paint:
if (source.defaultAction().objectName() == '_q_qlineeditclearaction' and
not self.text()):
return True
qp = QtGui.QPainter(source)
state = QtGui.QIcon.Disabled
if source.isEnabled():
state = QtGui.QIcon.Active if source.isDown() else QtGui.QIcon.Normal
iconSize = QtCore.QSize(*[self.property('iconSize')] * 2)
qp.drawPixmap(source.rect(), source.icon().pixmap(
self.windowHandle(), iconSize, state, QtGui.QIcon.Off))
return True
return super().eventFilter(source, event)
def resizeEvent(self, event):
if not IconSizeFix:
return
self.checkButtons()
buttons = self.findChildren(QtWidgets.QToolButton)
if not buttons:
return
left = []
right = []
center = self.rect().center().x()
for button in buttons:
geo = button.geometry()
if geo.center().x() < center:
left.append(button)
else:
right.append(button)
left.sort(key=lambda x: x.geometry().x())
right.sort(key=lambda x: x.geometry().x())
iconSize = self.property('iconSize')
margin = iconSize / 4
top = (self.height() - iconSize) / 2
leftMargin = rightMargin = 0
if left:
x = margin
leftEdge = left[-1].geometry().right()
for button in left:
geo = QtCore.QRect(x, top, iconSize, iconSize)
button.setGeometry(geo)
x += iconSize + margin
leftMargin = x - leftEdge - margin
if right:
rightEdge = self.width() - margin
x = rightEdge - len(right) * iconSize - (len(right) - 1) * margin
rightMargin = self.width() - rightEdge + margin
for button in right:
geo = QtCore.QRect(x, top, iconSize, iconSize)
button.setGeometry(geo)
x += iconSize + margin
self.setTextMargins(leftMargin, 0, rightMargin, 0)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle(Proxy())
w = LineEdit()
w.show()
sys.exit(app.exec_())
Consider the following:
using the pre-5.11 workaround the positioning is not pixel-perfect, I tried to mimic what QLineEdit does to keep the code as simple as possible;
the painting is not exactly the same, most importantly the icon is missing the "highlight" shade when clicked, and if the style uses fade in/out effects for the clear button those effects won't be available;
the QProxyStyle method also affects the sizeHint of the QLineEdit, so it can not be smaller than the icon size, so use it with care;

Print items to pdf

I have a window with a QGraphicsScene as painter, and i want to render its elements to a pdf file on press of a button.
def generateReport(self):
lineList = {}
for i in self.circleList:
for j,k in i.lineItems:
if j not in lineList:
lineList[j] = [i, k]
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(lineList):
# j.scene().render(printer)
# lineList[j][0].scene().render(printer)
# lineList[j][1].scene().render(printer)
printer.drawText(0, self.painter.height() + i*200, f'{j.nameItem.toPlainText()}: {lineList[j][0].m_items[4].toPlainText()}, {lineList[j][1].m_items[4].toPlainText()}')
printer.end()
nameItem on j is the name label for the line, m_items[4] is the name label for each circle.
My issue is that i cant seem to get the exact height of the rendered scene, moreover I have zero clue as to how i could overflow the text to the next page should the contents not fit in one.
it would be lovely if i could somehow render every line and its corresponding circles seperately for each connection, stored in lineList
note: the line is a child of every circle , and the names of every line and circle are children of theirs, implemented much in the same way as the answer to my previous question where in lies my final issue of the grip items also being rendered.
I have discovered that I can create a new scene, move each item one by one and render it out to the pdf but this raises two separate issues
I cant add a line break and avoid overdrawing the new render over the previous one, and
I cant position the text as addText doesnt take positional arguments.
MRE:
import random
from fbs_runtime.application_context.PyQt5 import ApplicationContext
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPdfWriter, QBrush, QPagedPaintDevice
from PyQt5.QtWidgets import (QDialog, QGraphicsScene,
QGraphicsView, QGridLayout,
QPushButton, QGraphicsEllipseItem)
class gui(QDialog):
def __init__(self, parent=None):
super(gui, self).__init__(parent)
self.resize(1280, 720)
self.painter = QGraphicsScene(0, 0, self.width() - 50, self.height() - 70)
self.painter.setBackgroundBrush(QBrush(Qt.white))
self.canvas = QGraphicsView(self.painter)
mainLayout = QGridLayout()
mainLayout.addWidget(self.canvas, 0, 0, -1, -1)
self.setLayout(mainLayout)
#property
def circleList(self):
return [item for item in self.painter.items() if isinstance(item, QGraphicsEllipseItem)]
def newCircle(self):
self.painter.addEllipse( random.randint(100, 400), random.randint(100, 400), 50 + random.random() * 200, 50 + random.random() * 200)
def generateReport(self):
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(self.circleList):
printer.drawText(0, printer.viewport().height() + i*200, 'test')
printer.end()
if __name__ == "__main__":
app = ApplicationContext()
test = gui()
test.newCircle()
test.newCircle()
test.newCircle()
test.generateReport()
test.show()
exit(app.app.exec_())
if possible , the ability to print, test then circle for all circles would be decent enough for me.
Incorrect output example:
To understand what painting is like, you have to understand how QGraphicsScene::render() method works:
void QGraphicsScene::render(QPainter *painter, const QRectF &target = QRectF(), const QRectF &source = QRectF(), Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio)
Renders the source rect from scene into target, using painter. This
function is useful for capturing the contents of the scene onto a
paint device, such as a QImage (e.g., to take a screenshot), or for
printing with QPrinter. For example:
QGraphicsScene scene;
scene.addItem(...
...
QPrinter printer(QPrinter::HighResolution);
printer.setPaperSize(QPrinter::A4);
QPainter painter(&printer);
scene.render(&painter);
If source is a null rect, this function will use sceneRect() to
determine what to render. If target is a null rect, the dimensions of
painter's paint device will be used.
The source rect contents will be transformed according to
aspectRatioMode to fit into the target rect. By default, the aspect
ratio is kept, and source is scaled to fit in target.
See also QGraphicsView::render().
In your case, if the source is not passed, the entire sceneRect (0, 0, 1230, 650) will be copied and painted on the pdf page, if the sizes do not match, the sizes will be scaled. So from the above it follows that if you want to print an item then you must pass as source the space it occupies in the scene and hide the other items, and the target is the place where you want to paint, which involves calculating the new position based on where the previous item was printed.
Considering the above, a possible solution is the following:
import random
from PyQt5 import QtCore, QtGui, QtWidgets
class Gui(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Gui, self).__init__(parent)
self.resize(1280, 720)
self.scene = QtWidgets.QGraphicsScene(
0, 0, self.width() - 50, self.height() - 70
)
self.scene.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white))
self.canvas = QtWidgets.QGraphicsView(self.scene)
mainLayout = QtWidgets.QGridLayout(self)
mainLayout.addWidget(self.canvas)
#property
def circleList(self):
return [
item
for item in self.scene.items()
if isinstance(item, QtWidgets.QGraphicsEllipseItem)
]
def newCircle(self):
self.scene.addEllipse(
random.randint(100, 400),
random.randint(100, 400),
50 + random.random() * 200,
50 + random.random() * 200,
)
def generateReport(self):
printer = QtGui.QPdfWriter("Output.pdf")
printer.setPageSize(QtGui.QPagedPaintDevice.A4)
printer.setResolution(100)
painter = QtGui.QPainter(printer)
delta = 20
f = painter.font()
f.setPixelSize(delta)
painter.setFont(f)
# hide all items
last_states = []
for item in self.scene.items():
last_states.append(item.isVisible())
item.setVisible(False)
target = QtCore.QRectF(0, 0, printer.width(), 0)
for i, item in enumerate(self.circleList):
item.setVisible(True)
source = item.mapToScene(item.boundingRect()).boundingRect()
target.setHeight(source.height())
if target.bottom() > printer.height():
printer.newPage()
target.moveTop(0)
self.scene.render(painter, target, source)
f = painter.font()
f.setPixelSize(delta)
painter.drawText(
QtCore.QRectF(
target.bottomLeft(), QtCore.QSizeF(printer.width(), delta + 5)
),
"test",
)
item.setVisible(False)
target.setTop(target.bottom() + delta + 20)
# restore visibility
for item, state in zip(self.scene.items(), last_states):
item.setVisible(state)
painter.end()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Gui()
for _ in range(200):
w.newCircle()
w.generateReport()
w.show()
sys.exit(app.exec_())

how to interactively adjust button position without layouts

I am trying to figure out how to create a custom expanding widget that is totally resizable, but with the ability of being able to freely place labels and buttons without the constraints of layouts.
here is a rough idea of what i would like to achieve:
The idea is I would use a QGroupBox with some kind of title set, inside it will be a layout of some kind to ensure the contents inside can be resized when the QGroupBox is resized, with a custom button with the image of an arrow placed ontop in the corner of the QGroupBox that totally ignores the layout of the QGroupBox. which means that the QGroupBox has to be contained in an area without a layout. unfortunately what this means is the QGroupBox will no longer be "resizable" because it is not following a layout. So i want to know how to do this.
so far I have this code:
from PyQt4 import QtCore, QtGui
import sys
class Window(QtGui.QWidget):
resized = QtCore.pyqtSignal()
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle('Test UI')
self.setMinimumSize(75, 50)
#self.setMaximumSize(250, 500)
self.oldWidth = 0.0
self.oldHeight = 0.0
self.oldButtonPosX = 0.0
self.oldButtonPosY = 0.0
self.resized.connect(self.someFunction)
position = QtGui.QCursor.pos()
self.move(position)
self.uiWidget = QtGui.QWidget()
self.freeWidget = QtGui.QWidget(self.uiWidget)
self.verticalLayout = QtGui.QVBoxLayout(self.freeWidget)
self.exportPushButton = QtGui.QPushButton(self.freeWidget)
self.exportPushButton.setText('Export')
self.verticalLayout.addWidget(self.exportPushButton)
self.exportPushButton2 = QtGui.QPushButton(self.freeWidget)
self.exportPushButton2.setText('Export')
self.verticalLayout.addWidget(self.exportPushButton2)
self.testButton = QtGui.QPushButton(self.uiWidget)
self.testButton.setText('.')
self.testButton.setGeometry(5,5,20,20)
#self.testButton = QtGui.QPushButton(self.uiWidget)
self.lyt = QtGui.QVBoxLayout()
self.lyt.addWidget(self.uiWidget)
self.setLayout(self.lyt)
#self.oldWidth = self.width()
#self.oldHeight = self.height()
def resizeEvent(self, event):
self.resized.emit()
self.oldWidth = self.width()
self.oldHeight = self.height()
self.oldButtonPosX = self.testButton.x()
self.oldButtonPosY = self.testButton.y()
return QtGui.QWidget.resizeEvent(self, event)
def someFunction(self):
newWidth = self.width() - 20.0
newHeight = self.height() - 20.0
self.freeWidget.setGeometry(0,0,newWidth,newHeight)
newButtonX = self.oldButtonPosX + (self.oldWidth - newWidth)
print self.oldButtonPosX
print 'self.oldWidth: %s' %self.oldWidth
print 'newWidth: %s' %newWidth
value = self.oldWidth-newWidth
print 'difference: %s' %str(value)
newButtonY = 20 + (self.oldHeight - newHeight)
#self.freeWidget.setGeometry(self.geometry)
#print 'Resizing label'
#
if self.oldButtonPosX != 0.0:
self.testButton.setGeometry(newButtonX,newButtonY,20,20)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
By incorporating this custom "resized" function I read about in another thread (and not setting a parent), the resizing of a widget itself seems possible without the need of it being in a Layout, but now I face an issue where the button that I want to overlay stays in its original position. WHat I would like it to do is follow with a widget while resizing, but also not necessarily having it contained in that widget's layout (you can see I sort have been finnicking to get this effect)
How can I achieve this?
UPDATE:
So I've managed to sort of figure out how the interactive resize/scaling works. Basically the widget's position increments 1 pixel depending on how many widgets are in a single layout. So I tried to replicate this effect (i have two widgets in this test layout, so in theory it should take a pixel change of 3 for the overlaying button to increment one pixel in position). it works somewhat, but I am noticing now that if i move the mouse too fast when dragging, the resize does not keep up, and in turn causes a misalignment. How do i go about this issue...?
from PyQt4 import QtCore, QtGui
import sys
#https://stackoverflow.com/questions/43126721/pyqt-detect-resizing-in-widget-window-resized-signal?rq=1
class Window(QtGui.QWidget):
resized = QtCore.pyqtSignal()
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle('Test UI')
self.setMinimumSize(100, 100)
#self.setMaximumSize(250, 500)
self.oldWidth = 0.0
self.oldHeight = 0.0
self.getOriginalDimensions = False
self.oldButtonPosX = 5.0
self.oldButtonPosY = 5.0
self.resized.connect(self.someFunction)
position = QtGui.QCursor.pos()
self.move(position)
self.uiWidget = QtGui.QWidget()
self.freeWidget = QtGui.QWidget(self.uiWidget)
self.verticalLayout = QtGui.QVBoxLayout(self.freeWidget)
self.exportPushButton = QtGui.QPushButton(self.freeWidget)
self.exportPushButton.setText('ExportV')
self.verticalLayout.addWidget(self.exportPushButton)
self.exportPushButton2 = QtGui.QPushButton(self.freeWidget)
self.exportPushButton2.setText('Export')
self.verticalLayout.addWidget(self.exportPushButton2)
self.testButton = QtGui.QPushButton(self.uiWidget)
self.testButton.setText('.')
self.testButton.setGeometry(5,5,20,20)
#self.testButton = QtGui.QPushButton(self.uiWidget)
self.lyt = QtGui.QVBoxLayout()
self.lyt.addWidget(self.uiWidget)
self.setLayout(self.lyt)
self.buttonYDifference = 0
#self.oldWidth = 100
#self.oldHeight = 100
def resizeEvent(self, event):
self.resized.emit()
#self.oldWidth = self.width()
#self.oldHeight = self.height()
self.oldButtonPosX = self.testButton.x()
self.oldButtonPosY = self.testButton.y()
if self.getOriginalDimensions:
self.oldWidth = self.width()
self.oldHeight = self.height()
return QtGui.QWidget.resizeEvent(self, event)
def someFunction(self):
if not self.getOriginalDimensions:
self.oldWidth = self.width()
self.oldHeight = self.height()
self.getOriginalDimensions = True
newWidth = self.width() - 20.0
newHeight = self.height() - 20.0
self.freeWidget.setGeometry(0,0,newWidth,newHeight)
#newButtonX = self.oldButtonPosX + ((self.width() - self.oldWidth))
yDifference = self.oldHeight - self.height()
print self.buttonYDifference
if self.buttonYDifference <= 3 and self.buttonYDifference >= -3:
if yDifference == 1:
self.buttonYDifference +=1
elif yDifference == -1:
self.buttonYDifference -=1
else:
newButtonY = self.oldButtonPosY - self.buttonYDifference
self.testButton.setGeometry(self.oldButtonPosX,newButtonY,20,20)
self.buttonYDifference = 0
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

PyQt Fading a QLabel

I'm currently trying to fade a specific QLabel in and out. My first try was to use the setAlphaChannel, however this just didn't work.
My current approach is to use a for-loop and set the stylesheet of the QLabel. Sadly this makes a unverifiable bug, sometimes the fading works properly, sometimes the QLabel doesn't fade out but is fading in and more random stuff. For me the problem is untraceable.
Here is my current code:
def fade_greeting(self, foo, bar):
for i in range(255, -1, -5):
print(i)
string = "font : 45px; font : bold; color : rgba(220, 220, 220, " + str (i) + "); font-family : HelveticaNeue-UltraLight"
time.sleep(0.2)
self.greeting_text.setStyleSheet(string)
time.sleep(2)
self.greeting_text.setText(greeting())
time.sleep(2)
for i in range(0, 256, 5):
print(i)
string = "font : 45px; font : bold; color : rgba(220, 220, 220, " + str (i) + "); font-family : HelveticaNeue-UltraLight"
time.sleep(0.2)
self.greeting_text.setStyleSheet(string)
Is there something I missed? Or is there maybe a different approach to this problem?
Already thanks for your help!
After some trial and error, I found a working recipe:
def fade(self, widget):
self.effect = QGraphicsOpacityEffect()
widget.setGraphicsEffect(self.effect)
self.animation = QtCore.QPropertyAnimation(self.effect, b"opacity")
self.animation.setDuration(1000)
self.animation.setStartValue(1)
self.animation.setEndValue(0)
self.animation.start()
def unfade(self, widget):
self.effect = QGraphicsOpacityEffect()
widget.setGraphicsEffect(self.effect)
self.animation = QtCore.QPropertyAnimation(self.effect, b"opacity")
self.animation.setDuration(1000)
self.animation.setStartValue(0)
self.animation.setEndValue(1)
self.animation.start()
I guess you can call it on any widget. I call it on QLabel. For example:
self.fade(self._your_widget_here_)
# or
self.unfade(self._your_widget_here_)
It will fadein or fadeout your widget.
sleep() is a blocking function that is not suitable to use in the main GUI thread, Qt provides tools to handle this type of tasks as QVariantAnimation, it provides the colors in an appropriate way for the required animation.
To change the colors you can use QPalette as I show below:
class AnimationLabel(QLabel):
def __init__(self, *args, **kwargs):
QLabel.__init__(self, *args, **kwargs)
self.animation = QVariantAnimation()
self.animation.valueChanged.connect(self.changeColor)
#pyqtSlot(QVariant)
def changeColor(self, color):
palette = self.palette()
palette.setColor(QPalette.WindowText, color)
self.setPalette(palette)
def startFadeIn(self):
self.animation.stop()
self.animation.setStartValue(QColor(0, 0, 0, 0))
self.animation.setEndValue(QColor(0, 0, 0, 255))
self.animation.setDuration(2000)
self.animation.setEasingCurve(QEasingCurve.InBack)
self.animation.start()
def startFadeOut(self):
self.animation.stop()
self.animation.setStartValue(QColor(0, 0, 0, 255))
self.animation.setEndValue(QColor(0, 0, 0, 0))
self.animation.setDuration(2000)
self.animation.setEasingCurve(QEasingCurve.OutBack)
self.animation.start()
def startAnimation(self):
self.startFadeIn()
loop = QEventLoop()
self.animation.finished.connect(loop.quit)
loop.exec_()
QTimer.singleShot(2000, self.startFadeOut)
class Widget(QWidget):
def __init__(self):
super().__init__()
lay = QVBoxLayout(self)
self.greeting_text = AnimationLabel("greeting_text")
self.greeting_text.setStyleSheet("font : 45px; font : bold; font-family : HelveticaNeue-UltraLight")
lay.addWidget(self.greeting_text)
btnFadeIn = QPushButton("fade in")
btnFadeOut = QPushButton("fade out")
btnAnimation = QPushButton("animation")
lay.addWidget(btnFadeIn)
lay.addWidget(btnFadeOut)
lay.addWidget(btnAnimation)
btnFadeIn.clicked.connect(self.greeting_text.startFadeIn)
btnFadeOut.clicked.connect(self.greeting_text.startFadeOut)
btnAnimation.clicked.connect(self.greeting_text.startAnimation)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Widget()
ex.show()
sys.exit(app.exec_())

Categories

Resources