Subpixel accuracy when drawing vector graphics in Qt? - qt

I have written a small test script in Python and PySide6 to test the vector graphics capabilities of Qt, in particular the accuracy when it comes to the exact shapes and positions of the drawn figures. My first test was to draw a disk with a slightly smaller disk within it:
import sys
from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtGui import QPainter, QBrush
radii_difference = 1
x_offset = 0
class MyWidget(QWidget):
def paintEvent(self, event):
self.setWindowTitle("radii_difference: {}; x_offset: {}".format(radii_difference, x_offset))
print("radii_difference: {}; x_offset: {}".format(radii_difference, x_offset))
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing, on=True)
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(Qt.red))
painter.drawEllipse(QPoint(300, 250), 200, 200)
painter.setBrush(QBrush(Qt.blue))
painter.drawEllipse(QPoint(300 + x_offset, 250), 200 - radii_difference, 200 - radii_difference)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
As can be seen in the code, I call the difference of the radii radii_difference, and I also create another variable x_offset to parameterize the position of the inner disk. When trying with two different values for radii_difference—0.5 and 1—I see no difference in the thickness of the red outline of the blue disk at all, which indicates that painter.drawEllipse rounds down the rx and ry arguments to the closest integer.
Likewise, if I change x_offset from 0 to 0.5, PyCharm highlights 300 + x_offset and gives me the mouse-over text "Expected type 'int', got 'float' instead", indicating that QPoint (and therefore also QPainter.drawEllipse) only accepts integer (center point) coordinates, and when I run the application, I see no difference from the previous figure (in particular, if drawEllipse would have supported non-integer center point coordinates, the red outline would have been about three times as thick on the left side than on the right side, but it instead appear equally thick everywhere).
Both of these discoveries makes it feel like QPainter offers very limited accuracy of the positions and sizes of drawn objects (I guess this translates to other shapes as well, not only ellipses).
Is there some way to increase this accuracy somehow when using QPainter? If not, is there some alternative way to draw vector graphics in Qt (especially with PySide6, but I might consider switching to C++ if that offers greater possibilities) that gives me subpixel accuracy of positions, shapes and sizes of drawn object?

I found the answer by looking at the method in the C++ API; there it exists in five different overloads, two of which can offer subpixel accuracy: void drawEllipse(const QRectF &rectangle) and void drawEllipse(const QPointF &center, qreal rx, qreal ry)
I tested substituting QPointF for QPoint, and it solved the problem.

Related

(Qt) Render SVG with custom color

I want to render a SVG icon in a QPixmap by choosing the drawing color.
This is my code using PyQt:
def svg_to_pixmap(svg_filename: str, width: int, height: int, color: QColor) -> QPixmap:
renderer = QSvgRenderer(svg_filename)
pixmap = QPixmap(width, height)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setPen(QPen(color))
renderer.render(painter)
painter.end()
return pixmap
And a test code to display the pixmap:
app = QApplication([])
pixmap = svg_to_pixmap("test.svg", 512, 512, Qt.GlobalColor.red)
label = QLabel()
label.setStyleSheet("background-color: yellow;")
label.setPixmap(pixmap)
label.show()
app.exec()
The issue is that painter.setPen has no effect as expected (the drawing remains black). The background is transparent as expected and we can see the label background color behind.
An example of SVG file to test here
My configuration: Ubuntu22.10, X11, PyQt6.3.1
The answer is not simple. But, as long as you are completely sure that you only want to use the shape (or mask) of the original SVG, then it is feasible.
The reason for which setting the pen doesn't change the result is that SVG is a vector image format: it usually explicitly tells the colors of anything it wants to draw, so, setting the painter pen is almost useless, unless the SVG content is so simple to not specify it, which is clearly not the case: icons normally define colors of their shapes, and for good reasons.
The problem comes when trying to understand compositing and the possible alternatives that QPainter provides.
To be honest, even after trying to put efforts in understanding the results and associating them with the QPainter.CompositionMode enums, I still need to testing and checking in order to understand the proper mode I need.
What you probably need is the CompositionMode_SourceIn, which is cryptically explained in the documentation:
The output is the source, where the alpha is reduced by that of the destination.
As far as I can understand, the source is what is going to be painted, while the destination is what was previously painted (consider the source as a brush painting, and the destination as the canvas on which you paint).
With that in mind, we need to use the original svg as the destination and fill the whole pixmap with the color we want. Since only the alpha of the destination will be used (the visible part of the svg), we will be practically doing some sort of "stencil".
def svg_to_pixmap(svg_filename: str, width: int, height: int, color: QColor) -> QPixmap:
renderer = QSvgRenderer(svg_filename)
pixmap = QPixmap(width, height)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter) # this is the destination, and only its alpha is used!
painter.setCompositionMode(
painter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), color)
painter.end()
return pixmap
Which seems to give the wanted result:

