Set Vertical Alignment of QFormLayout QLabel - qt

I'm using PySide/PyQt, but this is a general Qt question.
Is there a way to set up a QFormLayout so that labels are centered vertically without having to explicitly create the QLabel's and set their vertical size policy to expanding first? When the widget in column 2 is taller than my label, I want my label to be centered vertically with the widget, rather than aligned with it's top...
Here's an example script that demonstrates the problem. I've colored the labels red to better demonstrate their size behavior.
from PySide import QtCore, QtGui
app = QtGui.QApplication([])
widget = QtGui.QWidget()
widget.setStyleSheet("QLabel { background-color : red}")
layout = QtGui.QFormLayout()
layout.setFieldGrowthPolicy(QtGui.QFormLayout.AllNonFixedFieldsGrow)
layout.setLabelAlignment(QtCore.Qt.AlignCenter)
editor1 = QtGui.QLineEdit()
editor1.setFixedSize(300, 100)
editor2 = QtGui.QLineEdit()
editor2.setFixedSize(300, 100)
layout.addRow('Input', editor1)
layout.addRow('Longer Named Input', editor2)
widget.setLayout(layout)
widget.show()
app.exec_()
Here's the outcome:
Here's an example that demonstrates the desired result by explicitly creating QLabel's and giving them an expanding size policy:
from PySide import QtCore, QtGui
app = QtGui.QApplication([])
widget = QtGui.QWidget()
widget.setStyleSheet("QLabel { background-color : red}")
layout = QtGui.QFormLayout()
layout.setFieldGrowthPolicy(QtGui.QFormLayout.AllNonFixedFieldsGrow)
layout.setLabelAlignment(QtCore.Qt.AlignCenter)
editor1 = QtGui.QLineEdit()
editor1.setFixedSize(300, 100)
editor2 = QtGui.QLineEdit()
editor2.setFixedSize(300, 100)
label1 = QtGui.QLabel('Input')
expand = QtGui.QSizePolicy.Expanding
label1.setSizePolicy(expand, expand)
label2 = QtGui.QLabel('Longer Named Input')
label2.setSizePolicy(expand, expand)
layout.addRow(label1, editor1)
layout.addRow(label2, editor2)
widget.setLayout(layout)
widget.show()
app.exec_()
And here's that outcome...
I've tried QFormLayout.setLabelAlignment() which doesn't appear to help. The docs even mention that setLabelAlignment only does the horizontal alignment of the labels (and even then doesn't appear to do centered, just left or right).
As an aside, this lead me to also try to set the horizontal alignment to centered, but that proved even harder since the labels don't expand horizontally to fill the space (small labels don't expand to match the biggest label). The only way I could get horizontally centered labels was to explicitly find the width of the biggest label, after showing the widget, then set all other labels to have the same width...
labels = [layout.itemAt(i*2).widget() for i in range(layout.rowCount())]
max_width = max(label.width() for label in labels)
for w in labels:
w.setFixedWidth(max_width)
w.setAlignment(QtCore.Qt.AlignCenter)
Which results in this:
Is there anything I'm missing at the QFormLayout level that will center the labels? Do I have to make QLabels and set to expanding or turn on expanding after the fact (like below)? Thanks for any ideas!
expand = QtGui.QSizePolicy.Expanding
labels = [layout.itemAt(i*2).widget() for i in range(layout.rowCount())]
for w in labels:
w.setSizePolicy(expand, expand)

"[2] Labels can only be aligned left (by default) or right" - I don't believe this is true.
Your code doesn't run for me, so I can't test your exact code. However, this method works for other widgets: notice the use of | to separate the various positional commands.
label2.setAlignment(PyQt5.QtCore.Qt.AlignLeft|PyQt5.QtCore.Qt.AlignVCenter)
I get this is a few years old, so you've probably come up with another way, but this method works well for me.

