Positioning a top-level object relative to another - c++

I need to position a top-level object so that it always remains in a position relative to another top-level object. As an example, the rectangle in the image below should stick to the "front" of the ellipse:
When rotated 180 degrees, it should look like this:
Instead, the position of the rectangle is incorrect:
Please run the example below (the use of QGraphicsScene is for demonstration purposes only, as the actual use case is in physics).
#include <QtWidgets>
class Scene : public QGraphicsScene
{
Q_OBJECT
public:
Scene()
{
mEllipse = addEllipse(0, 0, 25, 25);
mEllipse->setTransformOriginPoint(QPointF(12.5, 12.5));
QGraphicsLineItem *line = new QGraphicsLineItem(QLineF(0, 0, 0, -12.5), mEllipse);
line->setPos(12.5, 12.5);
mRect = addRect(0, 0, 10, 10);
mRect->setTransformOriginPoint(QPointF(5, 5));
line = new QGraphicsLineItem(QLineF(0, 0, 0, -5), mRect);
line->setPos(5, 5);
connect(&mTimer, SIGNAL(timeout()), this, SLOT(timeout()));
mTimer.start(5);
}
public slots:
void timeout()
{
mEllipse->setRotation(mEllipse->rotation() + 0.5);
QTransform t;
t.rotate(mEllipse->rotation());
qreal relativeX = mEllipse->boundingRect().width() / 2 - mRect->boundingRect().width() / 2;
qreal relativeY = -mRect->boundingRect().height();
mRect->setPos(mEllipse->pos() + t.map(QPointF(relativeX, relativeY)));
mRect->setRotation(mEllipse->rotation());
}
public:
QTimer mTimer;
QGraphicsEllipseItem *mEllipse;
QGraphicsRectItem *mRect;
};
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QGraphicsView view;
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
view.setScene(new Scene);
view.resize(200, 200);
view.show();
return app.exec();
}
#include "main.moc"
Note that the position of the rectangle is not always the same, but it should always remain in the same position relative to the ellipse. For example, it may start off in this position:
But it should stay in that relative position when rotated:

If you want the two objects to keep the same relative position, they need to rotate around the same origin point.
Here your circle rotates around its center (the point 12.5, 12.5), but your rectangle rotates around another origin (5,5) instead of the circle's center (12.5, 12.5).
If you fix the origin, it'll work as you expect:
mRect->setTransformOriginPoint(QPointF(12.5, 12.5));
Even if the rectangle starts off with an offset:
mRect = addRect(-10, 0, 10, 10); // Start 10 units to the left

Related

What exactly are the positioning rules for text in Qt?

