Custom Widget in QScrollArea Badly Redrawing Only on Scroll - c++

I'm trying to get a custom scrolling widget in QT, and I'm getting redraw errors on scroll. Alt-tab or other redrawing events redraw correctly.
I'm basing it on the example at http://doc.qt.io/qt-5/qtwidgets-widgets-charactermap-example.html
repeatingwidget.cpp (excerpt):
QSize RepeatingWidget::sizeHint() const {
return QSize(500, itemHeight * displayItems.size() + 1);
}
void RepeatingWidget::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.fillRect(event->rect(), QBrush(Qt::white));
painter.setFont(displayFont);
QRect itemRect = event->rect();
int top = itemRect.top();
QFontMetrics fontMetrics(*displayFont);
for (auto item : displayItems) {
painter.setPen(QPen(Qt::gray));
painter.drawRect(itemRect.left(), top, itemRect.right(), itemHeight);
painter.setPen(QPen(Qt::black));
painter.drawText(8, 4 + top + fontMetrics.ascent(), item.name);
top += itemHeight;
}
}
mainwindow.cpp (excerpt):
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
QMenu *filemenu = menuBar()->addMenu(tr("File"));
filemenu->addAction(tr("Quit"), this, &QWidget::close);
auto *centralWidget = new QWidget;
scrollArea = new QScrollArea;
repeatingArea = new RepeatingWidget();
scrollArea->setWidget(repeatingArea);
auto *centralLayout = new QVBoxLayout;
centralLayout->addWidget(scrollArea, 1);
centralWidget->setLayout(centralLayout);
setCentralWidget(centralWidget);
setWindowTitle(tr("Widget Test"));
}
This seems to match the example, but I'm getting redraw errors that don't happen in charmap.
I've tried setGeometry, setWidgetResizable, and different size policies, but I'm still getting these redraw errors.
After scrolling:
I don't know what I'm doing wrong because it's largely identical in important ways to the example code from the charmap.
This is the full code: https://gist.github.com/jonasbuckner/2acc1a960e457946ce4756199de3fb57

QPaintEvent is a method that allows you to make an intelligent painting, that is, to paint where necessary, thus saving resources, for example it gives us the information of the rectangle that must be painted through event->rect(), with this we can calculate the items that have to be painted since others will be hidden and therefore it is not necessary to paint them:
void RepeatingWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.fillRect(event->rect(), QBrush(Qt::white));
painter.setFont(displayFont);
QFontMetrics fontMetrics(displayFont);
int i = std::max(event->rect().top()/itemHeight, 0);
int j = std::min(event->rect().bottom()/itemHeight+1, displayItems.size());
QRect itemRect(0, i*itemHeight, width(), itemHeight);
for(; i < j; i++){
painter.setPen(QPen(Qt::gray));
painter.drawRect(itemRect);
painter.setPen(QPen(Qt::black));
painter.drawText(8, 4 + itemRect.top() + fontMetrics.ascent(), displayItems[i].name);
itemRect.translate(0, itemHeight);
}
}

Your original code didn't work because you were drawing all of the items, but using the event->rect, which may only be part of the RepeatingWidget.
Sometimes it is not easy to calculate which items are in the event->rect as #eyllanesc shows. In these cases, just use clientRect instead - Qt will clip the drawing for you.

Related

qt remove layouts code