I don't think there is an elegant solution to your problem.
From QFormLayout's source code:
void QFormLayoutPrivate::arrangeWidgets(const QVector<QLayoutStruct>& layouts, QRect &rect)
{
// [...]
if (label) {
int height = layouts.at(label->vLayoutIndex).size;
if ((label->expandingDirections() & Qt::Vertical) == 0) {
/*
If the field on the right-hand side is tall,
we want the label to be top-aligned, but not too
much. So we introduce a 7 / 4 factor so that it
gets some extra pixels at the top.
*/
height = qMin(height,
qMin(label->sizeHint.height() * 7 / 4,
label->maxSize.height()));
}
[1] QSize sz(qMin(label->layoutWidth, label->sizeHint.width()), height);
int x = leftOffset + rect.x() + label->layoutPos;
[2] if (fixedAlignment(q->labelAlignment(), layoutDirection) & Qt::AlignRight)
[ ] x += label->layoutWidth - sz.width();
[ ] QPoint p(x, layouts.at(label->vLayoutIndex).pos);
// ### expansion & sizepolicy stuff
label->setGeometry(QStyle::visualRect(layoutDirection, rect, QRect(p, sz)));
}
// [...]
}
What do we see here?
[1] Labels do not stretch horizontally
[2] Labels can only be aligned left (by default) or right
So you have to either somehow manually synchronize labels' widths (e.g. set fixed one) or abandon QFormLayout and use QGridLayout instead.

Related

Window resizing based on label size doesn't consider title bar in pyQT5

I'm coding a simple image viewer and would like for the window to resize based on the image that I open.
The window I'm using is a QMainWindow and has a toolbar. The only widget I have is a QLabel which is set as the central widget. When I open the image I use self.resize(self.label.sizeHint()), but the window size doesn't take into account the size of the title bar and the toolbar, so for example if I open a 400x400 image the window will be of the correct width, but a little bit too short.
What would be the correct way to calculate the correct window size so that it resizes correctly on every platform? (Windows, macOS, Linux)
EDIT: the minimal code is:
import PyQt5.QtWidgets as w
import PyQt5.QtGui as g
import PyQt5.QtCore as c
import sys
class ImageViewerWindow(w.QMainWindow):
def __init__(self):
super().__init__()
self.loadedImagePaths = []
self.imageIndex = 0
self.scrollArea = w.QScrollArea()
self.label = w.QLabel()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)
self.label.setAlignment(c.Qt.AlignCenter)
self.label.setMinimumSize(1,1)
# Actions
self.openAction = w.QAction("Open...", self)
self.openAction.setShortcut(g.QKeySequence.Open)
self.openAction.triggered.connect(self.openMenuDialog)
# Toolbar elements
toolbar = w.QToolBar("Top toolbar")
toolbar.setMovable(False)
toolbar.setContextMenuPolicy(c.Qt.PreventContextMenu)
self.addToolBar(toolbar)
# Status bar elements
self.setStatusBar(w.QStatusBar(self))
# Add actions to toolbar and menu
toolbar.addAction(self.openAction)
def showImageAtIndex(self, index):
image = g.QPixmap(self.loadedImagePaths[index])
self.label.setPixmap(image)
self.scrollArea.setWidget(self.label)
self.imageIndex = index
self.angle = 0
self.label.adjustSize()
self.resize(self.label.sizeHint())
def openMenuDialog(self, firstStart = False):
self.loadedImagePaths, _ = w.QFileDialog.getOpenFileNames(parent=self, caption="Select one or more JPEG files to open:", filter="JPEG Image(*.jpg *.jpeg)")
if self.loadedImagePaths:
if firstStart:
self.show()
self.imageIndex = 0
self.showImageAtIndex(self.imageIndex)
elif firstStart:
sys.exit()
a = w.QApplication([])
ivw = ImageViewerWindow()
ivw.openMenuDialog(firstStart = True)
a.exec()
If you try and open an image and then resize the window you will notice that some of the image is covered by the title bar and the status bar.
The main problem is that using setWidgetResizable():
the scroll area will automatically resize the widget in order to avoid scroll bars where they can be avoided
So you have to remove that line, or use setFixedSize() on the label using the image size.
Then, calling adjustSize() on the label is not enough, as you actually need to call adjustSize() against the top level window: this is because calling resize() with the image size won't consider all other widgets in the window (in your case, the toolbar and status bar).
Unfortunately, that won't be enough, as QScrollArea caches the size hint of the widget, and calling again setWidget() with the same widget is useless.
The easiest solution is to use a subclass of QScrollArea and reimplement the sizeHint().
Finally, the alignment only has effect on the label contents, but when the widget is added to a container the alignment has to be set for the widget.
class ScrollAreaAdjust(w.QScrollArea):
def sizeHint(self):
if not self.widget():
return super().sizeHint()
frame = self.frameWidth() * 2
return self.widget().sizeHint() + c.QSize(frame, frame)
class ImageViewerWindow(w.QMainWindow):
def __init__(self):
super().__init__()
self.loadedImagePaths = []
self.imageIndex = 0
self.scrollArea = ScrollAreaAdjust()
self.label = w.QLabel()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidget(self.label)
self.scrollArea.setAlignment(c.Qt.AlignCenter)
# ...
def showImageAtIndex(self, index):
image = g.QPixmap(self.loadedImagePaths[index])
self.label.setPixmap(image)
self.label.setFixedSize(image.size())
self.imageIndex = index
self.angle = 0
self.scrollArea.updateGeometry()
self.adjustSize()
Note that the size hint of a top level window will only be respected until the size doesn't exceed 2/3 of the screen size. This means that if the image will force the window to a slightly bigger size, at least one scroll bar will be shown, even if it's not strictly necessary.
There is no obvious nor universal solution for that, and you need to find your own way. For instance, you can check if the scroll bars are visible after adjusting the size and eventually compare the size of the image and that of the scroll area's viewport, then if one of the image dimensions is just smaller by the size of the opposite scroll bar, force a resizing of the top level window by that scroll bar size.

