drawRect() in QTextEdit not functioning properly when cursor is moved - python

I've got this simple code in the paintEvent of my QTextEdit which draws a grey box under the currently selected QTextBlock:
def paintEvent(self, ev):
painter = QPainter()
painter.begin(self.viewport())
currentPos = self.textCursor().position()
block = self.document().findBlock(currentPos)
rect = self.document().documentLayout().blockBoundingRect(block)
margin = self.document().documentMargin()
rect.setTopLeft(QPoint(int(rect.topLeft().x()-margin), int(rect.topLeft().y()-margin)))
rect.setBottomRight(QPoint(int(rect.bottomRight().x()+margin), int(rect.bottomRight().y())))
painter.fillRect(rect, QBrush(QColor(10, 10, 10,20)))
if self._last_selected_block and (self._last_selected_block != block):
lastrect = self.document().documentLayout().blockBoundingRect(self._last_selected_block) #clean up artifacts
painter.eraseRect(lastrect)
painter.fillRect(self.contentsRect(), QBrush(QColor(123, 111, 145, 80))) #background color
painter.end()
self._last_selected_block = block
super().paintEvent(ev)
(Note, the "clean up artifacts" line erases anything drawn in the region of the previously selected QTextBlock, since a thin grey line would remain under the last block if text was drawn in it. Might be related.)
The effect of this is:
However, when the cursor is moved via clicking on another line, this happens:
The next rectangle is only partially drawn, around the character which the cursor was moved to, and the previous one is not erased. eraseRect() doesn't seem to be able to remove this artifact. When typing is continued or a newline is made, everything goes back to normal (this issue never occurs when changing lines via making a newline). I've confirmed that paintEvent() is called when the cursor moves, and the width of the rectangles to be drawn never changes. What's happening here?

