QT 5.7 QPainter line aligment - c++

I am working with QT 5.7 and C++.
At the moment I try to get used to draw my own widgets with the QPainter class.
But I noticed a problem I couldn't solve.
I try to draw a border line extactly at the widget border but if I do so:
void MyWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
painter.setBrush(Qt::cyan);
QBrush brush(Qt::black);
QPen pen(brush, 2);
painter.setPen(pen);
painter.drawRect(0, 0, size().width() - 1, size().height() - 1);
painter.end();
}
The Line is at the bottom and right site bigger than the others:
And before someone is telling me I have to remove the two -1 expressions,
you should know if I do this and also set the pen width to 1 there is no line anymore at the bottom and right side.
I think this artifact is caused by the "line aligment".
QT tries to tint the the pixels near the logical lines defined by the rectangle but actually because finally all have to be in pixels it has to decide.
If I am right, why there is no method to set the line aligment of the pen like in GDI+?
And how I can solve this?

Everything depends on whether you want the entire pen's width to be visible or not. By drawing the rectangle starting at 0,0, you're only showing half of the pen's width, and that makes things unnecessarily complicated - never mind that the line appears too thin. In Qt, the non-cosmetic pen is always drawn aligned to the middle of the line. Qt doesn't let you change it: you can change the drawn geometry instead.
To get it right for odd line sizes, you must give rectangle's coordinates as floating point values, and they must be fall in the middle of the line. So, e.g. if the pen is 3.0 units wide, the rectangle's geometry will be (1.5, 1.5, width()-3.0, width()-3.0).
Here's a complete example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/widget-pen-wide-38019846
#include <QtWidgets>
class Widget : public QWidget {
Q_OBJECT
Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth)
qreal m_penWidth = 1.0;
protected:
void paintEvent(QPaintEvent *) override {
QPainter p{this};
p.setPen({Qt::black, m_penWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin});
p.setBrush(Qt::cyan);
qreal d = m_penWidth/2.0;
p.drawRect(QRectF{d, d, width()-m_penWidth, height()-m_penWidth});
}
public:
explicit Widget(QWidget * parent = 0) : QWidget{parent} { }
qreal penWidth() const { return m_penWidth; }
void setPenWidth(qreal width) {
if (width == m_penWidth) return;
m_penWidth = width;
update();
}
QSize sizeHint() const override { return {100, 100}; }
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget top;
QVBoxLayout layout{&top};
Widget widget;
QSlider slider{Qt::Horizontal};
layout.addWidget(&widget);
layout.addWidget(&slider);
slider.setMinimum(100);
slider.setMaximum(1000);
QObject::connect(&slider, &QSlider::valueChanged, [&](int val){
widget.setPenWidth(val/100.0);
});
top.show();
return app.exec();
}
#include "main.moc"

Related

How to paint an outline when hovering over a QListWidget item?

