PDA

View Full Version : QGraphicsItem with a cosmetic pen of a fixed width in pixels bigger 1



hb
14th April 2008, 20:01
My item derived from QGraphicsItem plots a large set of small rectangles. I want those rectangles to be in a scale-invariant, fixed size (in pixels). The item itself, however, still needs to inherit the transformations from the parents, so I cannot use QGraphicsItem::ItemIgnoresTransformations.

As the paining has to be very efficient (lots of rectanlges at a high update rate), it would probably be the best if I plotted them as points with QPainter::drawPoints(). However, I was not yet able to figure out a way to make the width of a QPen transformation independant. Basically, what I'd need was a cosmetic pen with a width of not 1, but maybe 5 pixels.

Anybody got any idea how to solve this problem?

wysota
14th April 2008, 20:14
It would not be cosmetic anymore. You can try applying an inverted transformation on the item, but this won't increase rendering speed.

hb
15th April 2008, 19:18
It would not be cosmetic anymore. You can try applying an inverted transformation on the item, but this won't increase rendering speed.

Thanks for making it clear that there is no way to archieve this without triggering manual coordinate transformations, wysota. I am, however, having problems even with that. I found the thread (http://www.qtcentre.org/forum/f-qt-programming-2/t-implementing-scale-invariant-qgraphicsitems-4167.html)where spud claims to know how to draw the handles of an item scale- and rotation-invariant, however, the solution is not presented in the thread.

After trying out all kinds of different transformations, I found something that seems to yield the right result, but is not consistent with the documentation:


QPen pen;
pen.setWidthF(5./qAbs(painter->deviceTransform().m12()));

Qt's documentation says QTransform::m12() and m21() describe the shearing factors. They are for my (scaled, non-sheared) example however working: I get a pen with a constant size in pixels on my QGraphicsView. m11() and m22() on the other hand, which were supposed to describe the scaling, are constant zero.

So either I am doing this wrong, and this is just working by coincidence, or the documentation is wrong. I would be surprised by that though, as my intuition also says that scaling should be on the main diagonal of the transformation matrix..

I'll see if I can revive the before-mentioned old thread.

AndyK
2nd June 2012, 17:08
I have a similar issue (I think): I have a collection of QGraphicsView-based data visualization tools which can have very different horizontal and vertical scales. I want to be able to draw items (lines and polygons) with different (> 1 pixel) pen widths, but if I don't use a cosmetic pen the scaling adversely affects the visibility: If the horizontal scale is very large vertical lines are so compressed they don't appear. What I want is a pen whose width I can specify but which is scale-invariant (like a cosmetic pen). Is this not possible? Setting the ItemIgnoresTransformations flag is no good since I want to be able to zoom in and out of the items. The only solution I can think of is hideous: determining on the fly for each line segment what the effective pen width should be based on its angle, and the current horizontal and vertical scales (m11 and m22).

Thanks.



It would not be cosmetic anymore. You can try applying an inverted transformation on the item, but this won't increase rendering speed.

d_stranz
3rd June 2012, 04:54
What if you convert your line segments into a QPainterPath and use a QPainterPathStroker of the appropriate cosmetic width, then create a QGraphicsPathItem using the output of the stroker's createStroke() method?

AndyK
3rd June 2012, 18:17
Yeah, same problem. The width I specify for the path stroker object is still dependent on the scene coordinates. If the scale is very large the width is compressed to 1 pixel. If the scale is small the lines are drawn with a huge width. I'm surprised this isn't trivially easy to do in Qt. Is it that unusual to want to be able to control the line width in a scale-independent way?


What if you convert your line segments into a QPainterPath and use a QPainterPathStroker of the appropriate cosmetic width, then create a QGraphicsPathItem using the output of the stroker's createStroke() method?

AndyK
4th June 2012, 21:15
Ok, I got things to work with the messy solution I was trying to avoid. In the interest of the public good I'm posting below the core function which calculates the width in scene coordinates for each segment in a polyline so that the entire line displays with a constant user-defined width when the horizontal and vertical scaling factors (m11() and m22()) are different. The polyline object itself is derived from QGraphicsPathItem so this function calculates the QPainterPath that is used by the base class for drawing.
If there's a better way to do this I'd dearly like to know, but I'm starting to think that the reason Qt doesn't implement scale-invariant pen widths > 1 is because of the performance issues obvious below:


// If the horizontal and vertical scales of the graphics view are different the display width of a line segment will
// depend on the angle the segment makes with the horizontal axis. If we think of the two scaling factors
// as the major and minor axes of an ellipse the width scale will be the radius of the ellipse at an angle 90 degrees
// from the angle of line segment. The equation for an ellipse in polar coordinates centered at the origin is
// r = a b / sqrt(b^2 cos^2(theta) + a^2 sin^2(theta)) where a and b are the semi-major and semi-minor axes and theta
// is (in our case) the angle perpendicular to the line segment. We can get the angle of the line segment (alpha) using
// trigonometry, and since we really want the squares of the sine and cosine it's even easier:
// sin^2(alpha) = deltax^2/(deltax^2+deltay^2) and cos^2(alpha) = deltay^2/(deltax^2+deltay^2).
// (deltax and deltay are the x and y displacements of the segment)
// Since alpha and theta are perpendicular we can replace sin^2(theta) in the
// ellipse equation with cos^2(alpha) and cos^2(theta) with sin^2(alpha). This gives us the expression
// r = width scale = xScale yScale / sqrt(yScale^2 sin^2(alpha) + xScale^2 cos^2(alpha))

void MGraphicsLine::recalculateSegmentWidths(const QTransform &t)
{
double xScale = 1.0/t.m11();
double yScale = fabs(1.0/t.m22());

prepareGeometryChange(); // the bounding rect will change as a result of recalculating the widths
// recalculate the path for the polyline using the new widths
mShapePath = QPainterPath();

for (int i=0; i<mLineSegments.size(); i++)
{
QPointF p1 = mLineSegments.at(i).first;
QPointF p2 = mLineSegments.at(i).second;

double widthScale;
if (xScale == yScale)
widthScale = xScale;
else
{
double cosSq = 0, sinSq = 0;
calculateSquaredSegmentAngles(p1, p2, cosSq, sinSq);
widthScale = getEllipseRadius(xScale, yScale, sinSq, cosSq);
}

double width = mLineWidth*widthScale; // mLineWidth is the desired display width
QPainterPath segPath;
segPath.moveTo(p1);
segPath.lineTo(p2);

QPainterPathStroker stroker;
stroker.setWidth(width);
stroker.setDashPattern(mLineStyle);
stroker.setCapStyle(Qt::FlatCap); // can't use a cap, to avoid issues with different scales
QPainterPath strokePath = stroker.createStroke(segPath);
mShapePath.addPath(strokePath);

}

setPath(mShapePath);
}

static bool calculateSquaredSegmentAngles(const QPointF &p1, const QPointF &p2, double &cosSq, double &sinSq)
{
cosSq = 0;
sinSq = 0;

double deltaX = p1.x()-p2.x();
double deltaY = p1.y()-p2.y();
double hypotSquared = deltaX*deltaX+deltaY*deltaY;
if (hypotSquared)
{
cosSq = deltaX*deltaX/hypotSquared;
sinSq = deltaY*deltaY/hypotSquared;
return true;
}
return false;
}

static double getEllipseRadius(double a, double b, double squaredCos, double squaredSin)
{
double widthScale;
if (a == b)
return a;
if (squaredCos == 0)
return b;
else if (squaredSin == 0)
return a;

return a*b/sqrt(a*a*squaredSin + b*b*squaredCos);
}

d_stranz
6th June 2012, 17:54
Thanks, but please learn about [CODE] tags when posting source code. Click the "Go Advanced" editing button next time you make a post, and you will see a "#" symbol in the editing buttons. This inserts [CODE] tags which properly formats your code for display and permits easy cut / paste, line numbering, etc.

AndyK
6th June 2012, 19:38
Sorry! I actually looked for a code tag under advanced before I posted but somehow missed it. I can't seem to edit my post now.



Thanks, but please learn about [CODE] tags when posting source code. Click the "Go Advanced" editing button next time you make a post, and you will see a "#" symbol in the editing buttons. This inserts [CODE] tags which properly formats your code for display and permits easy cut / paste, line numbering, etc.

tzioboro
13th June 2012, 16:42
Hi I had the same problem ...
Anybody that get here - you might use QPen::setCosmetic - that will obey all transformations and draw line with given width of pixels.
If you are going to use it with different devices (ex. printer and screen) you might recalculate thickness regardles to device ppi or other metrics.

This thread was very helpfull about this:
http://lists.trolltech.com/qt-interest/2006-12/thread00163-0.html