How can I wrap text in QGraphicsItem?

1) How can I wrap text in a QGraphicsTextItem to fit a fixed rectangle, with width and height ?
Right now I am experimenting with creating a text, getting its bounding rectangle, and resizing it to fit the box - but I can't get wrapping.
class TTT: public QGraphicsTextItem {
TTT() {
{
setPlainText("abcd");
qreal x = m_itemSize.width()/boundingRect().width();
qreal y = m_itemSize.height()/boundingRect().height();
scale(x, y);
}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) {
// experiment with clip regions
// text gets covered by hole in clip
QRegion r0(boundingRect().toRect());
QRegion r1(QRect(5, 5, 10, 10), QRegion::Ellipse);
QRegion r2 = r0.subtracted(r1);
painter->setClipRegion(r2);
painter->setBrush(Qt::yellow);
painter->drawRect(boundingRect());
QGraphicsTextItem::paint(painter, option, widget);
}
}
What makes wrapping happen, how can I trigger it ?
Right now, as I keep typing, the box is automatically expanding.
2) Is it possible to wrap the text in a QGraphicsItem / QGraphicTextItem subclass in a shape that is not a rectangle ?
(Something like in the image above)
I tried to use clipRegion, see code above, but I guess it is not the right way to go, clipping cuts the text but did not wrap.
Maybe it would... If I could figure out how to wrap text in the first place ?
Qt 4.8
You did not specify Qt version but try:
void QGraphicsTextItem::setTextWidth(qreal width)
Sets the preferred width for the item's text. If the actual text is wider than >the specified width then it will be broken into multiple lines.
If width is set to -1 then the text will not be broken into multiple lines >unless it is enforced through an explicit line break or a new paragraph.
The default value is -1.
In answer to 1) I'd opt not to use the QGraphicsTextItem, but draw the text directly in your QGraphicsItem's paint function using the drawText overloaded function, which takes a QTextOption parameter.
Using this, you can set the WrapMode, for example, with a call to
QTextOption::setWrapMode(QTextOption:: WordWrap)
As for 2) with a non-rectangular shape, I don't think Qt will do this for you.
Doing it yourself you can use QFontMetrics, to work out just how much text would fit in each line, depending upon where it lies within its bounding item.
Alternatively, you could adapt the concept of a text-to-path method.

QVBoxLayout: How to vertically align widgets to the top instead of the center