How to detect Qt mouse events only over QPainted objects

I am attempting to produce a compass widget programatically that looks much like this:
I want each "slice" of the compass to act as a button for interaction w/ the rest of the app. To that end, I figured making them out of QAbstractButtons made the most logical sense, and started down this path:
class compassWedge(QAbstractButton):
def __init__(self, start_angle, parent=None):
super().__init__(parent)
self.start = start_angle
self.setFixedSize(550, 550)
self.setMouseTracking(True)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
brush = QBrush()
if self.underMouse():
brush.setColor(Qt.black)
else:
brush.setColor(Qt.white)
pen = QPen(Qt.black)
pen.setWidth(5)
painter.setBrush(brush)
painter.setPen(pen)
painter.drawPie(25, 25, 500, 500, self.start, 45 * 16)
painter.end()
class compassApplet(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(550, 550)
self.wedges = []
for start in range(0, 360 * 16, 45 * 16):
self.wedges.append(compassWedge(start, self))
And visually this works perfectly so far:
The problem is, for underMouse(), the entirety of the 550x550 slice widget area is considered. I want instead to detect when the mouse is inside the pixels generated within the paintEvent for each slice, i.e. the pie area created by painter.drawPie(...) in each object.
How can I accomplish this without having to do complicated geometry to check mouse position against pie-shaped areas?
Method 1: perform the geometric calculations yourself:
How can I accomplish this without having to do complicated geometry to check mouse position against pie-shaped areas?
Doing the geometry check yourself isn't that difficult:
Check if the mouse is inside the circle, i.e. distance between mouse and center of circle <= radius of circle.
Use atan2 to calculate the angle and use it to determine the correct segment
Method 2: Use QPieSeries
If you really do not want to implement the geometric calculations yourself, visualising the compass using a QPieSeries may be a solution as it provides a hovered function. However, it may be (more) difficult to obtain the exact desired visual representation.

get the exact height of QTextDocument in pixels

I need to get the actual height of QTextDocument in order to be able to set the containing QPlainTextEdit to a minimum height (while keeping its width constant) so that it shows the whole content without the vertical scrollbar. I tried to follow this question (closed with with accepted answer) How do I determine the height of a QTextDocument? but it does not do what it promises.
A piece of code:
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
app = QApplication([])
w = QPlainTextEdit()
w.setPlainText("Hello!")
print(w.document().size())
w.setPlainText("Hello!\nHello again!")
print(w.document().size())
prints out:
PyQt5.QtCore.QSizeF(35.0, 1.0)
PyQt5.QtCore.QSizeF(64.0, 2.0)
It seems that the width is measured correctly in pixels but the height just shows the number of lines instead of pixels. I think multiplying it with font pixel metric height does not help because there can be mixed formatting (in general it can be a rich text / HTML) and line spacing, document margins and maybe some other complicated stuff based on implementation details... etc.
So is there a way out?
So I finally found a solution but it is really ugly. If anyone knows anything better, please publish it.
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
app = QApplication([])
w = QPlainTextEdit()
# test various formatting
w.appendHtml("<h1>Hello!</h1>")
w.appendHtml("<b>Hello!</b>")
w.appendPlainText("Hello!")
doc = w.document()
layout = doc.documentLayout()
h = 0
b = doc.begin()
while b != doc.end():
h += layout.blockBoundingRect(b).height()
b = b.next()
# magic formula: I do not know why the document margin is already
# once included in the height of the last block, and I do not know
# why there must be the number 1 at the end... but it works
w.setFixedHeight(h + doc.documentMargin() + 2 * w.frameWidth() + 1)
w.show()
app.exec_()
So this should show the box without scroll bar. If you decrease the height by 1, the scroll bar appears. This should work with any number of lines, document margins, frame widths, formatting etc. Hopefully.
Shot in the dark without testing
Have you looked # pageSize?
From the docs:
This property holds the page size that should be used for laying out the document
The units are determined by the underlying paint device. The size is
measured in logical pixels when painting to the screen, and in points
(1/72 inch) when painting to a printer.
By default, for a newly-created, empty document, this property
contains an undefined size.
If you set the pageSize, as directed by the other thread, I'd expect you'd get the value out in the pixels that QPlainTextEdit::setMinimumHeight needs.

Use window/viewport to flip QPainter y-axis

I'm using Qt 4.7 QPainter to draw some polygons, etc into a widget. I am hoping to alter the coordinate system so that (0,0) is at the center of my widget, and the x/y axis behave in a standard "Cartesian" way (ie. y increases going "up" and decreases going "down"). In other words, I want the coordinates to be "math"-like not "computer graphics"-like, if you know what I mean. :-)
I'm trying to do this using setViewport() and setWindow() rather than do the math myself, as it would be nice to be able to just call the draw methods directly with my coordinates.
Here's what I've got so far:
// Setup coordinates
double screenWidth = width();
double screenHeight = height();
double windowWidth = 100.0;
double windowHeight = (screenHeight / screenWidth) * windowWidth;
painter.setViewport(0, 0, screenWidth, screenHeight);
painter.setWindow(-(windowWidth / 2.0), -(windowHeight / 2.0), windowWidth, windowHeight);
// Draw stuff
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::blue);
painter.drawRect(-10, -10, 20, 20);
Now this works just fine, in that it draws a nice blue square in the middle of the screen. The problem is, I have to say that the upper left corner is (-10, -10). I'd like to be able to make it (-10, 10), as that is what it would be in Cartesian coords.
I tried messing with setWindow/setViewport to get this "y-axis flip", but to no avail. This seems like a really easy/basic thing to do, but after scouring the Qt docs and the web, I can't figure it out!
Thanks,
Chris
Use class QMatrix. It specifies 2D transformations. QMatrix is set to QPainter.
But remember, in your case, if you convert your widget's coords to Cartesian coords, you will have to put first point at (-10,-10) (not at (-10,10) as you did mentioned) to draw a rect, which has center at (0,0), because Y-axis now grows up and X-Axis now grows right.
All you need is to transform your coord system this way:
translate origin from (0,0) to the middle of the widget:
scale Y-axis by -1 factor:
Here is the code, typed in paintEvent() function of a widget:
QPainter pn( this );
int w_2 = width() / 2;
int h_2 = height() / 2;
{ // X- and Y-Axis drawing
pn.setPen( Qt::blue );
pn.drawLine( 0, h_2, width(), h_2); // X-Axis
pn.drawLine( w_2, 0 , w_2, height() ); // Y-Axis
}
QMatrix m;
m.translate( w_2, h_2 );
m.scale( 1, -1 );
pn.setMatrix( m );
pn.setPen( Qt::NoPen );
pn.setBrush( QBrush( Qt::blue, Qt::Dense4Pattern ) );
pn.drawRect( -10, -10, 20, 20 );
result:
update apr 07, 2014
This question was asked a long time ago and many things have changed since. For those asking themselves the same question today (beginnings of 2014) then my personal answer is that since Qt 4.3 it is possible to solve problem with text flipping more easier.
You are right. Text also gets filpped because it is drawn with the same painter. You can draw text at the end, when all flipped drawings are done, if it is possible. This method is not convinient because of new calculations of texts position. Also you will need to drop settings for painter.
Now I would recommend you to use QGraphicsView, because of huge support of 2D painting. Also for each QGraphicsItem ItemIgnoresTransformations flag can be set, which allows it to ignore inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). This flag is useful for keeping text label items horizontal and unscaled, so they will still be readable if the graphics view is transformed
The above answer will also flip text, "p" will be "b". To avoid that you have to flip back the y-axis before text is drawn, and you have to change sign on y-coord for the text position when you draw it. This is a little bit ugly I think, or is there a better way?
As stated above, drawing text also appears flipped upside down. There is an easy solution to it, see below. We will temporary disable the world transform for the text drawing. Note that text is not scaled anymore.
in your painting code we want to draw text on coordinate QPointF P;
Painter pn( this );
// calculate the point with the transform
QPointF p = pm.transform().map(P);
// Disable Transform temporary
pn.setWorldMatrixEnabled(false);
// draw it ordinary, no scaling etc
pn.drawText(p, QString("HI FRIENDS!"));
// Enable the transform again
pn.setWorldMatrixEnabled(true);
I needed to flip the y-axis in order to paint lines and polygons using Qt from points defined in Java coordinates. I imagine others will need to do this in porting from Java to Qt coordinate systems. The discussion above was helpful. My solution was:
painter.translate(0,height());
painter.scale(1.0, -1.0);
and then proceed to draw the lines and polygons.