I have less than 1 day of experience in QT (that's why I do not know much of it) I have a window full of information (labels, text, buttons, etc) organized by layouts.
I need that after I press one button, all of the components in a window be hidden (which I already did) except for one label which should increase to barely the size of the whole window
Despite I tried modifying the "geometry" attribute (with code) the hidden layouts do not let the label to be increased. I thought also of using the option of layout breaking, but the label losses its dynamism. Could anyone please recommend me anything to do? Thanks.
Has anyone done something like this before. Thanks.
I once provided an answer to SO: Qt - How to create Image that scale with window, and keeps aspect ratio?. The actual intention was to scale an image in a QLabel with original aspect ratio to consume maximum available size.
However, I got the feedback that the suggested solution would not work properly when my Label would be used in a QGridLayout. (This sounds very similar to the issue of the OP.) Hence, I modified the sample to reproduce the issue and fiddled a little bit around with. For me, it seems that resize events of the main window are processed in the QGridLayout but affect layouted image label only partially. (Shrinking is applied but growing not.) Fortunately, I found a very simple work-around: Setting a non-empty frame to the QLabel solved the problem. I had a look into the source code on woboq.org. I hoped to get a hint what the changed frame style would activate (to apply this as fix for my resize issue). Finally, I was not patient enough and put it aside.
Beside of this QLabel in a QGridLayout resize issue, changing the visibility of widgets should cause a proper re-layout. I would prefer show/hide (instead of delete and re-new) as this is surely easier to implement, more efficient, and less error-prone.
I took the old sample code and added a tool button which can be used to toggle the visibilty of some of the layouted widgets:
// Qt header:
#include <QtWidgets>
class LabelImage: public QLabel {
private:
QPixmap _qPixmap, _qPixmapScaled;
public:
void setPixmap(const QPixmap &qPixmap) { setPixmap(qPixmap, size()); }
protected:
virtual void resizeEvent(QResizeEvent *pQEvent);
private:
void setPixmap(const QPixmap &qPixmap, const QSize &size);
};
void LabelImage::resizeEvent(QResizeEvent *pQEvent)
{
QLabel::resizeEvent(pQEvent);
setPixmap(_qPixmap, pQEvent->size());
}
void LabelImage::setPixmap(const QPixmap &qPixmap, const QSize &size)
{
_qPixmap = qPixmap;
_qPixmapScaled = _qPixmap.scaled(size, Qt::KeepAspectRatio);
QLabel::setPixmap(_qPixmapScaled);
}
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
// main application
QApplication app(argc, argv);
// setup GUI
QMainWindow qWin;
QToolBar qToolbar;
QAction qCmdTgl(QString::fromUtf8("Decoration"));
qCmdTgl.setCheckable(true);
qCmdTgl.setChecked(true);
qToolbar.addAction(&qCmdTgl);
qWin.addToolBar(&qToolbar);
QGroupBox qBox;
QGridLayout qGrid;
// a macro for the keyboard lazy:
#define Q_LBL_WITH_POS(ROW, COL) \
QLabel qLbl##ROW##COL(QString::fromLatin1(#ROW", "#COL)); \
/*qLbl##ROW##COL.setFrameStyle(QLabel::Raised | QLabel::Box);*/ \
qGrid.addWidget(&qLbl##ROW##COL, ROW, COL, Qt::AlignCenter)
Q_LBL_WITH_POS(0, 0);
Q_LBL_WITH_POS(0, 1);
Q_LBL_WITH_POS(0, 2);
Q_LBL_WITH_POS(1, 0);
LabelImage qLblImg;
qLblImg.setFrameStyle(QLabel::Raised | QLabel::Box);
qLblImg.setAlignment(Qt::AlignCenter);
//qLblImg.setMinimumSize(QSize(1, 1)); // seems to be not necessary
qLblImg.setSizePolicy(
QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored));
QPixmap qPM;
if (qPM.load("cats.jpg")) qLblImg.setPixmap(qPM);
else {
qLblImg.setText(
QString::fromLatin1("Sorry. Cannot find file 'cats.jpg'."));
}
qGrid.addWidget(&qLblImg, 1, 1, Qt::AlignCenter);
qGrid.setRowStretch(1, 1); // tell QGridLayout to stretch this cell...
qGrid.setColumnStretch(1, 1); // ...prior to other cells (w/ stretch 0)
Q_LBL_WITH_POS(1, 2);
Q_LBL_WITH_POS(2, 0);
Q_LBL_WITH_POS(2, 1);
Q_LBL_WITH_POS(2, 2);
qBox.setLayout(&qGrid);
qWin.setCentralWidget(&qBox);
qWin.show();
// install signal handlers
QObject::connect(&qCmdTgl, &QAction::triggered,
[&](bool on) {
qLbl00.setVisible(on); qLbl01.setVisible(on); qLbl02.setVisible(on);
qLbl10.setVisible(on); qLbl12.setVisible(on);
qLbl20.setVisible(on); qLbl21.setVisible(on); qLbl22.setVisible(on);
});
// run application
return app.exec();
}
I compiled and tested in VS2013 on Windows 10:
After toggling the Decoration tool button:
Note:
Out of curiosity, I commented the line which changes the frame style
qLblImg.setFrameStyle(QLabel::Raised | QLabel::Box);
and again, resizing of image didn't work properly anymore.
You can remove and hide widgets inside a layout using QLayout::removeWidget(*widget); but you do not need to actually remove it. You should use QWidget::hide() for the content to disappear and for the video label's cell to be able to take that space. I think you need to pay attention to the video label's size policy if it does not increase in size. Assuming you have a QGridLayout like so:
label1 label2 label3
label4 videoLabel label5
button1 button2 button3
And let's say, when you click button3, label1, label2 and label4 should all disappear and videoLabel takes the newly created space. I would group the widgets label1, label2, label4 and videoLabel into a single widget having its own sub-layout. I use QSizePolicy::Expaning to make sure my videoLabel takes the maximum space possible. Here is the implementation:
Widget::Widget(QWidget *parent) :
QWidget(parent)
{
setStyleSheet("QLabel{font-size:20px;}");
fullScreen = false; //current fullscreen state
//main grid layout
baseLayout = new QGridLayout(this);
baseLayout->setMargin(0);
baseLayout->setAlignment(Qt::AlignCenter);
setLayout(baseLayout);
//widget container for label1, label2, label4, videolabel
groupWidget = new QWidget();
//sub-layout inside the group layout
subLayout = new QGridLayout();
subLayout->setAlignment(Qt::AlignCenter);
subLayout->setMargin(0);
groupWidget->setLayout(subLayout);
//label and button instantializing. I set background colors to show their sizes
label1 = new QLabel("Label1");
label1->setStyleSheet("background-color:white;");
label2 = new QLabel("Label2");
label2->setStyleSheet("background-color:orange;");
label3 = new QLabel("Label3");
label4 = new QLabel("Label4");
label4->setStyleSheet("background-color:blue;color:white;");
label5 = new QLabel("Label5");
videoLabel = new QLabel("videoLabel");
videoLabel->setStyleSheet("background-color:red;");
videoLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
button1 = new QPushButton("button1");
button2 = new QPushButton("button2");
button3 = new QPushButton("button3");
//the grouped widget spans for 2 rows and columns, hence "2,2"
baseLayout->addWidget(groupWidget, 0,0,2,2);
subLayout->addWidget(label1, 0, 0);
subLayout->addWidget(label2, 0, 1);
subLayout->addWidget(label4, 1, 0);
subLayout->addWidget(videoLabel, 1, 1);
//adding rest of the labels and buttons to the base grid
baseLayout->addWidget(label3, 0, 2);
baseLayout->addWidget(label5, 1, 2);
baseLayout->addWidget(button1, 2, 0);
baseLayout->addWidget(button2, 2, 1);
baseLayout->addWidget(button3, 2, 2);
//button3 toggles fullscreen
connect(button3, SIGNAL(clicked(bool)), this, SLOT(onButton3Clicked(bool)));
}
//slot for button3 click
void Widget::onButton3Clicked(bool)
{
if (!fullScreen){
//removing widget from layouts is not really necessary. Make sure to hide
/*subLayout->removeWidget(label1);
subLayout->removeWidget(label2);
subLayout->removeWidget(label4);*/
label1->hide();
label2->hide();
label4->hide();
fullScreen = true;
}
else{
label1->show();
label2->show();
label4->show();
/*subLayout->addWidget(label1, 0, 0);
subLayout->addWidget(label2, 0, 1);
subLayout->addWidget(label4, 1, 0);*/
fullScreen = false;
}
}
I got the following results for this:
Keep in mind there are other approaches to this question. This one need not be necessarily the best when it comes to memory, but it is quite easy to follow.