In Qt, When I add widgets to my layout, they are vertically centered by default. Is there a way to "List" the widgets from top to bottom instead of centering them vertically?
If you have a QVBoxLayout and want your fixed size widgets to be stacked at the top, you can simply append a vertical stretch at the end:
layout.addStretch()
If you have multiple stretchers or other stretch items, you can specify an integer stretch factor argument that defines their size ratio.
See also addStretch and addSpacerItem.
Add two layout.addStretch() before and after adding the widgets to center them vertically:
layout.addStretch()
layout.addWidget(self.message)
layout.addWidget(self.userid_field)
layout.addWidget(self.password_field)
layout.addWidget(self.loginButton)
layout.addStretch()
Not sure whether this answers your original question, but it is the answer to the one that I had when googling and being led to this page - so it might be useful for others too.
use void QLayout::setAlignment ( Qt::Alignment alignment ) method to set alignment according to your choice.
I find this a little more complicated than just using layout.setAlignment(). It kept not working for me until just now, when I figured out that if you have expanding widgets that you set a maximum height for, then that widget will not be aligned the way you want.
Here is example code that does not top align the QTextBrowser() widget even though I call layout.setAlignment(Qt.AlignTop). Sorry that it is in Python, but it is pretty easy to translate to C++ (I have gone the other way many times).
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWidget(QWidget):
"""
Create a widget that aligns its contents to the top.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QVBoxLayout()
label = QLabel('label:')
layout.addWidget(label)
info = QTextBrowser(self)
info.setMinimumHeight(100)
info.setMaximumHeight(200)
layout.addWidget(info)
# Uncomment the next line to get this to align top.
# layout.setAlignment(info, Qt.AlignTop)
# Create a progress bar layout.
button = QPushButton('Button 1')
layout.addWidget(button)
# This will align all the widgets to the top except
# for the QTextBrowser() since it has a maximum size set.
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
widget.resize(QSize(900, 400))
app.exec_()
The following explicitly calls layout.setAlignment(info, Qt.AlignTop) to get the expanding text widget to work.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWidget(QWidget):
"""
Create a widget that aligns its contents to the top.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QVBoxLayout()
label = QLabel('label:')
layout.addWidget(label)
info = QTextBrowser(self)
info.setMinimumHeight(100)
info.setMaximumHeight(200)
layout.addWidget(info)
# Uncomment the next line to get this to align top.
layout.setAlignment(info, Qt.AlignTop)
# Create a progress bar layout.
button = QPushButton('Button 1')
layout.addWidget(button)
# This will align all the widgets to the top except
# for the QTextBrowser() since it has a maximum size set.
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
widget.resize(QSize(900, 400))
app.exec_()
After comparison between the two solutions, it seems that :
myLayout.setAlignment(Qt::AlignTop)
works for several widget alignement but :
myLayout.setAlignment(myWidget, Qt::AlignTop)
works only for the first widget you add to the layout.
After all, the solution depends also to the QSizePolicy of yours widgets.
If you are using QT creator, you just add a "Vertical Spacers" at the bottom of your widget.
In pyQt (and PySide) we have Qt.AlignCenter (align on main direction), Qt.AlignHCenter (align on horizontal direction), Qt.AlignVCenter (align on vertical direction), use one of it when you need it.

QListWidget adjust size to content

