Qt How to move view with mouseMoveEvent - c++

While using a QGraphicsScene and QGraphicsView I wanted to achieve that if the mouse is at one of the borders of the screen the view moves with it (like it is the case in most RTS games). However when dealing with the mouseMoveEvent I only get a stack overflow, most likely because the event is called infinitely many times once the mouse is at a certain location.
My camera class has a pointer to the view and inherits QGraphicsRectItem and is added to the scene in the main class.
Is there a way to prevent this event from happening at a certain point? Or is there even an elegant solution to this? One additional problem with my attempt is that the camera class has to grab the mouse when i want the mouseMoveEvent to work.
void Camera::mouseMoveEvent(QGraphicsSceneMouseEvent* e)
{
int view_x = view->mapFromScene(e->pos()).x();
int view_y = view->mapFromScene(e->pos()).y();
int horizontalSliderPos = view->horizontalScrollBar()->sliderPosition();
int verticalSliderPos = view->verticalScrollBar()->sliderPosition();
if (view_x < 100) {
view->horizontalScrollBar()->setSliderPosition(horizontalSliderPos - 5);
}
if (view_x > Constants::VIEWWIDTH - 100) {
view->horizontalScrollBar()->setSliderPosition(view->horizontalScrollBar()->sliderPosition() + 5);
}
if (view_y < 100) {
view->verticalScrollBar()->setSliderPosition(view->verticalScrollBar()->sliderPosition() - 5);
}
if (view_y > Constants::VIEWHEIGHT - 100) {
view->verticalScrollBar()->setSliderPosition(view->verticalScrollBar()->sliderPosition() + 5);
}
}

After some trying and experiments I obtained a solution, that might be a good starting point. As you already mentioned you have to break the recursive calls of mouseMoveEvent. I broke the recursive call with a simple boolean variable, but maybe also QSignalBlocker can be helpful here, even though it blocks all signals.
Another feature I tried to implement is that there is still a moving window in case of a non-moving mouse at the margin. I did this by using a QTimer, that fires every 25ms.
main.cpp
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMainWindow>
#include "MyScene.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
auto view = new QGraphicsView;
auto model = new MyScene;
view->setMouseTracking(true);
view->setScene(model);
model->setView(view);
model->addRect(QRectF(20, 20, 20, 20));
model->addRect(QRectF(300, 20, 20, 20));
model->addRect(QRectF(0, 0, 500, 500), QPen(Qt::blue)); // Complete Scene
view->show();
view->setSceneRect(QRectF(120, 20, 20, 20));
return app.exec();
}
MyScene.h
#pragma once
#include <QDebug>
#include <QGraphicsSceneEvent>
#include <QGraphicsView>
#include <QTimer>
class MyScene : public QGraphicsScene {
Q_OBJECT
public:
MyScene(QWidget* parent=nullptr) : QGraphicsScene(parent) {
}
void setView(QGraphicsView* view) {
mView = view;
}
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override {
if (mTranslating) return;
delete mRepeater; // Destroys connect
mRepeater = new QTimer;
if (!mView) return;
mTranslating = true;
int tx{ 0 };
int ty{ 0 };
const int margin = 20;
int sx = mView->mapFromGlobal(event->screenPos()).x();
int sy = mView->mapFromGlobal(event->screenPos()).y();
if (sx < margin) {
tx = -1;
}
else if (sx > mView->width() - margin) {
tx = 1;
}
if (sy < margin) {
ty = -1;
}
else if (sy > mView->height() - margin) {
ty = 1;
}
if (tx != 0 || ty != 0) {
auto rect = mView->sceneRect();
rect.translate(QPointF{ (qreal)tx,(qreal)ty });
mView->setSceneRect(rect);
connect(mRepeater, &QTimer::timeout, [=]() { // Moves even if mouse is not moved
auto rect = mView->sceneRect();
rect.translate(QPointF{ (qreal)tx,(qreal)ty });
mView->setSceneRect(rect);
});
mRepeater->start(25);
}
mTranslating = false;
}
QGraphicsView* mView{ nullptr };
bool mTranslating{ false };
QTimer* mRepeater{ nullptr };
};

I think this problem could be avoided if you would handle the mouse move event in your QGraphicsView class instead of doing it in the QGraphicsScene class.

Related

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

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

