I need to create a group box that supports HTML text when we set
MyGroupBox *gb = new MyGroupBox();
gb->setTitle("<u> This is underlined text</u>");
I have tried some searches but no results. In my head right now I think only about to set style for my groupbox. Something like this:
MyGroupBox.cpp
MyGroupBox::MyGroupBox( QWidget *p_parent ) : QGroupBox( p_parent )
{
setStyle( &m_style );
}
TitleStyle.hpp
class TitleStyle : public QProxyStyle
{
public:
TitleStyle() = default;
virtual void drawComplexControl( ComplexControl control, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget = nullptr ) const override;
};
TitleStyle.cpp
void TitleStyle::drawComplexControl( ComplexControl p_control, const QStyleOptionComplex *p_option, QPainter *p_painter, const QWidget *p_widget ) const
{
if ( p_control == CC_GroupBox )
{
if ( const QStyleOptionGroupBox *title = qstyleoption_cast<const QStyleOptionGroupBox *>( p_option ) )
{
QTextDocument td;
td.setHtml( title->text );
td.drawContents( p_painter );
}
}
else
{
QProxyStyle::drawComplexControl( p_control, p_option, p_painter, p_widget );
}
}
This still does not work. I know my drawComplexControl is weird, but that is what in my mind now. Can anyone tell me if I am going in the right direction? If yes, how could I change the class TitleStyle. If not, how could I do?
Solution:
It took me a while to find my mistake. With the code above, the title should be rich text supported already.
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QProxyStyle>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class TitleStyle : public QProxyStyle
{
public:
TitleStyle() = default;
virtual void drawComplexControl( ComplexControl control, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget = nullptr ) const override;
};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
TitleStyle m_style;
};
#endif // MAINWINDOW_H
MainWindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTextDocument>
void TitleStyle::drawComplexControl( ComplexControl p_control, const QStyleOptionComplex *p_option, QPainter *p_painter, const QWidget *p_widget ) const
{
if ( p_control == CC_GroupBox )
{
if ( const QStyleOptionGroupBox *title = qstyleoption_cast<const QStyleOptionGroupBox *>( p_option ) )
{
QTextDocument td;
td.setHtml( title->text );
td.drawContents( p_painter );
}
}
else
{
QProxyStyle::drawComplexControl( p_control, p_option, p_painter, p_widget );
}
}
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->groupBox->setStyle(&m_style);
ui->groupBox->setTitle("<b><u>This is an underlined title</u></b>");
}
MainWindow::~MainWindow()
{
delete ui;
}
Result:
Actually I have found my mistake in other positions. With this code, the title should be rich text supported already. I have tried with Qt Creator and it worked. I update my solution above.
Related
I would like my QGraphicsWidget to scale its size based on the size of the scene. The QGraphicsWidget I have currently is a fixed size depending on the return value of sizeHint (QGraphicsWidget is always 200 x 200). Attached below is minimal example:
MainWindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsView>
#include "RectangleWidget.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QGraphicsScene * m_scene;
QGraphicsView * m_view;
RectangleWidget * m_rectangleWidget;
};
#endif // MAINWINDOW_H
MainWindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_scene = new QGraphicsScene(this);
m_view = new QGraphicsView(m_scene, this);
m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
m_rectangleWidget = new RectangleWidget();
m_scene->addItem(m_rectangleWidget);
setCentralWidget(m_view);
}
MainWindow::~MainWindow()
{
delete ui;
}
RectangleWidget.h:
#ifndef RECTANGLEWIDGET_H
#define RECTANGLEWIDGET_H
#include <QGraphicsLinearLayout>
#include <QGraphicsWidget>
class RectangleWidget: public QGraphicsWidget
{
public:
RectangleWidget(QGraphicsWidget* parent = nullptr);
QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
void setGeometry(const QRectF &geom) override;
QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const override;
};
#endif // RECTANGLEWIDGET_H
RectangleWidget.cpp:
#include "rectanglewidget.h"
#include <QPainter>
RectangleWidget::RectangleWidget(QGraphicsWidget* parent)
{
}
void RectangleWidget::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option, QWidget *widget /*= 0*/)
{
Q_UNUSED(widget);
Q_UNUSED(option);
//Draw border
painter->drawRoundedRect(boundingRect(), 0.0, 0.0);
}
QRectF RectangleWidget::boundingRect() const
{
return QRectF(QPointF(0,0), geometry().size());
}
void RectangleWidget::setGeometry(const QRectF &geom)
{
prepareGeometryChange();
QGraphicsLayoutItem::setGeometry(geom);
setPos(geom.topLeft());
}
QSizeF RectangleWidget::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
{
switch (which) {
case Qt::MinimumSize:
return QSizeF(200, 200);
default:
break;
}
return constraint;
}
Any help on this would be appreciated.
Background
Your QGraphicsWidget have to be aware of two things:
When it is added to a scene
In order to do that you have to reimplement QGraphicsWidget::itemChange and look for a change of type QGraphicsItem::ItemSceneHasChanged.
When the size of this scene changes
This could be done by connecting a slot or a lambda function to the QGraphicsScene::sceneRectChanged signal.
Solution
Based on the given explanation, my solution would be the following:
In RectangleWidget.h after QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const override; add:
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
QSize m_rectSize;
In RectangleWidget.cpp change return QSizeF(200, 200); to return m_rectSize; and add at the end:
QVariant RectangleWidget::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == ItemSceneHasChanged) {
connect(value.value<QGraphicsScene *>(), &QGraphicsScene::sceneRectChanged, [this](const QRectF &rect){
m_rectSize.setWidth(rect.size().width());
m_rectSize.setHeight(rect.size().height());
});
}
return QGraphicsWidget::itemChange(change, value);
}
Finally, in MainWindow.cpp after m_scene->addItem(m_rectangleWidget); set the sceneRect as follows:
m_scene->setSceneRect(0, 0, 100, 400);
Note: The rectangle will respond to the changes of the scene, not the view. So if you resize the window, the rectangle will not be resized.
Adjustment
This will make the rectangle exactly the same size as the scene. If you want a different ratio, say 0.5, instead of m_rectSize.setWidth(rect.size().width()); write m_rectSize.setWidth(rect.size().width() / 2);, respectively m_rectSize.setHeight(rect.size().height() / 2);.
I want a widget in Qt that will act like a spreadsheet cell does. It can display text, then when the user double-clicks on it, it becomes editable. Once the user is done with editing and presses Enter, the text gets saved and the control is not editable anymore. If the user hits Escape while editing, then the control returns to its previous value.
One possible solution is sub-classing QWidget, QLabel and QLineEdit. Are there any other solutions available in Qt?
The following version also implements the same functionalities of your answer but instead of subclassing the QLineEdit and the QLabel only use eventFilter() and instead of managing the visibility manually let QStackedWidget do it.
#include <QApplication>
#include <QFormLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QStackedWidget>
#include <QVBoxLayout>
class MyEditableLabel: public QWidget{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
MyEditableLabel(QWidget *parent=nullptr):
QWidget(parent),
mLabel(new QLabel),
mLineEdit(new QLineEdit)
{
setLayout(new QVBoxLayout);
layout()->setMargin(0);
layout()->setSpacing(0);
layout()->addWidget(&stacked);
stacked.addWidget(mLabel);
stacked.addWidget(mLineEdit);
mLabel->installEventFilter(this);
mLineEdit->installEventFilter(this);
setSizePolicy(mLineEdit->sizePolicy());
connect(mLineEdit, &QLineEdit::textChanged, this, &MyEditableLabel::setText);
}
bool eventFilter(QObject *watched, QEvent *event){
if (watched == mLineEdit) {
if(event->type() == QEvent::KeyPress){
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if(keyEvent->key() == Qt::Key_Return ||
keyEvent->key() == Qt::Key_Escape ||
keyEvent->key() == Qt::Key_Enter)
{
mLabel->setText(mLineEdit->text());
stacked.setCurrentIndex(0);
}
}
else if (event->type() == QEvent::FocusOut) {
mLabel->setText(mLineEdit->text());
stacked.setCurrentIndex(0);
}
}
else if (watched == mLabel) {
if(event->type() == QEvent::MouseButtonDblClick){
stacked.setCurrentIndex(1);
mLineEdit->setText(mLabel->text());
mLineEdit->setFocus();
}
}
return QWidget::eventFilter(watched, event);
}
QString text() const{
return mText;
}
void setText(const QString &text){
if(text == mText)
return;
mText == text;
emit textChanged(mText);
}
signals:
void textChanged(const QString & text);
private:
QLabel *mLabel;
QLineEdit *mLineEdit;
QStackedWidget stacked;
QString mText;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QFormLayout *lay = new QFormLayout(&w);
MyEditableLabel el;
lay->addRow("MyEditableLabel: ", &el);
lay->addRow("QLineEdit: ", new QLineEdit);
w.show();
return a.exec();
}
#include "main.moc"
this solution is not as sexy but probably one of the more performant solutions available to you is to use a QInputdialog to change QLabel and override the mouseDoubleClickEvent to trigger the input dialog. I as some here have learned that there is no means to Pull edited text from a QLabel. Not without changing QLabels internal code. Here's an example using a QInputDialog as means.
//intrlbl.h
#ifndef INTRLBL_H
#define INTRLBL_H
#include <QWidget>
#include <QLabel>
#include <QMouseEvent>
class intrLbl: public QLabel
{
Q_OBJECT
public:
intrLbl(QWidget *parent);
void mouseDoubleClickEvent(QMouseEvent *event) override;
QString text;
};
#endif // INTRLBL_H
//intrlbl.cpp file
#include "intrlbl.h"
#include <QDebug>
#include <QInputDialog>
intrLbl::intrLbl(QWidget *parent)
{
this->setText("Text Changeable Via Double Click QInput Dialog");
this->setFocusPolicy(Qt::ClickFocus);
this->setWordWrap(false);
}
void intrLbl::mouseDoubleClickEvent(QMouseEvent *event)
{
QString title
= QInputDialog::getText(this,
tr("Enter your Idea Title:"),
tr("Title:"), QLineEdit::Normal,
tr("enter your title here"));
if(!title.isEmpty())
{
qDebug() << "Title set to:" << title;
this->setText(title);
}
else
{
title = "Title";
this->setText(title);
}
}
One of the solutions is to have a QLineEdit and set it to read-only and style it in a way that it will look like a label. I personally do not like this solution, because it's more of a hacking approach. I have come up with something that in my opinion is pretty cool, which includes sub-classing QWidget, QLabel and QLineEdit:
Let's first introduce a model, which will be created in the sub-classed version of our QWidget and this model will be passed to its child widgets, the sub-classed versions of QLabel and QLineEdit:
Model header - mymodel.h:
#ifndef MYMODEL_H
#define MYMODEL_H
#include <QObject>
class MyModel : public QObject {
Q_OBJECT
Q_PROPERTY(Mode mode READ getMode WRITE setMode NOTIFY modeChanged)
Q_PROPERTY(QString text READ getText WRITE setText NOTIFY textChanged)
public:
enum class Mode {
ReadOnly = 0,
Edit = 1,
};
explicit MyModel(QObject* parent = nullptr);
Mode getMode() const {
return _mode;
}
const QString& getText() const {
return _text;
}
signals:
void modeChanged(Mode mode);
void textChanged(const QString& text);
public slots:
void setMode(Mode mode);
void setText(const QString& text);
private:
Mode _mode;
QString _text;
};
#endif // MYMODEL_H
Model implementation - mymodel.cpp
#include "mymodel.h"
MyModel::MyModel(QObject *parent)
: QObject(parent)
, _mode(MyModel::Mode::ReadOnly)
, _text(QString()) {
}
void MyModel::setMode(MyModel::Mode mode) {
if (_mode != mode) {
_mode = mode;
emit modeChanged(_mode);
}
}
void MyModel::setText(const QString &text) {
if (_text != text) {
_text = text;
emit textChanged(text);
}
}
As we see the model has the text, which is common for both the QLabel and the QLineEdit, and it has a mode, which can be either read only or edit mode.
The label implementation is a sub-class of Label.
Header - mylabel.h:
#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>
#include <QSharedPointer>
#include "mymodel.h"
class MyLabel : public QLabel {
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = 0);
void setModel(QSharedPointer<MyModel> model);
protected:
void mouseDoubleClickEvent(QMouseEvent *) override;
private:
QSharedPointer<MyModel> _model;
};
#endif // MYLABEL_H
Implementation - mylabel.cpp:
#include "mylabel.h"
#include <QMouseEvent>
MyLabel::MyLabel(QWidget *parent)
: QLabel(parent) {
}
void MyLabel::setModel(QSharedPointer<MyModel> model) {
_model = model;
}
void MyLabel::mouseDoubleClickEvent(QMouseEvent *) {
_model->setText(text());
_model->setMode(MyModel::Mode::Edit);
}
As we our class MyLabel has a setModel() method, which will take the model from its parent. We are overriding the mouseDoubleClickEvent(), though which we are setting the text of the model to whatever text there is in the label, and setting the mode to edit, because when double-clicking we want to edit the text.
Now let's take a look at the QLineEdit. Our version of QLineEdit, called MyLineEdit, is listening to keyboard events and when Enter and Esc keys are pressed it either saves the text to the model, or discards it. Then it changes the mode to read-only.
MyLineEdit.h:
#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H
#include <QLineEdit>
#include <QSharedPointer>
#include "mymodel.h"
class MyLineEdit : public QLineEdit {
Q_OBJECT
public:
MyLineEdit(QWidget* parent = nullptr);
void setModel(QSharedPointer<MyModel> model);
protected:
void keyPressEvent(QKeyEvent* event) override;
void focusOutEvent(QFocusEvent*);
private:
QSharedPointer<MyModel> _model;
};
#endif // MYLINEEDIT_H
And here's the implementation - MyLineEdit.cpp:
#include "mylineedit.h"
#include <QKeyEvent>
MyLineEdit::MyLineEdit(QWidget *parent)
: QLineEdit(parent) {
}
void MyLineEdit::setModel(QSharedPointer<MyModel> model) {
_model = model;
}
void MyLineEdit::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Enter) {
_model->setText(text());
_model->setMode(MyModel::Mode::ReadOnly);
} else if (event->key() == Qt::Key_Escape) {
_model->setMode(MyModel::Mode::ReadOnly);
} else {
QLineEdit::keyPressEvent(event);
}
}
void MyLineEdit::focusOutEvent(QFocusEvent *) {
_model->setText(text());
_model->setMode(MyModel::Mode::ReadOnly);
}
So now we have the model, we have our version of QLabel and our version of QLineEdit. What we want now is a parent widget that will contain both of them, listen to signals from the model and change its appearance based on the signals. That class is derived from QWidget and is called MyEditableLabel:
MyEditableLabel.h:
#ifndef MYEDITABLELABEL_H
#define MYEDITABLELABEL_H
#include <QSharedPointer>
#include <QWidget>
#include "mylabel.h"
#include "mylineedit.h"
class MyEditableLabel : public QWidget {
Q_OBJECT
public:
explicit MyEditableLabel(QWidget *parent = nullptr);
QString getText() const {return _text;}
private:
MyLabel *_label;
MyLineEdit *_lineEdit;
QSharedPointer<MyModel> _model;
private slots:
void onModeChanged(MyModel::Mode mode);
void onTextChanged(const QString &text);
private:
QString _text;
};
#endif // MYEDITABLELABEL_H
MyEditableLabel.cpp:
#include "myeditablelabel.h"
#include <QHBoxLayout>
MyEditableLabel::MyEditableLabel(QWidget *parent)
: QWidget(parent) {
_model = QSharedPointer<MyModel>(new MyModel());
_model->setText("Click me!");
_label = new MyLabel(this);
_label->setModel(_model);
_lineEdit = new MyLineEdit(this);
_lineEdit->setModel(_model);
_lineEdit->setReadOnly(false);
QHBoxLayout *mainLayout = new QHBoxLayout();
mainLayout->setMargin(0);
mainLayout->setSpacing(0);
mainLayout->addWidget(_label);
mainLayout->addWidget(_lineEdit);
setLayout(mainLayout);
connect(_model.data(), &MyModel::modeChanged, this, &MyEditableLabel::onModeChanged);
onModeChanged(_model->getMode());
connect(_model.data(), &MyModel::textChanged, this, &MyEditableLabel::onTextChanged);
onTextChanged(_model->getText());
}
void MyEditableLabel::onModeChanged(MyModel::Mode mode) {
_lineEdit->setVisible(mode == MyModel::Mode::Edit);
_lineEdit->selectAll();
_label->setVisible(mode == MyModel::Mode::ReadOnly);
}
void MyEditableLabel::onTextChanged(const QString &text) {
_lineEdit->setText(text);
_label->setText(text);
_text = text;
}
Usage:
Using this is pretty straightforward. If you're using the Qt Creator designer, then you want to draw a QWidget and the right click on it and promote it to MyEditableLabel and you're done. If you're not using the Qt Creator designer then you just have to create and instance of MyEditableLabel and you're in business.
Improvements:
It probably is a better idea to not create the model in the constructor of MyEditableLabel, but outside of it and have a setModel method in MyEditableLabel.
I try to link the rubberBandChanged signal from QChartView class to a specific function in MainWindow class.
MainWindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
public slots:
void rubberZoomAdapt(QRect, QPointF, QPointF);
private:
Ui::MainWindow *ui;
QChartView* qcvChart;
Chart* chart;
};
MainWindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
qcvChart(new QChartView),
chart(new Chart)
{
ui->setupUi(this);
//Connexion
QObject::connect(qobject_cast<QGraphicsView*>(this->qcvChart),
&QGraphicsView::rubberBandChanged,
this,
&MainWindow::rubberZoomAdapt);
this->qcvChart->setChart(this->chart);
this->qcvChart->setRubberBand(QChartView::HorizontalRubberBand);
}
void MainWindow::rubberZoomAdapt(QRect r, QPointF fp, QPointF tp)
{
static int i = 0;
qDebug() << "(rubberZoomAdapt) RubberBand Event: " << QString::number(i++);
}
When I use the rubberBand in my chart, I never enter in the rubberZoomAdapt().
Any idee to fix this ?
Thanks.
The problem is that although QChartView inherits from QGraphicsView it does not use the same QRubberBand so the rubberBandChanged signal is never issued.
The solution is to look for the QRubberBand since it is a child of QChartView and filter it through the resizeEvent event, and then create our own signal:
*.h
...
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
bool eventFilter(QObject *watched, QEvent *event) override;
public slots:
void rubberZoomAdapt(QPointF fp, QPointF tp);
signals:
void rubberBandChanged(QPointF fp, QPointF tp);
private:
Ui::MainWindow *ui;
QChartView* qcvChart;
QChart* chart;
QRubberBand *rubberBand;
};
...
*.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
qcvChart(new QChartView),
chart(new QChart)
{
ui->setupUi(this);
qcvChart->setChart(chart);
qcvChart->setRubberBand(QChartView::HorizontalRubberBand);
rubberBand = qcvChart->findChild<QRubberBand *>();
rubberBand->installEventFilter(this);
connect(this, &MainWindow::rubberBandChanged,this, &MainWindow::rubberZoomAdapt);
setCentralWidget(qcvChart);
...
}
...
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
if(watched == rubberBand && event->type() == QEvent::Resize){
QPointF fp = chart->mapToValue(rubberBand->geometry().topLeft());
QPointF tp = chart->mapToValue(rubberBand->geometry().bottomRight());
emit rubberBandChanged(fp, tp);
}
return QMainWindow::eventFilter(watched, event);
}
void MainWindow::rubberZoomAdapt(QPointF fp, QPointF tp)
{
qDebug() << "(rubberZoomAdapt) RubberBand Event: "<<fp<<tp;
}
The complete example can be found in the following link
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;
}
I'm working on an application where I need to be able to draw a line between two QWidget objects. I have tried quite a few things, but my current attempt (which I think is in the right direction I just think I'm missing something) is to have the containing widget (which I called DrawWidget and which holds the QGridLayout that the QWidget objects are added to) override the paintEvent method and call the QPainter::drawLine() function.
The issues I'm having are that:
No matter how I try to get the position of the widgets, the endpoints of the line are always in the wrong place
Whenever I try to draw a second line, the first line that I drew gets erased.
Here is the paintEvent function of the containing widget:
void paintEvent(QPaintEvent *)
{
if (!drewSinceUpdate){
drewSinceUpdate = true;
QPainter painter(this);
painter.setPen(QPen(Qt::black));
painter.drawLine(start->geometry().center(), end->geometry().center());
}
}
I have tried many different ways to get the correct position of the widgets in the last line of paintEvent, which I will post some of the ways (I can't remember all of them):
painter.drawLine(start->pos(), end->pos());
painter.drawLine(start->mapToGlobal(start->geometry().center()), end->mapToGlobal(end->geometry().center()));
painter.drawLine(this->mapToGlobal(start->geometry().center()), this->mapToGlobal(end->geometry().center()));
painter.drawLine(start->mapTo(this, start->pos()), end->mapTo(this, end->pos()));
painter.drawLine(this->mapFrom(start, start->pos()), this->mapFrom(end, end->pos()));
And just to make my question clear, here is an example of what I am looking for, taken from QT Diagram Scene Example:
But this is what I end up getting:
Thank you for any help you can provide.
NOTE:
-start and end are both QWidget objects which I passed in using another method
-The hierarchy relevant to DrawWidget is:
QMainWindow
->QScrollArea
->DrawWidget
->QGridLayout
->Items <-- These are the things I want to connect
EDIT: To make a Complete and Verifiable example, here is the entirety of the relevant code.
MainWindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QTextEdit>
#include <QPushButton>
#include <QScrollBar>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
// Setting up the relevant hierarchy
ui->setupUi(this);
scrollArea = new QScrollArea();
setCentralWidget(scrollArea);
drawWidget = new DrawWidget();
gridLayout = new QGridLayout();
gridLayout->setSpacing(300);
drawWidget->setLayout(gridLayout);
scrollArea->setWidget(drawWidget);
scrollArea->setWidgetResizable(true);
AddItemSlot();
QApplication::connect(scrollArea->horizontalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(scrollHorizontal()));
}
// This is just creating a single one of the example widgets which I want to connect
QWidget* MainWindow::CreateNewItem(){
QWidget* itemWidget = new QWidget();
itemWidget->setStyleSheet("background-color: lightgray");
QHBoxLayout* singleItemLayout = new QHBoxLayout();
itemWidget->setLayout(singleItemLayout);
QTextEdit* textEdit = new QTextEdit(std::to_string(counter++).c_str());
textEdit->setStyleSheet("background-color:white;");
singleItemLayout->addWidget(textEdit);
QVBoxLayout* rightSidePanel = new QVBoxLayout();
rightSidePanel->setAlignment(Qt::AlignTop);
QPushButton* button1 = new QPushButton("Top Button");
QApplication::connect(button1, SIGNAL(clicked(bool)), this, SLOT(AddItemSlot()));
rightSidePanel->addWidget(button1);
QWidget* rightPanelWidget = new QWidget();
rightSidePanel->setMargin(0);
rightPanelWidget->setLayout(rightSidePanel);
singleItemLayout->addWidget(rightPanelWidget);
itemWidget->setLayout(singleItemLayout);
itemWidget->setMinimumWidth(400);
itemWidget->setFixedSize(400,200);
return itemWidget;
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::scrollHorizontal()
{
scrollArea->ensureWidgetVisible(noteItems.back());
}
void MainWindow::AddItemSlot()
{
QWidget* w = CreateNewItem();
gridLayout->addWidget(w,currRow, currCol++);
if (!noteItems.empty()){
drawWidget->updateEndpoints(noteItems.back(), w);
}
noteItems.push_back(w);
}
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QGridLayout>
#include <QWidget>
#include <QMainWindow>
#include <QScrollArea>
#include <drawwidget.h>
#include "drawscrollarea.h"
#include <vector>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
public slots:
void scrollHorizontal();
void AddItemSlot();
private:
Ui::MainWindow *ui;
QWidget* CreateNewItem();
int counter = 0, currCol = 0, currRow = 0;
std::vector<QWidget*> noteItems;
QScrollArea* scrollArea;
DrawWidget* drawWidget;
QGridLayout* gridLayout;
};
#endif // MAINWINDOW_H
DrawWidget.cpp:
#include "drawwidget.h"
#include <QDebug>
#include <QRect>
DrawWidget::DrawWidget(QWidget *parent) : QWidget(parent)
{
}
void DrawWidget::paintEvent(QPaintEvent *)
{
if (!drewSinceUpdate){
drewSinceUpdate = true;
QPainter painter(this);
painter.setPen(QPen(Qt::black));
for (ConnectedPair pair : items){
const QWidget* from = pair.from;
const QWidget* to =pair.to;
QPoint start = from->mapToGlobal(from->rect().topRight() + QPoint(0, from->height()/2));
QPoint end = to->mapToGlobal(to->rect().topLeft() + QPoint(0, to->height()/2));
painter.drawLine(mapFromGlobal(start), mapFromGlobal(end));
}
}
}
void DrawWidget::updateEndpoints(QWidget* startIn, QWidget* endIn){
drewSinceUpdate = false;
items.push_back(ConnectedPair{startIn, endIn});
}
DrawWidget.h
#ifndef DRAWWIDGET_H
#define DRAWWIDGET_H
#include <QWidget>
#include <QPainter>
#include <QtCore>
#include <vector>
class DrawWidget : public QWidget
{
Q_OBJECT
public:
explicit DrawWidget(QWidget *parent = nullptr);
void updateEndpoints(QWidget* startIn, QWidget* endIn);
virtual void paintEvent(QPaintEvent *);
signals:
private:
struct ConnectedPair {
const QWidget* from;
const QWidget* to;
};
std::vector<ConnectedPair> items;
bool drewSinceUpdate = true;
};
#endif // DRAWWIDGET_H
For this case we use the function mapToGlobal() and mapfromGlobal(), since pos() returns a position with respect to the parent and this can cause problems if the widget has different parents.
drawwidget.h
#ifndef DRAWWIDGET_H
#define DRAWWIDGET_H
#include <QWidget>
class DrawWidget : public QWidget
{
Q_OBJECT
public:
explicit DrawWidget(QWidget *parent = nullptr);
void addWidgets(const QWidget *from, const QWidget *to);
protected:
void paintEvent(QPaintEvent *);
private:
struct WidgetsConnected {
const QWidget* from;
const QWidget* to;
};
QList<WidgetsConnected> list;
};
#endif // DRAWWIDGET_H
drawwidget.cpp
#include "drawwidget.h"
#include <QPainter>
DrawWidget::DrawWidget(QWidget *parent) : QWidget(parent)
{
}
void DrawWidget::addWidgets(const QWidget * from, const QWidget * to)
{
list.append(WidgetsConnected{from , to});
update();
}
void DrawWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
for(const WidgetsConnected el: list){
const QWidget* from = el.from;
const QWidget* to = el.to;
QPoint start = from->mapToGlobal(from->rect().topRight() + QPoint(0, from->height()/2));
QPoint end = to->mapToGlobal(to->rect().topLeft() + QPoint(0, to->height()/2));
painter.drawLine(mapFromGlobal(start), mapFromGlobal(end));
}
}
The complete example can be found here.