I am using Qt 5.6, I want to draw a number of text labels around a circle and rotate the text labels to orientate the text according to its position around the circle, so 12 o'clock would have a 0 degree rotation, 3 o'clock would be rotated 90 degrees, 6 o'clock would be rotated 180 degrees etc.
I am aligning the text around its centre position:
void drawText(QPainter* pobjPainter, qreal fltX, qreal fltY
,int intFlags, const QString* pobjText) {
const qreal fltSize = 32767.0;
QPointF ptfCorner(fltX, fltY - fltSize);
if ( (intFlags & Qt::AlignHCenter) == Qt::AlignHCenter ) {
ptfCorner.rx() -= fltSize / 2.0;
} else if ( (intFlags & Qt::AlignRight) == Qt::AlignRight ) {
ptfCorner.rx() -= fltSize;
}
if ( (intFlags & Qt::AlignVCenter) == Qt::AlignVCenter ) {
ptfCorner.ry() += fltSize / 2.0;
} else if ( (intFlags & Qt::AlignTop) == Qt::AlignTop ) {
ptfCorner.ry() += fltSize;
}
QRectF rctPos(ptfCorner, QSizeF(fltSize, fltSize));
pobjPainter->drawText(rctPos, intFlags, *pobjText);
}
I would like to apply a rotation to the text.
I would like to reproduce something similar to what is shown:
http://www.informit.com/articles/article.aspx?p=1405545&seqNum=2
It seems that the rotate function rotates the entire painter canvas, so the coordinates must take into account the rotation which is really giving me a hard time. I want to position the text around the ellipse and then rotate it, how do I know what the coordinates should be?
Sticking with the clock example you could try something like...
virtual void paintEvent (QPaintEvent *event) override
{
QPainter painter(this);
double radius = std::min(width(), height()) / 3;
for (int i = 0; i < 12; ++i) {
int numeral = i + 1;
double radians = numeral * 2.0 * 3.141592654 / 12;
/*
* Calculate the position of the text centre as it would be required
* in the absence of a transform.
*/
QPoint pos = rect().center() + QPoint(radius * std::sin(radians), -radius * std::cos(radians));
/*
* Set up the transform.
*/
QTransform t;
t.translate(pos.x(), pos.y());
t.rotateRadians(radians);
painter.setTransform(t);
/*
* Specify a huge bounding rectangle centred at the origin. The
* transform should take care of position and orientation.
*/
painter.drawText(QRect(-(INT_MAX / 2), -(INT_MAX / 2), INT_MAX, INT_MAX), Qt::AlignCenter, QString("%1").arg(numeral));
}
}
Related
I'm attempting to implement mouse cursor centered zoom in/out on an image. For simplicity, assume I only need to zoom in/out in the horizontal axis. I am using Qt with QPainter to draw my scene and QWidget:: mouseWheelEvent override to calculate a zoom factor and a zoom position.
To put it into words before showing the code, the zoom factor, and zoom position define the transformation needed to transform from the original image into the zoomed image (to put it another way, I don't combine matrices or something similar, but calculate the absolute transformation in the paint event), as can be finally seen here:
void MyWidget::paintEvent(QPaintEvent* e)
{
QPainter painter{this};
painter.translate(m_zoomCenterX, 0.);
painter.scale(m_zoomFactor, 1.);
painter.translate(-m_zoomCenterX, 0.);
// content margins are all 0 btw
painter.drawImage(contentsRect(), m_image);
}
In the mouse wheel event, I try to calculate the scaled difference between the zoom center and the new position which I add to the zoom center. Zoom factor is computed simply by multiplying with a constant according to the angle delta. The final if-else branching does some boundaries checking (which is something I'd like to do so the image stays stretched in the view and doesn't move somewhere I don't want to):
void MyWidget::wheelEvent(QWheelEvent* e)
{
const QRect rect = contentsRect();
// m_zoomCenterX is first initialized to 0 in the MyWidget constructor, m_zoomFactor to 1.
const qreal diff = (e->pos().x() - m_zoomCenterX) / m_zoomFactor;
qDebug() << diff;
m_zoomCenterX += diff;
static constexpr const qreal ZOOM_IN_FACTOR = 1.1,
ZOOM_OUT_FACTOR = .9;
m_zoomFactor *= e->angleDelta().y() > 0. ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
if (m_zoomFactor < 1.)
{
m_zoomFactor = 1.;
m_zoomCenterX = 0.;
}
else if (m_zoomFactor > 5.)
{
m_zoomFactor = 5.;
}
if (m_zoomCenterX < 0)
{
m_zoomCenter = 0;
}
else if (m_zoomCenterX >= rect.width())
{
m_zoomCenterX = rect.width() - 1;
}
update();
}
This doesn't seem to work once I try to zoom in/out after let's say zooming in (see picture:
, sorry if it's not much), the zoom center position is likely not correctly computed but I'm not sure why. The qDebug line gives me non-zero differences after consecutive scrollings when the cursor is on a new position, but after the first scrolling it should be zero...
I guess it must be something trivial but I'm quite stuck at the moment.
Well I got it working. I use inverse matrix to compute the new zoom center, but once zoomed in, it will not match the mouse position, so the entire image must be translated additionally by the inverted_position - mouse_position. Here's the final code for any future wanderers. m_extraTranslation is obviously initialised 0 in the constructor.
QMatrix MyWidget::computeScaleMatrix() const noexcept
{
QMatrix scaleMatrix{1., 0., 0., 1., 0., 0.};
scaleMatrix.translate(-m_extraTranslation, 0.);
scaleMatrix.translate(m_zoomCenter.x(), 0.);
scaleMatrix.scale(m_zoomFactor, 1.);
scaleMatrix.translate(-m_zoomCenter.x(), 0.);
return scaleMatrix;
}
void MyWidget::wheelEvent(QWheelEvent* e)
{
const QMatrix invScaleMatrix = computeScaleMatrix().inverted();
const QPointF invPos = invScaleMatrix.map(e->pos());
m_zoomCenter.setX(invPos.x());
static constexpr const qreal ZOOM_IN_FACTOR = 1.1,
ZOOM_OUT_FACTOR = .9;
m_zoomFactor *= e->angleDelta().y() > 0. ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
m_extraTranslation = invPos.x() - e->pos().x();
// here I'm making sure the image corners do not wander off inside the widget, this is necessary to do for zooming out
const QMatrix scaleMatrix = computeScaleMatrix();
const qreal startX = scaleMatrix.map(QPointF{0., 0.}).x(),
endX = scaleMatrix.map(QPointF(contentsRect().width() - 1, 0.)).x();
if (startX > 0.)
{
m_extraTranslation += startX;
}
else if (endX < contentsRect().width() - 1)
{
m_extraTranslation -= contentsRect().width() - endX - 1;
}
if (m_zoomFactor < 1.)
{
m_zoomFactor = 1.;
m_zoomCenter = {0, 0};
m_extraTranslation = 0;
}
else if (m_zoomFactor > 5.)
{
m_zoomFactor = 5.;
}
if (m_zoomCenter.x() < 0)
{
m_zoomCenter.setX(0);
}
else if (m_zoomCenter.x() >= contentsRect().width())
{
m_zoomCenter.setX(contentsRect().width() - 1);
}
update();
e->accept();
}
void MyWidget::paintEvent(QPaintEvent* e)
{
QPainter painter{this};
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setMatrix(computeScaleMatrix()));
painter.drawImage(contentsRect(), m_image);
}
Extending this for the Y axis should be trivial since it would follow the same logic.
I have redefined my own QGraphicsItem to show a LineString. I redefined this because I need to create my own boundingbox and painter redefined the abstract method.
Now I have this code:
QRectF myQGraphicsLine::boundingRect() const
{
double lx = qMin(myLine->getX(0), myLine->getX(1));
double rx = qMax(myLine->getX(0), myLine->getX(1));
double ty = qMin(-myLine->getY(0), -myLine->getY(1));
double by = qMax(-myLine->getY(0), -myLine->getY(1));
return QRectF(lx-size/2,ty, rx -lx + size, by-ty).;
}
void myQGraphicsLine::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
{
pen.setColor(Qt::red);
QLine line(myLine->getX(0), -myLine->getY(0), myLine->getX(1), -myLine->getY(1));
pen.setWidth(size);
painter->setPen(pen);
painter->setBrush(brush);
painter->drawLine(line);
}
This all work fine, but I have a little problem with the boundingRec.
If the line follows the x- or y-axis I get this result:
And in other position I get this:
and I need this:
Does anyone know any way to rotate the boundinRec? Thanks a lot!
I fixed the problem redefining QGraphicsItem::shape() method.
For use this, I created a QPoligonF with my line shape, in my case I used this function:
void myQGraphicsLine::createSelectionPolygon()
{
QPolygonF nPolygon;
QLineF line(myLine->getX(0), -myLine->getY(0), myLine->getX(1), -myLine->getY(1));
qreal radAngle = line.angle()* M_PI / 180; //This the angle of my line vs X axe
qreal dx = size/2 * sin(radAngle);
qreal dy = size/2 * cos(radAngle);
QPointF offset1 = QPointF(dx, dy);
QPointF offset2 = QPointF(-dx, -dy);
nPolygon << line.p1() + offset1
<< line.p1() + offset2
<< line.p2() + offset2
<< line.p2() + offset1;
selectionPolygon = nPolygon;
update();
}
The paint(),boundingRect() and shape() methods stayed like this:
QRectF myQGraphicsLine::boundingRect() const
{
return selectionPolygon.boundingRect();
}
void myQGraphicsLine::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
{
pen.setColor(Qt::red);
QLineF line(myLine->getX(0), -myLine->getY(0), myLine->getX(1), -myLine->getY(1));
pen.setWidth(size);
painter->setPen(pen);
painter->setBrush(brush);
painter->drawLine(line);
}
QPainterPath myQGraphicsLine::shape() const
{
QPainterPath path;
path.addPolygon(selectionPolygon);
return path;
}
Thanks all for your answers!!
A QRect or QRectf cannot be rotated. Its sides are always parallel to the vertical and horizontal axes. Think of it as a point (x, y) giving the upper left corner of the rectangle plus 'width' and 'height' members. No matter what transformation you perform on the QRect it will always interpret 'width' as a horizontal distance and 'height' as vertical distance.
You can extract the four corners of the QRect as points and then rotate those points, but there will be no way to convert the rotated points back to a QRect unless they just happen to be parallel to the sides of the viewport/window.
I've got a "canvas" that the user can draw pixels, etc. onto. It works well, but my zoom functionality currently uses the same origin regardless of the position of the mouse. I'd like to implement functionality like that of Google Maps' zoom behaviour:
That is, the zoom's origin should always be the position of the mouse cursor.
What I currently have is not exactly right...
My attempts have mostly been stabs in the dark, but I've also tried using the code from this answer without success.
main.cpp:
#include <QGuiApplication>
#include <QtQuick>
class Canvas : public QQuickPaintedItem
{
Q_OBJECT
public:
Canvas() :
mTileWidth(25),
mTileHeight(25),
mTilesAcross(10),
mTilesDown(10),
mOffset(QPoint(400, 400)),
mZoomLevel(1)
{
}
void paint(QPainter *painter) override {
painter->translate(mOffset);
const int zoomedTileWidth = mTilesAcross * mZoomLevel;
const int zoomedTileHeight = mTilesDown * mZoomLevel;
const int zoomedMapWidth = qMin(mTilesAcross * zoomedTileWidth, qFloor(width()));
const int zoomedMapHeight = qMin(mTilesDown * zoomedTileHeight, qFloor(height()));
painter->fillRect(0, 0, zoomedMapWidth, zoomedMapHeight, QColor(Qt::gray));
for (int y = 0; y < mTilesDown; ++y) {
for (int x = 0; x < mTilesAcross; ++x) {
const QRect rect(x * zoomedTileWidth, y * zoomedTileHeight, zoomedTileWidth, zoomedTileHeight);
painter->drawText(rect, QString::fromLatin1("%1, %2").arg(x).arg(y));
}
}
}
protected:
void wheelEvent(QWheelEvent *event) override {
const int oldZoomLevel = mZoomLevel;
mZoomLevel = qMax(1, qMin(mZoomLevel + (event->angleDelta().y() > 0 ? 1 : -1), 30));
const QPoint cursorPosRelativeToOffset = event->pos() - mOffset;
if (mZoomLevel != oldZoomLevel) {
mOffset.rx() -= cursorPosRelativeToOffset.x();
mOffset.ry() -= cursorPosRelativeToOffset.y();
// Attempts based on https://stackoverflow.com/a/14085161/904422
// mOffset.setX((event->pos().x() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.x()));
// mOffset.setY((event->pos().y() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.y()));
// mOffset.setX((cursorPosRelativeToOffset.x() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.x()));
// mOffset.setY((cursorPosRelativeToOffset.y() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.y()));
update();
}
}
void keyReleaseEvent(QKeyEvent *event) override {
static const int panDistance = 50;
switch (event->key()) {
case Qt::Key_Left:
mOffset.rx() -= panDistance;
update();
break;
case Qt::Key_Right:
mOffset.rx() += panDistance;
update();
break;
case Qt::Key_Up:
mOffset.ry() -= panDistance;
update();
break;
case Qt::Key_Down:
mOffset.ry() += panDistance;
update();
break;
}
}
private:
const int mTileWidth;
const int mTileHeight;
const int mTilesAcross;
const int mTilesDown;
QPoint mOffset;
int mZoomLevel;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<Canvas>("App", 1, 0, "Canvas");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.5
import QtQuick.Window 2.2
import App 1.0 as App
Window {
visible: true
width: 1200
height: 900
title: qsTr("Hello World")
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
App.Canvas {
focus: true
anchors.fill: parent
}
}
What am I doing wrong in the wheelEvent() function?
You have a rectangle R = [x_0, x_0 + w] x [y_0, y_0 + h] with absolute coordinates. When you map it to a widget (another rectangle), you apply some transformation T to an area W of R. This transformation is linear with offset:
Values of a_x, b_x, a_y, b_y are calculated to satisfy some simple conditions, you have already done it.
You also have a cursor (x_c, y_c) in R. It's coordinates in W are T(x_c, y_c). Now you want to apply another transformation ,
changing scale coefficients a_x, a_y to known a_x', a_y' with following condition: you want your cursor to point at the same coordinates (x_c, y_c) in R. I.e. T'(x_c, y_c) = T(x_c, y_c) — the same point in relative coordinates points to the same position in absolute coordinates. We derive a system for unknown offsets b_x', b_y' with known rest values. It gives
Last work is to find (x_c, y_c) from widget cursor position (x_p, y_p) = T(x_c, y_c):
and to substitute it:
In your terms it is
mOffset = event->pos() - float(mZoomLevel) / float(oldZoomLevel) *
(event->pos() - mOffset);
If you want to zoom like Google maps then your origin must be at top-left corner of the image(lets say (x,y) = (0,0) and (width, height) = (100,100)) with initial zoomLevel 100.
If you want to zoom at point(40,20) with zoom Factor of 5% then,
the displacement can be calculated as-
newX = 40 - 40*(100.0/105)
newY = 20 - 20*(100.0/105)
newWidth = width - (100.0/105)
newHeight = height - (100.0/105)
then set newX, newY as your origin and change width, height to newWidth and newHeight.
By this implementation you'll be able to zoom at a particular point where the cursor is. But this implementation will not work when you move the cursor at some other positions.
I am also looking for that implementation.
i m working on a project that consist on creating an uml tool with qt and for now i have a problem with drawingtext on an arrow so this is my code :
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem * ,QWidget *)
{
if (myStartItem->collidesWithItem(myEndItem))
return;
QPen myPen = pen();
myPen.setColor(myColor);
qreal arrowSize = 20;
painter->setPen(myPen);
painter->setBrush(myColor);
QLineF centerLine(myStartItem->pos(), myEndItem->pos());
QPolygonF endPolygon = myEndItem->polygon();
QPointF p1 = endPolygon.first() + myEndItem->pos();
QPointF p2;
QPointF intersectPoint;
QLineF polyLine;
for (int i = 1; i < endPolygon.count(); ++i)
{
p2 = endPolygon.at(i) + myEndItem->pos();
polyLine = QLineF(p1, p2);
QLineF::IntersectType intersectType =
polyLine.intersect(centerLine, &intersectPoint);
if (intersectType == QLineF::BoundedIntersection)
break;
p1 = p2;
}
setLine(QLineF(intersectPoint, myStartItem->pos()));
double angle = ::acos(line().dx() / line().length());
if (line().dy() >= 0)
angle = (Pi * 2) - angle;
QPointF arrowP1 = line().p1() + QPointF(sin(angle + Pi / 3) * arrowSize,
cos(angle + Pi / 3) * arrowSize);
QPointF arrowP2 = line().p1() + QPointF(sin(angle + Pi - Pi / 3) * arrowSize,
cos(angle + Pi - Pi / 3) * arrowSize);
arrowHead.clear();
arrowHead << line().p1() << arrowP1 << arrowP2;
painter->drawLine(line());
//painter->drawPolygon(arrowHead);
if (isSelected())
{
painter->setPen(QPen(myColor, 1, Qt::DashLine));
QLineF myLine = line();
myLine.translate(0, 4.0);
painter->drawLine(myLine);
myLine.translate(0,-8.0);
painter->drawLine(myLine);
QPoint point = QPoint( 10, 20 );
painter->drawText( point, "You can draw text from a point..." );
}
}
and nothing happens i can draw the arrow but the text does not appear on the arrow what should i do ? please i need some help
IMO you did that wrong.
You should compose your graphics item using QGraphicsPathItem, QGraphicsPolygonItem, QGraphicsRectItem, and QGraphicsSimpleTextItem instead drawing everything yourself.
Just provide some root item responsible for managing children (lines text and polygons). It will be easier to do this properly.
Secondly your paint method is faulty. You should restore initial state of the painter!
And finally I'm pretty sure your problem is caused by incorrect implementation of boundingRect. It is quite common mistake when performing such complex drawing in paint method.
I'm attempting to draw celestial bodies moving around on simplified, perfectly circular orbits. I'm also drawing the projected orbital paths these objects will take. However, the problem is that the actual path the objects take doesn't agree with the projection on zooming in closely enough.
Video demonstrating the issue: https://www.youtube.com/watch?v=ALSVfx48zXw
If zoomed out, the problem is non-existent, because the deviation is too small. The apparent size of the deviation appears to be affected primarily by the visible curvature of the circles - notice how the paths of the moons agree with their motion. If one were to zoom in so that the moons' projected paths appear close to straight lines, they would have the same pattern of deviations as the planet shows.
Coordinates calculating methods:
double getX (long int time) {
return orbit * cos(offset + time * speed);
}
double getY (long int time) {
return orbit * sin(offset + time * speed);
}
Projected orbit drawing:
ellipse = scene->addEllipse(system.starX-body.orbit,
system.starY-body.orbit,
body.orbit*2,body.orbit*2,greenPen,transBrush);
Drawing the celestial bodies where they actually appear:
ellipse = scene->addEllipse(-body.radius,
-body.radius,
body.radius*2,body.radius*2,blackPen,greenBrush);
ellipse->setFlag(QGraphicsItem::ItemIgnoresTransformations);
ellipse->setPos(system.starX+body.getX(date2days(game.date)),
system.starY+body.getY(date2days(game.date)));
How do I fix this so that the celestial bodies are always on the predicted curve?
EDIT1:
I have attempted using the suggested algorithm for drawing my own ellipse. The version adapted for use with Qt I reproduce here:
QPoint get_point(double a, double b, double theta, QPoint center)
{
QPoint point;
point.setX(center.x() + a * cos(theta));
point.setY(center.y() + b * sin(theta));
return point;
}
void draw_ellipse(double a, double b, QPoint center, double zoom_factor, QGraphicsScene * scene, QPen pen)
{
double d_theta = 1.0d / zoom_factor;
double theta = 0.0d;
int count = 2.0d * 3.14159265358979323846 / d_theta;
QPoint p1, p2;
p1 = get_point(a, b, 0.0f, center);
for (int i = 0; i <= count; i++)
{
theta += d_theta;
p2 = p1;
p1 = get_point(a, b, theta, center);
scene->addLine(p1.x(),p1.y(),p2.x(),p2.y(),pen);
}
}
The results weren't encouraging:
In addition to not looking pretty at zoom_factor 360, the application ran extremely sluggishly, using much more resources than previously.
EDIT2:
The improved version gives much better results, but still slow. Here is the code:
QPointF get_point(qreal a, qreal b, qreal theta, QPointF center)
{
QPointF point;
point.setX(center.x() + a * cos(theta));
point.setY(center.y() + b * sin(theta));
return point;
}
void draw_ellipse(qreal a, qreal b, QPointF center, qreal zoom_factor, QGraphicsScene * scene, QPen pen)
{
qreal d_theta = 1.0d / zoom_factor;
qreal theta = 0.0d;
int count = 2.0d * 3.14159265358979323846 / d_theta;
QPointF p1, p2;
p1 = get_point(a, b, 0.0f, center);
for (int i = 0; i <= count; i++)
{
theta = i * d_theta;
p2 = p1;
p1 = get_point(a, b, theta, center);
scene->addLine(p1.x(),p1.y(),p2.x(),p2.y(),pen);
}
}
It appears that Qt does not auto-adjust the drawing precision or 'sampling resolution'.
You could try to draw the ellipse yourself, by drawing a loop of lines. Increase the sample resolution of the drawing when you zoom in - i.e. make the sampled points closer to each other.
Take the parametric equation of an ellipse
x = a cos (theta), y = b sin (theta)
where a and b are the semi-major and semi-minor axes of the ellipse, and sample the points with it:
(pseudo C++-style code)
point get_point(float theta, point center)
{
return point(center.x + a * cos(theta), center.y + b * sin(theta));
}
void draw_ellipse(float a, float b, point center, float zoom_factor)
{
float d_theta = 1.0f / zoom_factor;
float theta = 0.0f;
int count = 2.0f * PI / d_theta;
point p1, p2;
p1 = get_point(0.0f, center);
for (int i = 0; i < count; i++)
{
theta += d_theta;
p2 = p1;
p1 = get_point(theta, center);
drawline(p1, p2);
}
}
Sorry if the code looks arbitrary (I'm not familiar with Qt), but you get the point.
Assuming that all of the parameters you pass to addEllipse are of sufficient resolution, the issue seems to be with how Qt renders ellipses. The discretization used in ellipse drawing is not dependent on the transformation matrix of the view.
When a QGraphicsItem is being rendered in a view, its paint method certainly has access to the paint device (in this case: a widget). It could certainly determine the proper discretization step in terms of angle. Even if a graphics item were to render using regular painter calls, the painter has the same information, and the paint device certainly has this information in full. Thus there's no reason for Qt to do what it does, I think. I'll have to trace into this code and see why it fails so badly.
The only fix is for you to implement your own ellipse item, and chose the discretization step and begin/end angles according to the viewport size at the time of rendering.
qreal is a double - so that shouldn't be an issue unless Qt is configured with -qreal float.