Graphic item jumps to the end of path via QGraphicsItemAnimation without moving - qt

I have a circle which I want to move smoothly on a path. The path class is like a horizontal U derived from the QPainterPath. when I start timer (QTimeLine object) the circle just jumps from the start of path to the end (start of upper U fork to the end of lower fork) with no smooth animation. Unfortunately, the QTimeLine::setLoopCount(int n) doesn't work too.
Do you have any idea about the reason?
// UPath(int forkLen, int forksDistance, QPointF startPoint)
UPath* uPath = new UPath(500, 60, QPointF(10, 10));
QList<QPointF> points = uPath->pathPoints(0.006); // returns the points of the path
// implemented by QPainterPath::pointAtPercent()
QGraphicsItem *ball = new QGraphicsEllipseItem(0, 0, 10, 10);
QTimeLine *timer = new QTimeLine(5000);
timer->setFrameRange(0, 100);
timer->setLoopCount(2); // doesn't work
QGraphicsItemAnimation *animation = new QGraphicsItemAnimation;
animation->setItem(ball);
animation->setTimeLine(timer);
for (int i = 0; i < points.count(); ++i)
animation->setPosAt(i/points.count(), points.at(i));
QGraphicsScene *scene = new QGraphicsScene();
scene->addItem(ball);
QGraphicsView *view = new QGraphicsView(scene);
view->setRenderHint(QPainter::Antialiasing);
view->show();
timer->start();

The QGraphicsAnimation class is deprecated. What you want is an adapter between a QPainterPath and the animation system. See below for a complete example.
Using painter paths for animations requires some extra smoothing (resampling) as there will be velocity changes along the path, and it won't look all that great. You may notice it when you run the code below. Painter paths are meant for painting, not for animating stuff.
The extent of this misbehavior will depend on the kind of path you're using, so it may end up working OK for the particular use case you have.
#include <QApplication>
#include <QAbstractAnimation>
#include <QPainterPath>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QDebug>
class PathAnimation : public QAbstractAnimation {
Q_OBJECT
Q_PROPERTY(int duration READ duration WRITE setDuration)
QPainterPath m_path;
int m_duration;
QVector<QPointF> m_cache;
QGraphicsItem * m_target;
int m_hits, m_misses;
public:
PathAnimation(const QPainterPath & path, QObject * parent = 0) :
QAbstractAnimation(parent), m_path(path), m_duration(1000), m_cache(m_duration), m_target(0), m_hits(0), m_misses(0) {}
~PathAnimation() { qDebug() << m_hits << m_misses; }
int duration() const { return m_duration; }
void setDuration(int duration) {
if (duration == 0 || duration == m_duration) return;
m_duration = duration;
m_cache.clear();
m_cache.resize(m_duration);
}
void setTarget(QGraphicsItem * target) {
m_target = target;
}
void updateCurrentTime(int ms) {
QPointF point = m_cache.at(ms);
if (! point.isNull()) {
++ m_hits;
} else {
point = m_path.pointAtPercent(qreal(ms) / m_duration);
m_cache[ms] = point;
++ m_misses;
}
if (m_target) m_target->setPos(point);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-5, -5, 10, 10);
item->setPen(QPen(Qt::red, 2));
item->setBrush(Qt::lightGray);
QPainterPath path;
path.addEllipse(0, 0, 100, 100);
PathAnimation animation(path);
animation.setTarget(item);
QGraphicsScene scene;
scene.addItem(item);
QGraphicsView view(&scene);
view.setSceneRect(-50, -50, 200, 200);
animation.setLoopCount(-1);
animation.start();
view.show();
return a.exec();
}
#include "main.moc"

Related

How to connect a QRadioButton of a QTableWidget cell with another QTableWidget cell using an arrow?

