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);
Related
I have run into a very interesting problem. I have several widgets (they are QPushButton but in general they can be any QWidget), they are placed into an arbitrary hierarchy of layouts in a window (i.e. they are not necessarily in the same layout). But I want them all to keep the same width which should correspond to the maximum width of any of them. The width is of course determined by their content, which in case of QPushButton is the text of the button).
I created this code:
#include <QApplication>
#include <QGridLayout>
#include <QPointer>
#include <QPushButton>
#include <QVector>
#include <QWidget>
class WidthEqualizer : public QObject
{
public:
explicit WidthEqualizer(QObject *parent = nullptr) :
QObject(parent)
{
}
void addWidget(QWidget *widget)
{
m_widgets.append(widget);
}
int widthHint() const
{
// A hack: Recursion protection should prevent endless recursion
// when calling size hint from WidthEqualizer for its widgets.
m_recursionProtection = true;
// We get the maximum width of size hint of all widgets.
int width = 0;
for (QWidget *widget : qAsConst(m_widgets))
{
if (widget != nullptr)
{
width = qMax(width, widget->sizeHint().width());
}
}
m_recursionProtection = false;
return width;
}
bool recursionProtection() const
{
return m_recursionProtection;
}
private:
QVector<QPointer<QWidget>> m_widgets;
mutable bool m_recursionProtection = false;
};
class PushButton : public QPushButton
{
public:
explicit PushButton(const QString &text = {}, QWidget *parent = nullptr) :
QPushButton(text, parent)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
void setWidthEqualizer(WidthEqualizer* equalizer)
{
m_equalizer = equalizer;
m_equalizer->addWidget(this);
}
QSize sizeHint() const
{
if (m_equalizer != nullptr && !m_equalizer->recursionProtection())
{
int width = m_equalizer->widthHint();
int height = QPushButton::sizeHint().height();
return QSize(width, height);
}
return QPushButton::sizeHint();
}
private:
QPointer<WidthEqualizer> m_equalizer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget window;
auto equalizer1 = new WidthEqualizer(&window);
auto equalizer2 = new WidthEqualizer(&window);
auto layout = new QGridLayout();
window.setLayout(layout);
auto button1 = new PushButton("EQ1 very very very long");
button1->setWidthEqualizer(equalizer1);
layout->addWidget(button1, 0, 0);
auto button2 = new PushButton("EQ2");
button2->setWidthEqualizer(equalizer2);
layout->addWidget(button2, 0, 1);
auto button3 = new PushButton("EQ1");
button3->setWidthEqualizer(equalizer1);
layout->addWidget(button3, 1, 1);
auto button4 = new PushButton("EQ2 long");
button4->setWidthEqualizer(equalizer2);
layout->addWidget(button4, 1, 0);
window.show();
return a.exec();
}
It shows this situation:
This is exactly what I wanted to achieve. There are two groups of buttons "EQ1" and "EQ2". Note that the buttons in each group have equal width, which is given by the width of the button with the longer text. For demonstration purposes I put the buttons into a grid but this can be any possible hierarchy of layouts. And even if I change the text on any of them the correct widths will be maintained automatically.
My question is this: The code works but I find this code too complex for such a simple task. Does not Qt provide any simple trick to achieve the same without so much coding? Ideally make it even more general without the need to override the push button or any other type of widget which I want to equalize widths for? Maybe some trick with layouts? Or maybe some clever metaprogramming?
UPDATE:
Why I need this? I want my UI to be clean and elegant, for example to have a window/dialog which has equal width of buttons at the top and also at the bottom. See e.g.
It looks much better than if each button has a different width. Of course I cannot set the width to be fixed, because this would get broken when I applied translations.
I'm trying to animate a line around a fixed point in Qt. I assume I need to use the QPropertyAnimation class to do this but cannot figure out which property to use.
For clarity, here's what I'm trying to do.
| (5, 10)
| /
| /
| /
| / (10, 5)
| / .
| /
| /
|/
|--------------------------
^
|---(0,0)
Given (x1, y1) = (0, 0) & (x2, y2) = (5, 10), this would be the first frame of the animation. I'd like to then do a smooth animation from (x1, y1), (x2, y2), (with (x1, y1) being one end of the line and (x2, y2) being the other end) to (x1, y1), (x3, y3), with (x3, y3) = (10, 5). Similar to how a clock hand is animated. And before someone posts the analog clock example it uses a rotating pixmap which is not what I need.
I haven't found a whole lot of information on Qt animations, just a lot of basic GUI tutorials.
I have tried doing the following
QPropertyAnimation *anim = new QPropertyAnimation(widget, "geometry")
and the problem with this method is that in this technique the widget is moved between 2 points based on (0, 0) of the widget using the ->setStartValue(startX, startY, ...) and does not allow me to keep one of my lines at a fixed point.
and
QPropertyAnimation *anim = new QPropertyAnimation(widget, "rotation")
The problem with this method being similar to geometry in that it rotates said widget along a (0, 0) point.
Can someone tell me how to achieve the desired effect?
Thanks.
QGraphicsXXXItem do not support q-properties so they can not be used with QPropertyAnimation directly. So the solution is to create a class that inherits QObject and QGraphicsLineItem, plus we must add a q-property that handles the p2 position of the QLineF associated with the line as shown below:
lineitem.h
#ifndef LINEITEM_H
#define LINEITEM_H
#include <QGraphicsLineItem>
#include <QObject>
class LineItem: public QObject, public QGraphicsLineItem {
Q_OBJECT
Q_PROPERTY(QPointF p1 READ p1 WRITE setP1)
Q_PROPERTY(QPointF p2 READ p2 WRITE setP2)
public:
using QGraphicsLineItem::QGraphicsLineItem;
QPointF p1() const {
return line().p1();
}
void setP1(const QPointF & p){
QLineF l = line();
l.setP1(p);
setLine(l);
}
QPointF p2() const {
return line().p2();
}
void setP2(const QPointF & p){
QLineF l = line();
l.setP2(p);
setLine(l);
}
};
#endif // LINEITEM_H
main.cpp
#include "lineitem.h"
#include <QApplication>
#include <QGraphicsView>
#include <QPropertyAnimation>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene(-100, -100, 200, 200);
QGraphicsView view(&scene);
QGraphicsLineItem *item = scene.addLine(QLine(0, 100, 100, 0));
item->setPen(QPen(Qt::red, 5));
LineItem *lineItem = new LineItem(QLineF(QPointF(0, 0), QPointF(0, 100)));
scene.addItem(lineItem);
lineItem->setPen(QPen(Qt::green, 2));
QPropertyAnimation *anim = new QPropertyAnimation(lineItem, "p2");
anim->setStartValue(QPointF(0, 100));
anim->setEndValue(QPointF(100, 0));
anim->setDuration(2000);
anim->start();
view.resize(640, 480);
view.show();
return a.exec();
}
Another way is to use QVariantAnimation:
#include <QApplication>
#include <QGraphicsView>
#include <QPropertyAnimation>
#include <QGraphicsLineItem>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene(-100, -100, 200, 200);
QGraphicsView view(&scene);
scene.addLine(QLine(0, 100, 100, 0), QPen(Qt::green));
QGraphicsLineItem *item = scene.addLine(QLine(0, 0, 0, 100));
item->setPen(QPen(Qt::red, 5));
QVariantAnimation * anim = new QVariantAnimation(&scene);
anim->setStartValue(QPointF(0, 100));
anim->setEndValue(QPointF(100, 0));
anim->setDuration(2000);
anim->start();
QObject::connect(anim, &QVariantAnimation::valueChanged, [item](const QVariant & val){
QLineF l = item->line();
l.setP2(val.toPointF());
item->setLine(l);
});
view.resize(640, 480);
view.show();
return a.exec();
}
I have class named Pixmap deriving from QGraphicsPixmapItem and StartScreen class deriving from QGraphicsScene. I want to use animations (QPropertyAnimation class) to resize displayed image in certain time range. Other actions like setting position or rotation aren't problem but I couldn't find any property like size (e.g. setSize() method). How can I do that in the other way? Thanks for advance.
StartScreen::StartScreen(int windowWidth, int windowHeight)
{
setSceneRect(0, 0, windowWidth, windowHeight);
setBackgroundBrush(QBrush(QImage(":/images/background.png")));
Pixmap * logo = new Pixmap(":/images/logo.png");
addItem(logo);
logo->setPos((windowWidth - logo->pixmap().width()) / 2, (windowWidth - logo->pixmap().width()) / 2 - 75);
//QPropertyAnimation * animation = new QPropertyAnimation(logo, "");
}
QPropertyAnimation applies to Qt Properties, but only objects that inherit from QObject have Qt properties, so if you want to use animations you can use QGraphicsObject and create your own item, or create a class that inherits the item you want and QObject.
class Pixmap: public QObject, public QGraphicsPixmapItem{
Q_OBJECT
Q_PROPERTY(qreal scale READ scale WRITE setScale)
Q_PROPERTY(qreal rotation READ rotation WRITE setRotation)
Q_PROPERTY(QPointF pos READ pos WRITE setPos)
public:
using QGraphicsPixmapItem::QGraphicsPixmapItem;
};
In the previous example, take advantage of the fact that QGraphicsItem, and therefore its derived classes, have the methods pos(), setPos(), scale(), setScale(), rotation() and setRotation(), so only use them in Q_PROPERTY.
In the next part I show an example:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView w;
QGraphicsScene scene(0, 0, 640, 480);
w.setScene(&scene);
w.show();
Pixmap* logo = new Pixmap(QPixmap(":/image.jpg"));
scene.addItem(logo);
QSequentialAnimationGroup group;
QPropertyAnimation animation_scale(logo, "scale");
animation_scale.setDuration(1000);
animation_scale.setStartValue(2.0);
animation_scale.setEndValue(0.1);
QPropertyAnimation animation_pos(logo, "pos");
animation_pos.setDuration(1000);
animation_pos.setStartValue(QPointF(0, 0));
animation_pos.setEndValue(QPointF(100, 100));
/**
* it must indicate the center of rotation,
* in this case it will be the center of the item
*/
logo->setTransformOriginPoint(logo->boundingRect().center());
QPropertyAnimation animation_rotate(logo, "rotation");
animation_rotate.setDuration(1000);
animation_rotate.setStartValue(0);
animation_rotate.setEndValue(360);
group.addAnimation(&animation_scale);
group.addAnimation(&animation_pos);
group.addAnimation(&animation_rotate);
group.start();
return a.exec();
}
#include "main.moc"
In the following link there is an example
Or you can use QVariantAnimation instead of QPropertyAnimation:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView w;
QGraphicsScene scene(0, 0, 640, 480);
w.setScene(&scene);
w.show();
QGraphicsPixmapItem* logo = new QGraphicsPixmapItem(QPixmap(":/image.jpg"));
scene.addItem(logo);
QSequentialAnimationGroup group;
QVariantAnimation animation_scale;
animation_scale.setDuration(1000);
animation_scale.setStartValue(2.0);
animation_scale.setEndValue(0.5);
QObject::connect(&animation_scale, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setScale(value.toReal());
});
animation_scale.start();
QVariantAnimation animation_pos;
animation_pos.setDuration(1000);
animation_pos.setStartValue(QPointF(0, 0));
animation_pos.setEndValue(QPointF(100, 100));
QObject::connect(&animation_pos, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setPos(value.toPointF());
});
/**
* it must indicate the center of rotation,
* in this case it will be the center of the item
*/
logo->setTransformOriginPoint(logo->boundingRect().center());
QVariantAnimation animation_rotate;
animation_rotate.setDuration(1000);
animation_rotate.setStartValue(0);
animation_rotate.setEndValue(360);
QObject::connect(&animation_rotate, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setRotation(value.toReal());
});
group.addAnimation(&animation_scale);
group.addAnimation(&animation_pos);
group.addAnimation(&animation_rotate);
group.start();
return a.exec();
}
In the following link there is an example
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"
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