Is it possible to adjust QListWidget height and width to it's content?
sizeHint() always returns 256, 192 no matter what its content is.
QListWidgetItem's sizeHint() returns -1, -1, so I can not get content width.
Problem the same as here - http://www.qtcentre.org/threads/31787-QListWidget-width , but there is no solution.
import sys
from PyQt4.QtGui import *
class MainWindow(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
list = QListWidget()
list.addItem('111111111111111')
vbox = QVBoxLayout(self)
vbox.addWidget(list)
app = QApplication(sys.argv)
myapp = MainWindow()
myapp.show()
sys.exit(app.exec_())
sizeHint() always returns 256, 192 no
matter what its content is.
Thats because this is the size of the QListWidget, the viewport, not the items. sizeHintForColumn() will give you the max size over all items, so you can resize the widget like this:
list.setMinimumWidth(list.sizeHintForColumn(0))
If you don't want to force minimum width, then subclass and provide this as the size hint instead. E.g.:
class ListWidget(QListWidget):
def sizeHint(self):
s = QSize()
s.setHeight(super(ListWidget,self).sizeHint().height())
s.setWidth(self.sizeHintForColumn(0))
return s
Using takois answer I played around with the sizeHintForColumn or sizeHintForRow and found that you have to add slightly larger numbers, because there might be some style dependent margins still. ekhumoros comment then put me on the right track.
In short the full size of the list widget is:
list.sizeHintForColumn(0) + 2 * list.frameWidth()
list.sizeHintForRow(0) * list.count() + 2 * list.frameWidth())
According to the comment by Violet it may not work in Qt 5.
Also be aware that setting the size to the content, you don't need scrollbars, so I turn them off.
My full example for a QListWidget ajusted to its content size:
from PySide import QtGui, QtCore
app = QtGui.QApplication([])
window = QtGui.QWidget()
layout = QtGui.QVBoxLayout(window)
list = QtGui.QListWidget()
list.addItems(['Winnie Puh', 'Monday', 'Tuesday', 'Minnesota', 'Dracula Calista Flockhart Meningitis', 'Once', '123345', 'Fin'])
list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
list.setFixedSize(list.sizeHintForColumn(0) + 2 * list.frameWidth(), list.sizeHintForRow(0) * list.count() + 2 * list.frameWidth())
layout.addWidget(list)
window.show()
app.exec_()
In order to effectively use sizeHint, you have to override it, at least in c++. In my experience, the default implementations for widgets can be pretty useless when you want a specific behavior. Attempts to force what you want with spacers or layouts end in disaster. If you can derive from QListWidget and override sizeHint, you can iterate through your items and find the longest string, then do some kind of magic to determine how wide it should be.
That's what I'd do, anyway.
First you should get your largest string in the list, that is easy to obtain.
After you get that string, do the following:
QFontMetrics * fm = new QFontMetrics(widget->font());
QRect rect;
rect = fm->boundingRect(string);
rect.width() has the width in pixels of the largest string
rect.height() has it's height.
Set the QListWidget width to that rect's width (plus the margins)
and it's height to that rect's height times the number of items
I didn't test the code, but hope it puts you on the right track
QListWidget *valList;
valList = new QListWidget(this);
valList->setSizePolicy (QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored));
valList->setMinimumSize (QSize(1111, 111));
You need to get the QHeaderView of your QListWidget and adjust its resize mode.
Read this documentation for more information
http://doc.qt.nokia.com/latest/qheaderview.html#ResizeMode-enum

How to make QLineEdit expand inside QScrollArea

I have a QLabel and a QLineEdit inside a QWidget. When I have the widget inside a QScrollArea, the line edit does not expand to occupy the excess width of the window. When the widget is not inside the scroll area, it does expand.
I've tried setting the size policy of the line edit and the widget, to expand horizontally, but it doesn't occupy the excess space. I suspect the sizeHint() of the widget is compacted when inside a scroll area. Any ideas how to make this work?
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self, None)
self.setWindowTitle('Test Window')
self.resize(500, 250)
scrollArea = QtGui.QScrollArea()
scrollWidget = QtGui.QWidget()
scrollWidget.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)
layout = QtGui.QGridLayout(scrollWidget)
label = QtGui.QLabel("Name:")
layout.addWidget(label, 0, 0)
lineEdit = QtGui.QLineEdit("Value")
lineEdit.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)
layout.addWidget(lineEdit, 0, 1)
scrollWidget.setLayout(layout)
scrollArea.setWidget(scrollWidget)
self.setCentralWidget(scrollArea)
I believe I have solved your problem.
Make the following addition to your code and it should behave correctly:
...
scrollArea.setWidget(scrollWidget)
scrollArea.setWidgetResizable(True) #add this
self.setCentralWidget(scrollArea)
...
From the docs,
widgetResizable : bool
This property holds whether the scroll area should resize the view widget.
If this property is set to true, the scroll area will automatically resize the widget in order to avoid scroll bars where they can be avoided, or to take advantage of extra space.

Resources