QT Clear painter canvas Widget drawn using Overlay Widget

I have referenced this code to draw points on my widget.
OverLay::OverLay(std::vector<int> &points, QWidget *parent) :
QWidget(parent),
m_points(points)
{
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, true);
}
void OverLay::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setPen(QPen(Qt::red,2));
//painter.eraseRect( this->rect() );
int y_pos = height()/2;
for(int i=0; i<m_points.size();i++){
int x_pos = m_points[i];
QPointF p1 = QPointF(x_pos,y_pos);
painter.drawPoint(p1);
}
}
Here am creating new Overlay Widget and drawing
void Dialog::draw_points(std::vector<int> points)
{
OverLay *m_overlay = new OverLay(points,ui->view->parentWidget());
m_overlay->setGeometry(ui->view->geometry());
m_overlay->show();
}
So, everytime, Whenever I call draw_points functions, old drawing will be there. I want to clear the canvas everytime before drawing again.
painter.eraseRect( this->rect() ); will clear points. But, it will also clear underlying view.
Well, I always do it always specific:
painter.setBrush(Qt::NoBrush);
painter.setPen(Qt::NoPen);
painter.drawRect(rect());
I did not know it was so simple ..
Simply deleting instance of m_overlay worked.
if(m_overlay!=NULL){
delete m_overlay;
}