I have a QListWidget and a QGraphicsView both subclassed to overwrite some of their members. I prepared a minimal verifiable example showing the problem I have here
From the QListWidget I can drag and drop specific field (represented by a QTableWidget) and drop them into a QGraphicsView and in order to do that I am using a QGraphicsProxyWidget approach as shown below.
The Problem
Now, how do I connect 2 QRadioButton inside cell of a QTableWidget with another cell of another QTableWidget?
It is important to mention that the green QGraphicsRectItem it is used to move around the QTableWidget as well as adjusting its dimension.
Below the result I was able to arrive so far:
And below the expected result I have been trying to achieve:
Below the most important part of the code:
scene.h
#ifndef SCENE_H
#define SCENE_H
#include <QGraphicsScene>
class Scene : public QGraphicsScene
{
public:
Scene(QObject *parent = nullptr);
protected:
void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
void dragMoveEvent(QGraphicsSceneDragDropEvent *event);
void dropEvent(QGraphicsSceneDragDropEvent *event);
};
#endif // SCENE_H
scene.cpp
#include "arrow.h"
#include <QGraphicsSceneDragDropEvent>
#include <QMimeData>
#include <QTableWidget>
#include <QGraphicsProxyWidget>
#include <QVBoxLayout>
#include <QMetaEnum>
#include <QEvent>
#include <QSizeGrip>
#include <QRadioButton>
Scene::Scene(QObject *parent)
{
setBackgroundBrush(Qt::lightGray);
}
void Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void Scene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void Scene::dropEvent(QGraphicsSceneDragDropEvent *event) {
QByteArray encoded =
event->mimeData()->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encoded, QIODevice::ReadOnly);
QStringList rosTables;
QString newString;
while (!stream.atEnd()) {
int row, col;
QMap<int, QVariant> roleDataMap;
stream >> row >> col >> roleDataMap;
rosTables << roleDataMap[Qt::DisplayRole].toString();
}
for (const QString &tableType : rosTables) {
if (tableType == "Images") {
QPoint initPos(0, 0);
auto *wgt = new CustomTableWidget;
auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
QBrush(Qt::darkGreen));
auto *sizeGrip = new QSizeGrip(wgt);
auto *layout = new QHBoxLayout(wgt);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(sizeGrip, 0, Qt::AlignRight | Qt::AlignBottom);
connect(wgt, &CustomTableWidget::sizeChanged, [wgt, proxyControl](){
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
});
wgt->setColumnCount(4);
wgt->setRowCount(4);
for (int ridx = 0; ridx < wgt->rowCount(); ridx++) {
for (int cidx = 0; cidx < wgt->columnCount(); cidx++) {
QRadioButton *radio1, *radio2;
auto* item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
radio1 = new QRadioButton;
radio2 = new QRadioButton;
wgt->setCellWidget(cidx, 0, radio1);
wgt->setCellWidget(cidx, 3, radio2);
Arrow *arrow = new Arrow;
}
}
auto *const proxy = addWidget(wgt);
proxy->setPos(initPos.x(), initPos.y()
+ proxyControl->rect().height());
proxy->setParentItem(proxyControl);
proxyControl->setPos(initPos.x(), initPos.y());
proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
}
}
}
diagramitem.h
#ifndef DIAGRAMITEM_H
#define DIAGRAMITEM_H
#include <QGraphicsPolygonItem>
class Arrow;
class DiagramItem : public QGraphicsPolygonItem
{
public:
DiagramItem(QMenu *contextMenu, QGraphicsItem *parent = Q_NULLPTR);
void removeArrow(Arrow *arrow);
void removeArrows();
void addArrow(Arrow *arrow);
QPixmap image() const;
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
QPolygonF myPolygon;
QList<Arrow*> arrows;
QMenu *myContextMenu;
};
#endif // DIAGRAMITEM_H
diagramitem.cpp
#include "diagramitem.h"
#include "arrow.h"
#include <QPainter>
#include <QGraphicsScene>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>
DiagramItem::DiagramItem(QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent)
{
myContextMenu = contextMenu;
setPolygon(myPolygon);
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemIsSelectable, true);
setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}
void DiagramItem::removeArrow(Arrow *arrow)
{
int index = arrows.indexOf(arrow);
if (index != -1)
arrows.removeAt(index);
}
void DiagramItem::removeArrows()
{
foreach (Arrow *arrow, arrows) {
arrow->startItem()->removeArrow(arrow);
arrow->endItem()->removeArrow(arrow);
scene()->removeItem(arrow);
delete arrow;
}
}
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}
void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
scene()->clearSelection();
setSelected(true);
myContextMenu->exec(event->screenPos());
}
QVariant DiagramItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange) {
foreach (Arrow *arrow, arrows) {
arrow->updatePosition();
}
}
return value;
}
arrow.h
#ifndef ARROW_H
#define ARROW_H
#include <QGraphicsLineItem>
#include "diagramitem.h"
class Arrow : public QGraphicsLineItem
{
public:
enum { Type = UserType + 4 };
Arrow(DiagramItem *startItem, DiagramItem *endItem,
QGraphicsItem *parent = nullptr);
DiagramItem *startItem() const { return myStartItem; }
DiagramItem *endItem() const { return myEndItem; }
QPainterPath shape() const override;
void setColor(const QColor &color) {
myColor = color;
}
int type() const override { return Type; }
void updatePosition();
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
private:
QColor myColor;
DiagramItem *myStartItem;
DiagramItem *myEndItem;
QPolygonF arrowHead;
};
#endif // ARROW_H
arrow.cpp
#include "arrow.h"
#include <QPen>
#include <QPainter>
#include "qmath.h"
Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent)
{
myStartItem = startItem;
myEndItem = endItem;
myColor = Qt::GlobalColor::black;
setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
setFlag(QGraphicsItem::ItemIsSelectable, true);
}
QPainterPath Arrow::shape() const
{
QPainterPath path = QGraphicsLineItem::shape();
path.addPolygon(arrowHead);
return path;
}
void Arrow::updatePosition()
{
QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
setLine(line);
}
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
if (myStartItem->collidesWithItem(myEndItem))
return;
QPen myPen = pen();
myPen.setColor(myColor);
qreal arrowSize = 20;
painter->setPen(myPen);
painter->setBrush(myColor);
QLineF centerLine(myStartItem->pos(), myEndItem->pos());
QPolygonF endPolygon = myEndItem->polygon();
QPointF p1 = endPolygon.first() + myEndItem->pos();
QPointF p2;
QPointF intersectPoint;
QLineF polyLine;
for (int i = 1; i < endPolygon.count(); ++i) {
p2 = endPolygon.at(i) + myEndItem->pos();
polyLine = QLineF(p1, p2);
QLineF::IntersectType intersectType =
polyLine.intersect(centerLine, &intersectPoint);
if (intersectType == QLineF::BoundedIntersection)
break;
p1 = p2;
}
setLine(QLineF(intersectPoint, myStartItem->pos()));
double angle = std::atan2(-line().dy(), line().dx());
QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
cos(angle + M_PI / 3) * arrowSize);
QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
cos(angle + M_PI - M_PI / 3) * arrowSize);
arrowHead.clear();
arrowHead << line().p1() << arrowP1 << arrowP2;
painter->drawLine(line());
painter->drawPolygon(arrowHead);
if (isSelected()) {
painter->setPen(QPen(myColor, 1, Qt::DashLine));
QLineF myLine = line();
myLine.translate(0, 4.0);
painter->drawLine(myLine);
myLine.translate(0,-8.0);
painter->drawLine(myLine);
}
}
What I have done so far to solve the problem:
1) I came across this post which was useful to understand the initial idea on how to do that, but it didn't really provide a way, or an implementation idea on how to best proceed
2) I researched the official documentation and before asking this question I went through the whole Diagram Scene example provided and understood how to create an Arrow object. The documentation about that was very good and made me understand how the graphics line item has to be formed.
However I was not able (coming back to my example) how to make "aware" the QRadioButton that I am trying to use its center as starting point for an arrow ad, therefore, how do I make "aware" the destination QRadioButton in another cell that it has to be connected there?
Below a particular of what I mean:
So basically the start point of the QRadioButton change color (or style) and the arrival point also change color.
3) I thought that the Arrow object has to be created inside the subclassed QGraphicsScene since it already handles the mouse events.
4) Despite what I tried so far I could not find any other useful help. Although I am still investigating how to do that.
If anyone has ever been in the same situation please provide guidance on how to better proceed to solve this problem and find a solution to this issue.
Solution
When a start and end radio buttons are checked, you need to create the arrow with those buttons as start and end nodes, e.g.:
void Backend::onInputRadioButton(bool checked)
{
m_endNode = checked ? static_cast<QRadioButton *>(sender()) : nullptr;
if (m_startNode && m_endNode)
m_scene->addItem(new ArrowItem(m_startNode, m_endNode));
}
Then you need to connect the signal of the top-most graphics items, which hold the tables, with the updatePosition slot of the ArrowItem, e.g.:
connect(m_startItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
connect(m_endItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
Note: I am using a property to hold a reference to the container item.
Finally, you need to update the arrow line, e.g.:
void ArrowItem::updatePosition()
{
QPointF offset(7, 15);
QPointF p1 = m_startItem->property("item").value<MovableItem *>()->pos()
+ m_startItem->parentWidget()->mapToParent(m_startItem->pos())
+ offset;
QPointF p2 = m_endItem->property("item").value<MovableItem *>()->pos()
+ m_endItem->parentWidget()->mapToParent(m_endItem->pos())
+ offset;
setLine(QLineF(p1, p2));
}
Example
I have dared to suggest improvements in your code. You can find the complete example I wrote for you on GitHub.
Result
The provided example produces the following result:
Note: The arrow heads are missing. Check once again the Diagram Scene Example to get an idea of how to draw them.

Programmatic scrolling with QGraphicsView and QGraphicsItem?

I would like to programmatically scroll a scene to the left / right, but I am not sure how to do that properly. Note that I do not want to have (visible) scroll bars.
I use a standard QGraphicsView + QGraphicsScene + QGraphicsItem setup. I have downsized it to the minimum, with one single QGraphicsItem (a QGraphicsRectItem) in the scene.
I have managed to achieve programmatic scrolling by setting my view like this:
// view setup
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
and then, in another part of the code:
// programmatic scrolling
QScrollBar* const sb = view->horizontalScrollBar();
sb->setRange(0, 1000); // some values for experimenting
sb->setValue(sb->value() + 100 or -100); // some increment for experimenting
This works, but... scrolling through invisible scrollbars doesn't feel right.
I tried this more straightforward approach:
// programmatic scrolling - doesn't quite work
view->viewport()->scroll(100 or -100, 0); // some increment for experimenting
This code does scroll, but when the rectangle goes off the left edge of the view, and I reverse the scrolling direction (increment changed from 100 to -100 in the call to scroll()), the uncovered part of the rectangle is not repainted. The reason is that QGraphicsRectItem::paint() is not called in that case (it is called when using the scrollbar method).
So, is there a way to get viewport()->scroll() work? Or some other simple way to achieve programmatic scrolling? Or is the artificial scrollbar method just the way to go?
Moving the view assumes that it's smaller than its scene. If they're the same size, it won't move.
QGraphicsView can be set to centerOn any position in scene coordinates. Use a timer to call centerOn to move the view one frame at a time.
Here's a working example: -
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QTimer>
class MyView : public QGraphicsView
{
private:
public:
MyView(QGraphicsScene* pScene)
: QGraphicsView(pScene, NULL)
{}
void AnimateBy(int x)
{
float updateFrequency = (1000/30.0); // ~30 frames per second
QPointF currScenePos = sceneRect().center();
int curX = currScenePos.x();
int endPos = curX + x;
int distanceToAnimate = (endPos - curX);
// speed = dist / time
float updatePosInterval = (float)distanceToAnimate / updateFrequency;
printf("updatePosInterval: %f \n", updatePosInterval);
static float newXPos = sceneRect().center().x();
QTimer* pTimer = new QTimer;
QObject::connect(pTimer, &QTimer::timeout, [=](){
newXPos += updatePosInterval;
centerOn(newXPos, sceneRect().center().y());
// check for end position or time, then....
if(newXPos >= endPos)
{
pTimer->stop();
pTimer->deleteLater();
}
});
pTimer->start(updateFrequency);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene(0, 0, 10000, 20000);
MyView* view = new MyView(&scene);
QGraphicsRectItem* pRect = new QGraphicsRectItem(0, 0, 100, 100);
pRect->setPos(scene.width()/2, scene.height()/2);
scene.addItem(pRect);
// timer to wait for the window to appear, before starting to move
QTimer* pTimer = new QTimer;
pTimer->setSingleShot(true);
QObject::connect(pTimer, &QTimer::timeout,[=](){
view->centerOn(pRect); // centre on the rectangle
view->AnimateBy(100);
pTimer->deleteLater();
});
pTimer->start(1000);
view->show();
return a.exec();
}
So, we create the animation by moving the view frame-by-frame using the call to centerOn.
For simplicity, the code just deals with moving in one axis. To move in 2 axis, use 2D vector maths to calculate the interval position.
Try to change the view transformation with the QGraphicsView::translate() or QGraphicsView::setTransform().
But keep in mind that you can't move the viewport "outside" the scene, so make sure that your scene rectangle is large enough.
If I got your question correctly, there is a dojo classes library with such class as PanWebView that allow QWebView to scroll smoothly with mouse without any scrollbars. Take a look at sources. It supports panning and can be suitable for mobile apps, but maybe it'll help you too.
PanWebView class looks like this
#include <QWebView>
#include <QWebFrame>
#include <QMouseEvent>
#include <QApplication>
class PanWebView : public QWebView
{
Q_OBJECT
private:
bool pressed;
bool scrolling;
QPoint position;
QPoint offset;
QList<QEvent*> ignored;
public:
PanWebView(QWidget *parent = 0): QWebView(parent), pressed(false), scrolling(false) {
QWebFrame *frame = page()->mainFrame();
frame->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
frame->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);
}
protected:
void mousePressEvent(QMouseEvent *mouseEvent) {
if (ignored.removeAll(mouseEvent))
return QWebView::mousePressEvent(mouseEvent);
if (!pressed && !scrolling && mouseEvent->modifiers() == Qt::NoModifier)
if (mouseEvent->buttons() == Qt::LeftButton) {
pressed = true;
scrolling = false;
position = mouseEvent->pos();
QWebFrame *frame = page()->mainFrame();
int x = frame->evaluateJavaScript("window.scrollX").toInt();
int y = frame->evaluateJavaScript("window.scrollY").toInt();
offset = QPoint(x, y);
QApplication::setOverrideCursor(Qt::OpenHandCursor);
return;
}
return QWebView::mousePressEvent(mouseEvent);
}
void mouseReleaseEvent(QMouseEvent *mouseEvent) {
if (ignored.removeAll(mouseEvent))
return QWebView::mouseReleaseEvent(mouseEvent);
if (scrolling) {
pressed = false;
scrolling = false;
QApplication::restoreOverrideCursor();
return;
}
if (pressed) {
pressed = false;
scrolling = false;
QMouseEvent *event1 = new QMouseEvent(QEvent::MouseButtonPress,
position, Qt::LeftButton,
Qt::LeftButton, Qt::NoModifier);
QMouseEvent *event2 = new QMouseEvent(*mouseEvent);
ignored << event1;
ignored << event2;
QApplication::postEvent(this, event1);
QApplication::postEvent(this, event2);
QApplication::restoreOverrideCursor();
return;
}
return QWebView::mouseReleaseEvent(mouseEvent);
}
void mouseMoveEvent(QMouseEvent *mouseEvent) {
if (scrolling) {
QPoint delta = mouseEvent->pos() - position;
QPoint p = offset - delta;
QWebFrame *frame = page()->mainFrame();
frame- >evaluateJavaScript(QString("window.scrollTo(%1,%2);").arg(p.x()).arg(p.y()));
return;
}
if (pressed) {
pressed = false;
scrolling = true;
return;
}
return QWebView::mouseMoveEvent(mouseEvent);
}
};
And usage:
PanWebView web;
web.setUrl(QUrl("http://news.google.com"));
web.setWindowTitle("Web View - use mouse to drag and pan around");
web.show();
Also did you check this and this topics? I think it can be usefull.

Drawing by Windows GDI inside of Qt

I am trying to use Windows GDI inside of QGraphicsView paintEvent but have noticed some drawing issues, for example the drawing disappear or blinking when I resize window or minimize and maximize. When I use Qt instead of GDI its working perfectly. Here is the code:
[UPDATED CODE]
#include "CustomView.h"
#include <QPainter>
#include <QPaintEngine>
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
CustomView::CustomView(QWidget *parent)
: QGraphicsView(parent)
{
setAutoFillBackground(true);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setAttribute(Qt::WA_NativeWindow, true);
}
CustomView::~CustomView()
{
}
void CustomView::paintEvent(QPaintEvent * event)
{
QPainter painter(viewport());
painter.beginNativePainting();
// Drawing by Windows GDI
HWND hwnd = (HWND)viewport()->winId();
HDC hdc = GetDC(hwnd);
QString text("Test GDI Paint");
RECT rect;
GetClientRect(hwnd, &rect);
HBRUSH hbrRed = CreateSolidBrush(RGB(0, 255, 0));
FillRect(hdc, &rect, hbrRed);
Ellipse(hdc, 50, 50, rect.right - 100, rect.bottom - 100);
SetTextAlign(hdc, TA_CENTER | TA_BASELINE);
TextOutW(hdc, width() / 2, height() / 2, (LPCWSTR)text.utf16(), text.size());
ReleaseDC(hwnd, hdc);
painter.endNativePainting();
QGraphicsView::paintEvent(event);
// Drawing the same by Qt
// QPainter painter(viewport());
// painter.save() ;
// QBrush GreenBrush(Qt::green);
// QBrush WhiteBrush(Qt::white);
// QRect ViewportRect = viewport()->rect();
// painter.fillRect(ViewportRect, GreenBrush);
// painter.drawEllipse(50, 50, ViewportRect.right() - 100, ViewportRect.bottom() - 100);
// QPainterPath EllipsePath;
// EllipsePath.addEllipse(50, 50, ViewportRect.right() - 100, ViewportRect.bottom() - 100);
// painter.fillPath(EllipsePath, WhiteBrush);
// painter.drawText(ViewportRect.width() / 2, ViewportRect.height() / 2, "Test Qt Paint");
// painter.restore();
}
and here is the whole project(Visual Studio 2010 + Qt 5.4.1)[UPDATED ARCHIVE]
https://dl.dropboxusercontent.com/u/105132532/Stackoverflow/Qt5TestApplication.7z
Any ideas?
Solution(edited the code after Kuba Ober's answer)
#include "CustomView.h"
#include <QPainter>
#include <QPaintEngine>
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
CustomView::CustomView(QWidget *parent)
: QGraphicsView(parent)
{
// This brings the original paint engine alive.
QGraphicsView::paintEngine();
setAttribute(Qt::WA_NativeWindow);
setAttribute(Qt::WA_PaintOnScreen);
setRenderHint(QPainter::Antialiasing);
}
CustomView::~CustomView()
{
}
QPaintEngine* CustomView::paintEngine() const
{
return NULL;
}
bool CustomView::event(QEvent * event) {
if (event->type() == QEvent::Paint)
{
bool result = QGraphicsView::event(event);
drawGDI();
return result;
}
else if (event->type() == QEvent::UpdateRequest)
{
bool result = QGraphicsView::event(event);
drawGDI();
return result;
}
return QGraphicsView::event(event);
}
void CustomView::drawGDI()
{
// Drawing by Windows GDI
HWND hwnd = (HWND)viewport()->winId();
HDC hdc = GetDC(hwnd);
QString text("Test GDI Paint");
RECT rect;
GetClientRect(hwnd, &rect);
HBRUSH hbrRed = CreateSolidBrush(RGB(0, 255, 0));
FillRect(hdc, &rect, hbrRed);
Ellipse(hdc, 50, 50, rect.right - 100, rect.bottom - 100);
SetTextAlign(hdc, TA_CENTER | TA_BASELINE);
TextOutW(hdc, width() / 2, height() / 2, (LPCWSTR)text.utf16(), text.size());
ReleaseDC(hwnd, hdc);
}
A QWidget that intends to paint via GDI only must:
Reimplement paintEngine to return nullptr.
Set the WA_PaintOnScreen attribute.
Optionally set the WA_NativeWindow attribute. This only speeds up the first redraw of the widget.
Reimplement QObject::event and catch Paint and UpdateRequest events. These events should result in calling a method that does the GDI painting. The events must not be forwarded to the base class.
Additionally, a QWidget that paints via GDI on top of contents painted through paintEvent/QPainter, must additionally:
Call base class's paintEngine() method once in the constructor. This will instantiate the native paint engine for the widget.
In the QObject::event implementation, the base class's event must be called before doing the GDI painting. This will draw the contents using the raster paint engine, and return control to you to overdraw some other contents on top of it.
The example below shows how to overpaint on top of the drawing done by Qt's paint system. Of course, since the painting is done on top contents already drawn by Qt, there's flicker.
Doing GDI painting on top of the Qt's backing store, avoiding the flicker, is also possible, but requires a somewhat different approach.
#include <QApplication>
#include <QGraphicsObject>
#include <QPropertyAnimation>
#include <QGraphicsView>
#include <QPainter>
#include <QPaintEvent>
#include <windows.h>
class View : public QGraphicsView {
public:
View(QWidget *parent = 0) : QGraphicsView(parent)
{
// This brings the original paint engine alive.
QGraphicsView::paintEngine();
//setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setAttribute(Qt::WA_NativeWindow);
setAttribute(Qt::WA_PaintOnScreen);
setRenderHint(QPainter::Antialiasing);
}
QPaintEngine * paintEngine() const Q_DECL_OVERRIDE { return 0; }
bool event(QEvent * event) Q_DECL_OVERRIDE {
if (event->type() == QEvent::Paint) {
bool result = QGraphicsView::event(event);
paintOverlay();
return result;
}
if (event->type() == QEvent::UpdateRequest) {
bool result = QGraphicsView::event(event);
paintOverlay();
return result;
}
return QGraphicsView::event(event);
}
void resizeEvent(QResizeEvent *) {
fitInView(-2, -2, 4, 4, Qt::KeepAspectRatio);
}
virtual void paintOverlay();
};
void View::paintOverlay()
{
// We're called after the native painter has done its thing
HWND hwnd = (HWND)viewport()->winId();
HDC hdc = GetDC(hwnd);
HBRUSH hbrGreen = CreateHatchBrush(HS_BDIAGONAL, RGB(0, 255, 0));
RECT rect;
GetClientRect(hwnd, &rect);
SetBkMode(hdc, TRANSPARENT);
SelectObject(hdc, hbrGreen);
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
SelectObject(hdc, GetStockObject(NULL_BRUSH));
Ellipse(hdc, 50, 50, rect.right - 100, rect.bottom - 100);
QString text("Test GDI Paint");
SetTextAlign(hdc, TA_CENTER | TA_BASELINE);
TextOutW(hdc, width() / 2, height() / 2, (LPCWSTR)text.utf16(), text.size());
DeleteObject(hbrGreen);
ReleaseDC(hwnd, hdc);
}
class EmptyGraphicsObject : public QGraphicsObject
{
public:
EmptyGraphicsObject() {}
QRectF boundingRect() const { return QRectF(0, 0, 0, 0); }
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) {}
};
void setupScene(QGraphicsScene &s)
{
QGraphicsObject * obj = new EmptyGraphicsObject;
QGraphicsRectItem * rect = new QGraphicsRectItem(-1, 0.3, 2, 0.3, obj);
QPropertyAnimation * anim = new QPropertyAnimation(obj, "rotation", &s);
s.addItem(obj);
rect->setPen(QPen(Qt::darkBlue, 0.1));
anim->setDuration(2000);
anim->setStartValue(0);
anim->setEndValue(360);
anim->setEasingCurve(QEasingCurve::InBounce);
anim->setLoopCount(-1);
anim->start();
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene s;
setupScene(s);
View view;
view.setScene(&s);
view.show();
return a.exec();
}

How to write a text around a circle using QPainter class?

The question is simple ! I want something like this. Either using QPainter class or using Qt Graphics Framework:
There are several ways to do this using a QPainterPath specified here.
Here is the second example from that page:
#include <QtGui>
#include <cmath>
class Widget : public QWidget
{
public:
Widget ()
: QWidget() { }
private:
void paintEvent ( QPaintEvent *)
{
QString hw("hello world");
int drawWidth = width() / 100;
QPainter painter(this);
QPen pen = painter.pen();
pen.setWidth(drawWidth);
pen.setColor(Qt::darkGreen);
painter.setPen(pen);
QPainterPath path(QPointF(0.0, 0.0));
QPointF c1(width()*0.2,height()*0.8);
QPointF c2(width()*0.8,height()*0.2);
path.cubicTo(c1,c2,QPointF(width(),height()));
//draw the bezier curve
painter.drawPath(path);
//Make the painter ready to draw chars
QFont font = painter.font();
font.setPixelSize(drawWidth*2);
painter.setFont(font);
pen.setColor(Qt::red);
painter.setPen(pen);
qreal percentIncrease = (qreal) 1/(hw.size()+1);
qreal percent = 0;
for ( int i = 0; i < hw.size(); i++ ) {
percent += percentIncrease;
QPointF point = path.pointAtPercent(percent);
qreal angle = path.angleAtPercent(percent); // Clockwise is negative
painter.save();
// Move the virtual origin to the point on the curve
painter.translate(point);
// Rotate to match the angle of the curve
// Clockwise is positive so we negate the angle from above
painter.rotate(-angle);
// Draw a line width above the origin to move the text above the line
// and let Qt do the transformations
painter.drawText(QPoint(0, -pen.width()),QString(hw[i]));
painter.restore();
}
}
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
Widget widget;
widget.show();
return app.exec();
}

Finding the point of intersection between a line and a QPainterPath

I'm trying to determine the point where a hitscan projectile's path (basically a line, but I've represented it as a QPainterPath in my example) intersects with an item in my scene. I am not sure if there is a way to find this point using the functions provided by QPainterPath, QLineF, etc. The code below illustrates what I'm trying to do:
#include <QtWidgets>
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// hitPos = /* the point at which projectilePath hits targetShape */
hitPos = closest->pos(); // incorrect; always gives top left
qDebug() << projectilePath.intersects(targetShape); // true
qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
// To show that they do actually intersect..
QPen p1(Qt::green);
p1.setWidth(2);
QPen p2(Qt::blue);
p2.setWidth(2);
scene->addPath(projectilePath, p1);
scene->addPath(targetShape, p2);
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
projectilePath.intersects(targetShape) returns true, but projectilePath.intersected(targetShape) returns an empty path.
Is there a way to achieve this?
As the answer to Intersection point of QPainterPath and line (find QPainterPath y by x) points out, QPainterPath::intersected() only accounts for paths which have fill areas. The rectangular path trick which is also mentioned there can be implemented like this:
#include <QtWidgets>
/*!
Returns the closest element (position) in \a sourcePath to \a target,
using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance = sourcePath.elementAt(0) - target;
qreal shortestLength = shortestDistance.manhattanLength();
for (int i = 1; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}
/*!
Returns \c true if \a projectilePath intersects with any items in \a scene,
setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// QLineF has normalVector(), which is useful for extending our path to a rectangle.
// The path needs to be a rectangle, as QPainterPath::intersected() only accounts
// for intersections between fill areas, which projectilePath doesn't have.
QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
// Extend the first point in the path out by 1 pixel.
QLineF startEdge = pathAsLine.normalVector();
startEdge.setLength(1);
// Swap the points in the line so the normal vector is at the other end of the line.
pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
QLineF endEdge = pathAsLine.normalVector();
// The end point is currently pointing the wrong way; move it to face the same
// direction as startEdge.
endEdge.setLength(-1);
// Now we can create a rectangle from our edges.
QPainterPath rectPath(startEdge.p1());
rectPath.lineTo(startEdge.p2());
rectPath.lineTo(endEdge.p2());
rectPath.lineTo(endEdge.p1());
rectPath.lineTo(startEdge.p1());
// Visualize the rectangle that we created.
scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
// Visualize the intersection of the rectangle with the item.
scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
// The hit position will be the element (point) of the rectangle that is the
// closest to where the projectile was fired from.
hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
This has pretty good precision (± 1 pixel, since QLineF::length() is an integer), but there might be a neater way to achieve the same thing.
Just for the record (and if someone else steps here). The above answer is excellent. There's just a little bug in the closestPoint function that may happens if the first point is already the closest one. It should return elementAt(0) instead of elementAt(0) - target.
Here is the fixed function:
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance;
qreal shortestLength = std::numeric_limits<int>::max();
for (int i = 0; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}

Resources