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__
Related
.h
class myButton : public QPushButton
{
Q_OBJECT
public:
QPropertyAnimation* anim;
struct WidgetPos { int x = 0; int y = 0; int w = 0; int h = 0; };
WidgetPos wp;
void CreateAnimation(QByteArray propertyName)
{
if (propertyName == "geometry")
{
anim = new QPropertyAnimation(this, propertyName);
this->anim->setDuration(100);
this->anim->setEasingCurve(QEasingCurve::Linear);
this->wp.x = this->x();
this->wp.y = this->y();
this->wp.w = this->width();
this->wp.h = this->height();
}
}
myButton(QWidget* parent = 0) : QPushButton(parent) {}
bool eventFilter(QObject* obj, QEvent* event)
{
if (event->type() == QEvent::Enter)
{
if (!this->wp.x)
this->CreateAnimation("geometry");
this->anim->stop();
this->anim->setStartValue(
QRect(this->x(), this->y(), this->width(), this->height()));
this->anim->setEndValue(
QRect(this->x(), this->y(), (this->wp.w + 200) - this->width(), this->height()));
this->anim->start();
}
else if (event->type() == QEvent::Leave)
{
this->anim->stop();
this->anim->setStartValue(
QRect(this->x(), this->y(), (this->wp.w + 200) - this->width(), this->height()));
this->anim->setEndValue(
QRect(this->wp.x, this->wp.x, this->wp.w, this->wp.h));
this->anim->start();
}
return QWidget::eventFilter(obj, event);
}
};
.cpp
QtWidgetsApplication::QtWidgetsApplication(QWidget * parent)
: QMainWindow(parent)
{
ui.setupUi(this);
QPushButton* btn = new myButton(this);
btn->setGeometry(100, 100, 50, 40);
btn->setStyleSheet(R"(QPushButton {
background-image: url(:/tutorial.png);
background-repeat: no-repeat; }
)");
QLabel* labl = new QLabel(btn);
labl->setObjectName("label");
labl->setGeometry(32, 0, btn->width() + 32, btn->height());
labl->setText("Hello World");
labl->setAlignment(Qt::AlignCenter);
labl->show();
btn->installEventFilter(btn);
return;
}
So far what I did result on:
If I move the mouse on it so fast it becomes messy, and the "closing" animation <= isn't working.
I'm struggling with the calculation of the animation QRect and handling it when there's an animation already running.
The goal is to create a smooth animation effect similar to see in this gif:
I think the reason for the issue you are having is because when you are leaving the widget you set the start animation to the maximum width the button could take instead of starting it from the current width. I've implemented my own QPushButton subclass in the following way which seems to achieve the result you need. Instead of creating an event filter, I'll just override the enter and leave event. We'll also need to update the initial geometry every time the widget is moved or resized (outside of the animation), so I'm overriding the move and resize event as well.
// MyButton.h
class MyButton : public QPushButton
{
public:
MyButton(QWidget* parent = nullptr);
~MyButton() = default;
protected:
void enterEvent(QEvent *event) override;
void leaveEvent(QEvent* event) override;
void moveEvent(QMoveEvent *event) override;
void resizeEvent(QResizeEvent* event) override;
private:
QPropertyAnimation* m_animation;
QRect m_init_geometry;
double m_duration;
double m_extension;
};
Here is the implementation:
// MyButton.cpp
MyButton::MyButton(QWidget* parent)
: QPushButton(parent)
, m_animation(nullptr)
, m_init_geometry()
, m_duration(200)
, m_extension(100)
{
m_animation = new QPropertyAnimation(this, "geometry", this);
m_animation->setDuration(m_duration);
m_animation->setEasingCurve(QEasingCurve::Linear);
m_init_geometry = geometry();
}
void MyButton::enterEvent(QEvent *event)
{
QPushButton::enterEvent(event);
m_animation->stop();
// update the duration so that we get a uniform speed when triggering this animation midway
m_animation->setDuration(((m_init_geometry.width() + m_extension - width())/m_extension)*m_duration);
m_animation->setStartValue(geometry());
m_animation->setEndValue(QRectF(m_init_geometry.x(), m_init_geometry.y(), m_init_geometry.width() + m_extension, m_init_geometry.height()));
m_animation->start();
}
void MyButton::leaveEvent(QEvent *event)
{
QPushButton::leaveEvent(event);
m_animation->stop();
// update the duration so that we get a uniform speed when triggering this animation midway
m_animation->setDuration(((width() - m_init_geometry.width())/m_extension)*m_duration);
m_animation->setStartValue(geometry());
m_animation->setEndValue(m_init_geometry);
m_animation->start();
}
void MyButton::moveEvent(QMoveEvent *event)
{
// ignore the move event if it's due to the animation, otherwise store the new geometry
if(m_animation->state() == QPropertyAnimation::Running) return;
QPushButton::moveEvent(event);
m_init_geometry.setTopLeft(event->pos());
}
void MyButton::resizeEvent(QResizeEvent *event)
{
// ignore the move event if it's due to the animation, otherwise store the new geometry
if(m_animation->state() == QPropertyAnimation::Running) return;
QPushButton::resizeEvent(event);
m_init_geometry.setSize(event->size());
}
Notice that the start value of the closing animation is the current geometry and not the initial geometry plus the extended width. I'm updating reducing the duration of the opening animation linearly depending on how close the current width is to the full extended width; similarly for the closing animation. The rest now is very similar to your code:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
auto* btn = new MyButton(this);
btn->setGeometry(100, 100, 60, 80);
btn->setStyleSheet(R"(QPushButton {
background-image: url(:/ubuntu.png);
background-repeat: no-repeat;
background-origin: content;
background-position: left center;}
)");
auto* labl = new QLabel("Hello World", btn);
labl->setAlignment(Qt::AlignCenter);
labl->setGeometry(btn->width(), 0, labl->width(), btn->height());
}
The result looks like this
I try this way:
in mybutton.h
#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QLabel>
#include <QPushButton>
class MyButton : public QPushButton
{
Q_OBJECT
public:
MyButton(QWidget* parent = nullptr);
// QObject interface
public:
bool eventFilter(QObject *watched, QEvent *event);
signals:
void mouseEnter();
void mouseLeave();
private:
};
#endif // MYBUTTON_H
in mybutton.cpp
#include "mybutton.h"
#include <QEvent>
#include <QLabel>
MyButton::MyButton(QWidget *parent):
QPushButton(parent)
{
}
bool MyButton::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::HoverEnter)
{
emit mouseEnter();
}
else if (event->type() == QEvent::HoverLeave)
{
emit mouseLeave();
}
}
I use signal and in MainWindow class UI I add widget and layouts:
in mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QLabel *labl;
QLabel *labl2;
};
#endif // MAINWINDOW_H
in mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <mybutton.h>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
MyButton* btn = new MyButton(this);
btn->setObjectName("button 1 ");
btn->setText("btn 1");
btn->setGeometry(ui->widget->x(), 100, 50, 50);
ui->widget->layout()->addWidget(btn);
// QLabel
labl= new QLabel(btn,Qt::ToolTip);
connect(btn,&MyButton::mouseEnter,this,[btn,this](){
labl->setObjectName("label");
labl->setGeometry(btn->x()+this->x()+btn->width()+10, btn->y()+this->y()+btn->height()+15,
labl->width(), labl->height());
labl->setText("Hello World");
labl->setAlignment(Qt::AlignCenter);
labl->show();
});
connect(btn,&MyButton::mouseLeave,this,[this](){
labl->hide();
});
btn->installEventFilter(btn);
MyButton* btn2 = new MyButton(this);
btn2->setObjectName("button 2 ");
btn2->setText("btn 2");
btn2->setGeometry(ui->widget->x(), 100, 50, 50);
ui->widget->layout()->addWidget(btn2);
// QLabel
labl2= new QLabel(btn2,Qt::ToolTip);
connect(btn2,&MyButton::mouseEnter,this,[btn2,this](){
labl2->setObjectName("label");
labl2->setGeometry(btn2->x()+this->x()+btn2->width()+10, btn2->y()+this->y()+btn2->height()+15,
labl2->width(), labl2->height());
labl2->setText("Hello World 2");
labl2->setAlignment(Qt::AlignCenter);
labl2->show();
});
connect(btn2,&MyButton::mouseLeave,this,[this](){
labl2->hide();
});
btn->installEventFilter(btn);
btn2->installEventFilter(btn2);
}
MainWindow::~MainWindow()
{
delete ui;
}
and this is my result:
By using the same color in the stylesheet you can have what you show in your Gif.
You can try this approach:
Calculate the start and end rect only once and set in the animation object.
On enter you start the animation as before.
On leave you can change the direction of the animation from play forward to backward.
You can run the animation backwards with
this->anim->setDirection( QAbstractAnimation::Backward );
I don't know if you must surround it with
this->anim->pause();
//[...]
this->anim->resume();
Maybe you must experience with it a little.
Also, maybe you must keep track of
1.) Did you start a animation in forward and backward already for not start it twice or even more often, e.g. use an enum to safe state in a member:
enum class eState { Stopped, Forward, Backward };
2.) Test if the animation is still running or finished already for eventually start a new animation in either forward or backward direction, e.g. test with
this->anim->currentTime() < this->anim->totalDuration();
// or just query the state
this->anim->state() == QAbstractAnimation::Stopped;
I hope I could help you to solve your problem.
EDIT My point 1.) you can also solve with the methods of the animation class by testing:
this->anim->direction(); // and...
this->anim->state();
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.
I am trying to design something like a timeline view for my video player. I decided to use QTableWidget as the timeline since it suits my purpose. My widget looks like this:
I want the green line to run through the widget when i click on play. Here is my MVCE example:
//View.cpp
View::View(QWidget* parent) : QGraphicsView(parent)
{
QGraphicsScene* scene = new QGraphicsScene(this);
TableWidget* wgt = new TableWidget;
scene->addWidget(wgt);
QGraphicsLineItem* item = new QGraphicsLineItem(30, 12, 30, wgt->height() - 9);
item->setPen(QPen(QBrush(Qt::green), 3));
item->setFlags(QGraphicsItem::ItemIsMovable);
scene->addItem(item);
setScene(scene);
}
Here is TableWidget
TableWidget::TableWidget(QWidget* parent) : QTableWidget(parent)
{
setColumnCount(10);
setRowCount(10);
//Hides the numbers on the left side of the table
verticalHeader()->hide();
//Prevents top header from highlighting on selection
horizontalHeader()->setHighlightSections(false);
//Makes the cells un-editable
setEditTriggers(QAbstractItemView::NoEditTriggers);
setSelectionMode(QAbstractItemView::MultiSelection);
}
Problem:
Moving the line item reflects changes to the scene it has been added to i.e. when i drag the line using mouse, the line moves in the scene but not inside the TableWidget.
What do i want
I want the green bar to act like a horizontal slider. It should go through the TableWidget horizontally making the widget scroll along with it showing the current position of the frame indicated by the numbers shown on the header.
Something like as shown below (notice the Red line):
I know this might not be the best way to implement a timeline but i would appreciate any other ideas to implement.
A possible solution is to overwrite the itemChange method to restrict movement as shown below:
#include <QApplication>
#include <QGraphicsRectItem>
#include <QGraphicsView>
#include <QTableWidget>
#include <QHeaderView>
#include <QGraphicsProxyWidget>
class SeekBarItem: public QGraphicsRectItem{
public:
SeekBarItem(QRectF rect, QGraphicsItem *parent=nullptr)
: QGraphicsRectItem(rect, parent)
{
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
setBrush(Qt::red);
}
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant &value){
if(change == QGraphicsItem::ItemPositionChange){
QPointF p = value.toPointF();
qreal max = parentItem()->boundingRect().bottom()- boundingRect().bottom();
qreal min = parentItem()->boundingRect().top()-boundingRect().top();
if(p.y() > max) p.setY(max);
else if (p.y() < min) p.setY(min);
p.setX(pos().x());
return p;
}
return QGraphicsRectItem::itemChange(change, value);
}
};
class TableWidget: public QTableWidget
{
public:
TableWidget(QWidget* parent=nullptr) : QTableWidget(10, 10, parent)
{
verticalHeader()->hide();
horizontalHeader()->setHighlightSections(false);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setSelectionMode(QAbstractItemView::MultiSelection);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
QGraphicsProxyWidget *proxy = scene->addWidget(new TableWidget);
QGraphicsRectItem *it = new QGraphicsRectItem(QRectF(0, 0, 10, proxy->boundingRect().height()), proxy);
it->setBrush(Qt::green);
SeekBarItem *seekBarItem = new SeekBarItem(QRectF(-5, 0, 20, 50));
seekBarItem->setParentItem(it);
view.resize(640, 480);
view.show();
return a.exec();
}
Update:
#include <QApplication>
#include <QGraphicsRectItem>
#include <QGraphicsView>
#include <QTableWidget>
#include <QHeaderView>
#include <QGraphicsProxyWidget>
#include <QScrollBar>
class TableWidget: public QTableWidget
{
public:
TableWidget(QWidget* parent=nullptr) : QTableWidget(10, 10, parent)
{
verticalHeader()->hide();
horizontalHeader()->setHighlightSections(false);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setSelectionMode(QAbstractItemView::MultiSelection);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
}
};
class SeekBarItem: public QGraphicsRectItem{
public:
SeekBarItem(int width, QAbstractItemView *view, QGraphicsScene *scene)
: QGraphicsRectItem(nullptr),
proxy(new QGraphicsProxyWidget()),
m_view(view)
{
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();
}
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant &value){
if(change == QGraphicsItem::ItemPositionChange){
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;
}
return QGraphicsRectItem::itemChange(change, value);
}
private:
QGraphicsProxyWidget *proxy;
QAbstractItemView *m_view;
QScrollBar *scrollbar;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
TableWidget *table = new TableWidget;
SeekBarItem *seekBarItem = new SeekBarItem(15, table, scene);
view.resize(640, 480);
view.show();
return a.exec();
}
I am learning Qt for fun. And I got a question:
How could I drag and drop the QLabel in Qt among two different windows?
Here is what I have so far:
As you can tell from the .gif(which does not want to become downloaded and visible here for some reasons, but if you click on the link to it, you can clearly see it) provided above right now there are two main problems:
I can not move the QLabel outside of the window (and hence am not able to register the drag and drop event).
The label is flashing for some reasons when I am moving it.
Here is the relevant part of the implementation from the .gif:
#ifndef DRAGGERP_H
#define DRAGGERP_H
#include <QLabel>
#include <QApplication>
#include <QMouseEvent>
#include <QPoint>
class DraggerP : public QLabel
{
QPoint offset;
QPoint startingPosition;
public:
DraggerP(QWidget* parent = nullptr) : QLabel(parent){ }
protected:
void enterEvent(QEvent* event) override
{
QApplication::setOverrideCursor(Qt::PointingHandCursor);
}
void leaveEvent(QEvent* event) override
{
QApplication::restoreOverrideCursor();
}
void mousePressEvent(QMouseEvent* event) override
{
startingPosition = pos();
offset = QPoint(
event->pos().x() - pos().x() + 0.5*width(),
event->pos().y() - pos().y() + 0.5*height()
);
}
void mouseMoveEvent(QMouseEvent* event) override
{
move(event->pos() + offset);
}
void mouseReleaseEvent(QMouseEvent* event) override
{
move(startingPosition);
}
};
#endif // DRAGGERP_H
This is the extension of the QLabel I am using to create the drag and drop effect.
I do not need the whole solution, at least an idea of how to accomplish this and what am I doing wrong here.
Here is a pretty good example and I used it as a starting point.
That strange movement that the QLabel suffers is because the position of the QLabel now depends on the layout, the job of the layout is to establish the position of the widgets depending on the policies you establish.
The solution is not to implement those actions in the QLabel but in the MainWindow as I show below:
#include <QApplication>
#include <QLabel>
#include <QMainWindow>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QTime>
#include <QDrag>
#include <QMimeData>
#include <QMouseEvent>
class MainWindow: public QMainWindow {
QScrollArea scrollArea;
QWidget contentWidget;
QVBoxLayout lay;
public:
MainWindow(QWidget* parent=nullptr): QMainWindow(parent){
qsrand((uint) QTime::currentTime().msec());
setCentralWidget(&scrollArea);
scrollArea.setWidget(&contentWidget);
contentWidget.setLayout(&lay);
scrollArea.setWidgetResizable(true);
for(int i=0; i< 20; i++){
QLabel *label = new QLabel(QString("label %1").arg(i));
QPalette pal = label->palette();
pal.setColor(QPalette::Background, QColor(10 +qrand() % 240, 10 +qrand() % 240, 10 +qrand() % 240));
label->setAutoFillBackground(true);
label->setPalette(pal);
lay.addWidget(label);
}
setAcceptDrops(true);
}
protected:
void mousePressEvent(QMouseEvent *event){
QMainWindow::mousePressEvent(event);
QWidget *child = childAt(event->pos());
if(qobject_cast<QLabel *>(child))
createDrag(event->pos(), child);
}
void dropEvent(QDropEvent *event){
QByteArray byteArray = event->mimeData()->data("Label");
QWidget * widget = *reinterpret_cast<QWidget**>(byteArray.data());
QLabel * new_label = qobject_cast<QLabel *>(widget);
QWidget *current_children = childAt(event->pos());
QLabel * current_label = qobject_cast<QLabel*>(current_children);
int index = 0;
if(new_label){
if(current_label)
index = lay.indexOf(current_label);
else{
index = 0;
QLayoutItem *item = lay.itemAt(index);
while(item->widget()->pos().y() < event->pos().y() && item)
item = lay.itemAt(index++);
}
lay.insertWidget(index, new_label);
}
}
private:
void createDrag(const QPoint &pos, QWidget *widget){
if(widget == Q_NULLPTR)
return;
QByteArray byteArray(reinterpret_cast<char*>(&widget),sizeof(QWidget*));
QDrag *drag = new QDrag(this);
QMimeData * mimeData = new QMimeData;
mimeData->setData("Label",byteArray);
drag->setMimeData(mimeData);
QPoint globalPos = mapToGlobal(pos);
QPoint p = widget->mapFromGlobal(globalPos);
drag->setHotSpot(p);
drag->setPixmap(widget->grab());
drag->exec(Qt::CopyAction | Qt::MoveAction);
}
protected:
void dragEnterEvent(QDragEnterEvent *event){
if(event->mimeData()->hasFormat("Label"))
event->acceptProposedAction();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w1;
MainWindow w2;
w1.show();
w2.show();
return a.exec();
}
in my application I try to connect nodes with lines. I use a QGraphicsView with a QGraphicsScene and my own QGraphicsItems. Now if I click on an item I want to draw a line to another node. To give a visual feedback, the goal should change color if the mouse hovers over the goal. The basics works so far, but my problem is that if I drag a line with the mouse (via mouseMoveEvent), I do not get any hoverEvents any more. I replicated the behaviour with this code:
Header File:
#pragma once
#include <QtWidgets/Qwidget>
#include <QGraphicsItem>
#include <QGraphicsScene>
class HaggiLearnsQt : public QWidget
{
Q_OBJECT
public:
HaggiLearnsQt(QWidget *parent = Q_NULLPTR);
};
class MyScene : public QGraphicsScene
{
public:
MyScene(QObject* parent = 0);
void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent);
};
class MyItem : public QGraphicsItem
{
public:
MyItem(QGraphicsItem* parent = Q_NULLPTR);
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event);
bool mouseOverItem;
};
Implementation:
#include "HaggiLearnsQt.h"
#include <QMessageBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QGraphicsView>
MyScene::MyScene(QObject* parent)
{}
void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
QGraphicsScene::mouseMoveEvent(mouseEvent);
}
MyItem::MyItem(QGraphicsItem* parent) : mouseOverItem(false)
{
setAcceptHoverEvents(true);
}
QRectF MyItem::boundingRect() const
{
return QRectF(-50, -50, 50, 50);
}
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
QBrush b = QBrush(Qt::black);
if(mouseOverItem)
b = QBrush(Qt::yellow);
painter->setBrush(b);
painter->drawRect(boundingRect());
}
void MyItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
mouseOverItem = true;
QGraphicsItem::hoverEnterEvent(event);
}
void MyItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
mouseOverItem = false;
QGraphicsItem::hoverLeaveEvent(event);
}
HaggiLearnsQt::HaggiLearnsQt(QWidget *parent)
: QWidget(parent)
{
QHBoxLayout* layout = new QHBoxLayout(this);
MyScene* graphicsScene = new MyScene();
QGraphicsView* graphicsView = new QGraphicsView();
graphicsView->setRenderHint(QPainter::RenderHint::Antialiasing, true);
graphicsView->setScene(graphicsScene);
layout->addWidget(graphicsView);
graphicsView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
graphicsView->setMinimumHeight(200);
graphicsView->setMinimumWidth(200);
graphicsView->setStyleSheet("background-color : gray");
MyItem* myitem = new MyItem();
myitem->setPos(50, 50);
graphicsScene->addItem(myitem);
}
And the default main.cpp:
#include "HaggiLearnsQt.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
HaggiLearnsQt w;
w.show();
return a.exec();
}
If you run the code, a box appears in the middle of the window. If you hover over the box, it changes color. Now try to klick outside the box and drag wiht pressed button into the box. The box does not receive a hover and does not change color.
So my question is: Can I somehow change the item while I move the mouse with a pressed button?
You can get the hovered item passing mouseEvent->scenePos() to the QGraphicsScene::itemAt method inside the scene mouse move event handler.
Have a pointer to a MyItem instance, in MyScene:
class MyScene : public QGraphicsScene
{
MyItem * hovered;
//...
initialize it to zero in MyScene constructor:
MyScene::MyScene(QObject* parent)
{
hovered = 0;
}
then use it to track the current highlighted item (if there's one):
void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
if(mouseEvent->buttons())
{
QGraphicsItem * item = itemAt(mouseEvent->scenePos(), QTransform());
MyItem * my = dynamic_cast<MyItem*>(item);
if(my != 0)
{
qDebug() << mouseEvent->scenePos();
if(!my->mouseOverItem)
{
my->mouseOverItem = true;
my->update();
hovered = my;
}
}
else
{
if(hovered != 0)
{
hovered->mouseOverItem = false;
hovered->update();
hovered = 0;
}
}
}
QGraphicsScene::mouseMoveEvent(mouseEvent);
}
The line if(mouseEvent->buttons()) at the beginning prevents the check to be performed if no mouse button is held.
Don't forget to initialize mouseOverItem to false in MyItem constructor:
MyItem::MyItem(QGraphicsItem* parent) : mouseOverItem(false)
{
setAcceptHoverEvents(true);
mouseOverItem = false;
}