The main window of my Qt/C++ program looks like this:
As you can see on the picture, the window consists essentially of:
a menu on the left
two "canvases"
What I want is straightforward to describe: I want that under resizing of the window, both canvases take as much space as possible, but still remain squares (width = height). I've been trying to achieve that unsuccessfully.
Let me quickly describe the objects:
The window is a class Window that I created deriving QWidget. It has a QGridLayout for a layout.
The window's layout has three widgets: the left menu LeftMenu *menu, and the canvases Canvas *leftCanvas, *rightCanvas. Both LeftMenu and Canvas are custom classes deriving QWidget.
(NB: the left menu actually consists of 3 different widgets (submenus), and the window also has a status bar and a top menu, but I don't think it matters for my question.)
I have been "playing" (not having fun the least bit) with QSizePolicy's etc to try to get the Canvases' sizes to behave like I want (be as large as possible inside the window, but keep height/width ratio = 1), unsuccessfully. Let me describe my latest attempt in case that is useful for you (if you already know a solution to my problem, you don't have to keep reading):
I overrode the methods heightForWidth(), sizeHint() and minimumSizeHint() for Canvas like so:
class Canvas : public QWidget
{
Q_OBJECT
friend class Window;
public:
explicit Canvas(Window* window);
...
private:
void resizeEvent(QResizeEvent *resizeEvent) override;
int heightForWidth(int width) const override {return width;}
QSize sizeHint() const override
{
int size = std::min(width(), height());
return QSize(size, size);
}
QSize minimumSizeHint() const override {return QSize(200,200);}
...
};
And the constructor of my class Window looks like (a bit simplified):
Window::Window(ActionHandler *handler)
{
leftMenu = new LeftMenu(this);
leftMenu->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
leftCanvas = new Canvas(this);
rightCanvas = new Canvas(this);
QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Maximum);
policy.setHeightForWidth(true);
leftCanvas->setSizePolicy(policy);
rightCanvas->setSizePolicy(policy);
layout = new QGridLayout;
layout->setColumnMinimumWidth(0, menuWidth());
layout->addWidget(leftMenu, 0, 0);
layout->addWidget(leftCanvas, 0, 1);
layout->addWidget(rightCanvas, 0, 2);
setLayout(layout);
}
My idea was that as long as the width of the canvases is the limiting factor, the sizePolicy of the canvases should be (QSizePolicy::Expanding, QSizePolicy::Maximum). And as soon as the height of the canvases becomes the limiting factor, I would change the sizePolicy of the canvases (probably in Canvas::resizeEvent()) to the opposite: (QSizePolicy::Maximum, QSizePolicy::Expanding). Does that sound too complicated?
Anyway, it already fails, and I don't understand why. If I shrink the window horizontally it gives me this:
So, the height of the canvases does not shrink. I do not understand this behavior. In the Qt documentation (http://doc.qt.io/qt-4.8/qsizepolicy.html#Policy-enum), I read:
QSizePolicy::Maximum The sizeHint() is a maximum. The widget can be shrunk any amount without detriment if other widgets need the space (e.g. a separator line). It cannot be larger than the size provided by sizeHint().
The behavior of my canvases here seems to contradict this: their height is larger than the height provided by their sizeHint(). (I did make sure by std::couting "live": the canvas sizeHint, its height, its sizePolicy and its hasHeightForWidth parameters).
Related
I'm currently having a problem where I try to render an SVG file to a QLabel and it does not display correctly:
stop sign SVG displayed wrongly in main window
This is how the SVG actually looks like:
stop sign SVG
I want the SVG to be displayed without scaling (I mean, that's what SVG is all about, isn't it) in it's original square shape. The SVG has a size specified of 256x256, but I don't care about that as long as it fills the layout's cell and it's displayed in its correct aspect ratio.
This is the meaningful part of the header:
class LoginUnavailableWindow : public QWidget {
Q_OBJECT
public:
explicit LoginUnavailableWindow(QWidget *parent = nullptr);
~LoginUnavailableWindow() override;
private:
QHBoxLayout errorLayout {this};
QLabel errorIconLabel {this};
QLabel errorTextLabel {this};
};
And this is the meaningful part of the main cpp:
LoginUnavailableWindow::LoginUnavailableWindow(QWidget *parent) : QWidget(parent) {
/* set overall layout */
errorLayout.addWidget(&errorIconLabel);
errorLayout.addWidget(&errorTextLabel);
errorLayout.setStretch(0, 1);
errorLayout.setStretch(1, 3);
/* apply size constraints */
setMinimumSize(800, 200);
setMaximumSize(800, 200);
errorTextLabel.setAlignment(Qt::AlignCenter);
errorIconLabel.setAlignment(Qt::AlignCenter);
/* set text */
QFont font = errorTextLabel.font();
font.setPointSize(48);
errorTextLabel.setFont(font);
errorTextLabel.setText("Login is currently\nunavailable");
/* render SVG */
QSvgRenderer renderer(ERROR_SVG_PATH);
QPixmap pm(errorIconLabel.size());
pm.fill(QColorConstants::Transparent);
QPainter painter(&pm);
renderer.render(&painter, pm.rect());
errorIconLabel.setPixmap(pm);
}
The actual SVG rendering code was taken from here. One of the problems that I can see is that errorIconLabel.size() is returning 100x30 which I find very confusing. How do I get the actual size of the layouts cell so that I can calculate at which resolution to render the SVG?
A lot of answers I found would use setScaledContents() which does cause the pixmap to be displayed at a more reasonable size, but then it's all blurry/pixelated since the SVG is still rendered at the wrong resolution. I would like to achieve this without introducing scaling artifacts.
Okay, I was able to solve this on my own. I've realized that the QLabel would only have the size 100x30 returned in the constructor. After the constructor finishes, a resizeEvent is issued directly and inside this resizeEvent the size of that label is returned correctly.
So my solution in the end was to implement void resizeEvent(QResizeEvent *) override in LoginUnavailableWindow and to always redraw the SVG when the size would change. In my case I force the window size fixed, but if someone else would do this, it would also solve display problems when the user would resize the window.
This is the structure I have. I want to find the total size of label_2, or the QVBoxLayout, as it's displayed. When I use verticalLayout_2->width(), I always get 100 and verticalLayout_2->height() always returns 30. It's set to expanding, so I thought it would fill the area, which is 385x379, according to Qt Creator.
Doing label_2-width() and label_2-height() also results in 100x30, regardless of the window size and the area I thought it would expand to.
There is nothing much visually going on after your widget's constructor has been run. The setupUi call happens in the constructor. The real work happens once the event loop gets going.
Your real problem is that you should not be checking the size at an arbitrary point in time. You should be checking it each time it changes. To do this, you need your own layout. All it takes is to derive from an existing layout, and reimplement setGeometry. This method is called each time the parent widget or parent layout resizes the given layout. That is the only correct approach, and it doesn't require any hacks to accomplish.
For example, the following class could be used to signal when there's a new geometry:
class SigBoxLayout : public QBoxLayout {
Q_OBJECT
protected:
void setGeometry(const QRect & r) Q_DECL_OVERRIDE {
if (r != geometry()) emit hasNewGeometry(r);
QBoxLayout::setGeometry(r);
}
public:
SigBoxLayout(QBoxLayout::Direction dir, QWidget * parent = 0) :
QBoxLayout(dir, parent) {}
Q_SIGNAL void hasNewGeometry(const QRect & r);
};
QGraphicsPixmapItem, like QGraphicsItem, has a method update(x0, y0, width, height), in order to redraw a pixmap only partly on a QGraphicsScene. Calling this will schedule a paint() (in Qt's event loop) on the QGraphicsItem, and after this paint() is executed the boundingbox (x,y,width,height) will be redrawn to the QGraphcisScene.
The unfortunate part is that there is no way to schedule the paint-event with a boundingbox, meaning that QGraphicsPixmapItem::paint() is forced to repaint the whole QPixmap, therefore reimplementing this paint()-method in a subclass gives no way to only partly update the QPixmap, therefore making a small (local) update to the QPixmap unacceptably slow.
Such a subclass would look something like this:
class LocallyUdatablePixmapItem : public QGraphicsPixmapItem {
private:
QImage ℑ
public:
LocallyUdatablePixmapItem(QImage &img) : QGraphicsPixmapItem(), image(img) {}
paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QStyle *widget) {
//locall update, unfortunately without a boundig box :( therefore -> slow
}
};
Another option would be to keep the 'internal QPixmap' of the QGraphicsPixmapItem, and draw the QImage to it partly, like this:
//some initialization of variables
QGraphicsScene scene = ...;
QImage img = ...; //some image data that I wish to manipulate from time to time
QPixmap pixmap = QPixmap::fromImage(this->shown);
QPainter painter = new QPainter(&this->pixmap);
QGraphicsPixmapItem item = this->addPixmap(this->pixmap);
item->setPixmap(this->pixmap);
//this should not matter, but actually it does, as I will explain shortly
//delete painter;
//painter = new QPainter(item->pixmap());
//For some reason I decide to update (manimulate) img within a small boundigbox
int x0, y0, width, height; //bounding box, assume they are set to whatever is appropriate for the previous update
painter->drawImage (x0, y0, img, x0, y0, width, height);
//now the pixmap is updated, unfortunately the item is not. This does not affect it:
item->update(x0, y0, width, height);
//nor does this:
item->update();
//but this, which makes the whole thing slow, does:
item.setPixmap(&pixmap);
Given I that I needed to set the pixmap to fix it, I assumed it was somehow not set in the initialization, therefore uncommenting the mentioned lines before seemed like a nice idea. Unfortunately, the drawImage() call then segfaults into:
QPaintDevice: Cannot destroy paint device that is being painted
I would like to have an alternative to the "item.setPixmap(&pixmap);", which does not redraw the whole thing, but does work nicely. Any input is very well appreciated :)
Before I propose a solution, a few thoughts:
First, the Graphics View framework is intended to be a solution for displaying many graphic objects, so one large image isn't really that fitting. Of course, I realize your example is probably just a contrived one, so this point might not really apply. Second, since the framework is very transform-centric, it might not make sense to only redraw parts of a QGraphicsItem unless all the transforms are identity, there is no scrolling, etc.
Anyways, if you only want to draw part of a QGraphicsItem, you could simply store the rect that needs to be updated, and access it from inside your paint() method. For example:
CustomItem::setPaintRect(const QRectF &rect)
{
paintRect = rect;
update();
}
CustomItem::paint(QPainter *painter /* etc. */)
{
painter->fillRect(paintRect, brush);
}
I have found this: Getting the size of a QGraphicsView
But I can't figure out what does it mean to "move my initialization code to showEvent" and I can't comment on that answer.
I am want to resize a QPixmap so it could fit my QGraphicsView. I've placed my graphicsview in Designer and set GridLayout for my main window. In a MainWindow constructor I have written the following code:
ui->setupUi(this);
// Get GView size
g_sizeX = ui->mapView->width();
g_sizeY = ui->mapView->height();
// Init scene
scene = new QGraphicsScene(this);
// Init MAP pixmap and add it to scene
mapImage = new QPixmap(":/Map/europe.jpg");
QPixmap newmapImage = mapImage->scaled(g_sizeX, g_sizeY);
scene->addPixmap(newmapImage);
// Display scene in gview.
ui->mapView->setScene(scene);
But I always get size of 100x30. If I break the gridLayout, I get the correct size.
So, how should I deal with this?
Thank you.
The QGraphicsView will be resized by the QGridLayout after the widget is shown, and can be also resized later when the window is itself resized.
So you should change the size of the pixmap as a result of a QResizeEvent, either by subclassing QGraphicsView to redefine resizeEvent(), and then promoting your view object to your new class in the designer to use it instead of QGraphicsView, or by installing your MainWindow object as an event filter for the view to handle to the resize event from the MainWindow::eventFilter function.
You probably don't want to change the pixmap size in the scene, but rather adjust the view matrix so that your QGraphicsPixmapItem fits perfectly inside the view, with QGraphicsView::fitInView.
For example:
/* QGraphicsPixmapItem *pixmapItem; as a MainWindow member */
pixmapItem = scene->addPixmap(newmapImage);
/* Either always disable or enable the scrollbars (see fitInView doc) */
ui->mapView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
ui->mapView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
ui->mapView->installEventFilter(this);
...
bool MainWindow::eventFilter(QObject *obj, QEvent *evt) {
if(obj == ui->mapView && evt->type() == QEvent::Resize) {
ui->mapView->fitInView(pixmapItem, Qt::KeepAspectRatioByExpanding);
}
// Call the base class implementation
return QMainWindow::eventFilter(obj, evt);
}
I believe that what is happening is that Qt only applies layouts and sets widget sizes when the widget is first displayed.
One way to work that is to override QWidget::showEvent(), and put your sizing code in there.
However, one simpler way, that often works in constructors, is to ask the widget for its sizeHint(), rather than for its not-yet-layed-out size.
In your case, that would mean changing two lines of code to:
g_sizeX = ui->mapView->sizeHint().width();
g_sizeY = ui->mapView->sizeHint().height();
If your layout isn't too complicated, and if you haven't overridden the default size policies, this may well fix things for you.
I am having some issues with the size of qt4 widgets when their content changes.
I will illustrate my problems with two simple scenarios:
Scenario 1:
I have a QLineEdit widget. Sometimes, when I'm changing its content using QLineEdit.setText(), the one-line string doesn't fit into the widget at its current size anymore. I must select the widget and use the arrow keys to scroll the string in both directions in order to see it all.
Scenario 2:
I have a QTextEdit widget. Sometimes, when I'm changing its content using QTextEdit.setHtml(), the rendered HTML content doesn't fit into the widget at its current size anymore. The widget starts displaying horizontal and/or vertical scroll bars and I can use them to scroll the HTML content.
What I would want in such scenarios is to have some logic that decides if after a content change, the new content won't fit anymore into the widget and automatically increase the widget size so everything would fit.
How are these scenarios handled?
I'm using PyQt4.
Edit: after reading both the comment and the first answer (which mentions typing content into the widget), I went over the question one more time. I was unpleasantly surprised to find out a horrible typo. I meant QTextBrowser when I wrote QTextEdit, my apologies for misleading you. That is: I have a widget which renders HTML code that I'm changing and I would want the widget to grow enough to display everything without having scrollbars.
As for QLineEdit instead of QLabel - I went for QLineEdit since I've noticed I can't select text from a QLabel with the mouse for copying it. With QLineEdit it is possible.
I'm answering in C++ here, since that's what I'm most familiar with, and your problem isn't specific to PyQt.
Normally, you just need to call QWidget::updateGeometry() when the sizeHint() may have changed, just like you need to call QWidget::update() when the contents may have changed.
Your problem, however, is that the sizeHint() doesn't change when text is added to QLineEdit and QTextEdit. For a reason: People don't expect their dialogs to grow-as-they-type :)
That said, if you really want grow-as-you-type behaviour in those widgets you need to inherit from them and reimplement sizeHint() and minimumSizeHint() to return the larger size, and potentially setText(), append() etc. to call updateGeometry() so the sizehint change is noticed.
The sizehint calculation won't be entirely trivial, and will be way easier for QLineEdit than for QTextEdit (which is secretly a QAbstractScrollArea), but you can look at the sizeHint() and minimumSizeHint() implementations for inspiration (also the one for QComboBox, which has a mode to do exactly what you want: QComboBox::AdjustToContents.
EDIT: Your two usecases (QTextBrowser w/o scrollbars and QLineEdit instead of QLabel just for selecting the text in there) can be solved by using a QLabel and a recent enough Qt. QLabel has gained both link-clicking notification and so-called "text-interaction flags" (one of which is TextSelectableByMouse) in Qt 4.2. The only difference that I was able to make out is that loading new content isn't automatic, there's no history, and there's no micro focus hinting (ie. tabbing from link to link) in QLabel.
For the QTextBrowser case you should be able to get the size of the document using
QTextBrowser::document()->size();
after setting the html, and then resizing it the QTextBrowser afterwards.
i achieve a similar effect by using the following C++ class:
textedit.h
#ifndef TEXTEDIT_H
#define TEXTEDIT_H
#include <QTextEdit>
class TextEdit : public QTextEdit
{
Q_DISABLE_COPY( TextEdit )
public:
TextEdit( QWidget* parent = NULL );
TextEdit( const QString& text, QWidget* parent = NULL );
virtual ~TextEdit();
void fitToDocument( Qt::Orientations orientations );
virtual QSize sizeHint() const;
private:
int fittedHeight_;
Qt::Orientations fittedOrientations_;
int fittedWidth_;
};
#include "textedit-inl.h"
#endif // TEXTEDIT_H
textedit-inl.h
#ifndef TEXTEDITINL_H
#define TEXTEDITINL_H
#include "textedit.h"
inline TextEdit::TextEdit( QWidget* parent ) :
QTextEdit( parent ), fittedOrientations_( 0 )
{ }
inline TextEdit::TextEdit( const QString& text, QWidget* parent ) :
QTextEdit( text, parent ), fittedOrientations_( 0 )
{ }
inline TextEdit::~TextEdit()
{ }
inline QSize TextEdit::sizeHint() const
{
QSize sizeHint = QTextEdit::sizeHint();
if( fittedOrientations_ & Qt::Horizontal )
sizeHint.setWidth( fittedWidth_ );
if( fittedOrientations_ & Qt::Vertical )
sizeHint.setHeight( fittedHeight_ );
return sizeHint;
}
#endif // TEXTEDITINL_H
textedit.cpp
#include "textedit.h"
void TextEdit::fitToDocument( Qt::Orientations orientations )
{
QSize documentSize( document()->size().toSize() );
QSizePolicy sizePolicy( QSizePolicy::Preferred, QSizePolicy::Preferred );
if( orientations & Qt::Horizontal ) {
fittedWidth_ = documentSize.width() + (width() - viewport()->width());
sizePolicy.setHorizontalPolicy( QSizePolicy::Fixed );
}
if( orientations & Qt::Vertical ) {
fittedHeight_ = documentSize.height() + (width() - viewport()->width());
sizePolicy.setVerticalPolicy( QSizePolicy::Fixed );
}
fittedOrientations_ = orientations;
setSizePolicy( sizePolicy );
updateGeometry();
}
for example, calling TextEdit::fitToDocument( Qt::Horizontal ) will set the widget's width to a fixed width exactly large enough to fit the document and its surroundings (e.g. a vertical scrollbar, if there is one). if your goal is to have this happen whenever the contents change, connect the QTextEdit::textChanged() signal to a slot which calls TextEdit::fitToDocument().
as for your issue with QLabel, the solution is simple: call QLabel::setTextInteractionFlags( Qt::LinksAccessibleByMouse | Qt::TextSelectableByMouse ).
Maybe take a look at Python QT Automatic Widget Resizer. It's written in python though but it may give you some ideas on how to go about doing what you need.
Ok implement sizeHint() method. And every time your content change size call updateGeometry()
When content change without changing size use update(). (updateGeometry() automatically call update()).