Horizontal Scrolling issue with QTableView with a child QGraphicsRectItem

I have a QGraphicsView with a scene containing QGraphicsProxyWidget (QTableView) and a QGraphicsRectItem on top of QTableView. Unfortunately, i am not able to upload any images (Imgur error) but here is a post to visualize how it looks.
My QGraphicsRectItem a vertical line, acts like a seek bar which runs horizontally on the QTableView. Basically, it looks like a time line with a seek bar commonly found in Audio and Video editing tools.
Issues:
Moving the horizontal scroll bar makes the seek bar disappear on hitting the boundaries of QTableView and it doesn't reappear anymore.
While moving the scrollbar if the seekbar hits the boundaries, scrollbar cannot be moved further.
What did i do:
I have managed to create a MVCE (Not really Minimal)
//SeekBar.h
#include <QGraphicsRectItem>
#include <QAbstractItemView>
#include <QObject>
#include <QScrollBar>
#include <QGraphicsProxyWidget>
#include "tableview.h"
class SeekBar : public QObject, public QGraphicsRectItem
{
Q_OBJECT
public:
SeekBar(int width, QTableView* view, QGraphicsScene* scene);
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant& value);
public slots:
void scrollBarMoved();
private:
QGraphicsProxyWidget* proxy;
QTableView* m_view;
QScrollBar* scrollbar;
bool m_isScrollMoving;
};
//SeekBar.cpp
#include <QBrush>
#include <QGraphicsScene>
#include "seekbar.h"
SeekBar::SeekBar(int width, QTableView* view, QGraphicsScene* scene) :
QGraphicsRectItem(nullptr),
proxy(new QGraphicsProxyWidget()),
m_view(view),
m_isScrollMoving(false)
{
proxy->setWidget(m_view);
scene->addItem(proxy);
setParentItem(proxy);
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
setBrush(Qt::red);
setRect(0, 0, width, m_view->height());
scrollbar = m_view->horizontalScrollBar();
connect(m_view->horizontalScrollBar(), &QScrollBar::sliderMoved, this, &SeekBar::scrollBarMoved);
}
QVariant SeekBar::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant& value)
{
if (change == QGraphicsItem::ItemPositionChange && !m_isScrollMoving)
{
QPointF p = value.toPointF();
qreal max = parentItem()->boundingRect().right() - boundingRect().right();
qreal min = parentItem()->boundingRect().left() - boundingRect().left();
if (p.x() > max)
{
p.setX(max);
}
else if (p.x() < min)
{
p.setX(min);
}
p.setY(pos().y());
float percentage = (p.x() - min) * 1.0 / (max - min);
int value = scrollbar->minimum() + percentage * (scrollbar->maximum() - scrollbar->minimum());
scrollbar->setValue(value);
return p;
}
else if (change == QGraphicsItem::ItemPositionChange && m_isScrollMoving)
{
QPointF p = value.toPointF();
qreal max = parentItem()->boundingRect().right() - boundingRect().right();
qreal min = parentItem()->boundingRect().left() - boundingRect().left();
setVisible(true);
if (p.x() > max)
{
hide();
}
else if (p.x() < min)
{
hide();
}
p.setY(pos().y());
return p;
}
return QGraphicsRectItem::itemChange(change, value);
}
void SeekBar::scrollBarMoved()
{
m_isScrollMoving = true;
int col = m_view->columnAt(pos().x());
setPos(m_view->columnViewportPosition(col), 0);
m_isScrollMoving = false;
}
//TableView.h
#include <QTableView>
class TableView : public QTableView
{
public:
TableView(QWidget* parent = nullptr);
};
//TableView.cpp
#include "tableview.h"
#include <QHeaderView>
#include <QStandardItemModel>
#include <QScrollBar>
TableView::TableView(QWidget* parent) : QTableView(parent)
{
QStandardItemModel* model = new QStandardItemModel(10, 10);
setModel(model);
verticalHeader()->hide();
horizontalHeader()->setHighlightSections(false);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setSelectionMode(QAbstractItemView::MultiSelection);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
}
//main.cpp
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsProxyWidget>
#include "tableview.h"
#include "seekbar.h"
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
QGraphicsView view;
QGraphicsScene* scene = new QGraphicsScene;
view.setScene(scene);
TableView* table = new TableView;
SeekBar* seekBarItem = new SeekBar(5, table, scene);
view.resize(640, 480);
view.show();
return a.exec();
}
What do i want:
I want to smoothly scroll the table view whenever i drag the seek bar using mouse but it should stay inside the view's boundaries.
I want to keep the seek at a fixed position(column) while scrolling using the horizontal bar. But it should NOT have any boundaries i.e. while using scrollbar, the seek should be able to pass through the view or hide but should appear at the fixed position when scrolled back.
Seek at column 5
Horizontal bar scrolled a little bit to the right
Scrolled till the end of the view
Scrolled back to column 5
EDIT1:
For instance, if the seek is at column 5, while scrolling using horizontal scroll bar it should stick to column 5 and should pass through the boundaries(or hide it when it hits the boundaries?). Seek should be visible whenever the column 5 is in the view while scrolling.
I would appreciate some ideas.