For optimization reasons, Qt only try to repaint only the portions of the widget that actually need updates.
In the case of a QTextEdit, this means that only the portions in which the "caret" was before moving it (by editing, using arrow keys or mouse) and it is now will be updates, while ignoring everything else.
This clearly is an issue in your case, because it will only update small portions of the widget, with the result that the previously highlighted block will not be redrawn showing your custom background.
The solution is to track the current cursor position and correctly update both the previous block and the new one as soon as the block changes. This is achieved by calling update() using a QRegion, created by merging the current block bounding rect and the previous (if any); this will schedule an update that only redraws the contents within that region (which is what QTextEdit normally does, but we're extending it to the whole block area and the previous).
Note that I've completely changed your implementation of the paintEvent, as it was mostly unnecessary, for the following reasons:
the margins of the document should not be used for the block;
the background painting doesn't consider the scroll area background (more about this later);
there is no need to "erase" the previous block rectangle: our update() call includes that area, and since it's not the current block, the background will be just (re)drawn there instead;
the current block shouldn't be updated in the paint event;
The rendering of a scroll area always involves painting of its background based on the backgroundRole, which is automatically set to the Base palette role for QTextEdit. The result is that your background color will not be what you believe, but a composition of the base (usually, a nearly white color) and your background.
In order to ensure that the background is exactly the color you want, you have to update the palette on the widget using that color for that Base role, which should also be an opaque color (otherwise it will be composed using the Window color role).
class TextEdit(QtWidgets.QTextEdit):
_last_selected_block = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
palette = self.palette()
palette.setColor(palette.Base, QtGui.QColor(203, 200, 210))
self.setPalette(palette)
self.cursorPositionChanged.connect(self.trackCursorPosition)
def trackCursorPosition(self):
block = self.textCursor().block()
currentRect = self.document().documentLayout().blockBoundingRect(block)
updateRegion = QtGui.QRegion(currentRect.toRect())
if self._last_selected_block and self._last_selected_block != block:
oldRect = self.document().documentLayout().blockBoundingRect(
self._last_selected_block)
updateRegion |= QtGui.QRegion(oldRect.toRect())
updateRegion.translate(0, -self.verticalScrollBar().value())
self._last_selected_block = block
self.viewport().update(updateRegion)
def paintEvent(self, ev):
painter = QtGui.QPainter(self.viewport())
block = self.textCursor().block()
rect = self.document().documentLayout().blockBoundingRect(block)
rect.translate(0, -self.verticalScrollBar().value())
painter.fillRect(rect, QtGui.QColor(10, 10, 10,20))
super().paintEvent(ev)

Related

How to draw rectangle on Qlabel of image with mouse in the PyQt5 [duplicate]

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.
for examples my image is 800*753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800*753)
def resimac(self):
filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
self.image=QtGui.QImage(filename[0])
self.pixmap=QtGui.QPixmap.fromImage(self.image)
self.resim1.setPixmap(self.pixmap)
self.resim1.mousePressEvent=self.getPixel
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
print("X=",x," y= ",y)
Since you didn't provide a minimal, reproducible example, I'm going to assume that you're probably setting the scaledContents property, but that could also be not true (in case you set a maximum or fixed size for the label).
There are some other serious issues about your answer, I'll address them at the end of this answer.
The point has to be mapped to the pixmap coordinates
When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the scaledContents property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it's maximized to a screen smaller than the space the window needs).
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, not on the pixmap.
First of all, even if it doesn't seem to be that important, you'll have to consider that every widget can have some contents margins: the widget will still receive events that happen inside the area of those margins, even if they are outside its actual contents, so you'll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that's true, you'll have to translate the event position to that rectangle to get its position according to the pixmap.
Then, if the scaledContents property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you'll need to scale the position.
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.
# click on the horizontal center of the widget
mouseX = 100
pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0
pixmapX = mouseX * xRatio
# the resulting "x" is the horizontal center of the pixmap
# pixmapX = 200
On the other hand, if the contents are not scaled you'll have to consider the QLabel alignment property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use and the localization (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you'll have to be aware of that.
In the following example I'm trying to take care about all of that (I'd have to be honest, I'm not 100% sure, as there might be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting mousePressEvent as you did, I'm using an event filter, I'll explain the reason for it afterwards.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.getImageButton = QtWidgets.QPushButton('Select')
layout.addWidget(self.getImageButton)
self.getImageButton.clicked.connect(self.resimac)
self.resim1 = QtWidgets.QLabel()
layout.addWidget(self.resim1)
self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
# I'm assuming the following...
self.resim1.setScaledContents(True)
self.resim1.setFixedSize(701,451)
# install an event filter to "capture" mouse events (amongst others)
self.resim1.installEventFilter(self)
def resimac(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
if not filename:
return
self.resim1.setPixmap(QtGui.QPixmap(filename))
def eventFilter(self, source, event):
# if the source is our QLabel, it has a valid pixmap, and the event is
# a left click, proceed in trying to get the event position
if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and
event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton):
self.getClickedPosition(event.pos())
return super().eventFilter(source, event)
def getClickedPosition(self, pos):
# consider the widget contents margins
contentsRect = QtCore.QRectF(self.resim1.contentsRect())
if pos not in contentsRect:
# outside widget margins, ignore!
return
# adjust the position to the contents margins
pos -= contentsRect.topLeft()
pixmapRect = self.resim1.pixmap().rect()
if self.resim1.hasScaledContents():
x = pos.x() * pixmapRect.width() / contentsRect.width()
y = pos.y() * pixmapRect.height() / contentsRect.height()
pos = QtCore.QPoint(x, y)
else:
align = self.resim1.alignment()
# for historical reasons, QRect (which is based on integer values),
# returns right() as (left+width-1) and bottom as (top+height-1),
# and so their opposite functions set/moveRight and set/moveBottom
# take that into consideration; using a QRectF can prevent that; see:
# https://doc.qt.io/qt-5/qrect.html#right
# https://doc.qt.io/qt-5/qrect.html#bottom
pixmapRect = QtCore.QRectF(pixmapRect)
# the pixmap is not left aligned, align it correctly
if align & QtCore.Qt.AlignRight:
pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
elif align & QtCore.Qt.AlignHCenter:
pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
# the pixmap is not top aligned (note that the default for QLabel is
# Qt.AlignVCenter, the vertical center)
if align & QtCore.Qt.AlignBottom:
pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
elif align & QtCore.Qt.AlignVCenter:
pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)
if not pos in pixmapRect:
# outside image margins, ignore!
return
# translate coordinates to the image position and convert it back to
# a QPoint, which is integer based
pos = (pos - pixmapRect.topLeft()).toPoint()
print('X={}, Y={}'.format(pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Now. A couple of suggestions.
Don't overwrite existing child object methods with [other] object's instance attributes
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses function caching for virtual functions; this means that as soon as a virtual is called the first time, that function will always be called in the future. While your approach could work in simple cases (especially if the overwriting happens within the parent's __init__), it's usually prone to unexpected behavior that's difficult to debug if you're not very careful.
And that's exactly your case: I suppose that resimac is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. But if the user, for some reason, clicks on the label before a new pixmap is loaded, your supposedly overwritten method will never get called: at that time, you've not overwritten it yet, so the user clicks the label, Qt calls the QLabel's base class mousePressEvent implementation, and then that method will always be called from that point on, no matter if you try to overwrite it.
To work around that, you have at least 3 options:
use an event filter (as the example above); an event filter is something that "captures" events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget's parent or not (that's mostly the case of key/mouse events: if a widget isn't "interested" about one of those events, it "tells" its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
subclass the widget and manually add it to your GUI within your code;
subclass it and "promote" the widget if you're using Qt's Designer;
You don't need to use a QImage for a QLabel.
This is not that an issue, it's just a suggestion: QPixmap already uses (sort of) fromImage within its C++ code when constructing it with a path as an argument, so there's no need for that.
Always, always provide usable, Minimal Reproducible Example code.
See:
https://stackoverflow.com/help/how-to-ask
https://stackoverflow.com/help/minimal-reproducible-example
It could take time, even hours to get an "MRE", but it's worth it: there'll always somebody that could answer you, but doesn't want to or couldn't dig into your code for various reasons (mostly because it's incomplete, vague, inusable, lacking context, or even too expanded). If, for any reason, there'll be just that one user, you'll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you'll probably get plenty of interactions and useful insight from it.

widget's leaveEvent() is missed when scrolling through the encapsulating QScrollArea()

I have a grid of QWidget()s in a QScrollArea(). I override their leaveEvent() for a particular reason (more on that later). When moving around with the mouse pointer, the leaveEvent() is triggered just fine. But when I keep the mouse pointer still and scroll through the QScrollArea() with my scrollwheel, the mouse pointer effectively leaves the widget without triggering a leaveEvent().
1. Some context
I'm working on an application to browse through Arduino libraries:
What you see in the screenshot is a QScrollArea() in the middle with a scrollbar on the right and one at the bottom. This QScrollArea() contains a large QFrame() that holds all the widgets in its QGridLayout(). Each widget - usually a QLabel() or QPushButton() - has a lightgrey border. This way, I visualize the grid they're in.
2. Lighting up a whole row
When clicking on a widget, I want the whole row to light up. To achieve this, I override the mousePressEvent() for each of them:
Note: In practice, I just make each widget a subclasses from a custom class
CellWidget(), such that I need this method-override only once.
def mousePressEvent(self, e:QMouseEvent) -> None:
'''
Set 'blue' property True for all neighbors.
'''
for w in self.__neighbors:
w.setProperty("blue", True)
w.style().unpolish(w)
w.style().polish(w)
w.update()
super().mousePressEvent(e)
return
The variable self.__neighbors is a reference each widget keeps to all its neighboring widgets at the left and right side. This way, each widget has access to all widgets from its own row.
As you can see, I set the property "blue" from each widget. This has an effect in the StyleSheet. For example, this is the stylesheet I give to the QLabel() widgets:
# Note: 'self' is the QLabel() widget.
self.setStyleSheet(f"""
QLabel {
background-color: #ffffff;
color: #2e3436;
border-left: 1px solid #eeeeec;
border-top: 1px solid #eeeeec;
border-right: 1px solid #d3d7cf;
border-bottom: 1px solid #d3d7cf;
}
QLabel[blue = true] {
background-color: #d5e1f0;
}
""")
The unpolish() and polish() methods ensure that the stylesheet gets reloaded (I think?) and the update() method invokes a repaint. The procedure works nice. I click on any given widget, and the whole row lights up blue!
3. Turn off the (blue) light
To turn it off, I override the leaveEvent() method:
def leaveEvent(self, e:QEvent) -> None:
'''
Clear 'blue' property for all neighbors.
'''
for w in self.__neighbors:
w.setProperty("blue", False)
w.style().unpolish(w)
w.style().polish(w)
w.update()
super().leaveEvent(e)
return
So the whole row remains lighted up until you leave the widget with your mouse pointer. The leaveEvent() clears the 'blue' property for all neighbors and forces a repaint on them.
4. The problem
But there is a weakness. When I keep my mouse at the same position and I scroll the mousewheel down, the mouse pointer effectively leaves the widget without triggering a leaveEvent(). The absence of such leaveEvent() breaks my solution. While keeping the mouse still and scrolling down, you can click several rows and they all remain blue.
5. System info
I'm working on an application for both Windows and Linux. The application is written in Python 3.8 and uses PyQt5.
Both enterEvent and leaveEvent obviously depend on mouse movements, and since the scrolling does not involve any movement your approach won't work until the user slightly moves the mouse.
I can think about two possible workarounds, but both of them are a bit hacky and not very elegant. In both cases, I'm subclassing the scroll area and override the viewportEvent
Slightly move the mouse
Since those events require a mouse movement, let's move the mouse (and then move it back):
class ScrollArea(QtWidgets.QScrollArea):
def fakeMouseMove(self):
pos = QtGui.QCursor.pos()
# slightly move the mouse
QtGui.QCursor.setPos(pos + QtCore.QPoint(1, 1))
# ensure that the application correctly processes the mouse movement
QtWidgets.QApplication.processEvents()
# restore the previous position
QtGui.QCursor.setPos(pos)
def viewportEvent(self, event):
if event.type() == event.Wheel:
# let the viewport handle the event correctly, *then* move the mouse
QtCore.QTimer.singleShot(0, self.fakeMouseMove)
return super().viewportEvent(event)
Emulate the enter and leave events
This uses widgetAt() and creates fake enter and leave events sent to the relative widgets, which is not very good for performance: widgetAt might be slow, and repolishing the widgets also takes some time.
class ScrollArea(QtWidgets.QScrollArea):
def viewportEvent(self, event):
if event.type() == event.Wheel:
old = QtWidgets.QApplication.widgetAt(event.globalPos())
res = super().viewportEvent(event)
new = QtWidgets.QApplication.widgetAt(event.globalPos())
if new != old:
QtWidgets.QApplication.postEvent(old,
QtCore.QEvent(QtCore.QEvent.Leave))
QtWidgets.QApplication.postEvent(new,
QtCore.QEvent(QtCore.QEvent.Enter))
return res
return super().viewportEvent(event)

In PyQt QTextEdit.setTextColor() doesn't work before moving cursor

Setting the QTextEdit color with the setTextColor method doesn't have effect if done after the moveCursor method.
terminal = QTextEdit()
terminal.setTextColor(color)
terminal.moveCursor(QTextCursor.End)
terminal.insertPlainText('Test\n')
But if the cursor is moved before setting the color, it works.
terminal = QTextEdit()
terminal.moveCursor(QTextCursor.End)
terminal.setTextColor(color)
terminal.insertPlainText('Test\n')
Why does this happen? The documentation doesn't seem to have anything about this behavior.
What is most likely happening is the call to setTextColor is inserting something invisible into your document that changes the color. Perhaps it is adding an opening and a closing tag that specify a colored region of text and putting your cursor in the middle of these tags. When you call moveCursor and jump to the end you are jumping out of this colored region and your color stops working.
The second example works because you do not move out off the colored region.

Draw vertical lines on QTextEdit in PyQt

I am trying to develop a GUI that contains a QTextEdit widget. When the gui loads, it pulls in data from a file where the data is in columns of fixed widths.
I want the user to be able to click at various points in the QTextEdit widget, to mark the positions where new columns start, and I want vertical lines to be drawn on the widget at those positions, to show the columns.
In my GUI init() method I had the following line to intercept the paintEvent from the text widget:
self.mytextviewer.paintEvent = self.handlePaintEvent
and I had a handlePaintEvent() method:
def handlePaintEvent(self, event):
painter = QPainter(self.mytextviewer)
pen = QPen(Qt.SolidLine)
pen.setColor(Qt.black)
pen.setWidth(1)
painter.setPen(pen)
painter.drawLine(20, 0, 20, 100)
However when I tried to run the code I started to get QPainter errors about the painter not being active.
I then tried a different direction, subclassing QTextEdit and adding basically the same code as above to the paintEvent() method of my subclass. However I am still getting the errors.
I then tried adding painter.begin(self) and painter.end()to the paintEvent() method, but had no joy with that either.
Also, the text that was initially being displayed in the widget is no longer displayed since I added my custom paintEvent() method.
Am I doing something really stupid here, or is there a better/easier way to go about this?
Thanks.
I found an answer, hopefully it might help someone else....
You need to supply QPainter with the widgets viewport when creating an instance of QPainter in the paintEvent().
To get it to display the text, include the super() method of the parent class.
def paintEvent(self, event):
painter = QPainter(self.viewport())
pen = QPen(Qt.SolidLine)
pen.setColor(Qt.black)
pen.setWidth(1)
painter.setPen(pen)
painter.drawLine(20, 0, 20, 100)
super(TextWidgetWithLines, self).paintEvent(event)

What is the correct sequence for bliting surfaces to the screen in pygame?

I am creating a simple mp3 player and my first task was to create a simple button that a user could press. I created a class called Button which handled this behavior and detects if a user has clicked it and then changes color. I am now trying to have a default text that the button displays and another string (pres_string) which will be displayed if the button is being pressed.
The only problem is my background surface seems to be in the wrong place and is drawing over any changes I have made.
Here is my code:
http://pastebin.com/Nh3yy01X
As you can see I've commented out the lines I described and tried it with basic variables in the main function just to test what was going wrong.
Thanks for any help.
(Feel free to change the title of the question, I wasn't sure what most accuratelydescribed my problem)
Clear the surface every loop
def draw(self):
# clear screen."
self.screen.fill( self.color_bg )
# first draw background
# Then buttons
# then any extra top level text
# update
pygame.display.flip()
tip: For colors, you can call pygame.Color() with human-names like red ( gray20 and gray80 have a nice contrast, to use for bg and text. )
from pygame import Color
text = Color('gray20')
Your button, psuedocode. Fix: moved color as an instance member.
class Button(object):
def __init__(self, text, rect=None):
self.color_bg = Color("gray20")
self.color_text = color("gray80")
if rect is None: rect = Rect(0,0,1,1)
self.rect = rect
self._render()
def _render(self):
# draw button and .render() font, cache to surface for later.
self.surface_cached = self.surface.copy()
# render text
#if size changes, save rect.size of cached surface , mantaining location
self.rect.size = cache.get_rect.size
def draw(self):
# draw cached surface
screen.blit( self.surface_cached, self.rect)
For testClick use Rect.collidepoint http://www.pygame.org/docs/ref/rect.html#Rect.collidepoint
2D bitmap-based computer graphics are like drawing or painting - you put the new ink on top of whatever was there already. So your background must be the first thing you draw each time.

Categories

Resources