I am trying to solve a graphics problem using the latest version of Qt. The image below shows what I managed to get so far and I will use it to explain the expected result.
I'm using a main vertical layout and within the biggest widget of the layout there is a horizontal layout with only one child: the square widget. The expected behavior would be of course to have the square widget centered horizontally and taking up the biggest space available. It is not required to use the same layout configuration, but the look of the interface should be the same.
The image above has been obtained by setting a QSizePolicy of minimumExpanding for both vertical and horizontal to the square widget and by forcing it to be square with the following code:
void SquareWidget::resizeEvent(QResizeEvent *event) {
//This is an override to the QWidget method
QSize s = size();
if (s.height()<s.width()) {
resize(s.height(), s.height());
} else {
resize(s.width(), s.width());
}
return;
}
While trying to solve this problem I went through the documentation some of the answers on this website and I couldn't find a clear answer about how to do two tasks.
First problem: how to make the widget square and keep its aspect ratio?
In this question
it is said that the method heightForWidth () doesn't work in newer versions of qt, and after a test it doesn't work for me either. The above override of resizeEvent, on the other hand, causes recursion because there are calls to resize() (and as far as I understand the layout should handle the resizing).
Second problem: how to center the square?
I tried using the layout alignment properties (center horizontally and vertically) but they cause the widget size to be immutable.
Maybe I am not understanding something about how Qt handles the widget placement. Any suggestion or clarification will be greatly appreciated.
You can do it with a QGridLayout.
Please see the attached code.
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class MyWidget final : public QWidget
{
Q_OBJECT
protected:
virtual void resizeEvent(QResizeEvent * event) override;
};
class MainWindow final : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow() = default;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include <QGridLayout>
#include <QLabel>
#include <QResizeEvent>
#include <QSpacerItem>
void MyWidget::resizeEvent(QResizeEvent * event)
{
event->accept();
const QSize current_size = size();
const int min = std::min(current_size.width(), current_size.height());
resize(min, min);
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
auto main_widget = new QWidget;
auto header = new QLabel("Hello World");
auto center_widget = new MyWidget;
auto footer = new QLabel("Good bye World");
auto spacer_left = new QSpacerItem(10, 10, QSizePolicy::Expanding);
auto spacer_right = new QSpacerItem(10, 10, QSizePolicy::Expanding);
auto grid_layout = new QGridLayout(main_widget);
auto center_palette = center_widget->palette();
center_palette.setColor(QPalette::Background, Qt::blue);
center_widget->setAutoFillBackground(true);
center_widget->setPalette(center_palette);
grid_layout->addWidget(header, 0, 1);
grid_layout->addItem(spacer_left, 1, 0);
grid_layout->addWidget(center_widget, 1, 1);
grid_layout->addItem(spacer_right, 1, 2);
grid_layout->addWidget(footer, 2, 1);
header->setAlignment(Qt::AlignCenter);
footer->setAlignment(Qt::AlignCenter);
setCentralWidget(main_widget);
}
Please see the result here
Rather than trying to get the widget to keep itself square and centred it might be simpler to reparent it and put the required logic in the parent widget type.
So, something like...
class keep_child_square_and_centred: public QWidget {
using super = QWidget;
public:
explicit keep_child_square_and_centred (QWidget *parent = nullptr)
: super(parent)
, m_widget(nullptr)
{}
virtual void set_widget (QWidget *widget)
{
if ((m_widget = widget))
m_widget->setParent(this);
}
virtual QSize sizeHint () const override
{
return(m_widget ? m_widget->sizeHint() : super::sizeHint());
}
protected:
virtual void resizeEvent (QResizeEvent *event) override
{
super::resizeEvent(event);
fixup();
}
private:
void fixup ()
{
if (m_widget) {
QRect r(QPoint(), QSize(height(), height()));
r.moveCenter(rect().center());
m_widget->setGeometry(r);
}
}
QWidget *m_widget;
};
Then use as...
keep_child_square_and_centred w;
SquareWidget sq;
w.set_widget(&sq);
You may still need to play around with a few settings if the parent is in a layout though.
Related
I've been noticing an odd behavior with QSlider, which can be reproduced whenever slider widgets have different width, eg. being resized. Is it a problem with my signals? if so, how would I go about updating slider2 when slider1 has been updated, without affecting the width of the slider?
Visual:
The problem:
Changing the width of slider1, repaints slider2 to look the same, but repositioning slider2 manually will show that the length hasn't been changed, making it look like a glitch. Any ideas what's going on here or if it's a bug?
Expected behavior:
The sliders should not have any connection to each other regarding their layout, the only change I'd expect to see, is the knob being moved the corresponding position on the resized widget, not the exact same position.
Actual behavior:
The knob moves to the exact same position on the widget it signals to, instead of an offset position that should match its new width.
The following example was compiled using:
Qt 5.12.3 on Mac OS 10.14.5
Example
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QHBoxLayout>
#include <QSlider>
#include <QMainWindow>
#include <QDebug>
#include <QDockWidget>
class DockSliderWidget : public QDockWidget
{
Q_OBJECT
public:
DockSliderWidget(QWidget* parent = nullptr)
: QDockWidget(parent)
{
QWidget* container = new QWidget(this);
QHBoxLayout* hBoxLayout = new QHBoxLayout();
mSlider = new QSlider(Qt::Horizontal, this);
mSlider->setMinimum(0);
mSlider->setMaximum(100);
connect(mSlider, &QSlider::valueChanged, this, &DockSliderWidget::valueChanged);
hBoxLayout->addWidget(mSlider);
container->setLayout(hBoxLayout);
setWidget(container);
}
void changeValue(qreal value)
{
qDebug() << value;
QSignalBlocker b(mSlider);
mSlider->setValue(value);
}
Q_SIGNALS:
void valueChanged(qreal value);
private:
QSlider* mSlider = nullptr;
};
class DockSliderWidget;
class Widget : public QMainWindow
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr)
: QMainWindow(parent)
{
sliderWidget1 = new DockSliderWidget(parent);
sliderWidget2 = new DockSliderWidget(parent);
connect(sliderWidget1, &DockSliderWidget::valueChanged, sliderWidget2, &DockSliderWidget::changeValue);
connect(sliderWidget2, &DockSliderWidget::valueChanged, sliderWidget1, &DockSliderWidget::changeValue);
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, sliderWidget1);
addDockWidget(Qt::DockWidgetArea::LeftDockWidgetArea, sliderWidget2);
}
~Widget()
{
}
private:
DockSliderWidget* sliderWidget1;
DockSliderWidget* sliderWidget2;
};
#endif // WIDGET_H
I need to implement a QFrame that has a title (see image). However, after reading QFrame's documentation and trying to re-implement the paintEvent(QPaintEvent*) method, I failed to find any solution.
I was wondering if any of you could provide a small exemple demonstrating how I can achieve somthing like this:
Thank you!
As an alternative to creating your own compound widget you might be able to use the contents margins to fake a title bar...
#include <QFont>
#include <QFrame>
#include <QPainter>
class titled_frame: public QFrame {
using super = QFrame;
public:
explicit titled_frame (const QString &title = "A Title Here", QWidget *parent = nullptr)
: super(parent)
, m_title(title)
{
/*
* Set the top margin based on the font height.
*/
setContentsMargins(0, 2 * fontInfo().pixelSize(), 0, 0);
}
protected:
virtual void paintEvent (QPaintEvent *event) override
{
/*
* Draw the title centred in the top margin.
*/
QPainter painter(this);
QRect title_rect(QPoint(0, 0), QSize(width(), contentsMargins().top()));
painter.fillRect(title_rect, Qt::blue);
painter.setPen(Qt::black);
painter.drawText(title_rect, Qt::AlignCenter, m_title);
/*
* Defer to the base class implementation to update everything else.
*/
super::paintEvent(event);
}
private:
QString m_title;
};
Then use as...
titled_frame tf("A Title Here");
auto *layout = new QVBoxLayout(&tf);
layout->addWidget(new QLabel("Any QLayout or QWidget here..."));
tf.show();
I am trying to create a round button by subclassing and setting the region mask so that I can reuse it in my project. I know we can override paintEvent method and draw a circle to show it as a round button. But the problem with this approach is that if user clicks outside the circle (but within button rect) it will be treated as a button click. This problem we don't see when set the region mask.
I tried to set the region by calling setmask method inside resizeEvent/paintEvent. In either of case, button will be blank. I am trying to figure out the place inside the subclass to set the region mask.
RoundAnimatingButton.h ->
#include <QPushButton>
namespace Ui {
class CRoundAnimatingBtn;
}
class CRoundAnimatingBtn : public QPushButton
{
Q_OBJECT
public:
explicit CRoundAnimatingBtn(QWidget *parent = nullptr);
~CRoundAnimatingBtn();
void StartAnimation(QColor r);
void StopAnimation();
public slots:
void timerEvent(QTimerEvent *e);
private:
Ui::CRoundAnimatingBtn *ui;
bool m_Spinning;
// QWidget interface
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent * e) override;
};
#endif // ROUNDANIMATINGBTN_H
RoundAnimatingButton.cpp
CRoundAnimatingBtn::CRoundAnimatingBtn(QWidget *parent)
: QPushButton (parent)
, ui(new Ui::CRoundAnimatingBtn)
, m_Spinning(false)
{
ui->setupUi(this);
}
CRoundAnimatingBtn::~CRoundAnimatingBtn()
{
delete ui;
}
void CRoundAnimatingBtn::paintEvent(QPaintEvent *e)
{
QPushButton::paintEvent(e);
if(m_Spinning)
{
// Animating code
}
}
void CRoundAnimatingBtn::StartAnimation(QColor r)
{
m_Spinning=true;
startTimer(5);
}
void CRoundAnimatingBtn::StopAnimation()
{
m_Spinning=false;
this->update();
}
void CRoundAnimatingBtn::timerEvent(QTimerEvent *e)
{
if(m_Spinning)
this->update();
else
killTimer(e->timerId());
}
void CRoundAnimatingBtn::DrawRing()
{
}
void CRoundAnimatingBtn::resizeEvent(QResizeEvent *event)
{
// -----------------------------------
// This code didn't work
// -----------------------------------
QRect rect = this->geometry();
QRegion region(rect, QRegion::Ellipse);
qDebug() << "PaintEvent Reound button - " << region.boundingRect().size();
this->setMask(region);
// ----------------------------------
// ------------------------------------
// This code worked
// -------------------------------------
int side = qMin(width(), height());
QRegion maskedRegion(width() / 2 - side / 2, height() / 2 - side / 2, side,
side, QRegion::Ellipse);
setMask(maskedRegion);
}
Qt doc. provides a sample for “non-rectangular” widgets – Shaped Clock Example.
(Un-)Fortunately, I remembered this not before I got my own sample running.
I started in Qt doc. with
void QWidget::setMask(const QBitmap &bitmap)
Causes only the pixels of the widget for which bitmap has a corresponding 1 bit to be visible. If the region includes pixels outside the rect() of the widget, window system controls in that area may or may not be visible, depending on the platform.
Note that this effect can be slow if the region is particularly complex.
The following code shows how an image with an alpha channel can be used to generate a mask for a widget:
QLabel topLevelLabel;
QPixmap pixmap(":/images/tux.png");
topLevelLabel.setPixmap(pixmap);
topLevelLabel.setMask(pixmap.mask());
The label shown by this code is masked using the image it contains, giving the appearance that an irregularly-shaped image is being drawn directly onto the screen.
Masked widgets receive mouse events only on their visible portions.
See also mask(), clearMask(), windowOpacity(), and Shaped Clock Example.
(When reading this, I still missed the link to example.)
At first, I prepared a suitable pixmap for my purpose – dialog-error.png:
for which I converted an SVG from one of my applications.
I tried to apply it to a QPushButton as icon and as mask. This looked very strange. I'm not quite sure what exactly was the problem:
- using the resp. QPushButton as toplevel widget (i.e. main window)
- the fact that QPushButtons icon rendering and the mask may not match concerning position or size.
Without digging deeper, I changed the code and fixed both issues in next try:
making a derived button (like described by OP)
using the button as non-toplevel widget.
This worked soon. I added some code to make the effect more obvious:
a mouse press event handler for main window to show whether shape is considered correctly
a signal handler to show whether clicks on button (in shape) are received correctly.
So, I came to the following sample – testQPushButtonMask.cc:
#include <QtWidgets>
class MainWindow: public QWidget {
public:
explicit MainWindow(QWidget *pQParent = nullptr):
QWidget(pQParent)
{ }
virtual ~MainWindow() = default;
MainWindow(const MainWindow&) = delete;
MainWindow& operator=(const MainWindow&) = delete;
protected:
virtual void mousePressEvent(QMouseEvent *pQEvent) override;
};
void MainWindow::mousePressEvent(QMouseEvent *pQEvent)
{
qDebug() << "MainWindow::mousePressEvent:" << pQEvent->pos();
QWidget::mousePressEvent(pQEvent);
}
class RoundButton: public QPushButton {
private:
QPixmap _qPixmap;
public:
RoundButton(const QPixmap &qPixmap, QWidget *pQParent = nullptr):
QPushButton(pQParent),
_qPixmap(qPixmap)
{
setMask(_qPixmap.mask());
}
virtual ~RoundButton() = default;
RoundButton(const RoundButton&) = delete;
RoundButton& operator=(const RoundButton&) = delete;
virtual QSize sizeHint() const override;
protected:
virtual void paintEvent(QPaintEvent *pQEvent) override;
};
QSize RoundButton::sizeHint() const { return _qPixmap.size(); }
void RoundButton::paintEvent(QPaintEvent*)
{
QPainter qPainter(this);
const int xy = isDown() * -2;
qPainter.drawPixmap(xy, xy, _qPixmap);
}
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
QPixmap qPixmap("./dialog-error.png");
// setup GUI
MainWindow qWin;
qWin.setWindowTitle(QString::fromUtf8("QPushButton with Mask"));
QVBoxLayout qVBox;
RoundButton qBtn(qPixmap);
qVBox.addWidget(&qBtn);
qWin.setLayout(&qVBox);
qWin.show();
// install signal handlers
QObject::connect(&qBtn, &RoundButton::clicked,
[](bool) { qDebug() << "RoundButton::clicked()"; });
// runtime loop
return app.exec();
}
The corresponding Qt project file testQPushButtonMask.pro
SOURCES = testQPushButtonMask.cc
QT += widgets
Compiled and tested on cygwin64:
$ qmake-qt5 testQPushButtonMask.pro
$ make && ./testQPushButtonMask
Qt Version: 5.9.4
MainWindow::mousePressEvent: QPoint(23,22)
MainWindow::mousePressEvent: QPoint(62,24)
MainWindow::mousePressEvent: QPoint(62,61)
MainWindow::mousePressEvent: QPoint(22,60)
RoundButton::clicked()
Concerning the output:
I clicked into the four corners of button.
I clicked on the center of button.
I'm creating a marquee selection type tool in a QWidget and it all works fine, except for a rendering bug when dragging the marquee. If I call update without any arguments everything works beautifully, but if I call update to only include the region of the marquee as the user is dragging, then two of the edges get cutoff if the mouse is moving at a moderate speed.
Here's an image of what it looks like while dragging towards the lower right corner:
Screenshot while clicking and dragging in the widget
I thought replacing update() with repaint() might fix it but that didn't work either.
What is the correct way that I should be doing this? I've included some very basic code that demonstrates the problem.
#include <QPainter>
#include <QPaintEvent>
#include <QWidget>
class Widget : public QWidget
{
public:
Widget(QWidget *parent = nullptr)
: QWidget(parent)
{
}
void mousePressEvent(QMouseEvent *e) override
{
startPt = e->pos();
rect = QRect();
update(); // clear the entire widget
}
void mouseMoveEvent(QMouseEvent *e) override
{
rect = QRect(startPt, e->pos()).normalized();
update(rect.adjusted(-2,-2,2,2)); // adjusted to include stroke
}
void paintEvent(QPaintEvent *event) override
{
QPainter p(this);
p.setBrush(Qt::NoBrush);
p.setPen(QPen(Qt::black, 2));
p.drawRect(rect);
}
private:
QRect rect;
QPoint startPt;
};
Is it possible to show minimum, maximum and current selected value of QSlider? Of course I can use labels to display this, but I think there must be such possibility in QSlider
You have two options..
1) as being mentioned in comments - sub - class
2) add as many QLabel's as you like with QSlider as a parent, install eventHandler() on QSlider to catch resize event to proper position them, and obviously handle scroll events, so you can update them... So labels will just float on top of QSlider
Here is my quick implementation of a fancy slider which subclass qslider to displays the current value just below the slider handle into a tooltip.
Header
#ifndef FANCYSLIDER_H
#define FANCYSLIDER_H
#include <QSlider>
class FancySlider : public QSlider
{
Q_OBJECT
public:
explicit FancySlider(QWidget *parent = 0);
explicit FancySlider(Qt::Orientation orientation, QWidget *parent = 0);
protected:
virtual void sliderChange(SliderChange change);
};
#endif // FANCYSLIDER_H
Cpp
#include "FancySlider.h"
#include <QStyleOptionSlider>
#include <QToolTip>
FancySlider::FancySlider(QWidget * parent)
: QSlider(parent)
{
}
FancySlider::FancySlider(Qt::Orientation orientation, QWidget * parent)
: QSlider(orientation, parent)
{
}
void FancySlider::sliderChange(QAbstractSlider::SliderChange change)
{
QSlider::sliderChange(change);
if (change == QAbstractSlider::SliderValueChange )
{
QStyleOptionSlider opt;
initStyleOption(&opt);
QRect sr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
QPoint bottomRightCorner = sr.bottomLeft();
QToolTip::showText(mapToGlobal( QPoint( bottomRightCorner.x(), bottomRightCorner.y() ) ), QString::number(value()), this);
}
}
Here is my implementation, without subclassing. Instanciate a slider with a label at right, showing the current value of slider :
QWidget *tmpW1 = new QWidget(this);
QHBoxLayout *tmpH1 = new QHBoxLayout(this);
QLabel *tmpLabel = new QLabel("0");
QSlider *tmpSlider = new QSlider(Qt::Horizontal, this);
tmpSlider->setMinimum(0);
tmpSlider->setMaximum(10);
tmpSlider->setValue(5);
tmpH1->addWidget(tmpSlider);
tmpH1->addWidget(tmpLabel);
tmpW1->setLayout(tmpH1);
QObject::connect(tmpSlider, &QSlider::valueChanged, this, [=] () {
tmpLabel->setText(QString::number(tmpSlider->value()));
});