Positioning a resizable widget inside a scroll area - c++

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();
}

Related

How to paint an outline when hovering over a QListWidget item?

I am trying to paint an outline around a QListWidget item when the mouse is over that item. I've subclassed QStyledItemDelegate and overrode paint to account for the QStyle::State_MouseOver case as follows:
class MyDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent){}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) painter->drawRect(option.rect);
}
~MyDelegate(){}
};
I then instantiate a QListWidget with some items and enable the Qt::WA_Hover attribute:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.setItemDelegate(new MyDelegate(&w));
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Unfortunately, the behaviour is not what I expected. In particular, the outline is painted when I move the mouse over an item, but when I move to another item the outline around the first item is not erased. Instead, it keeps drawing outlines around all the items I move my mouse over and eventually there is an outline around all items. Is this normal? I know that an alternative solution would be to use QStyleSheets but I'd like to understand why the current approach doesn't behave as I expected.
Here is what the widget looks like before a mouse over:
Here it is after hovering over item2:
And then after item3:
I am using Qt 5.15.1 on a MacOS 10.15.6 platform.
EDIT 1:
Based on the answer from scopchanov, to ensure the outline thickness is indeed 1px, I've changed the paint method to this:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
int outlineWidth = 1;
QPen pen;
pen.setWidth(outlineWidth);
painter->setPen(pen);
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) {
int a = round(0.5*(outlineWidth - 1));
int b = round(-0.5*outlineWidth);
painter->drawRect(option.rect.adjusted(a, a, b, b));
}
}
Unfortunately, the behaviour is very similar; here is a screenshot after hovering over all items from top to bottom:
Cause
QPainter::drawRect draws a rectangle, that is slightly bigger (by exactly one pixel in height and width) than the painted area. The reason for this behavior could be seen in the way QPaintEngine draws a rectangle:
for (int i=0; i<rectCount; ++i) {
QRectF rf = rects[i];
QPointF pts[4] = { QPointF(rf.x(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y() + rf.height()),
QPointF(rf.x(), rf.y() + rf.height()) };
drawPolygon(pts, 4, ConvexMode);
}
QPaintEngine draws a closed polygon, starting at the point (x, y), going to (x + width, y), then to (x + width, y + height) and finally to (x, y + height). This looks intuitive enough, but let's see what happens, if we substitute these variables with real numbers:
Say, we want to draw a 4x2 px rectangle at (0, 0). QPaintEngine would use the following coordinates: (0, 0), (4, 0), (4, 2) and (0, 2). Represented as pixels, the drawing would look like this:
So, instead of 4x2 px, we end up with a 5x3 px rectangle, i.e. indeed one pixel wider and taller.
You can further prove this by clipping the painter to option.rect before calling drawRect like this:
if (option.state & QStyle::State_MouseOver) {
painter->setClipRect(option.rect);
painter->drawRect(option.rect);
}
The result is clipped bottom and right edges of the outline (the very edges, we have expected to be within the painted area):
In any case, the part of the outline, that falls outside of the painted area, is not repainted correctly, hence the unwanted remains of the previous drawings in the form of lines.
Solution
Reduce the height and the width of the outline, using QRect::adjusted.
You might just write
painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
However, this would work only for an outline, which is 1px thick and the devicePixelRatio is 1, as on a PC. If the border of the outline is thicker than 1px and/or the devicePixelRatio is 2, as on a Mac, of course more of the outline will stick out of the painted area, so you should take that into consideration and adjust the rectangle accordingly, e.g.:
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
m_outineWidth and m_devicePixelRatio are class members, representing the desired outline width, resp. the ratio between physical pixels and device-independent pixels for the paint device. Provided that you have created public setter methods for them, you could set their values like this:
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(1);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
Example
Here is an example I have written for you to demonstrate how the proposed solution could be implemented:
#include <QApplication>
#include <QStyledItemDelegate>
#include <QListWidget>
#include <QPainter>
class MyDelegate : public QStyledItemDelegate
{
int m_outineWidth;
int m_devicePixelRatio;
public:
MyDelegate(QObject *parent = nullptr) :
QStyledItemDelegate(parent),
m_outineWidth(1),
m_devicePixelRatio(1) {
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
QStyledItemDelegate::paint(painter, option, index);
if (option.state & QStyle::State_MouseOver) {
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->setPen(QPen(QBrush(Qt::red), m_outineWidth, Qt::SolidLine,
Qt::SquareCap, Qt::MiterJoin));
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
}
}
void setOutlineWidth(int outineWidth) {
m_outineWidth = outineWidth;
}
void setDevicePixelRatio(int devicePixelRatio) {
m_devicePixelRatio = devicePixelRatio;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(3);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Result
The provided example produces the following result for 3px thick outline on Windows:

How to get Image pixel position loaded in QGraphicsView - Strange MapToScene() behaviour

I am originally loading image in QGraphicsView and using this method for basic zoom out and zoom in functionality.
However, I am unable to retrieve actual image pixel position using mapToScene functionality in eventFilter function of Graphics_view_zoom class. The below code produces behaviour exactly as windows photo viewer zooming only selected region.
MapToScene() returns same Point as mouse event position.
Here is the class which deals with zooming.
#include "Graphics_view_zoom.h"
#include <QMouseEvent>
#include <QApplication>
#include <QScrollBar>
#include <qmath.h>
Graphics_view_zoom::Graphics_view_zoom(QGraphicsView* view)
: QObject(view), _view(view)
{
_view->viewport()->installEventFilter(this);
_view->setMouseTracking(true);
_modifiers = Qt::ControlModifier;
_zoom_factor_base = 1.0015;
}
void Graphics_view_zoom::gentle_zoom(double factor) {
_view->scale(factor, factor);
_view->centerOn(target_scene_pos);
QPointF delta_viewport_pos = target_viewport_pos - QPointF(_view->viewport()->width() / 2.0,
_view->viewport()->height() / 2.0);
QPointF viewport_center = _view->mapFromScene(target_scene_pos) - delta_viewport_pos;
_view->centerOn(_view->mapToScene(viewport_center.toPoint()));
emit zoomed();
}
void Graphics_view_zoom::set_modifiers(Qt::KeyboardModifiers modifiers) {
_modifiers = modifiers;
}
void Graphics_view_zoom::set_zoom_factor_base(double value) {
_zoom_factor_base = value;
}
bool Graphics_view_zoom::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::MouseMove) {
QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
QPointF delta = target_viewport_pos - mouse_event->pos();
// Here I want to get absolute image coordinates
if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) {
target_viewport_pos = mouse_event->pos();
target_scene_pos = _view->mapToScene(mouse_event->pos());
}
} else if (event->type() == QEvent::Wheel) {
QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
if (QApplication::keyboardModifiers() == _modifiers) {
if (wheel_event->orientation() == Qt::Vertical) {
double angle = wheel_event->angleDelta().y();
double factor = qPow(_zoom_factor_base, angle);
gentle_zoom(factor);
return true;
}
}
}
Q_UNUSED(object)
return false;
In mainwindow.cpp,
I am creating object of this class and loading an image as below:
m_GraphicsScene = new QGraphicsScene();
pixmapItem = new QGraphicsPixmapItem();
m_GraphicsScene->addItem(multiview[i].pixmapItem);
view_wrapper = new Graphics_view_zoom(ui->GraphicsView);
ui->GraphicsView->setScene(multiview[i].m_GraphicsScene);
pixmapItem->setPixmap(QPixmap::fromImage("img.jpg"));
multiview[view].m_GraphicsView->fitInView(QRectF(0,0,640,320),Qt::KeepAspectRatio);
Can anyone help with how do I achieve this ?
Keep in mind that the scaling you use only scales the scene, not the items. Given this, the position of the pixel can be obtained, so the algorithm is:
Obtain the mouse position with respect to the QGraphicsView
Transform that position with respect to the scene using mapToScene
Convert the coordinate with respect to the scene in relation to the item using mapFromScene of the QGraphicsItem.
Considering the above, I have implemented the following example:
#include <QtWidgets>
#include <random>
static QPixmap create_image(const QSize & size){
QImage image(size, QImage::Format_ARGB32);
image.fill(Qt::blue);
std::random_device rd;
std::mt19937_64 rng(rd());
std::uniform_int_distribution<int> uni(0, 255);
for(int i=0; i< image.width(); ++i)
for(int j=0; j < image.height(); ++j)
image.setPixelColor(QPoint(i, j), QColor(uni(rng), uni(rng), uni(rng)));
return QPixmap::fromImage(image);
}
class GraphicsView : public QGraphicsView
{
Q_OBJECT
Q_PROPERTY(Qt::KeyboardModifiers modifiers READ modifiers WRITE setModifiers)
public:
GraphicsView(QWidget *parent=nullptr): QGraphicsView(parent){
setScene(new QGraphicsScene);
setModifiers(Qt::ControlModifier);
auto item = scene()->addPixmap(create_image(QSize(100, 100)));
item->setShapeMode(QGraphicsPixmapItem::BoundingRectShape);
item->setPos(40, 40);
fitInView(QRectF(0, 0, 640, 320),Qt::KeepAspectRatio);
resize(640, 480);
}
void setModifiers(const Qt::KeyboardModifiers &modifiers){
m_modifiers = modifiers;
}
Qt::KeyboardModifiers modifiers() const{
return m_modifiers;
}
signals:
void pixelChanged(const QPoint &);
protected:
void mousePressEvent(QMouseEvent *event) override{
if(QGraphicsPixmapItem *item = qgraphicsitem_cast<QGraphicsPixmapItem *>(itemAt(event->pos()))){
QPointF p = item->mapFromScene(mapToScene(event->pos()));
QPoint pixel_pos = p.toPoint();
emit pixelChanged(pixel_pos);
}
QGraphicsView::mousePressEvent(event);
}
void wheelEvent(QWheelEvent *event) override{
if(event->modifiers() == m_modifiers){
double angle = event->orientation() == Qt::Vertical ? event->angleDelta().y(): event->angleDelta().x();
double factor = qPow(base, angle);
applyZoom(factor, event->pos());
}
}
private:
void applyZoom(double factor, const QPoint & fixedViewPos)
{
QPointF fixedScenePos = mapToScene(fixedViewPos);
centerOn(fixedScenePos);
scale(factor, factor);
QPointF delta = mapToScene(fixedViewPos) - mapToScene(viewport()->rect().center());
centerOn(fixedScenePos - delta);
}
Qt::KeyboardModifiers m_modifiers;
const double base = 1.0015;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
GraphicsView *view = new GraphicsView;
QLabel *label = new QLabel;
QObject::connect(view, &GraphicsView::pixelChanged, label, [label](const QPoint & p){
label->setText(QString("(%1, %2)").arg(p.x()).arg(p.y()));
});
label->setAlignment(Qt::AlignCenter);
QWidget w;
QVBoxLayout *lay = new QVBoxLayout(&w);
lay->addWidget(view);
lay->addWidget(label);
w.show();
return a.exec();
}
#include "main.moc"
It may be better to use a custom graphics scene subclassed from QGraphicsScene as this makes extracting the necessary coordinates much simpler. The only snag is you have to have the QGraphicsPixmapItem::pos available in the custom QGraphicsScene class - I have included a full working example which uses Graphics_view_zoom.h and Graphics_view_zoom.cpp from the linked question. The position of the QGraphicsPixmapItem is passed to a member of the QGraphicsScene subclass, Frame, in order to make the necessary correction.
#include <QPixmap>
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <qfont.h>
#include "Graphics_view_zoom.h"
class Frame : public QGraphicsScene {
Q_OBJECT
public:
QGraphicsTextItem * coords;
QPointF pic_tl;
Frame::Frame(QWidget* parent)
: QGraphicsScene(parent) {
coords = new QGraphicsTextItem();
coords->setZValue(1);
coords->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
addItem(coords);
}
void Frame::tl(QPointF p) {
pic_tl = p;
}
protected:
void Frame::mouseMoveEvent(QGraphicsSceneMouseEvent* event) {
QPointF pos = event->scenePos();
coords->setPlainText("(" + QString("%1").arg(int(pos.x() - pic_tl.x())) + ", "
+ QString("%1").arg(int(pos.y() - pic_tl.y())) + ")");
coords->setPos(pos);
coords->adjustSize();
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QMainWindow* main = new QMainWindow();
QGraphicsView* GraphicsView = new QGraphicsView(main);
Graphics_view_zoom* view_wrapper = new Graphics_view_zoom(GraphicsView);
Frame* frame = new Frame(main);
QGraphicsPixmapItem* pixmapItem = new QGraphicsPixmapItem();
frame->addItem(pixmapItem);
GraphicsView->setScene(frame);
// Loads a 497x326 pixel test image
pixmapItem->setPixmap(QPixmap(":/StackOverflow/test"));
// small offset to ensure it works for pictures which are not at
// (0,0). Larger offsets produce the same result but require manual
// adjustments of the view, I have neglected those for brevity as
// they are not in the scope of the question.
pixmapItem->setPos(-20, 20);
frame->tl(pixmapItem->pos());
GraphicsView->fitInView(QRectF(0, 0, 640, 320), Qt::KeepAspectRatio);
GraphicsView->centerOn(pixmapItem->pos());
main->resize(1920, 1080);
main->show();
GraphicsView->resize(main->width(), main->height());
return a.exec();
}
This will display the image coordinates of the pixel under the mouse relative to (0,0) at the top left corner.
The mouse is not visible in these screenshots but in the first it is in exactly the upper left corner and the second it is in exactly the lower right corner. If these coordinates are needed inside the Graphics_view_zoom object then you simply have to scope the Frame instance appropriately, or pass the value as needed.
Note - the exact coordinates displayed may not precisely represent the position of the mouse in this example since they are cast to ints for demonstration, but the floating point values can be easily accessed since QGraphicsSceneMoveEvent::scenePos() returns a QPointF. Additionally, note that in running this demonstration there may be some (hopefully very small) variation on where the mouse appears to be relative to it's 'actual' position - I recommend using Qt::CrossCursor to allay this. For example on my system the default cursor is off by about a pixel for certain areas on my smaller display, this is also affected by the zoom level - higher zoom will produce more accurate results, less zoom will be less accurate.

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"

Positioning a top-level object relative to another

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

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);