Create a QDockWidget that resizes to it's contents

I have an application where fixed-size child widgets need to be added programatically to a dock widget at run time based on user input. I want to add these widgets to a dock on the Qt::RightDockArea, from top to bottom until it runs out of space, then create a new column and repeat (essentially just the reverse of the flow layout example here, which I call a fluidGridLayout)
I can get the dock widget to resize itself properly using an event filter, but the resized dock's geometry doesn't change, and some of the widgets are drawn outside of the main window. Interestingly, resizing the main window, or floating and unfloating the dock cause it to 'pop' back into the right place (I haven't been able to find a way to replicate this programatically however)
I can't use any of the built-in QT layouts because with the widgets in my real program, they end up also getting drawn off screen.
Is there some way that I can get the dock to update it's top left coordinate to the proper position once it has been resized?
I think this may be of general interest as getting intuitive layout management behavior for dock widgets in QT is possibly the hardest thing known to man.
VISUAL EXMAPLE:
The code to replicate this is example given below.
Add 4 widgets to the program using the button
Resize the green bottom dock until only two widgets are shown. Notice that the 3 remaining widgets are getting painted outside the main window, however the dock is the right size, as evidenced by the fact that you can't see the close button anymore
Undock the blue dock widget. Notice it snaps to it's proper size.
Re-dock the blue dock to the right dock area. Notice it appears to be behaving properly now.
Now resize the green dock to it's minimum size. Notice the dock is now IN THE MIDDLE OF THE GUI. WTf, how is this possible??
THE CODE
Below I give the code to replicate the GUI from the screenshots.
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>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "QFluidGridLayout.h"
#include "QDockResizeEventFilter.h"
#include <QDockWidget>
#include <QGroupBox>
#include <QPushButton>
#include <QWidget>
#include <QDial>
class QTestWidget : public QGroupBox
{
public:
QTestWidget() : QGroupBox()
{
setFixedSize(50,50);
setStyleSheet("background-color: red;");
QDial* dial = new QDial;
dial->setFixedSize(40,40);
QLayout* testLayout = new QVBoxLayout;
testLayout->addWidget(dial);
//testLayout->setSizeConstraint(QLayout::SetMaximumSize);
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
setLayout(testLayout);
}
QSize sizeHint()
{
return minimumSize();
}
QDial* dial;
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QDockWidget* rightDock = new QDockWidget();
QDockWidget* bottomDock = new QDockWidget();
QGroupBox* central = new QGroupBox();
QGroupBox* widgetHolder = new QGroupBox();
QGroupBox* placeHolder = new QGroupBox();
placeHolder->setStyleSheet("background-color: green;");
placeHolder->setMinimumHeight(50);
widgetHolder->setStyleSheet("background-color: blue;");
widgetHolder->setMinimumWidth(50);
widgetHolder->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
widgetHolder->setLayout(new QFluidGridLayout);
widgetHolder->layout()->addWidget(new QTestWidget);
QPushButton* addWidgetButton = new QPushButton("Add another widget");
connect(addWidgetButton, &QPushButton::pressed, [=]()
{
widgetHolder->layout()->addWidget(new QTestWidget);
});
central->setLayout(new QVBoxLayout());
central->layout()->addWidget(addWidgetButton);
rightDock->setWidget(widgetHolder);
rightDock->installEventFilter(new QDockResizeEventFilter(widgetHolder,dynamic_cast<QFluidGridLayout*>(widgetHolder->layout())));
bottomDock->setWidget(placeHolder);
this->addDockWidget(Qt::RightDockWidgetArea, rightDock);
this->addDockWidget(Qt::BottomDockWidgetArea, bottomDock);
this->setCentralWidget(central);
central->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
this->setMinimumSize(500,500);
}
};
QFluidGirdLayout.h
#ifndef QFluidGridLayout_h__
#define QFluidGridLayout_h__
#include <QLayout>
#include <QGridLayout>
#include <QRect>
#include <QStyle>
#include <QWidgetItem>
class QFluidGridLayout : public QLayout
{
public:
enum Direction { LeftToRight, TopToBottom};
QFluidGridLayout(QWidget *parent = 0)
: QLayout(parent)
{
setContentsMargins(8,8,8,8);
setSizeConstraint(QLayout::SetMinAndMaxSize);
}
~QFluidGridLayout()
{
QLayoutItem *item;
while ((item = takeAt(0)))
delete item;
}
void addItem(QLayoutItem *item)
{
itemList.append(item);
}
Qt::Orientations expandingDirections() const
{
return 0;
}
bool hasHeightForWidth() const
{
return false;
}
int heightForWidth(int width) const
{
int height = doLayout(QRect(0, 0, width, 0), true, true);
return height;
}
bool hasWidthForHeight() const
{
return true;
}
int widthForHeight(int height) const
{
int width = doLayout(QRect(0, 0, 0, height), true, false);
return width;
}
int count() const
{
return itemList.size();
}
QLayoutItem *itemAt(int index) const
{
return itemList.value(index);
}
QSize minimumSize() const
{
QSize size;
QLayoutItem *item;
foreach (item, itemList)
size = size.expandedTo(item->minimumSize());
size += QSize(2*margin(), 2*margin());
return size;
}
void setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
doLayout(rect);
}
QSize sizeHint() const
{
return minimumSize();
}
QLayoutItem *takeAt(int index)
{
if (index >= 0 && index < itemList.size())
return itemList.takeAt(index);
else
return 0;
}
private:
int doLayout(const QRect &rect, bool testOnly = false, bool width = false) const
{
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom);
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
int lineWidth = 0;
QLayoutItem* item;
foreach(item,itemList)
{
QWidget* widget = item->widget();
if (y + item->sizeHint().height() > effectiveRect.bottom() && lineWidth > 0)
{
y = effectiveRect.y();
x += lineWidth + right;
lineWidth = 0;
}
if (!testOnly)
{
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
}
y += item->sizeHint().height() + top;
lineHeight = qMax(lineHeight, item->sizeHint().height());
lineWidth = qMax(lineWidth, item->sizeHint().width());
}
if (width)
{
return y + lineHeight - rect.y() + bottom;
}
else
{
return x + lineWidth - rect.x() + right;
}
}
QList<QLayoutItem *> itemList;
Direction dir;
};
#endif // QFluidGridLayout_h__
QDockResizeEventFilter.h
#ifndef QDockResizeEventFilter_h__
#define QDockResizeEventFilter_h__
#include <QObject>
#include <QLayout>
#include <QEvent>
#include <QDockWidget>
#include <QResizeEvent>
#include "QFluidGridLayout.h"
class QDockResizeEventFilter : public QObject
{
public:
QDockResizeEventFilter(QWidget* dockChild, QFluidGridLayout* layout, QObject* parent = 0)
: QObject(parent), m_dockChild(dockChild), m_layout(layout)
{
}
protected:
bool eventFilter(QObject *p_obj, QEvent *p_event)
{
if (p_event->type() == QEvent::Resize)
{
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(p_event);
QMainWindow* mainWindow = static_cast<QMainWindow*>(p_obj->parent());
QDockWidget* dock = static_cast<QDockWidget*>(p_obj);
// determine resize direction
if (resizeEvent->oldSize().height() != resizeEvent->size().height())
{
// vertical expansion
QSize fixedSize(m_layout->widthForHeight(m_dockChild->size().height()), m_dockChild->size().height());
if (dock->size().width() != fixedSize.width())
{
m_dockChild->resize(fixedSize);
m_dockChild->setFixedWidth(fixedSize.width());
dock->setFixedWidth(fixedSize.width());
mainWindow->repaint();
//dock->setGeometry(mainWindow->rect().right()-fixedSize.width(),dock->geometry().y(),fixedSize.width(), fixedSize.height());
}
}
if (resizeEvent->oldSize().width() != resizeEvent->size().width())
{
// horizontal expansion
m_dockChild->resize(m_layout->sizeHint().width(), m_dockChild->height());
}
}
return false;
}
private:
QWidget* m_dockChild;
QFluidGridLayout* m_layout;
};
#endif // QDockResizeEventFilter_h__
The problem is, nothing in the code above actually causes the QMainWindowLayout to recalculate itself. That function is buried within the QMainWindowLayout private class, but can be stimulated by adding and removing a dummy QDockWidget, which causes the layout to invalidate and recalcualte the dock widget positions
QDockWidget* dummy = new QDockWidget;
mainWindow->addDockWidget(Qt::TopDockWidgetArea, dummy);
mainWindow->removeDockWidget(dummy);
The only problem with this is that if you dig into the QT source code, you'll see that adding a dock widget causes the dock separator to be released, which causes unintuitive and choppy behavior as the user tries to resize the dock, and the mouse unexpectedly 'lets go'.
void QMainWindowLayout::addDockWidget(Qt::DockWidgetArea area,
QDockWidget *dockwidget,
Qt::Orientation orientation)
{
addChildWidget(dockwidget);
// If we are currently moving a separator, then we need to abort the move, since each
// time we move the mouse layoutState is replaced by savedState modified by the move.
if (!movingSeparator.isEmpty())
endSeparatorMove(movingSeparatorPos);
layoutState.dockAreaLayout.addDockWidget(toDockPos(area), dockwidget, orientation);
emit dockwidget->dockLocationChanged(area);
invalidate();
}
That can be corrected by moving the cursor back onto the separator and simulating a mouse press, basically undoing the endSeparatorMove callafter the docks have been repositioned. It's important to post the event, rather than send it, so thatit occurs after the resize event. The code for doing so looks like:
QPoint mousePos = mainWindow->mapFromGlobal(QCursor::pos());
mousePos.setY(dock->rect().bottom()+2);
QCursor::setPos(mainWindow->mapToGlobal(mousePos));
QMouseEvent* grabSeparatorEvent =
new QMouseEvent(QMouseEvent::MouseButtonPress,mousePos,Qt::LeftButton,Qt::LeftButton,Qt::NoModifier);
qApp->postEvent(mainWindow, grabSeparatorEvent);
Where 2 is a magic number that accounts for the group box border.
Put that all together, and here is the event filter than gives the desired behavior:
Corrected Event Filter
#ifndef QDockResizeEventFilter_h__
#define QDockResizeEventFilter_h__
#include <QObject>
#include <QLayout>
#include <QEvent>
#include <QDockWidget>
#include <QResizeEvent>
#include <QCoreApplication>
#include <QMouseEvent>
#include "QFluidGridLayout.h"
class QDockResizeEventFilter : public QObject
{
public:
friend QMainWindow;
friend QLayoutPrivate;
QDockResizeEventFilter(QWidget* dockChild, QFluidGridLayout* layout, QObject* parent = 0)
: QObject(parent), m_dockChild(dockChild), m_layout(layout)
{
}
protected:
bool eventFilter(QObject *p_obj, QEvent *p_event)
{
if (p_event->type() == QEvent::Resize)
{
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(p_event);
QMainWindow* mainWindow = dynamic_cast<QMainWindow*>(p_obj->parent());
QDockWidget* dock = static_cast<QDockWidget*>(p_obj);
// determine resize direction
if (resizeEvent->oldSize().height() != resizeEvent->size().height())
{
// vertical expansion
QSize fixedSize(m_layout->widthForHeight(m_dockChild->size().height()), m_dockChild->size().height());
if (dock->size().width() != fixedSize.width())
{
m_dockChild->setFixedWidth(fixedSize.width());
dock->setFixedWidth(fixedSize.width());
// cause mainWindow dock layout recalculation
QDockWidget* dummy = new QDockWidget;
mainWindow->addDockWidget(Qt::TopDockWidgetArea, dummy);
mainWindow->removeDockWidget(dummy);
// adding dock widgets causes the separator move event to end
// restart it by synthesizing a mouse press event
QPoint mousePos = mainWindow->mapFromGlobal(QCursor::pos());
mousePos.setY(dock->rect().bottom()+2);
QCursor::setPos(mainWindow->mapToGlobal(mousePos));
QMouseEvent* grabSeparatorEvent = new QMouseEvent(QMouseEvent::MouseButtonPress,mousePos,Qt::LeftButton,Qt::LeftButton,Qt::NoModifier);
qApp->postEvent(mainWindow, grabSeparatorEvent);
}
}
if (resizeEvent->oldSize().width() != resizeEvent->size().width())
{
// horizontal expansion
// ...
}
}
return false;
}
private:
QWidget* m_dockChild;
QFluidGridLayout* m_layout;
};
#endif // QDockResizeEventFilter_h__