Here's a very basic piece of code which:
Measures the size a piece of text would take.
Draws the rectangle which corresponds to this size at coordinates (100, 25).
Displays text at coordinates (100, 25).
auto str = "Hello, World!";
auto metrix = window->fontMetrics();
auto text = scene->addText(str);
text->setPos(100, 25);
text->setDefaultTextColor(Qt::white);
auto r = metrix.boundingRect(str);
int x, y, w, h;
r.getRect(&x, &y, &w, &h);
scene->addRect(100, 25, w, h, QPen(Qt::white));
The scene in code is a QGraphicsScene with no specific customizations, with the exception of a border set to zero.
I would expect the text to be exactly inside the rectangle. The text is however shifted by a few pixels to the left and a few more pixels to the bottom. Why?
Solution
Setting the document margins to 0, as #NgocMinhNguyen suggested, might seem to work, but it is not a real solution, because you lose the margins. It would be better, if you could get the actual geometry, including margins etc. For that purpose you can use QGraphicsTextItem::boundingRect() instead of QFontMetrics::boundingRect.
Example
Here is a minimal and complete example I have written for you, in order to demonstrate the proposed solution:
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QBoxLayout>
struct MainWindow : public QWidget
{
MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
QPointF p(100, 25);
auto *l = new QVBoxLayout(this);
auto *view = new QGraphicsView(this);
auto *textItem = new QGraphicsTextItem(tr("HHHHHHHH"));
auto *rectItem = new QGraphicsRectItem(textItem->boundingRect()
.adjusted(0, 0, -1, -1));
textItem->setPos(p);
rectItem->setPos(p);
view->setScene(new QGraphicsScene(this));
view->scene()->addItem(textItem);
view->scene()->addItem(rectItem);
l->addWidget(view);
resize(300, 300);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
Note: Please note how I create the rectangle. There is a difference between
auto *item = new QGraphicsRectItem(100, 25, w, h);
and
auto *item = new QGraphicsRectItem(0, 0, w, h);
item->setPos(100, 25);
Result
This example produces the following result:
QGraphicsTextItem is held by QTextDocument, which can have a margin.
Setting the margin to 0 and the rectangle will be correctly drawn.
text->document()->setDocumentMargin(0);

Positioning a resizable widget inside a scroll area

I have a scroll area that contains a resizable widget. The user is able to increment and decrement the integer scale factor of this inner widget (which changes its size). When the user increments the scale, the part of the inner widget visible in the top left corner remains fixed. This gives the effect of zooming into the top left corner of the view.
When the size of the inner widget changes, I need to scroll the inner widget to make it look like the user is zooming into the center of the view. I want to keep the part of the widget in the center of the view fixed in the center while resizing.
I drew some diagrams to help visualise the problem. The pink rectangle is the inner widget. The brown rectangle is the view onto the widget through the scroll area. The green dots are fixed points on the inner widget.
Before scaling
After scaling (current undesirable behaviour)
After scaling (desired behaviour)
As you can (hopefully) see from these crudely drawn diagrams. Simply increasing the size of a widget inside a scroll area results on zooming into the top left corner or the view. I have to do something more to zoom into the center of the view. Also, the inner widget can be much smaller than the scroll area. I only want to shift the inner widget when it is larger than the scroll area.
This is a minimal example of the undesirable behaviour. Pressing Z (after clicking on the inner widget to change focus) will zoom into the top left corner of the view. I want to zoom into the center of the view.
#include <QtGui/qpainter.h>
#include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qscrollarea.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qapplication.h>
class InnerWidget final : public QWidget {
public:
explicit InnerWidget(QScrollArea *parent)
: QWidget{parent}, parent{parent} {
updateSize();
setFocusPolicy(Qt::StrongFocus);
}
private:
QScrollArea *parent;
int scale = 1;
void updateSize() {
setFixedSize(256 * scale, 256 * scale);
}
void paintEvent(QPaintEvent *) override {
QPainter painter{this};
const QColor green = {0, 255, 0};
painter.fillRect(0, 0, width(), height(), {255, 255, 255});
painter.fillRect(32 * scale, 32 * scale, 16 * scale, 16 * scale, green);
painter.fillRect(128 * scale, 128 * scale, 16 * scale, 16 * scale, green);
}
void keyPressEvent(QKeyEvent *event) override {
if (event->isAutoRepeat()) return;
QScrollBar *hbar = parent->horizontalScrollBar();
QScrollBar *vbar = parent->verticalScrollBar();
if (event->key() == Qt::Key_Z) {
// need to call bar->setValue and bar->value here
scale = std::min(scale + 1, 64);
updateSize();
} else if (event->key() == Qt::Key_X) {
// here too
scale = std::max(scale - 1, 1);
updateSize();
}
}
};
int main(int argc, char **argv) {
QApplication app{argc, argv};
QMainWindow window;
QScrollArea scrollArea{&window};
InnerWidget inner{&scrollArea};
window.setBaseSize(512, 512);
window.setCentralWidget(&scrollArea);
scrollArea.setAlignment(Qt::AlignCenter);
scrollArea.setWidget(&inner);
window.show();
return app.exec();
}
To reproduce the problem, zoom in a couple of times then position one of the rectangles in the center of the window. Zooming will move the rectangle toward the bottom right corner. I want zooming to keep the rectangle in the center.
This feels like an easy problem but I can’t seem to get my head around the math. I’ve tried various calculations on the scroll values but none of them seem to behave as I want.
I messed around with the numbers for a while and then I figured it out. Here’s the fully working solution.
#include <QtGui/qpainter.h>
#include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qscrollarea.h>
#include <QtWidgets/qapplication.h>
class InnerWidget final : public QWidget {
public:
explicit InnerWidget(QScrollArea *parent)
: QWidget{parent}, parent{parent}, scale{1} {
updateSize();
setFocusPolicy(Qt::StrongFocus);
}
private:
QScrollArea *parent;
int scale;
void updateSize() {
setFixedSize(256 * scale, 256 * scale);
}
void paintEvent(QPaintEvent *) override {
QPainter painter{this};
const QColor green = {0, 255, 0};
painter.fillRect(0, 0, width(), height(), {255, 255, 255});
painter.fillRect(32 * scale, 32 * scale, 16 * scale, 16 * scale, green);
painter.fillRect(128 * scale, 128 * scale, 16 * scale, 16 * scale, green);
}
void adjustScroll(const int oldScale) {
if (scale == oldScale) return;
QScrollBar *hbar = parent->horizontalScrollBar();
QScrollBar *vbar = parent->verticalScrollBar();
if (width() >= parent->width()) {
const int halfWidth = parent->width() / 2;
hbar->setValue((hbar->value() + halfWidth) * scale / oldScale - halfWidth);
}
if (height() >= parent->height()) {
const int halfHeight = parent->height() / 2;
vbar->setValue((vbar->value() + halfHeight) * scale / oldScale - halfHeight);
}
}
void keyPressEvent(QKeyEvent *event) override {
if (event->isAutoRepeat()) return;
const int oldScale = scale;
if (event->key() == Qt::Key_Z) {
scale = std::min(scale + 1, 64);
updateSize();
adjustScroll(oldScale);
} else if (event->key() == Qt::Key_X) {
scale = std::max(scale - 1, 1);
updateSize();
adjustScroll(oldScale);
}
}
};
int main(int argc, char **argv) {
QApplication app{argc, argv};
QMainWindow window;
QScrollArea scrollArea{&window};
InnerWidget inner{&scrollArea};
window.setBaseSize(512, 512);
window.setCentralWidget(&scrollArea);
scrollArea.setAlignment(Qt::AlignCenter);
scrollArea.setWidget(&inner);
window.show();
return app.exec();
}

How to keep the graphics in the center of main window

I am learning Qt5 and I use Qt 5.9.1. Now I have a problem: how to keep the graphics in the center of main window even when the size of main window changes?
Last year I learned MFC in my class and the teacher told us that in order to make the graphics always stay in the window, we should do as followings:
Let the origin of viewport be the center of client area;
Let the size of viewport be the size of client area;
Let the origin of window be the center of graphics' bounding rectangle;
Let the size of window be the size of graphics' bounding rectangle .
So I do the same thing in Qt5:
// main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QtGuiApplication1 w;
// get the size and center of client area
// and then pass the values to QtGuiApplication
int width = QApplication::desktop()->availableGeometry().width();
int height = QApplication::desktop()->availableGeometry().height();
QPoint center = QApplication::desktop()->availableGeometry().center();
w.setViewport(center,width,height);
w.show();
return a.exec();
}
// GtGuiApplication.cpp
void QtGuiApplication1::paintEvent(QPaintEvent *)
{
QPainter painter(this);
// set the viewport
painter.setViewport(centerOfViewport.x(),centerOfViewport.y(), widthOfViewport, heightOfViewport);
static const QPointF points[4] = {
QPointF(10.0, 10.0),
QPointF(10.0, 80.0),
QPointF(50.0, 80.0),
QPointF(10.0, 10.0)
};
// set the window
painter.setWindow(30,45,40,70);
painter.drawPolyline(points, 4);
}
However all these things didn't work. Before I set the viewport and window:
And after I did the setting:
I do not understand what you indicate in the line, maybe it is fulfilled within MFC, but the rules are not accepted without analyzing them, you have to understand them and it seems that you want to apply them without understanding them correctly.
According to what you point out, you want the polygon to always be centered within the window, and in your code you use the size of the window which does not make sense since the window can be anywhere on the screen, from there we go badly.
If you want the center of the polygon to be the center of the window, then you must calculate both points considering in the first reference source the topleff of the rectangle bordering the polygon, and in the second the rectangle of the window, if we subtract both positions we obtain what must be moved by the painter so that both points coincide.
void QtGuiApplication1::paintEvent(QPaintEvent *)
{
QPainter painter(this);
static const QVector<QPointF> points = {
QPointF(10.0, 10.0),
QPointF(10.0, 80.0),
QPointF(50.0, 80.0),
QPointF(10.0, 10.0)
};
QPainterPath path;
path.addPolygon(QPolygonF(points));
QPointF center_path = path.boundingRect().center();
painter.translate(rect().center()-center_path);
painter.drawPath(path);
}