Scale a QTableWidget

I need to scale a QtableWidget (the behaviour should be like a zoom).
But when I reimplement the paintEvent of QtableWidget and set the scale manually, like this:
void MyTableWidget::paintEvent ( QPaintEvent * event )
{
QTableWidget::paintEvent(event);
QPainter p(viewport());
p.scale(m_scale,m_scale);
p.drawRect( 0, 0, width()-1, height()-1);
}
only the border is rescaled :
And I don't see any paintEvent on QTableWidgetItem, so how can I rescale all my table ?
Thank you in advance and have a good day.
EDIT ----------------------------------------
The behaviour may seems strange so here is some explanations:
This QT window is a child of an acrobat reader window. When I unzoom the PDF on acrobat, I resize my QT window to keep the same proportions, but I would like to scale the content of the window.
example: If I unzoom my PDF, I decrease the size of my QT window to keep the same proportions, and I want to scale the content of my QT window to this new size (decrease the display size, like an unzoom). Is it clear ? :o
But for instance the view donesn't fit the window, I have this:
And I want this:
And when I zoom on my PDF, I increase the window size and scale up the content, like this:
Thank you very much for your help and your time.
Use QGraphicsScene with your QTableWidget instead, but it is not very difficult:
QGraphicsScene *scene = new QGraphicsScene(this);
ui->graphicsView->setScene(scene);
QTableWidget *wgt = new QTableWidget;
wgt->setColumnCount(10);
wgt->setRowCount(10);
for (int ridx = 0 ; ridx < wgt->rowCount() ; ridx++ )
{
for (int cidx = 0 ; cidx < wgt->columnCount() ; cidx++)
{
QTableWidgetItem* item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
}
}
QGraphicsProxyWidget *pr = scene->addWidget( wgt );
pr->moveBy(10,10);
Scale view with:
ui->graphicsView->scale(2,2);
Better way for zooming is zoom in out by wheel. Subclass view or use eventFilter. For example:
Header:
#ifndef MYQGRAPHICSVIEW_H
#define MYQGRAPHICSVIEW_H
#include <QGraphicsView>
class MyQGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
explicit MyQGraphicsView(QWidget *parent = 0);
signals:
protected:
void wheelEvent(QWheelEvent* event);
};
#endif // MYQGRAPHICSVIEW_H
Cpp:
#include "myqgraphicsview.h"
#include <QPointF>
MyQGraphicsView::MyQGraphicsView(QWidget *parent) :
QGraphicsView(parent)
{
}
void MyQGraphicsView::wheelEvent(QWheelEvent* event) {
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
// Scale the view / do the zoom
double scaleFactor = 1.15;
if(event->delta() > 0) {
// Zoom in
scale(scaleFactor, scaleFactor);
} else {
// Zooming out
scale(1.0 / scaleFactor, 1.0 / scaleFactor);
}
// Don't call superclass handler here
// as wheel is normally used for moving scrollbars
}
Usage:
MyQGraphicsView *view = new MyQGraphicsView;
view->setScene(scene);//same scene
view->show();
Result as you want:
Note that user still able to edit data etc, functionality didn't change.
Additional example, like in the Qt books.
(same MyQGraphicsView class)
#include "myqgraphicsview.h"
#include <QGraphicsProxyWidget>//and other needed includes
//just to show that signals and slots works
//with widget which placed in graphicsview
void print(int row, int column)
{
qDebug() << row+1 << column+1;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget *widget = new QWidget;//container
QVBoxLayout *lay = new QVBoxLayout;
MyQGraphicsView * view = new MyQGraphicsView;//view as part of UI
QGraphicsScene * scene = new QGraphicsScene;
QTableWidget *wgt = new QTableWidget;//table which will be added to graphicsView
QObject::connect(wgt,&QTableWidget::cellPressed,print);//connection using Qt5 style(and power)
wgt->setColumnCount(10);
wgt->setRowCount(10);
for (int ridx = 0 ; ridx < wgt->rowCount() ; ridx++ )
{
for (int cidx = 0 ; cidx < wgt->columnCount() ; cidx++)
{
QTableWidgetItem* item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
}
}
QPushButton *butt = new QPushButton("click");
lay->addWidget(view);
lay->addWidget(butt);
widget->setLayout(lay);
QGraphicsProxyWidget *pr = scene->addWidget( wgt );
pr->moveBy(10,10);
view->setScene(scene);
widget->show();
return a.exec();
}
Result:
As you can see, user can scale table, edit data and signals and slots works. All fine.