I am trying to paint an outline around a QListWidget item when the mouse is over that item. I've subclassed QStyledItemDelegate and overrode paint to account for the QStyle::State_MouseOver case as follows:
class MyDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent){}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) painter->drawRect(option.rect);
}
~MyDelegate(){}
};
I then instantiate a QListWidget with some items and enable the Qt::WA_Hover attribute:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.setItemDelegate(new MyDelegate(&w));
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Unfortunately, the behaviour is not what I expected. In particular, the outline is painted when I move the mouse over an item, but when I move to another item the outline around the first item is not erased. Instead, it keeps drawing outlines around all the items I move my mouse over and eventually there is an outline around all items. Is this normal? I know that an alternative solution would be to use QStyleSheets but I'd like to understand why the current approach doesn't behave as I expected.
Here is what the widget looks like before a mouse over:
Here it is after hovering over item2:
And then after item3:
I am using Qt 5.15.1 on a MacOS 10.15.6 platform.
EDIT 1:
Based on the answer from scopchanov, to ensure the outline thickness is indeed 1px, I've changed the paint method to this:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
int outlineWidth = 1;
QPen pen;
pen.setWidth(outlineWidth);
painter->setPen(pen);
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) {
int a = round(0.5*(outlineWidth - 1));
int b = round(-0.5*outlineWidth);
painter->drawRect(option.rect.adjusted(a, a, b, b));
}
}
Unfortunately, the behaviour is very similar; here is a screenshot after hovering over all items from top to bottom:
Cause
QPainter::drawRect draws a rectangle, that is slightly bigger (by exactly one pixel in height and width) than the painted area. The reason for this behavior could be seen in the way QPaintEngine draws a rectangle:
for (int i=0; i<rectCount; ++i) {
QRectF rf = rects[i];
QPointF pts[4] = { QPointF(rf.x(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y() + rf.height()),
QPointF(rf.x(), rf.y() + rf.height()) };
drawPolygon(pts, 4, ConvexMode);
}
QPaintEngine draws a closed polygon, starting at the point (x, y), going to (x + width, y), then to (x + width, y + height) and finally to (x, y + height). This looks intuitive enough, but let's see what happens, if we substitute these variables with real numbers:
Say, we want to draw a 4x2 px rectangle at (0, 0). QPaintEngine would use the following coordinates: (0, 0), (4, 0), (4, 2) and (0, 2). Represented as pixels, the drawing would look like this:
So, instead of 4x2 px, we end up with a 5x3 px rectangle, i.e. indeed one pixel wider and taller.
You can further prove this by clipping the painter to option.rect before calling drawRect like this:
if (option.state & QStyle::State_MouseOver) {
painter->setClipRect(option.rect);
painter->drawRect(option.rect);
}
The result is clipped bottom and right edges of the outline (the very edges, we have expected to be within the painted area):
In any case, the part of the outline, that falls outside of the painted area, is not repainted correctly, hence the unwanted remains of the previous drawings in the form of lines.
Solution
Reduce the height and the width of the outline, using QRect::adjusted.
You might just write
painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
However, this would work only for an outline, which is 1px thick and the devicePixelRatio is 1, as on a PC. If the border of the outline is thicker than 1px and/or the devicePixelRatio is 2, as on a Mac, of course more of the outline will stick out of the painted area, so you should take that into consideration and adjust the rectangle accordingly, e.g.:
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
m_outineWidth and m_devicePixelRatio are class members, representing the desired outline width, resp. the ratio between physical pixels and device-independent pixels for the paint device. Provided that you have created public setter methods for them, you could set their values like this:
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(1);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
Example
Here is an example I have written for you to demonstrate how the proposed solution could be implemented:
#include <QApplication>
#include <QStyledItemDelegate>
#include <QListWidget>
#include <QPainter>
class MyDelegate : public QStyledItemDelegate
{
int m_outineWidth;
int m_devicePixelRatio;
public:
MyDelegate(QObject *parent = nullptr) :
QStyledItemDelegate(parent),
m_outineWidth(1),
m_devicePixelRatio(1) {
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
QStyledItemDelegate::paint(painter, option, index);
if (option.state & QStyle::State_MouseOver) {
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->setPen(QPen(QBrush(Qt::red), m_outineWidth, Qt::SolidLine,
Qt::SquareCap, Qt::MiterJoin));
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
}
}
void setOutlineWidth(int outineWidth) {
m_outineWidth = outineWidth;
}
void setDevicePixelRatio(int devicePixelRatio) {
m_devicePixelRatio = devicePixelRatio;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(3);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Result
The provided example produces the following result for 3px thick outline on Windows:

Positioning a resizable widget inside a scroll area

I have a scroll area that contains a resizable widget. The user is able to increment and decrement the integer scale factor of this inner widget (which changes its size). When the user increments the scale, the part of the inner widget visible in the top left corner remains fixed. This gives the effect of zooming into the top left corner of the view.
When the size of the inner widget changes, I need to scroll the inner widget to make it look like the user is zooming into the center of the view. I want to keep the part of the widget in the center of the view fixed in the center while resizing.
I drew some diagrams to help visualise the problem. The pink rectangle is the inner widget. The brown rectangle is the view onto the widget through the scroll area. The green dots are fixed points on the inner widget.
Before scaling
After scaling (current undesirable behaviour)
After scaling (desired behaviour)
As you can (hopefully) see from these crudely drawn diagrams. Simply increasing the size of a widget inside a scroll area results on zooming into the top left corner or the view. I have to do something more to zoom into the center of the view. Also, the inner widget can be much smaller than the scroll area. I only want to shift the inner widget when it is larger than the scroll area.
This is a minimal example of the undesirable behaviour. Pressing Z (after clicking on the inner widget to change focus) will zoom into the top left corner of the view. I want to zoom into the center of the view.
#include <QtGui/qpainter.h>
#include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qscrollarea.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qapplication.h>
class InnerWidget final : public QWidget {
public:
explicit InnerWidget(QScrollArea *parent)
: QWidget{parent}, parent{parent} {
updateSize();
setFocusPolicy(Qt::StrongFocus);
}
private:
QScrollArea *parent;
int scale = 1;
void updateSize() {
setFixedSize(256 * scale, 256 * scale);
}
void paintEvent(QPaintEvent *) override {
QPainter painter{this};
const QColor green = {0, 255, 0};
painter.fillRect(0, 0, width(), height(), {255, 255, 255});
painter.fillRect(32 * scale, 32 * scale, 16 * scale, 16 * scale, green);
painter.fillRect(128 * scale, 128 * scale, 16 * scale, 16 * scale, green);
}
void keyPressEvent(QKeyEvent *event) override {
if (event->isAutoRepeat()) return;
QScrollBar *hbar = parent->horizontalScrollBar();
QScrollBar *vbar = parent->verticalScrollBar();
if (event->key() == Qt::Key_Z) {
// need to call bar->setValue and bar->value here
scale = std::min(scale + 1, 64);
updateSize();
} else if (event->key() == Qt::Key_X) {
// here too
scale = std::max(scale - 1, 1);
updateSize();
}
}
};
int main(int argc, char **argv) {
QApplication app{argc, argv};
QMainWindow window;
QScrollArea scrollArea{&window};
InnerWidget inner{&scrollArea};
window.setBaseSize(512, 512);
window.setCentralWidget(&scrollArea);
scrollArea.setAlignment(Qt::AlignCenter);
scrollArea.setWidget(&inner);
window.show();
return app.exec();
}
To reproduce the problem, zoom in a couple of times then position one of the rectangles in the center of the window. Zooming will move the rectangle toward the bottom right corner. I want zooming to keep the rectangle in the center.
This feels like an easy problem but I can’t seem to get my head around the math. I’ve tried various calculations on the scroll values but none of them seem to behave as I want.
I messed around with the numbers for a while and then I figured it out. Here’s the fully working solution.
#include <QtGui/qpainter.h>
#include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qscrollarea.h>
#include <QtWidgets/qapplication.h>
class InnerWidget final : public QWidget {
public:
explicit InnerWidget(QScrollArea *parent)
: QWidget{parent}, parent{parent}, scale{1} {
updateSize();
setFocusPolicy(Qt::StrongFocus);
}
private:
QScrollArea *parent;
int scale;
void updateSize() {
setFixedSize(256 * scale, 256 * scale);
}
void paintEvent(QPaintEvent *) override {
QPainter painter{this};
const QColor green = {0, 255, 0};
painter.fillRect(0, 0, width(), height(), {255, 255, 255});
painter.fillRect(32 * scale, 32 * scale, 16 * scale, 16 * scale, green);
painter.fillRect(128 * scale, 128 * scale, 16 * scale, 16 * scale, green);
}
void adjustScroll(const int oldScale) {
if (scale == oldScale) return;
QScrollBar *hbar = parent->horizontalScrollBar();
QScrollBar *vbar = parent->verticalScrollBar();
if (width() >= parent->width()) {
const int halfWidth = parent->width() / 2;
hbar->setValue((hbar->value() + halfWidth) * scale / oldScale - halfWidth);
}
if (height() >= parent->height()) {
const int halfHeight = parent->height() / 2;
vbar->setValue((vbar->value() + halfHeight) * scale / oldScale - halfHeight);
}
}
void keyPressEvent(QKeyEvent *event) override {
if (event->isAutoRepeat()) return;
const int oldScale = scale;
if (event->key() == Qt::Key_Z) {
scale = std::min(scale + 1, 64);
updateSize();
adjustScroll(oldScale);
} else if (event->key() == Qt::Key_X) {
scale = std::max(scale - 1, 1);
updateSize();
adjustScroll(oldScale);
}
}
};
int main(int argc, char **argv) {
QApplication app{argc, argv};
QMainWindow window;
QScrollArea scrollArea{&window};
InnerWidget inner{&scrollArea};
window.setBaseSize(512, 512);
window.setCentralWidget(&scrollArea);
scrollArea.setAlignment(Qt::AlignCenter);
scrollArea.setWidget(&inner);
window.show();
return app.exec();
}

Qt Overriding QLabel PaintEvent

I've been struggling with this problem for the past couple of days. I want to be able to grow and shrink whatever the assigned Pixmap is in a QLabel as the user resizes the window. The issue is preserving the aspect ratio and image quality. Another user on here suggested that I reimplement the paint event for the label - but I'm still very lost. I'm not even sure if I have overridden the paintEvent correctly. I would kill for a bit of sample code here.
This is where I'm at:
void MyLabel::paintEvent(QPaintEvent * event)
{
//if this widget is assigned a pixmap
//paint that pixmap at the size of the parent, aspect ratio preserved
//otherwise, nothing
}
Here is a possible implementation of a QLabel subclass that scales its pixmap content while keeping its aspect ratio. It is implemented based on the way QLabel::paintEvent is implemented.
If you are using it in a layout you can also set its size policy to QSizePolicy::Expanding, so that additional space in the layout is taken up by the QLabel for a larger display of the pixmap content.
#include <QApplication>
#include <QtWidgets>
class PixmapLabel : public QLabel{
public:
explicit PixmapLabel(QWidget* parent=nullptr):QLabel(parent){
//By default, this class scales the pixmap according to the label's size
setScaledContents(true);
}
~PixmapLabel(){}
protected:
void paintEvent(QPaintEvent* event);
private:
//QImage to cache the pixmap()
//to avoid constructing a new QImage on every scale operation
QImage cachedImage;
//used to cache the last scaled pixmap
//to avoid calling scale again when the size is still at the same
QPixmap scaledPixmap;
//key for the currently cached QImage and QPixmap
//used to make sure the label was not set to another QPixmap
qint64 cacheKey{0};
};
//based on the implementation of QLabel::paintEvent
void PixmapLabel::paintEvent(QPaintEvent *event){
//if this is assigned to a pixmap
if(pixmap() && !pixmap()->isNull()){
QStyle* style= PixmapLabel::style();
QPainter painter(this);
drawFrame(&painter);
QRect cr = contentsRect();
cr.adjust(margin(), margin(), -margin(), -margin());
int align= QStyle::visualAlignment(layoutDirection(), alignment());
QPixmap pix;
if(hasScaledContents()){ //if scaling is enabled
QSize scaledSize= cr.size() * devicePixelRatioF();
//if scaledPixmap is invalid
if(scaledPixmap.isNull() || scaledPixmap.size()!=scaledSize
|| pixmap()->cacheKey()!=cacheKey){
//if cachedImage is also invalid
if(pixmap()->cacheKey() != cacheKey){
//reconstruct cachedImage
cachedImage= pixmap()->toImage();
}
QImage scaledImage= cachedImage.scaled(
scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
scaledPixmap= QPixmap::fromImage(scaledImage);
scaledPixmap.setDevicePixelRatio(devicePixelRatioF());
}
pix= scaledPixmap;
} else { // no scaling, Just use pixmap()
pix= *pixmap();
}
QStyleOption opt;
opt.initFrom(this);
if(!isEnabled())
pix= style->generatedIconPixmap(QIcon::Disabled, pix, &opt);
style->drawItemPixmap(&painter, cr, align, pix);
} else { //otherwise (if the label is not assigned to a pixmap)
//call base paintEvent
QLabel::paintEvent(event);
}
}
//DEMO program
QPixmap generatePixmap(QSize size) {
QPixmap pixmap(size);
pixmap.fill(Qt::white);
QPainter p(&pixmap);
p.setRenderHint(QPainter::Antialiasing);
p.setPen(QPen(Qt::black, 10));
p.drawEllipse(pixmap.rect());
p.setPen(QPen(Qt::red, 2));
p.drawLine(pixmap.rect().topLeft(), pixmap.rect().bottomRight());
p.drawLine(pixmap.rect().topRight(), pixmap.rect().bottomLeft());
return pixmap;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPixmap pixmap= generatePixmap(QSize(1280, 960));
PixmapLabel label;
label.setPixmap(pixmap);
label.setAlignment(Qt::AlignCenter);
label.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
label.setMinimumSize(320, 240);
label.show();
return a.exec();
}
I think this is better than the solution in this answer, as the QLabel here takes care of resizing its pixmap. So, there is no need to resize it manually every time the parent widget is resized and everytime a new pixmap is set on it.
First of all I wanted to say thanks to Mike.
Additionally I would like to add to his answer from 21.10.2016 - however I am not able to add a comment as of yet - thus here is a full fledged answer. [If anyone is able to move this to the comment section, feel free.]
I also added an overwrite of the other constructor of QLabel and the window flags [with default arguments as in QLabel] and added the Q_OBJECT macro, as to be compatible to the Qt framework:
Header:
class PixmapLabel : public QLabel
{
Q_OBJECT
public:
explicit PixmapLabel(QWidget* parent=nullptr, Qt::WindowFlags f = Qt::WindowFlags());
explicit PixmapLabel(const QString &text, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
...
}
Module:
PixmapLabel::PixmapLabel(QString const & text, QWidget * parent, Qt::WindowFlags f) :
QLabel(text, parent, f)
{
//By default, this class scales the pixmap according to the label's size
setScaledContents(true);
}

How to draw a rectangle on a graph with XY axes Qt

I want to draw a rectangle on a graph with XY axes using Qt. I have found QCustomPlot widget, but it is not what i need (or i did not understand how to apply it to solve my problem).
Please any suggestions how to make it work?
This is an example of what you need:
#include <QWidget>
#include <QPainter>
class MyPlot : public QWidget
{
Q_OBJECT
public:
MyPlot(QWidget *parent = 0)
: QWidget(parent)
{
}
protected:
void paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.save();
painter.translate(2, height() -2); // 2 pixels between axes and the windows frame
painter.scale(1,-1);
QPen pen;
pen.setWidth(2);
painter.setPen(pen);
// X Axis
painter.drawLine(0,0, width(),0);
// Y Axis
painter.drawLine(0,0, 0,height());
pen.setWidth(4);
painter.setPen(pen);
// Rect
painter.drawRect(10,10, 60,80);
painter.restore();
}
};
You can do it by adding QCPItemRect to QCPLayer of QCustomPlot. It seems to be the easiest solution.
The simplest override paintEvent in QWidget:
void MyWidget::paintEvent(QPaintEvent * event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.drawLine(0, 10, 100, 10);
painter.drawLine(10, 0, 10, 100);
painter.drawRect(20, 20, 30, 30);
}
You can use just the plain QWidget and reimplement it's paintEvent() function. The painting would be realized by the QPainter.
void CMyWidget::paintEvent(QPaintEvent* event)
{
QPainter p(this);
p.drawLine(...);
p.drawRect(...);
p.drawText(...);
}
Or you can use a QGraphicsView / QGraphicsScene framework: http://doc.qt.io/qt-4.8/graphicsview.html

Tracking mouse coordinates in Qt

Let's say I have a widget in main window, and want to track mouse position ONLY on the widget: it means that left-low corner of widget must be local (0, 0).
Q: How can I do this?
p.s. NON of functions below do that.
widget->mapFromGlobal(QCursor::pos()).x();
QCursor::pos()).x();
event->x();
I am afraid, you won't be happy with your requirement 'lower left must be (0,0). In Qt coordinate systems (0,0) is upper left. If you can accept that. The following code...
setMouseTracking(true); // E.g. set in your constructor of your widget.
// Implement in your widget
void MainWindow::mouseMoveEvent(QMouseEvent *event){
qDebug() << event->pos();
}
...will give you the coordinates of your mouse pointer in your widget.
If all you want to do is to report position of the mouse in coordinates as if the widget's lower-left corner was (0,0) and Y was ascending when going up, then the code below does it. I think the reason for wanting such code is misguided, though, since coordinates of everything else within said widget don't work this way. So why would you want it, I can't fathom, but here you go.
#include <QtWidgets>
class Window : public QLabel {
public:
Window() {
setMouseTracking(true);
setMinimumSize(100, 100);
}
void mouseMoveEvent(QMouseEvent *ev) override {
// vvv That's where the magic happens
QTransform t;
t.scale(1, -1);
t.translate(0, -height()+1);
QPoint pos = ev->pos() * t;
// ^^^
setText(QStringLiteral("%1, %2").arg(pos.x()).arg(pos.y()));
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Window w;
w.show();
return a.exec();
}