QT 5.7 QPainter line aligment

I am working with QT 5.7 and C++.
At the moment I try to get used to draw my own widgets with the QPainter class.
But I noticed a problem I couldn't solve.
I try to draw a border line extactly at the widget border but if I do so:
void MyWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
painter.setBrush(Qt::cyan);
QBrush brush(Qt::black);
QPen pen(brush, 2);
painter.setPen(pen);
painter.drawRect(0, 0, size().width() - 1, size().height() - 1);
painter.end();
}
The Line is at the bottom and right site bigger than the others:
And before someone is telling me I have to remove the two -1 expressions,
you should know if I do this and also set the pen width to 1 there is no line anymore at the bottom and right side.
I think this artifact is caused by the "line aligment".
QT tries to tint the the pixels near the logical lines defined by the rectangle but actually because finally all have to be in pixels it has to decide.
If I am right, why there is no method to set the line aligment of the pen like in GDI+?
And how I can solve this?
Everything depends on whether you want the entire pen's width to be visible or not. By drawing the rectangle starting at 0,0, you're only showing half of the pen's width, and that makes things unnecessarily complicated - never mind that the line appears too thin. In Qt, the non-cosmetic pen is always drawn aligned to the middle of the line. Qt doesn't let you change it: you can change the drawn geometry instead.
To get it right for odd line sizes, you must give rectangle's coordinates as floating point values, and they must be fall in the middle of the line. So, e.g. if the pen is 3.0 units wide, the rectangle's geometry will be (1.5, 1.5, width()-3.0, width()-3.0).
Here's a complete example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/widget-pen-wide-38019846
#include <QtWidgets>
class Widget : public QWidget {
Q_OBJECT
Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth)
qreal m_penWidth = 1.0;
protected:
void paintEvent(QPaintEvent *) override {
QPainter p{this};
p.setPen({Qt::black, m_penWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin});
p.setBrush(Qt::cyan);
qreal d = m_penWidth/2.0;
p.drawRect(QRectF{d, d, width()-m_penWidth, height()-m_penWidth});
}
public:
explicit Widget(QWidget * parent = 0) : QWidget{parent} { }
qreal penWidth() const { return m_penWidth; }
void setPenWidth(qreal width) {
if (width == m_penWidth) return;
m_penWidth = width;
update();
}
QSize sizeHint() const override { return {100, 100}; }
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget top;
QVBoxLayout layout{&top};
Widget widget;
QSlider slider{Qt::Horizontal};
layout.addWidget(&widget);
layout.addWidget(&slider);
slider.setMinimum(100);
slider.setMaximum(1000);
QObject::connect(&slider, &QSlider::valueChanged, [&](int val){
widget.setPenWidth(val/100.0);
});
top.show();
return app.exec();
}
#include "main.moc"

