I have a QScrollArea that contains a painter widget. This painter widget needs to draw several boxes at different horizontal x locations. However, I'd like there to always be a "legend" on the left of the screen when scrolling.
I achieved this by calling QScrollArea.horizontalScrollBar().value()
, however when scrolling, the repaint method seems to draw multiple lines when I use this horizontalScrollBar().value()
to get the scroll pos.
StackOverflow won't let me upload the gif, but when I scroll to the left or right, the line drawn using horizontalScrollBar().value()
is drawn multiple times in different locations. If you stop scrolling and resize the window, the paint event corrects itself and draws one single line on the left of the screen. Does anyone know how to fix this or what the scrollevent signal is so I can manually have it call a repaintevent?
Here is my example code:
class Capacity_Drawer(QWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(10000, 10000)
self.setMinimumSize(1000, 500)
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
if self.scroll_area:
painter.setPen(QPen(QColor("Red"), 5))
x_pos = self.scroll_area.horizontalScrollBar().value()+1
painter.drawLine(x_pos, 0, x_pos, 500)
rect = QRectF(800, 0, 100, 100)
painter.drawRect(rect)
def set_scroll_area(self, scroll_area:QScrollArea):
"""Set a reference to the parent scroll area."""
self.scroll_area = scroll_area
class Capacity_Scroll_Area(QScrollArea):
def __init__(self):
super().__init__()
self.capacity_viewer = Capacity_Drawer()
self.capacity_viewer.set_scroll_area(self)
# Set the GanttWidget as the scrollable area content
self.setWidget(self.capacity_viewer)
self.setWidgetResizable(True) # Allow resizing of the widget within the scroll area
class window(QMainWindow):
def __init__(self):
super().__init__()
cap_scroll_area = Capacity_Scroll_Area()
self.setCentralWidget(cap_scroll_area)
app = QApplication(sys.argv)
# Create and show the main window
window = window()
window.show()
sys.exit(app.exec())
The problem comes from the fact that you're drawing contents based on external factors, without considering that they may change in the meantime, and what previously drawn may need to be redrawn (which also includes clearing anything that was previously drawn).
Since the widget does not have any knowledge about those changes, the solution is to properly update()
it whenever those changes happen.
Like any proper GUI toolkit, Qt always tries to optimize contents drawing by using buffering [see framebuffer]; when an object is displayed, its appearance is internally buffered, so that it does not need to call paint functions (which can be expansive) to draw the same content again: when no real update is necessary, it will just repaint the previously buffered content.
This is also why it's fundamental to at least call the following QWidget functions whenever any contents on the widget needs updating, including clearing previously drawn content:
update()
slot, which schedules the widget for a complete repaint;update()
functions:
update(rect)
, which updates a given QRect;update(x, y, w, h)
, similar to the above, but providing explicit coordinates for the rectangle;update(region)
to update a QRegion;Note that the above update()
functions only schedule a widget for repaint, does not repaint it immediately. This is done for optimization reasons, so that a widget is actually repainted only when necessary: multiple calls to updates()
normally result in a single repaint operation.
For other optimization reasons, when a QAbstractScrollArea is scrolled, its contained widgets are only scheduled for updates when new contents are going to be shown: typically, when what was previously outside the scroll area becomes actually visible.
This means that if any content was already visible, it will not be "physically" redrawn in most cases; the paint engine will just draw the contents shown in the "new area", while "scrolling" the previously drawn buffer, which is exactly your case: the "added" lines you see are just the result of previous calls to paintEvent()
that have been buffered and "scrolled" to their new position.
Since the drawing contents should change based on the value of the scroll bar, a possible solution would be to schedule an update whenever the scroll bar value changes:
def set_scroll_area(self, scroll_area:QScrollArea):
"""Set a reference to the parent scroll area."""
self.scroll_area = scroll_area
scroll_area.horizontalScrollBar().valueChanged.connect(self.update)
In case you also want to show contents based on the vertical scroll bar, you would need to connect to the related verticalScrollBar()
signal too.
Note though that, in more complex situations, it is possible that the scroll bar range may change, but not its value. This may result in a similar issue nonetheless.
Connecting to the rangeChanged
signal of both scroll bars may be a solution, but probably not the ultimate one, also considering that it's possible to set different QScrollBars on the scroll area.
All of the above then brings us to the following point.
As said in the introduction, the problem comes from the fact that the widget (A
) uses aspects that outside of its scope: an unrelated object (B
), with its own properties that may change in the meantime.
Therefore, there are two options:
A
is properly updated whenever any change in the widget B
may affect it, even indirectly (by properly connecting to all related signals, as explained above);There is no absolute rule telling which approach is better, but, in your case, the second one is probably more accurate and effective, for both code writing/reading and logical aspects.
Consider the following changes:
class Capacity_Drawer(QWidget):
x_pos = 0
...
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(QColor("Red"), 5))
painter.drawLine(self.x_pos, 0, self.x_pos, 500)
rect = QRectF(800, 0, 100, 100)
painter.drawRect(rect)
def setXPos(self, pos):
if self.x_pos != pos:
self.x_pos = pos
self.update()
class Capacity_Scroll_Area(QScrollArea):
def __init__(self):
super().__init__()
self.capacity_viewer = Capacity_Drawer()
self.setWidget(self.capacity_viewer)
self.horizontalScrollBar().valueChanged.connect(
self.capacity_viewer.setXPos)
...
Other than what written above, there are further issues in your code, and you should seriously consider them:
update()
in the __init__
of a widget is completely pointless: whenever it will be shown the first time, it will be "updated" anyway;red
, not Red
); besides, QColor("Red")
is equal to QColor(Qt.red)
, with the advantage that similar alternatives (including Qt.GlobalColor.red
or QColorContsants.Red
in some cases) are more performant, as they don't need string parsing and lookup;window = window()
) is a terrible coding choice, unless you really know what you're doing;