how to avoid paintevent() being called when the widget was obscured and uncovered

i want to draw some rhombuses with random colors in a Qwidget. The widget should be repainted only when the window is resized .The problem is that when the widget was obscured and has now been uncovered , it is repainted. How can i avoid calling paintEvent() in this case? Thanks in advance.
void Dialog::paintEvent(QPaintEvent *e)
{
QPainter painter(this);
QRect background(0,0,this->geometry().width(),this->geometry().height());
painter.setBrush( QBrush( Qt::white ) );
painter.setPen( Qt::NoPen );
// QBrush bbrush(Qt::black,Qt::SolidPattern);
painter.drawRect(background);
int width = this->geometry().width();
int height = this->geometry().height();
int rec_size=64;
int rows=floor((double)height/(double)rec_size);
int cols=floor((double)width/(double)rec_size);
QPointF points[4];
for (int i=0;i<floor(rows);i++)
{
for (int j=0;j<floor(cols);j++)
{
painter.setBrush( QBrush( colors[rand() % color_size] ) );
points[0] = QPointF(rec_size*(j),rec_size*(i+0.5));
points[1] = QPointF(rec_size*(j+0.5),rec_size*(i));
points[2] = QPointF(rec_size*(j+1),rec_size*(i+0.5));
points[3] = QPointF(rec_size*(j+0.5),rec_size*(i+1));
painter.drawPolygon(points, 4);
}
}
}
You are presupposing a solution, where in fact you should be focusing on the problem instead. Your problem isn't about when paintEvent is called. Qt's semantics are such that the paintEvent can be called at any time in the main thread. You must cope with it.
What you need to do, instead, is store the randomized colors in a container, to re-use them when painting. In the example below, the colors are generated on demand, and are stored in a dynamically growing list indexed by row and column. This way when you resize the widget, the colors of the existing items don't have to change. You can then regenerate the colors at any time by simply clearing the container and forcing an update.
The example below allows you to select whether to preserve the colors during resizing.
The code uses Qt 5 and C++11. Note that the use of rand() % range is discouraged in modern code - it doesn't preserve the uniformity of the distribution.
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QCheckBox>
#include <QGridLayout>
#include <QPainter>
#include <random>
std::default_random_engine rng;
class Dialog : public QWidget {
Q_OBJECT
Q_PROPERTY(bool recolorOnResize READ recolorOnResize WRITE setRecolorOnResize)
QList<QColor> m_palette;
QList<QList<QColor>> m_chosenColors;
bool m_recolorOnResize;
void paintEvent(QPaintEvent *) {
QPainter p(this);
p.fillRect(rect(), Qt::white);
p.setRenderHint(QPainter::Antialiasing);
int rec_size=64;
int rows=height()/rec_size;
int cols=width()/rec_size;
std::uniform_int_distribution<int> dist(0, m_palette.size()-1);
while (m_chosenColors.size() < rows) m_chosenColors << QList<QColor>();
for (QList<QColor> & colors : m_chosenColors)
while (colors.size() < cols)
colors << m_palette.at(dist(rng));
QPointF points[4];
for (int i=0; i<rows; i++) {
for (int j=0; j<cols; j++) {
points[0] = QPointF(rec_size*(j),rec_size*(i+0.5));
points[1] = QPointF(rec_size*(j+0.5),rec_size*(i));
points[2] = QPointF(rec_size*(j+1),rec_size*(i+0.5));
points[3] = QPointF(rec_size*(j+0.5),rec_size*(i+1));
p.setBrush(m_chosenColors[i][j]);
p.drawPolygon(points, 4);
}
}
}
void resizeEvent(QResizeEvent *) {
if (m_recolorOnResize) m_chosenColors.clear();
}
public:
Dialog(QWidget * parent = 0) : QWidget(parent), m_recolorOnResize(false) {
m_palette << "#E2C42D" << "#E5D796" << "#BEDA2C" << "#D1DD91" << "#E2992D" << "#E5C596";
setAttribute(Qt::WA_OpaquePaintEvent);
}
Q_SLOT void randomize() {
m_chosenColors.clear();
update();
}
bool recolorOnResize() const { return m_recolorOnResize; }
void setRecolorOnResize(bool recolor) {
m_recolorOnResize = recolor;
setAttribute(Qt::WA_StaticContents, !m_recolorOnResize);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QGridLayout l(&w);
Dialog d;
QCheckBox recolor("Recolor on Resize");
QPushButton update("Repaint"), randomize("Randomize");
d.setMinimumSize(256, 128);
l.addWidget(&d, 0, 0, 1, 2);
l.addWidget(&recolor, 1, 0, 1, 2);
l.addWidget(&update, 2, 0);
l.addWidget(&randomize, 2, 1);
recolor.setChecked(d.recolorOnResize());
QObject::connect(&recolor, &QAbstractButton::toggled, [&d](bool checked){
d.setRecolorOnResize(checked);}
);
QObject::connect(&update, &QAbstractButton::clicked, &d, static_cast<void(QWidget::*)()>(&QWidget::update));
QObject::connect(&randomize, &QAbstractButton::clicked, &d, &Dialog::randomize);
w.show();
return a.exec();
}
#include "main.moc"

QGraphicsScene scaled weirdly in QGraphicsView

I'm messing around with QGraphicsView and QGraphicsScene to create a Tic Tac Toe clone. I add some QGraphicsLineItems to my scene and override the resizeEvent method of the Widget that contains the view, so that when the window is resized, the view and its contents are scaled appropriately. This works fine, except for the first time that I run the program:
Once I resize the window by any amount, the scene is scaled correctly:
Here's the code:
main.cpp:
#include <QtGui>
#include "TestApp.h"
int main(int argv, char **args)
{
QApplication app(argv, args);
TestApp window;
window.show();
return app.exec();
}
TestApp.h:
#ifndef TEST_APP_H
#define TEST_APP_H
#include <QtGui>
class TestApp : public QMainWindow
{
Q_OBJECT
public:
TestApp();
protected:
void resizeEvent(QResizeEvent* event);
QGraphicsView* view;
QGraphicsScene* scene;
};
#endif
TestApp.cpp:
#include "TestApp.h"
TestApp::TestApp()
: view(new QGraphicsView(this))
, scene(new QGraphicsScene(this))
{
resize(220, 220);
scene->setSceneRect(0, 0, 200, 200);
const int BOARD_WIDTH = 3;
const int BOARD_HEIGHT = 3;
const QPoint SQUARE_SIZE = QPoint(66, 66);
const int LINE_WIDTH = 10;
const int HALF_LINE_WIDTH = LINE_WIDTH / 2;
QBrush lineBrush = QBrush(Qt::black);
QPen linePen = QPen(lineBrush, LINE_WIDTH);
for(int x = 1; x < BOARD_WIDTH; ++x)
{
int x1 = x * SQUARE_SIZE.x();
scene->addLine(x1, HALF_LINE_WIDTH, x1, scene->height() - HALF_LINE_WIDTH, linePen);
}
for(int y = 1; y < BOARD_HEIGHT; ++y)
{
int y1 = y * SQUARE_SIZE.y();
scene->addLine(HALF_LINE_WIDTH, y1, scene->width() - HALF_LINE_WIDTH, y1, linePen);
}
view->setScene(scene);
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->show();
view->installEventFilter(this);
setCentralWidget(view);
}
void TestApp::resizeEvent(QResizeEvent* event)
{
view->fitInView(0, 0, scene->width(), scene->height());
QWidget::resizeEvent(event);
}
I've tried adding a call to fitInView at the end of TestApp's constructor, but it doesn't seem to do anything - resizeEvent seems to be called once at the start of the program's execution anyway.
Cheers.
Handle the view fit also inside the showEvent:
void TestApp::showEvent ( QShowEvent * event )
{
view->fitInView(0, 0, scene->width(), scene->height());
QWidget::showEvent(event);
}