Qt uses a 3x3 transformation matrix for both perspective and affine transformations. A matrix is considered affine if the last row of the matrix equals [0 0 1]. For this reason, the x- and y-axis rotation matrices are "non-affine" and perspective distortion results. But there are further implications. Every pixel the user paints has the coordinates (x, y, 1). There is no way to set the z coordinate to 0, rotate and set z to something else after. Is it still possible to somehow fake being able to rotate around an arbitrary point? Perhaps by setting z to some value "close" to zero and then rotating?
EDIT:
What I want to do, exactly. By crossing out the 3rd row and column of the "usual" perspective projection matrix, one can obtain one usable with Qt. I also use a window matrix to transform into the QGraphicsItem. That makes for 2 matrices for now:
window * projection
Some code:
float const w(rect.width());
float const h(rect.height());
// aspect ratio
auto const ar(h / w);
// symmetrical infinite frustum
f_[4] = 1.f;
f_[1] = ::std::tan(.5f * hFov_) * f_[4];
f_[0] = -f_[1];
f_[3] = ar * f_[1];
f_[2] = -f_[3];
// perspective projection matrix
auto const rml(f_[1] - f_[0]);
auto const tmb(f_[3] - f_[2]);
::etl::matrix<float, 3, 3> const pMatrix{
2.f * f_[4] / rml, 0.f , (f_[1] + f_[0]) / rml,
0.f , 2.f * f_[4] / tmb, (f_[3] + f_[2]) / tmb,
0.f , 0.f , -1.f};
auto const halfW(.5f * w);
auto const halfH(.5f * h);
// window matrix
::etl::matrix<float, 3, 3> const wMatrix{
halfW, 0.f , halfW,
0.f , -halfH, halfH,
0.f , 0.f , 1.f};
wpvMatrix_ = wMatrix * pMatrix;
Now we want to transform a QPixmap into the world we project. This is done, for example with this matrix:
zMatrix =
worldPixmapWidth / pixmap.width(), 0, p(0),
0, -worldPixmapHeight / pixmap.height(), p(1),
0, 0, p(2);
So we can use wMatrix * pMatrix * zMatrix to transform the pixmap.
p is the point we want to translate the QPixmap to. Now that the pixmap is at p = (p(0), p(1), p(2)). We would like to rotate around an axis parallel to the y axis, going through p. How to do it? Usually, we would do T(p) * Ry(phi) * T(-p), but we can't, as there is no translation matrix, that could set the z coordinate. Applying Ry directly will rotate around the origin. Even without doing any transformations at all, z will be 1, not 0, as we would like it to be for Ry.
How to rotate a vertex around a certain point?
The order of when you do a translation, then a rotation, and then translating back will effectively rotate about whatever point you translate to and from.
This applies the same way with matrix transformations and the order that they are called and applied.
UPDATE:
After reading your question over again, it sounds like what you are really asking for is a 3D projection of a QPixmap to simulate it in the process of flipping over, like a playing card flipping over in a card game. This rotation about some axis parallel to the y-axis needs to show the proper distortions to make it look like one side of the image is farther away and the other side is closer.
http://en.wikipedia.org/wiki/3D_projection
Qt's rotations plan on you using the third 3x3 matrix in the equation above. Rotations about the z axis.
Or even more specifically, you can see it here:
http://qt-project.org/doc/qt-5.1/qtgui/qmatrix.html#details, it shows the 2x2 matrix with added rows to account for a translation.
Now with all that being said... this is the closest thing I could find for faking it without using a full on 3D graphics library, like OpenGL:
http://qt-project.org/forums/viewthread/18615/
http://gerrysweeney.com/horizontal-and-vertical-flip-transformations-of-a-qgraphicsitem-in-qt-qgraphicsview/
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtGui>
#include <QPropertyAnimation>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
QGraphicsScene * m_pScene;
QGraphicsView * m_pView;
QSlider * sliderx;
QSlider * slidery;
QGraphicsPolygonItem* transformedItem;
QPointF itemCenter;
public slots:
void updateRotation();
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
this->setCentralWidget(new QWidget);
QVBoxLayout * layout = new QVBoxLayout;
m_pScene = new QGraphicsScene(0,0,800,480);
m_pView = new QGraphicsView(m_pScene);
m_pView->setFrameStyle(QFrame::NoFrame);
m_pView->setGeometry(0,0,800,480);
m_pView->setAutoFillBackground(false);
layout->addWidget(m_pView);
sliderx = new QSlider(Qt::Horizontal);
slidery = new QSlider(Qt::Horizontal);
sliderx->setRange(-100,100);
sliderx->setSingleStep(1);
sliderx->setValue(100);
slidery->setRange(-100,100);
slidery->setSingleStep(1);
slidery->setValue(100);
QObject::connect(sliderx, SIGNAL(valueChanged(int)),this, SLOT(updateRotation()));
QObject::connect(slidery, SIGNAL(valueChanged(int)),this, SLOT(updateRotation()));
layout->addWidget(sliderx);
layout->addWidget(slidery);
this->centralWidget()->setLayout(layout);
QPolygonF polygon;
polygon << QPointF(100.0,250.0);
polygon << QPointF(170.0, 350.0);
polygon << QPointF(30.0, 350.0);
QGraphicsPolygonItem* testItem = new QGraphicsPolygonItem(polygon);
m_pScene->addItem(testItem);
transformedItem = new QGraphicsPolygonItem(polygon);
transformedItem->setPen(QColor(Qt::red));
m_pScene->addItem(transformedItem);
// Here the fun starts:
itemCenter = transformedItem->mapToParent(transformedItem->boundingRect().center());
// // Method 1
// QTransform transform = QTransform();
// transform.translate(itemCenter.x(),
// itemCenter.y());
// transform.scale(1.0, -1.0);
// transform.translate(-itemCenter.x(),
// -itemCenter.y());
// transformedItem->setTransform(transform);
// // Method 2
// transformedItem->setTransform(QTransform::fromTranslate(itemCenter.x(),
// itemCenter.y()),true);
// transformedItem->setTransform(QTransform::fromScale(1.0, -1.0),true);
// transformedItem->setTransform(QTransform::fromTranslate(-itemCenter.x(),
// -itemCenter.y()), true);
// // Method 3
// transformedItem->translate(itemCenter.x(),
// itemCenter.y());
// transformedItem->scale(1.0, -1.0);
// transformedItem->translate(-itemCenter.x(),
// -itemCenter.y());
}
void MainWindow::updateRotation()
{
transformedItem->resetTransform();
transformedItem->translate(itemCenter.x(),
itemCenter.y());
transformedItem->scale((qreal)sliderx->value()/100., (qreal)slidery->value()/100.);
transformedItem->translate(-itemCenter.x(),
-itemCenter.y());
}
MainWindow::~MainWindow() { }
Hope that helps.
Related
Im doing a little Plotting program with a group of students , we are using Qt's QGraphicsScene on a QGraphicsView to let the user plot custom Points on Specific Positions (x , y) , each point has to have a text on top of it.
Here is the function responsible for adding Points to the Scene :
void MainWindow::AddPoint(float x, float y, QString name)
{
y = y * -1; // To Flip Y-Axis
float Radius = 1; // Point's (Eclipse) Radius
QGraphicsItem *Point = p_Scene->addEllipse(x , y , Radius , Radius , QPen(QColor(Qt::red)) , QBrush(QColor(Qt::red))); // Creates a Red Colored Point on the given Coordinates (x , y)
/*
QGraphicsTextItem *Text = p_Scene->addText(name); // Creates a Text
Text->setDefaultTextColor(QColor(Qt::red)); // Sets Text's Color to Red
Text->setFont(QFont("Courier New" , 4)); // Sets Text's Font Size
Text->setPos(x , y - 10); // Set Text's Position (On top of the Point)
ui->graphicsView->setScene(p_Scene); // Adds Text to the Scene
*/
}
so the Implementation would be like :
AddPoint(0 , 0 , "P1"); // Point 1
AddPoint(50 , 100 , "P2"); // Point 2
AddPoint(100 , 0 , "P3"); // Point 3
This will results in :
We are using :
ui->graphicsView->fitInView(ui->graphicsView->scene()->sceneRect() , Qt::KeepAspectRatio);
to make sure that QGraphicsView shows only whats visible (pretty important).
so the problem here is , if we were to make the drawing larger , say for example :
AddPoint(0 , 0 , "P1");
AddPoint(0 , 1000 , "P2"); // y = 1000
This will draw a very long line which will make the Points + Text we created so small that it cant even be seen :
So what we need here is to somehow calculate the SceneRect (i think) and find out the radius value + font size that we should use for both the Point and the Text so they stay the same size regardless of the Scene's Size.
EDIT :
This is the NEW code (according to vcloarec's solution) :
GraphicsWindow.h (QGraphicsView Subclass) :
#ifndef GRAPHICSVIEW_H
#define GRAPHICSVIEW_H
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QDebug>
class GraphicsView : public QGraphicsView
{
Q_OBJECT
public:
explicit GraphicsView(QWidget *parent = 0);
void AddPoint(float x , float y , QString name = "");
void resize();
private:
QGraphicsScene *p_Scene;
int p_SizeInView;
};
#endif // GRAPHICSVIEW_H
GraphicsWindow.cpp :
#include "GraphicsView.h"
GraphicsView::GraphicsView(QWidget *parent) : QGraphicsView(parent)
{
p_PointRadius = 0.0;
p_PointsLastN = 0;
p_SizeInView = 5;
p_Scene = new QGraphicsScene(this);
this->setScene(p_Scene);
}
void GraphicsView::AddPoint(float x, float y, QString name)
{
y = y * -1;
QGraphicsItem *_Point = p_Scene->addEllipse(x , y , 1 , 1 , QPen(QColor(Qt::red)) , QBrush(QColor(Qt::red)));
this->fitInView(scene()->sceneRect() , Qt::KeepAspectRatio);
resize();
}
void GraphicsView::resize()
{
qreal scale = p_SizeInView / this->transform().m11();
for(int i = 0; i < this->scene()->items().count(); i++)
{
this->scene()->items().at(i)->setScale(scale);
}
}
MainWindow.cpp :
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->toolBar->addWidget(ui->ZoomUp_Button);
ui->toolBar->addWidget(ui->ZoomDown_Button);
setCentralWidget(ui->graphicsView);
ui->graphicsView->AddPoint(0 , 0);
ui->graphicsView->AddPoint(1000 , 0);
ui->graphicsView->AddPoint(1000 , 50);
ui->graphicsView->AddPoint(0 , 50);
}
MainWindow::~MainWindow()
{
delete ui;
}
This code scales the Points according to a fixed Scale but still results in Scrollbars which is something we have to solve.
Somehow it ignores fitInView() , OR it does actually fit it but when the Points are resized it resizes the SceneRect or something
Here is the result :
PS : We subclassed QGraphicsView because we will need MouseEvents and other things later.
EDIT : Solved by vcloarec :
The solution was to insert the Points at (-0.5 , -0.5) and than setPose(x , y) which will set the Position to the x , y we pass to the AddPoint(x , y).
The Points now keep the same size regardless of the Scene's size , and it will show all the Points created at once without any scrollbars or anything.
Thank You !
The dimensions of the points end the text is define in the scene coordinate, not in the viewport (the windows) coordinate .
If you want the points and text keep their dimension on the display, you have to update the dimension depending of the "zoom" of your viewport.
Edit :
I try an analogy :
QGraphicsView is a camera
QGraphicScene is the real life
QGraphicsItems are people ant things
If you want to see a particular part of the scene, you use QGraphicsView::setSceneRect(const QRectF & rect) to "zoom" on the part define by rect.
When you "zoom" or "unzoom" with the camera on objects, this objects don't change their size in the real life, but in the screen the size change. It is the same behaviour with QGraphicsView.
If you want a fix size of the representation of your object, you have to adapt the size of your object with the scale of your scene. In your example with addPoint(0 , 0 , "P1"); addPoint(0 , 1000 , "P2"), the two points are far away from each other, and the points and texts are very small in comparison of this distance.
The solution of your problem depends of the type of the representation you want (dynamic, static,...)
Maybe you can use the matrix returned by QTransform QGraphicsView::transform() const and their diagonal elements to find the scale to use.
Look at this :
class MyView:public QGraphicsView
{
public:
MyView(QWidget *parent):QGraphicsView(parent),sizeInView(5) {}
void resize();
protected:
void wheelEvent(QWheelEvent *event);
private:
int sizeInView;
};
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void putPoint();
private:
Ui::MainWindow *ui;
QGraphicsScene *scene;
MyView *view;
};
And
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
view=new MyView(this);
centralWidget()->setLayout(new QHBoxLayout());
centralWidget()->layout()->addWidget(view);
scene=new QGraphicsScene(this);
view->setScene(scene);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::putPoint()
{
QGraphicsEllipseItem *point1= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point1->setPos(0,0);
QGraphicsEllipseItem *point2= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point2->setPos(0,100);
QGraphicsEllipseItem *point3= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point3->setPos(0,1000);
QGraphicsEllipseItem *point4= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point4->setPos(100,0);
QGraphicsEllipseItem *point5= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point5->setPos(100,100);
QGraphicsEllipseItem *point6= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point6->setPos(100,1000);
scene->addItem(point1);
scene->addItem(point2);
scene->addItem(point3);
scene->addItem(point4);
scene->addItem(point5);
scene->addItem(point6);
view->fitInView(scene->sceneRect(),Qt::KeepAspectRatio);
view->resize();
}
void MyView::resize()
{
qreal scale=sizeInView/transform().m11();
for (int i=0;i<scene()->items().count();++i)
scene()->items().at(i)->setScale(scale);
}
void MyView::wheelEvent(QWheelEvent *event)
{
float fact=1.5;
if (event->delta()>=120)
{
setTransform(transform()*fact);
resize();
}
if (event->delta()<=-120)
{
setTransform(transform()/fact);
resize();
}
}
Be careful, the insertion point of your QGraphicsItem in the scene is define by QGraphicsItem::setPos. The (x,y), you use when you create the point, is the position in the local coordinate system, not in the scene coordinate systeme and it is not the center on you point but the topleft rectangle containing the ellipse.
So if the center of your point is not on the point of insertion, when you resize, the point move ... That's why i place the point at (-0.5,-0.5) in local coordinate with a height and width equal to 1. Then, I place the point with setPos in the scene coordinate.
If you want to disable the scrollbar :
setHorizontalScrollBarPolicy ( Qt::ScrollBarAlwaysOff )
I'm lost with understanding the scale value of QGraphicsScene/View.
Here is how I'm placing my targets in the scene.
QPointF Mainwindow::pointLocation(double bearing, double range){
int offset = 90; //used to offset Cartesian system
double centerX = baseSceneSize/2;//push my center location out to halfway point
double centerY = baseSceneSize/2;
double newX = centerX + qCos(qDegreesToRadians(bearing - offset)) * range;
double newY = centerY + qSin(qDegreesToRadians(bearing - offset)) * range;
QPointF newPoint = QPointF(newX, newY);
return newPoint;
}
So each target has a bearing and range. As long as I don't scale, or zoom, the scene, these values work sufficiently. My problem is that I need to implement the zooming.
Here's where things go wrong:
I have a target at Bearing 270, Range 10.
When the app runs, and my vertical slider is at a value of zero, I can see this target in my view. I should not. I need for this target to only come into view when the slider has gotten to a value of 10. Just think each position value on the slider equates to 1 nautical mile. So if a target is at 10 NMs it should only be visible once the slider is >= 10.
here is how I'm doing the zooming:
void MainWindow:: on_PlotSlider_sliderMoved(int position){
const qreal factor = 1.01;
viewScaleValue = qPow(factor, -position);//-position to invert the scale
QMatrix matrix;
matrix.scale(viewScaleValue, viewScaleValue);
view->setMatrix(matrix);
}
I've tried making the View bigger, the Scene bigger, but nothing is having the proper effect.
Here is my Scene setup:
view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 355;
scene->setSceneRect(0,0,baseSize,baseSize);
baseSceneSize = scene->sceneRect().width();
view->setScene(scene);
How do I take the range of my target and push it out into the scene so that it lines up with the slider value?
QGraphicsView::fitInView is everything you need to select the displayed range and center the view.
Here's how you might do it. It's a complete example.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-radar-40680065
#include <QtWidgets>
#include <random>
First, let's obtain random target positions. The scene is scaled in e.g. Nautical Miles: thus any coordinate in the scene is meant to be in these units. This is only a convention: the scene otherwise doesn't care, nor does the view. The reference point is at 0,0: all ranges/bearings are relative to the origin.
QPointF randomPosition() {
static std::random_device dev;
static std::default_random_engine eng(dev());
static std::uniform_real_distribution<double> posDis(-100., 100.); // NM
return {posDis(eng), posDis(eng)};
}
Then, to aid in turning groups of scene items on and off (e.g. graticules), it helps to have an empty parent item for them:
class EmptyItem : public QGraphicsItem {
public:
QRectF boundingRect() const override { return QRectF(); }
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
};
A scene manager sets up the display. The empty items act as item collections and they can be easily made hidden/visible without having to modify child items. They also enforce the relative Z-order of their children.
class SceneManager : public QObject {
Q_OBJECT
Q_PROPERTY(bool microGraticuleVisible READ microGraticuleVisible WRITE setMicroGraticuleVisible)
QGraphicsScene m_scene;
QPen m_targetPen{Qt::green, 1};
EmptyItem m_target, m_center, m_macroGraticule, m_microGraticule;
An event filter can be installed on the view to signal when the view has been resized. This can be used to keep the view centered in spite of resizing:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::Resize
&& qobject_cast<QGraphicsView*>(watched))
emit viewResized();
return QObject::eventFilter(watched, event);
}
Scene has the following Z-order: center cross, macro- and micro-graticule, then the targets are on top.
public:
SceneManager() {
m_scene.addItem(&m_center);
m_scene.addItem(&m_macroGraticule);
m_scene.addItem(&m_microGraticule);
m_scene.addItem(&m_target);
m_targetPen.setCosmetic(true);
addGraticules();
}
We can monitor a graphics view for resizing; we also expose the visibility of the micro graticule.
void monitor(QGraphicsView *view) { view->installEventFilter(this); }
QGraphicsScene * scene() { return &m_scene; }
Q_SLOT void setMicroGraticuleVisible(bool vis) { m_microGraticule.setVisible(vis); }
bool microGraticuleVisible() const { return m_microGraticule.isVisible(); }
Q_SIGNAL void viewResized();
Targets can be randomly generated. A target has a fixed size in view coordinates. Its position, though, is subject to any scene-to-view transformations.
The pens for targets and graticules are cosmetic pens: their width is given in the view device units (pixels), not scene units.
void newTargets(int count = 200) {
qDeleteAll(m_target.childItems());
for (int i = 0; i < count; ++i) {
auto target = new QGraphicsEllipseItem(-1.5, -1.5, 3., 3., &m_target);
target->setPos(randomPosition());
target->setPen(m_targetPen);
target->setBrush(m_targetPen.color());
target->setFlags(QGraphicsItem::ItemIgnoresTransformations);
}
}
The graticules are concentric circles centered at the origin (range reference point) and a cross at the origin. The origin cross has fixed size in view units - this is indicated by the ItemIgnoresTransformations flag.
void addGraticules() {
QPen pen{Qt::white, 1};
pen.setCosmetic(true);
auto center = {QLineF{-5.,0.,5.,0.}, QLineF{0.,-5.,0.,5.}};
for (auto l : center) {
auto c = new QGraphicsLineItem{l, &m_center};
c->setFlags(QGraphicsItem::ItemIgnoresTransformations);
c->setPen(pen);
}
for (auto range = 10.; range < 101.; range += 10.) {
auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_macroGraticule);
circle->setPen(pen);
}
pen = QPen{Qt::white, 1, Qt::DashLine};
pen.setCosmetic(true);
for (auto range = 2.5; range < 9.9; range += 2.5) {
auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_microGraticule);
circle->setPen(pen);
}
}
};
The mapping between the scene units and the view is maintained as follows:
Each time the view range is changed (from e.g. the combo box), the QGraphicsView::fitInView method is called with a rectangle in scene units (of nautical miles). This takes care of all of the scaling, centering, etc.. E.g. to select a range of 10NM, we'd call view.fitInView(QRect{-10.,-10.,20.,20.), Qt::KeepAspectRatio)
The graticule(s) can be disabled/enabled as appropriate for a given range to unclutter the view.
int main(int argc, char ** argv) {
QApplication app{argc, argv};
SceneManager mgr;
mgr.newTargets();
QWidget w;
QGridLayout layout{&w};
QGraphicsView view;
QComboBox combo;
QPushButton newTargets{"New Targets"};
layout.addWidget(&view, 0, 0, 1, 2);
layout.addWidget(&combo, 1, 0);
layout.addWidget(&newTargets, 1, 1);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setBackgroundBrush(Qt::black);
view.setScene(mgr.scene());
view.setRenderHint(QPainter::Antialiasing);
mgr.monitor(&view);
combo.addItems({"10", "25", "50", "100"});
auto const recenterView = [&]{
auto range = combo.currentText().toDouble();
view.fitInView(-range, -range, 2.*range, 2.*range, Qt::KeepAspectRatio);
mgr.setMicroGraticuleVisible(range <= 20.);
};
QObject::connect(&combo, &QComboBox::currentTextChanged, recenterView);
QObject::connect(&mgr, &SceneManager::viewResized, recenterView);
QObject::connect(&newTargets, &QPushButton::clicked, [&]{ mgr.newTargets(); });
w.show();
return app.exec();
}
#include "main.moc"
So as Kuba suggested, I was overcomplicating this a bit. With his help this is what ended up getting me the result I needed. Not 100% sure on some of it, but for now it's working the way I need it to.
view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 1000; // MAGIC value that works, anything other than this, not so much
view->setSceneRect(0,0,baseSize,baseSize);
baseViewSize = view->sceneRect().width();
view->setScene(scene);
My drawPoint method works fine, no changes were needed.
Finally, here is my slider
void MainWindow:: on_PlotSlider_sliderMoved(int position){
const qreal factor = 1.01;
viewScaleValue = qPow(factor, -position);//-position to invert the scale
QMatrix matrix;
// below is the update, again 6 is a MAGIC number, no clue why 6 works...
matrix.scale((baseViewSize/6 / position, baseViewSize/6 / position);
view->setMatrix(matrix);
}
While my problem is solved, I would love some explanation as to my 2 MAGIC numbers.
Why does it all only work is the baseSize is 1000?
Why does it only scale correctly if I divide the BaseViewSize by 6?
I'm trying to rotate a rectangle using a slider.
The slider has -100 to +100 values. I'd like to use this to rotate the rectangle.
I have tried a few things, including calculating the corners of the rectangle and then messing around with a matrix, thus unsuccessful.
(I prefer using the matrix)
An easier solution would be to define fixed points from the beginning, instead of only working with the sides of the rectangle.
In the code below I can resize the rectangle using a slider.
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);
a = 230; //positioning
b = 150; //positioning
c = 200; //size of drawing
d = 150; //size of drawing
connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(update()));
}
Dialog::~Dialog()
{
delete ui;
}
void Dialog::paintEvent(QPaintEvent *e)
{
QPainter painter(this);
painter.fillRect(a - ui->horizontalSlider->value(),
b - ui->horizontalSlider->value(),
c + ui->horizontalSlider->value() * 2,
d + ui->horizontalSlider->value() * 2, Qt::green);
}
Any form of help is much appreciated.
You can move your painter and rotate canvas to achieve it:
void Dialog::paintEvent(QPaintEvent *e)
{
QPainter painter(this);
painter.save();
painter.translate(a, b);//Move pen.
painter.rotate(30);//rotate canvas.
painter.fillRect(-ui->horizontalSlider->value(),
-ui->horizontalSlider->value(),
c + ui->horizontalSlider->value() * 2,
d + ui->horizontalSlider->value() * 2, Qt::green);
painter.restore();
}
Firstly here is the rotation matrix that will rotate any point in X-Y space around the Z axis (which is facing you) by the angle given by t radians:
R = [ cos(t), -sin(t);
sin(t), cos(t)]
To get the rotate point you must be rotating around a specific point on the X-Y plane. I assume you want to rotate the rect around its centre.
Let c be the pixel coordinates of the centre:
c = [cx;
cy]
The rotated points are then:
P = R*(p-c) + c
where P is the rotated point and p is the rectangle point to rotate, both are [2x1] vectors, as is c. Note that I am using Matrix algebra, so the multiplication is matrix multiplication, not scalar multiplication.
The next issue is then drawing the rectangle. I am not familiar with Qt rect drawing but from the documentation, doing what you want with fillRect will not work, as it assumes that the rectangle is unrotated.
You can use drawPolygon to do what you want, when you have the rotated coordinates.
I solved it by using "painter.drawLine( ax, ay, bx, by );" to draw the rectangle. Afterwards I used the following formula for each line:
double ax = MidPointWidth + (-(Size + minWidth)) * cos(Angle) - (Size + minHeight) * sin(Angle);
double ay = MidPointHeight + (-(Size + minWidth)) * sin(Angle) + (Size + minHeight) * cos(Angle);
Hope this will help someone.
I've got a class: mySquare which inherits from QGraphicsRectItem
added only my constructor, painter and an animation:
ANIMATION:
void mySquare::animation(mySquare *k)
{
QTimeLine *timeLine = new QTimeLine();
timeLine->setLoopCount(1);
QGraphicsItemAnimation *animation = new QGraphicsItemAnimation();
animation->setItem(k);
animation->setTimeLine(timeLine);
int value = 30;
animation->setTranslationAt(0.3, value, value);
timeLine->start();
// (*)
// x += 30;
// y += 30;
}
PAINTER:
void Klocek::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *widget)
{
bokKwadratu = (min(widget->width(), widget->height()))/5;
setRect(x * 30, y * 30, 30 - 3, 30 - 3);
QRectF rect = boundingRect();
painter->setBrush(brush);
painter->setPen(pen);
QFont font;
font.setPixelSize(bokKwadratu/3);
painter->setFont(font);
painter->drawRect(rect);
painter->drawText(rect,Qt::AlignCenter, QString::number(wartosc));
}
CONSTRUCTOR:
mySquare::mySquare(qreal x, qreal y) : QGraphicsRectItem(x * 10, y * 10, 10, 10)
{
setAcceptHoverEvents(true);
this->x = x;
this->y = y;
pen.setColor(Qt::red);
pen.setWidth(2);
brush.setColor(Qt::blue);
brush.setStyle(Qt::SolidPattern);
}
after performing animation (translation) I need to change the object coordinates so they are compatible which the situation on the screen. In other words after the translation (30, 30) I want the coordinates of the rectangle to be change (x += 30, y += 30)
my problem is that when i try to do this ( (*) fragment in the code) the triangle is put far away from its position (just as if the translation was performed twice)
My question is how to translate it and change the coordinates without such complications.
To begin with, I think you're misunderstanding the use of the function setTranslationAt in the QGraphicsItem Animation.
An animation has a normalised value over time, so can start at 0.0 and end at 1.0 (or the reverse). Therefore, by calling
animation->setTranslationAt(0.3, value, value);
You've stated that at the point when the normalised value reaches 0.3, you want the x and y position to be set to 'value'. That's fine, but you also need to set other values for the animation to occur (especially at the val of 1.0!). If you use a for loop, you can iterate through values from 0.0 to 1.0 and set where you want the position of your item to be. Take a look at the example code in the Qt help files for QGraphicsItemAnimation. The QGraphicsItemAnimation uses interpolation to work out the position of your object between the known points that you've given it. If you're interested: -
http://en.wikipedia.org/wiki/Linear_interpolation
Secondly, the item's rect is the definition of the item in its local coordinate space. So if you wanted a rect with its axis in the centre, you'd define it with x,y,w,h of (-w/2, -h/2, w, h). As these are local coordinates, they then get mapped into world coordinates in the GraphicsScene, which is where you set its actual position in the world.
Once you've setup your QGraphicsItemRect's local coordinates and world position, you can then simply paint it with drawRect and should not be setting positions in the paint function.
I'm trying to write a graphical program in C++ with QT where users can scale and rotate objects with the mouse (just like inkscape or CorelDraw does), however after many months trying to make it happen I still cannot make it work. It currently works for example by just rotating or just scaling, but not when the user want to transform the object in an arbitrary way. There is an example in QT about affine transformation but it is very simple (e.g., it scales using a single factor not x and Y factors), it not provides scale directions or fixed scaling point) so I don't know how to extend it or use it.
This is how the program is expected to behave:
The user drop a polygon in the canvas.
If the user clicks on the polygon a set of blue boxes will appear around the object. These boxes are used to scale the object in any direction (e.g., up, down, left, right, etc)
If the user clicks again in the polygon a set of red boxes will appear around the object. These boxes are used to rotate the object in any direction.
So, how can I implement at least the following:
If the user click on the top blue box (scale towards up), hold the left button and moves the mouse toward up, how can I make the polygon to scale towards up? Do I need scale direction? Do I need a general Fixed-point of scaling? How can I calculate the scale factors as the mouse move towards up so the polygon is scaled in "real time"?
Here is the code that in my perspective could make it work: See the code here But it does not work :-( . If you can help me with a better implementation I will appreciate it.
Sorry to put to many questions but I am completely frustrated.
Thanks,
Carlos.
cannot make it work
the result is just wrong
Doesn't describe your problem very well.
Basically I don't know what is needed in terms of the concatenation/multiplications of matrices
In object store:
1. position
2. rotation
3. scale
When you need to draw object, perform operations in this order:
1. Scale using stored scale factor
2. Rotate using stored angle
3. Translate to position
Given scale factor s and rotation angle r, to rotate/scale object (point array, or whatever) around arbitrary point (p.x, p.y), do this:
1. Translate object to -p.x, -p.y . I.e. for every vertex do vertex -= p;
2. Scale object. For every vertex do vertex *= s
3. Rotate object. Rotate every vertex around point zero using angle r.
4. Translate object to p.x, p.y.
Also I'd recommend to take a look at "Affine Transformations" demo in Qt 4. To view demo, launch qtdemo, select "Demonstrations->Affine Transformations".
Consider hiring a geometry tutor. "Months" is too long to deal with rotate/scale/translate problem.
But, I have no clue on how to combine of these function in a proper order
If you're rotating and scaling around same point, the order of operations doesn't matter.
--EDIT--
Live example:
Points indicate pivot, start of transform, and end of transform.
Wireframe letters represent original image.
Red letter represent "rotate and uniformly scale" transform.
Green letters represent "2D scale" transform.
For both transform you need pivot, point where you began to drag shape, and point where you stopped dragging shape.
I will not ever explain this again.
transformtest.pro:
TEMPLATE = app
TARGET =
DEPENDPATH += .
INCLUDEPATH += .
# Input
HEADERS += MainWindow.h
SOURCES += main.cpp MainWindow.cpp
main.cpp:
#include <QApplication>
#include "MainWindow.h"
int main(int argc, char** argv){
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
MainWindow.h:
#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
#include <QGLWidget>
class QPaintEvent;
class MainWindow: public QWidget{
Q_OBJECT
public:
MainWindow(QWidget* parent = 0);
protected slots:
void updateAngle();
protected:
void paintEvent(QPaintEvent* ev);
float angle;
float distAngle;
};
#endif
MainWindow.cpp:
#include "MainWindow.h"
#include <QTimer>
#include <QPainter>
#include <QColor>
#include <QVector2D>
#include <math.h>
static const int timerMsec = 50;
static const float pi = 3.14159265f;
MainWindow::MainWindow(QWidget* parent)
:QWidget(parent), angle(0), distAngle(0){
QTimer* timer = new QTimer(this);
timer->start(timerMsec);
connect(timer, SIGNAL(timeout()), this, SLOT(update()));
connect(timer, SIGNAL(timeout()), this, SLOT(updateAngle()));
}
float randFloat(){
return (qrand()&0xFF)/255.0f;
}
float randFloat(float f){
return randFloat()*f;
}
inline QVector2D perp(const QVector2D v){
return QVector2D(-v.y(), v.x());
}
void MainWindow::updateAngle(){
angle = fmod(angle + pi*5.0f/180.0f, pi*2.0f);
distAngle = fmod(distAngle + pi*1.0f/180.0f, pi*2.0f);
}
QTransform buildRotateScale(QVector2D pivot, QVector2D start, QVector2D end){
QVector2D startDiff = start - pivot;
QVector2D endDiff = end - pivot;
float startLength = startDiff.length();
float endLength = endDiff.length();
if (startLength == 0)
return QTransform();
if (endLength == 0)
return QTransform();
float s = endLength/startLength;
startDiff.normalize();
endDiff.normalize();
QVector2D startPerp = perp(startDiff);
float rotationAngle = acos(QVector2D::dotProduct(startDiff, endDiff))*180.0f/pi;
if (QVector2D::dotProduct(startPerp, endDiff) < 0)
rotationAngle = -rotationAngle;
return QTransform().translate(pivot.x(), pivot.y()).rotate(rotationAngle).scale(s, s).translate(-pivot.x(), -pivot.y());
}
QTransform buildScale(QVector2D pivot, QVector2D start, QVector2D end){
QVector2D startDiff = start - pivot;
QVector2D endDiff = end - pivot;
float startLength = startDiff.length();
float endLength = endDiff.length();
if ((startDiff.x() == 0)||(startDiff.y() == 0))
return QTransform();
QVector2D s(endDiff.x()/startDiff.x(), endDiff.y()/startDiff.y());
return QTransform().translate(pivot.x(), pivot.y()).scale(s.x(), s.y()).translate(-pivot.x(), -pivot.y());
}
void MainWindow::paintEvent(QPaintEvent* ev){
QPainter painter(this);
QPointF pivot(width()/2, height()/2);
QPointF transformStart(pivot.x() + 100.0f, pivot.y() - 100.0f);
float r = sinf(distAngle)*100.0f + 150.0f;
QPointF transformEnd(pivot.x() + r*cosf(angle), pivot.y() - r*sinf(angle));
painter.fillRect(this->rect(), QBrush(QColor(Qt::white)));
QPainterPath path;
QString str(tr("This is a test!"));
QFont textFont("Arial", 40);
QFontMetrics metrics(textFont);
QRect rect = metrics.boundingRect(str);
path.addText(QPoint((width()-rect.width())/2, (height()-rect.height())/2), textFont, str);
painter.setPen(QColor(200, 200, 255));
painter.drawPath(path);
painter.setTransform(buildRotateScale(QVector2D(pivot), QVector2D(transformStart), QVector2D(transformEnd)));
painter.fillPath(path, QBrush(QColor(255, 100, 100)));
painter.setPen(QColor(100, 255, 100));
painter.setTransform(buildScale(QVector2D(pivot), QVector2D(transformStart), QVector2D(transformEnd)));
painter.fillPath(path, QBrush(QColor(100, 255, 100)));
painter.setTransform(QTransform());
QPainterPath coords;
r = 10.0f;
coords.addEllipse(pivot, r, r);
coords.addEllipse(transformStart, r, r);
coords.addEllipse(transformEnd, r, r);
painter.setPen(QPen(QBrush(Qt::red), 5.0f));
painter.setBrush(QBrush(QColor(127, 0, 0)));
painter.setPen(QPen(QBrush(Qt::green), 5.0f));
painter.drawLine(QLineF(pivot, transformStart));
painter.setPen(QPen(QBrush(Qt::blue), 5.0f));
painter.drawLine(QLineF(transformStart, transformEnd));
painter.setPen(Qt::red);
painter.drawPath(coords);
painter.end();
}
Basically, you have a point (or series of points) that you want to transform with two linear transformations, R (rotation) and S (scaling). So you're trying to calculate something like
R(S(x))
where x is a point. If you represent these operations using matrices, then performing consecutive operations is equivalent to multiplying the matrices, i.e.
R*S*x
Unfortunately, you haven't given enough information for me to be more specific...could you post some code (just the small, relevant parts) showing what you're doing? What do you mean by "natural way"? What about your result is "just wrong"?