Qt: QGraphicsView content not erased before drawForeground() is called

I'm facing an annoying issue using Qt QGraphicsView framework.
I write a basic image viewer to display large B&W tiff images.
Images are displayed using a QGraphicsPixmapItem and added to the scene.
I also need to draw a colored rectangle around the viewport.
void ImageView::drawForeground( QPainter * painter, const QRectF & rect )
{
if( m_image ) {
qDebug() << "drawForeground(), rect = " << rect;
QBrush br(m_image->isValidated() ? Qt::green : Qt::red);
qreal w = 40. / m_scaleFactor;
QPen pen(br, w);
painter->save();
painter->setOpacity(0.5);
painter->setPen(pen);
painter->drawRect(rect);
painter->restore();
}
}
It works fine, at first glance.
But when I scroll the viewport content, things are getting ugly.
drawForeground() method is effectively called but it seems the content of the viewport is not erased before. So the drawing becomes horrible on screen.
Is there a better way to achieve it?
EDIT
As leems mentioned it, Qt internals don't alow me to achieve it with drawForeground().
I found a workaround by using a QGraphicsRectItem which gets resized in the viewportEvent().
Loos like:
bool ImageView::viewportEvent(QEvent *ev)
{
QRect rect = viewport()->rect();
if( m_image ) {
QPolygonF r = mapToScene(rect);
QBrush br2(m_image->isValidated() ? Qt::green : Qt::red);
qreal w2 = 40. / m_scaleFactor;
QPen pen2(br2, w2);
m_rect->setPen(pen2);
m_rect->setOpacity(0.5);
m_rect->setRect(r.boundingRect());
}
return QGraphicsView::viewportEvent(ev);
}
The code is not finalized but will basically looks like that.
It works fine, though it blinks a little bit when scrolling too fast...

QT QPixmap won't scroll in QScrollArea

I'm trying to draw with QPainter in a QPixmap, put that QPixmap in a QLabel, and that QLabel in a QScrollArea.
Sometimes the painted pixmap is bigger then my ScrollArea allows, but somehow it doesn't scroll to show the rest. What am I doing wrong?
QPixmap *pixmap = new QPixmap(10000,500);
QLabel *labeltime = new QLabel;
QHBoxLayout *layout = new QHBoxLayout;
pixmap->fill(QColor("transparent"));
int currentX = 0;
const int currentY = 220;
const int height = 50; // Coming from some static data initialization
QPainter *painter = new QPainter(pixmap);
QPen pen(Qt::gray, 2);
painter->setPen(pen);
for(int i = 0; i< viewlist.size(); i++)
{
QBrush brush(QColor(viewlist[i].color));
painter->setBrush(brush);
painter->drawRect(currentX, currentY, viewlist[i].length, height);
currentX += viewlist[i].length;
}
labeltime->setPixmap(*pixmap);
layout->addWidget(labeltime);
ui->overview->setLayout(layout);
I know this is a long and weird way to add a pixmap, but I want it to be scrollable, and I can't paint on a QScrollArea. Is there a better way? Or can someone tell me what is wrong?
Thanks
Since you only have one child widget, it is simpler to eliminate your layout. Change these lines:
layout->addWidget(labeltime);
ui->overview->setLayout(layout);
to:
ui->overview->setWidget(labeltime);