Position parentless scene item under another scene item

In my game, I'd like to fire rockets from a rocket launcher. The player holds a rocket launcher as a child item. The rockets must be parentless. I'm trying to position the rocket so that its back lines up with the back of the rocket launcher (the player is facing north in the screenshots) and centered horizontally within it:
Instead, what I'm getting is:
The rotation is also incorrect (run the example and move the mouse cursor around to see what I mean). Where am I going wrong in my code?
#include <QtWidgets>
QPointF moveBy(const QPointF &pos, qreal rotation, float distance)
{
return pos - QTransform().rotate(rotation).map(QPointF(0, distance));
}
float directionTo(const QPointF &source, const QPointF &target) {
QPointF toTarget(target.x() - source.x(), target.y() - source.y());
float facingTarget = qRadiansToDegrees(atan2(toTarget.y(), toTarget.x())) + 90.0f;
facingTarget = fmod(facingTarget, 360.0f);
if(facingTarget < 0)
facingTarget += 360.0f;
return facingTarget;
}
class Controller : public QObject
{
public:
Controller(QGraphicsScene *scene) :
mScene(scene)
{
mPlayer = scene->addRect(0, 0, 25, 25, QPen(Qt::blue));
mPlayer->setTransformOriginPoint(mPlayer->boundingRect().width() / 2, mPlayer->boundingRect().height() / 2);
mRocketLauncher = scene->addRect(0, 0, 16, 40, QPen(Qt::green));
mRocketLauncher->setParentItem(mPlayer);
mRocketLauncher->setPos(mPlayer->boundingRect().width() * 0.9 - mRocketLauncher->boundingRect().width() / 2,
-mRocketLauncher->boundingRect().height() * 0.3);
mRocket = scene->addRect(0, 0, 16, 20, QPen(Qt::red));
scene->installEventFilter(this);
QGraphicsTextItem *playerText = scene->addText("Player");
playerText->setPos(0, 100);
playerText->setDefaultTextColor(Qt::blue);
QGraphicsTextItem *rocketLauncherText = scene->addText("Rocket launcher");
rocketLauncherText->setPos(0, 120);
rocketLauncherText->setDefaultTextColor(Qt::green);
QGraphicsTextItem *rocketText = scene->addText("Rocket");
rocketText->setPos(0, 140);
rocketText->setDefaultTextColor(Qt::red);
}
bool eventFilter(QObject *, QEvent *event) {
if (event->type() == QEvent::GraphicsSceneMouseMove) {
const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
mPlayer->setRotation(directionTo(mPlayer->sceneBoundingRect().center(), mouseEvent->scenePos()));
qreal rocketX = mRocketLauncher->sceneBoundingRect().center().x() - mRocket->boundingRect().width() / 2;
QPointF rocketPos(rocketX, 0);
rocketPos = moveBy(rocketPos, mPlayer->rotation(), mRocketLauncher->boundingRect().height() - mRocket->boundingRect().height());
mRocket->setPos(rocketPos);
mRocket->setRotation(mPlayer->rotation());
return true;
}
return false;
}
private:
QGraphicsScene *mScene;
QGraphicsRectItem *mPlayer;
QGraphicsRectItem *mRocketLauncher;
QGraphicsRectItem *mRocket;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setMouseTracking(true);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
Controller controller(scene);
view.resize(300, 300);
view.show();
return app.exec();
}
The idea is to:
set rotation of both items;
get positions of bottom left corner of launcher and rocket in global (scene) coordinates;
shift rocket to make positinos equal.
Code:
mPlayer->setRotation(directionTo(mPlayer->sceneBoundingRect().center(),
mouseEvent->scenePos()));
mRocket->setRotation(mPlayer->rotation());
QPointF launcherPos = mRocketLauncher->mapToScene(
mRocketLauncher->boundingRect().bottomLeft());
QPointF currentRocketPos = mRocket->mapToScene(
mRocket->boundingRect().bottomLeft());
mRocket->setPos(mRocket->pos() - currentRocketPos + launcherPos);