Is there a way to make drawText() update a QPicture's bounding rect?

Drawing on a QPicture should update its bounding rect. Like this:
>>> picture = QPicture()
>>> painter = QPainter(picture)
>>> picture.boundingRect()
QRect(0,0,0,0)
>>> painter.drawRect(20,20,50,50)
>>> picture.boundingRect()
QRect(20,20,50,50)
But if I draw text on it, the bounding rect isn't updated:
>>> picture = QPicture()
>>> painter = QPainter(picture)
>>> picture.boundingRect()
QRect(0,0,0,0)
>>> painter.drawText(10,10, "Hello, World!")
>>> picture.boundingRect()
QRect(0,0,0,0)
Obviously, it doesn't update the bounding rect.
Is there a way to make it repsect drawn text or do I have to do it manually? (Not too hard, but I hope that Qt can assist me here.)
Take a look at these overload methods, where you must specify the Bounding Rectangle after the text parameter (which is apparently different than the rectangle in the first argument's position):
Draws the given text within the
provided rectangle according to the
specified flags. The boundingRect (if
not null) is set to the what the
bounding rectangle should be in order
to enclose the whole text.
QPainter.drawText (1), QPainter.drawText (2)
Update:
It appears if you want to generate a bounding rectangle for the drawText() method in advance, you just call the boundingRect() method on QPainter, which does the following:
Returns the bounding rectangle of the
text as it will appear when drawn
inside the given rectangle with the
specified flags using the currently
set font(); i.e the function tells you
where the drawText() function will
draw when given the same arguments.
If the text does not fit within the
given rectangle using the specified
flags, the function returns the
required rectangle.
QPainter.boundingRect
I linked to BoundingRect with QRectF output, but the information applies to the other versions as well.
So basically, pass the result of QPainter.boundingRect() into the boundingRect parameter of the QPainter.drawText() method (the second QRect argument).
Update 2:
I APOLOGIZE PROFUSELY for being so damn dense. I forgot that drawText works differently in PyQt than in Qt. The bounding rectangle is RETURNED by the drawText function (not passed in like Qt) and in addition, you have to specify alignment flags before you get a bounding rectangle given back to you. (I even included the p.end() as per Aaron Digulla's comment):
pic = Qt.QPicture()
p = QtGui.QPainter(pic)
brect = p.drawText(10,10,200,200, QtCore.Qt.AlignCenter, "blah")
p.end()
print brect
print pic.boundingRect()
Here is the output:
PyQt4.QtCore.QRect(100, 103, 20, 14)
PyQt4.QtCore.QRect(0, 0, 0, 0)
So it appears you will have to set the bounding rectangle yourself, though at least it is returned to you by the output of the drawText() method when passing in flags.
This does not seem like ideal behaviour, that you would have to set the bounding rectangle yourself. I hope someone else has the answer you're looking for, but I suspect you may want to report this bug.
Painting doesn't change the size of something in Qt. The main reason is this:
A component has to paint itself
The paint triggers a resize
The resize triggers painting -> endless loop
So the resize has to happen during the layout phase. After that, the bounds should not change.
To solve your problem, use QFontMetric to figure out how big your text is going to be during or close to the construction of your picture and then resize it accordingly.
[EDIT] Hm ... try to call end() before requesting the bounding rect. If that works, you've found a bug (can't see a reason why the bounding rect should not exist as you add elements...)

Resources