Empty spaces between Widgets even with setSpacing(0) - python

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
data = {'dia': 23, 'mes': 8, 'ano': 2017}
class FormConfigBkpBD(QWidget):
senha = None
pathLine = None
def __init__(self, parent=None,instancia=None):
super(FormConfigBkpBD, self).__init__(parent)
mainLayout = QVBoxLayout()
mainLayout.setSpacing(0)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.senha = QLineEdit()
self.senha.setEchoMode(QLineEdit.Password)
self.senha.setFixedSize(385, 25)
self.pathLine = QLineEdit()
self.pathLine.setFixedSize(355,25)
self.senha.setContentsMargins(0,0,0,0)
self.pathLine.setContentsMargins(0,0,0,0)
mainLayout.addWidget(self.pathLine,0,Qt.AlignTop)
mainLayout.addWidget(self.senha,0,Qt.AlignTop)
self.setWindowTitle("ConfiguraĆ§Ć£o de Backup - Banco de Dados")
self.setFixedSize(460,640)
self.setLayout(mainLayout)
def main():
app = QApplication(sys.argv)
bd = FormConfigBkpBD()
bd.show()
app.exec()
if __name__ == "__main__" :
main()
The above code shows only two widgets (QLineEdit), the original window has much more widgets, this is only for post here.
Why, even with setSpacing(0) and setContentsMargins(0,0,0,0) there's a space between widgets?

It's because of the combination between the fixed size of your window and the layout you are using. The vertical layout tries to distribute evenly (unless told otherwise) all of its children vertically and fill the whole window.
Here you have two widgets so the first one goes in the first cell and the second one goes in the second cell. Since you haven't set any alignment the default behaviour is present that is top left corner of each cell is where the widget is positioned. The rest is just filling the free space of the parent window which the layout is part of.
If you want not space between the widgets
either remove the fixed size of the window
or put more widgets in the layout until it's "full".
The first option might not be what you want to do (otherwise you would have probably not set it to fixed size in the first place). The second can be achieved by simply using a QSpacerItem (vertical one) that will take care of the empty space for you and push the widgets to the top where you want these to be:
mainLayout.addStretch(1)
You can also add the spacer manually if you need more control over its behaviour:
self.spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) # or Fixed
mainLayout.addItem(self.spacer)

Related

QLabel Wrap-text height not resizing

How can i fix the QLabel to not clip the text when resizing? This is a widget that will be placed inside a QDialog eventually. So the resizing of the Dialog will happen if a user resizes the main dialog.
'''
Main Navigation bar
'''
################################################################################
# imports
################################################################################
import os
import sys
import inspect
from PySide2 import QtWidgets, QtCore, QtGui
################################################################################
# widgets
################################################################################
class Context(QtWidgets.QWidget):
def __init__(self):
super(Context, self).__init__()
# controls
self.uiThumbnail = QtWidgets.QLabel()
self.uiThumbnail.setMinimumSize(QtCore.QSize(100, 75))
self.uiThumbnail.setMaximumSize(QtCore.QSize(100, 75))
self.uiThumbnail.setScaledContents(True)
self.uiThumbnail.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.uiThumbnail.setObjectName('thumbnail')
self.uiDetailsText = QtWidgets.QLabel()
self.uiDetailsText.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.uiDetailsText.setWordWrap(True)
self.uiDetailsText.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
self.uiDetailsText.setOpenExternalLinks(True)
self.uiMenuButton = QtWidgets.QPushButton()
self.uiMenuButton.setFixedSize(QtCore.QSize(24, 24))
self.uiMenuButton.setFocusPolicy(QtCore.Qt.NoFocus)
# header layout
self.headerLayout = QtWidgets.QHBoxLayout()
self.headerLayout.setSpacing(6)
self.headerLayout.setContentsMargins(6, 6, 6, 6)
self.headerLayout.setAlignment(QtCore.Qt.AlignTop)
self.headerLayout.addWidget(self.uiThumbnail)
self.headerLayout.addWidget(self.uiDetailsText)
self.headerLayout.addWidget(self.uiMenuButton)
self.headerLayout.setAlignment(self.uiThumbnail, QtCore.Qt.AlignTop)
self.headerLayout.setAlignment(self.uiMenuButton, QtCore.Qt.AlignTop)
# frames
self.headerFrame = QtWidgets.QFrame()
self.headerFrame.setObjectName('panel')
self.headerFrame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.headerFrame.setFrameShadow(QtWidgets.QFrame.Raised)
self.headerFrame.setLayout(self.headerLayout)
# layout
self.mainLayout = QtWidgets.QHBoxLayout()
self.mainLayout.setSpacing(6)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.addWidget(self.headerFrame)
self.setLayout(self.mainLayout)
self.setStyleSheet('''
#thumbnail {
background-color: rgb(70,70,70);
}
#panel {
background-color: rgb(120,120,120);
border-radius:3px;
}
''')
self.updateContext()
# methods
def updateContext(self):
self.uiDetailsText.setText('''
<span style="font-size:14px;">
<b>A title goes here which can wrap</b>
</span>
<br>
<span style="font-size:11px;">
<b>Status:</b> Additional details go here
<br>
<b>User:</b>
User information goes here
<br>
<b>About:</b> Some more information
<br>
<b>Date:</b> 2021-07-03
<br>
</span>
''')
################################################################################
# main
################################################################################
if __name__ == '__main__':
pass
app = QtWidgets.QApplication(sys.argv)
ex = Context()
ex.resize(500,70)
ex.show()
sys.exit(app.exec_())
I tried adding this and it didn't help at all...
# methods
def resizeEvent(self, event):
newHeight = self.uiDetailsText.heightForWidth(self.uiDetailsText.width())
self.uiDetailsText.setMaximumHeight(newHeight)
event.accept()
The size policy of a QLabel is always Preferred (in both directions), and if word wrapping or rich text is used, the text layout engine will try to find an optimal width based on the contents. This unfortunately creates some issues in layout managers, as explained in the layout documentation:
The use of rich text in a label widget can introduce some problems to the layout of its parent widget. Problems occur due to the way rich text is handled by Qt's layout managers when the label is word wrapped.
Also consider the following line:
self.headerLayout.setAlignment(QtCore.Qt.AlignTop)
it will align the layout item (headerLayout) to the top of the headerFrame, which creates problems with the size hint of the label, since the size policy is Preferred. It's normally unnecessary (and often discouraged) to set the alignment of a layout if it's the top level layout of a widget.
Unfortunately, there's no easy solution for these situations, because:
to allow "free" resizing, the label cannot have any size constraints;
as soon as a widget is mapped, the size hint is just that: a hint; if the user resizes a window that contains wrapped text, the size choosen by the user is respected, even if that results in partially hiding the text;
the size hint considers the heightForWidth of children only when the layout is activated, after that the top level window will ignore any hint and will only honor the user choice, limited by the minimum size (or minimum size hint) of widgets;
heightForWidth() is called only for widgets that do not have a layout, otherwise the layout item's heightForWidth() will be called;
There are workarounds, but they are not always reliable.
A possible solution is to resize the top level window whenever the height doesn't respect the widget's heightForWidth(). Note that this is not completely reliable, and it's not 100% safe, as it requires to call a resize inside a resize event. If any of the parent has some functions that acts on delayed calls related to layouts (including changing the geometry), it might cause recursion problems.
class Context(QtWidgets.QWidget):
resizing = False
# ...
def resizeEvent(self, event):
super().resizeEvent(event)
if self.resizing:
return
diff = self.heightForWidth(event.size().width()) - self.height()
if diff > 0:
self.resizing = True
target = self
while not target.windowFlags() & (QtCore.Qt.Window | QtCore.Qt.SubWindow):
target = target.parent()
target.resize(target.width(), target.height() + diff)
self.resizing = False
Note that for this to work you still have to set the Expanding vertical size policy or remove the line for the layout alignment.
Most importantly, this can only be used only for a single widget in a window, so you should consider implementing it in the top level widget.

PyQt6 auto-resize QTextEdit to fit content problem

I have a bunch of QTextEdits in a QVBoxLayout in a QScrollArea.
The texts can often get very long and the horizontal space is limited by design, and QTextEdit automatically wraps text in multiple lines which is good.
I want to automatically resize the QTextEdit to fit to the wrapped text, the text itself will always be in one line, and the wrapped text can have multiple lines, I want the QTextEdits to fit to the height of wrapped lines.
By hours upon hours of Google searching, I have found a solution, but it doesn't work as expected, there can sometimes be one extra line at the bottom, I will post example code below:
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
font = QFont('Noto Serif', 9)
class Editor(QTextEdit):
doubleClicked = pyqtSignal(QTextEdit)
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
self.margins = self.contentsMargins()
self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
#pyqtSlot(QMouseEvent)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
self.doubleClicked.emit(self)
def autoResize(self):
self.show()
height = int(self.document().size().height() + self.margins.top() + self.margins.bottom())
self.setFixedHeight(height)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.resize(405, 720)
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
self.centralwidget = QWidget(self)
self.vbox = QVBoxLayout(self.centralwidget)
self.scrollArea = QScrollArea(self.centralwidget)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.vbox.addWidget(self.scrollArea)
self.setCentralWidget(self.centralwidget)
def addItems():
items = [
"L'Estro Armonico No. 9 in D Major\u2014III. Allegro",
"L'Estro Armonico No. 6 in A Minor\u2014III. Presto",
"L'Estro Armonico No. 6 in A Minor\u2014I. Allegro",
"L'Estro Armonico No. 1 in D major\u2014I. Allegro",
"12 Concertos Op.3 \u2014 L'estro Armonico \u2014 Concerto No. 6 In A Minor For Solo Violin RV 356\u2014Presto",
"Ultimate Mozart\u2014 The Essential Masterpieces",
"Serenade in G K.525 Eine kleine Nachtmusik\u20141. Allegro",
"Vivaldi\u2014 L'estro Armonico",
"Academy of St. Martin in the Fields",
"Are You With Me \u2014 Reality"
]
for i in items:
textbox = Editor()
textbox.setText(i)
window.verticalLayout.addWidget(textbox)
app = QApplication([])
window = Window()
window.show()
addItems()
app.exec()
Note you will need Noto Serif for it to run correctly (and of course you can just replace it), and of course you need PyQt6.
In the example, the first seven textboxs all have one extra empty line at the bottom, and the last three don't have it.
What caused the extra line and how to remove it?
Update:
I have to set Qt.WidgetAttribute.WA_DontShowOnScreen because if I don't set it, calling .show() of QTextEdit will cause the QTextEdit show up in the middle of the screen and quickly disappear, and it's annoying.
I have to call .show() because calling document().size() without show(), the values will all be 0 and I don't know why it is like this.
The problem comes from the fact that you're trying to set the height too early. In fact, if you add a print(self.show()) just after show(), you'll see that all will show a default size (probably, 256x192).
This depends on two aspects:
when a widget is shown the first time, it's not yet completely "mapped" in the OS window management, so it will use default sizes depending on many aspects;
you're setting the text before adding it to the layout, so the QTextEdit will know nothing about the required size of the parent;
Then another problem arises: if the window is resized, the contents will not adapt until the text is changed.
In order to properly set a vertical height based on the contents, you should set the document's textWidth, and also call autoResize everytime the widget is resized.
class Editor(QTextEdit):
def __init__(self):
super().__init__()
self.setReadOnly(True)
self.setFont(font)
self.textChanged.connect(self.autoResize)
def autoResize(self):
self.document().setTextWidth(self.viewport().width())
margins = self.contentsMargins()
height = int(self.document().size().height() + margins.top() + margins.bottom())
self.setFixedHeight(height)
def resizeEvent(self, event):
self.autoResize()
Note that:
the margins should be dynamically accessed, not stored in the __init__;
mouseDoubleClickEvent is a function that is called on a mouse event, it's not (nor it should) be a slot, so using the pyqtSlot decorator is pointless;
while conceptually fine for a "main" layout like in the case of a layout for the scroll area contents, setting the alignment of a layout doesn't set the alignment of its items, but only that of the layout; while the result is often the same, in practice it's very different (so the result is not always the same, especially if more layouts are added to the same parent layout);
double click in text fields is very commonly used for advanced selection (usually, select the word under the cursor), and choosing to prevent such action (thus changing a known UI convention) should be taken into careful consideration;

Issue about PyQt5

I am trying to create a simple interface like this:
from PyQt5 import QtWidgets,QtGui
class program():
def __init__(self):
self.window = QtWidgets.QWidget()
self.window.setWindowTitle("how many click")
self.text = QtWidgets.QLabel(self.window)
self.text.setText(("not clicked"))
self.text.setGeometry(240,200,300,50)
self.text2 = QtWidgets.QLabel(self.window)
self.picture = QtWidgets.QLabel(self.window)
self.button=QtWidgets.QPushButton(self.window)
self.button.setText("click")
self.button.setFont(QtGui.QFont('',10))
self.button.setGeometry(250,100,200,50)
self.window.setGeometry(600,200,800,600)
self.window.show()
self.count=0
self.button.clicked.connect(self.click)
def click(self):
self.count+= 1
self.text.setText(((f"you clicked {self.count} times")))
self.text.setFont(QtGui.QFont('',10))
if self.count == 5:
self.text2.setText(("You clicked too much"))
self.text2.setGeometry(250, 250, 300, 50)
self.picture.setPixmap(QtGui.QPixmap("C:/Users/Administrator/Desktop/mypic.png"))
self.picture.move(300, 300)
app = QtWidgets.QApplication(sys.argv)
run= program()
sys.exit(app.exec_())
In this code my picture appears when I click 5 times to button but picture becomes very tiny as in pic1. However when I write setPixmap and picture.move codes into init function picture becomes normal size as in pic2.
pic1:
pic2:
The simple solution to your issue is to add the following line after setting the pixmap:
self.picture.adjustSize()
The direct reason is that when when the widget is shown the label has not yet a pixmap, so its geometry is already set to its minimum size (defaults to 100x30). Then, when the pixmap is set, the label doesn't automatically update its size.
The logical reason is that you are using fixed geometries for your widget, and this approach is generaly discouraged for lots of reasons, with the most important being the fact that elements within a window should always adapt their geometries (size and position) to the size of the parent, possibly by occupying all the available space and preventing the elements to become invisible if the window is resized to a smaller size.
To avoid that, you should always use layout managers (in your case, a QVBoxLayout could be enough).
For example:
class program():
def __init__(self):
self.window = QtWidgets.QWidget()
# ...
layout = QtWidgets.QVBoxLayout(self.window)
layout.addWidget(self.text)
layout.addWidget(self.text2)
layout.addWidget(self.picture)
layout.addWidget(self.button)
# it's good practice to always show the window *after* adding all elements
self.window.show()

How to prevent window and widgets in a pyqt5 application from changing size when the visibility of one widget is altered

I want to create a dialog, in which the user should first select one item in a drop down, and for some choices specify an additional parameter. For the sake of the example let's say that the possible choices are A and B and for B the user has to enter a text. The text field should not be visible when A is selected.
Here is a MWE:
#!/usr/bin/env python
import sys
from PyQt5.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QLineEdit
class Example(QDialog) :
def __init__(self, parent=None) :
super(QDialog, self).__init__(parent)
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.comboBox = QComboBox()
self.comboBox.addItems(['A', 'B'])
self.mainLayout.addWidget(self.comboBox, 0, 0)
self.lineEdit = QLineEdit('')
self.lineEdit.setMinimumWidth(50)
self.mainLayout.addWidget(self.lineEdit, 0, 1)
self.comboBox.activated[str].connect(self.update)
self.update(str(self.comboBox.currentText()))
def update(self, choice) :
if 'B' in choice :
self.lineEdit.setVisible(True)
else :
self.lineEdit.setVisible(False)
if __name__ == '__main__':
app = QApplication(sys.argv)
example = Example()
example.show()
sys.exit(app.exec_())
The problem is, that when initially choice A is presented, the size of the dialog is just enough for the comboBox. When option B is selected, the window is expanded and everything is as it should be. However, when option A is selected again, the comboBox' width increases, taking up all of the avalaible space, instead of leaving empty space to the right.
How can I have space allocated for the text field, no matter if visible or not? What am I missing here?
EDIT The answer by S.Nick solves the problem of the MWE in a way, but not the way I was hoping for: As soon as the scenario is more complex, widgets get reallocated again, e.g. if a QLabel is added in front of the comboBox
self.label = QLabel('label')
self.mainLayout.addWidget(self.label, 0, 0)
self.comboBox = QComboBox()
self.comboBox.addItems(['A', 'B'])
self.mainLayout.addWidget(self.comboBox, 0, 1, alignment=Qt.AlignLeft)
self.lineEdit = QLineEdit('', self)
self.lineEdit.setMinimumWidth(50)
self.mainLayout.addWidget(self.lineEdit, 0, 2)
then the comboBox is flipped around when changing the selection. What I want is that, once in the beginning space and position is allocated for each widget and that the space and position is permanent no matter if any widget is visible or not.
You could try something like this:
def __init__(self, parent=None) :
super(QDialog, self).__init__(parent)
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.label = QLabel('label')
self.mainLayout.addWidget(self.label, 0, 0)
self.comboBox = QComboBox()
self.comboBox.addItems(['A', 'B'])
self.mainLayout.addWidget(self.comboBox, 0, 1)
self.lineEdit = QLineEdit('', self)
self.lineEdit.setMinimumWidth(200)
self.mainLayout.addWidget(self.lineEdit, 0, 2)
self.comboBox.activated[str].connect(self.update)
self.mainLayout.setColumnStretch(2,1)
self.adjustSize()
self.update(str(self.comboBox.currentText()))
self.mainLayout.setColumnStretch(2,1) will make sure that the last column will take up all the extra horizontal space even when the line edit widget is hidden.
self.adjustSize() adjusts the size of the main window to the sum of the sizes of all its child widgets. Since at this point the line edit widget is still visible, its size is taken into account as well when the size of the main window is adjusted.
Screenshots
Initial window:
After selecting B:

PyQt Image(pixmap) gets cropped when other content changes width in a widget

I'm making a table-like widget that displays an image, the file name, and two box-selection areas. I have two objects 'grid_row' & 'grid_table' (both using QGridLayout), grid_row being a single row and grid_table containing x number of grid_rows (I'm designing it like this because it's simply easier to keep track of my custom properties).
The tool looks like this
The final layout is a QVBoxLayout, then from top to bottom, I have QHBoxLayout(the one with a label and combobox), grid_row(for the headers 1,2,3), a scroll_area that contains the grid_table with each one being grid_rows. Lastly another QHBoxLayout for the buttons.
Each grid_row contains a 'image-widget', and two region labels(QLabel). The image widget contains a label(I used setPixmap for display) and a pushbutton. Here are my grid_row and image_widget classes:
class grid_row(QWidget):
def __init__(self, parent=None):
super().__init__()
#self.frame = frame_main()
self.grid_layout = QGridLayout()
self.grid_layout.setSpacing(50)
self.image_widget = image_widget()
self.grid_layout.addWidget(self.image_widget, 0, 0, 1, 1, Qt.AlignHCenter)
self.region_2 = QLabel('null')
self.grid_layout.addWidget(self.region_2, 0, 2, 1, 1, Qt.AlignHCenter)
self.setLayout(self.grid_layout)
self.region_1 = QLabel('null')
self.grid_layout.addWidget(self.region_1, 0, 1, 1, 1, Qt.AlignHCenter)
class image_widget(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.image_widget_layout = QHBoxLayout()
self.image_widget_label = QLabel()
self.image_widget_label.setPixmap(QPixmap('default.png').scaled(96, 54))
self.image_widget_layout.addWidget(self.image_widget_label)
self.img_btn = QPushButton()
self.img_btn.setEnabled(False)
self.img_btn.setText('Drag Here!')
self.image_widget_layout.addWidget(self.img_btn)
self.setLayout(self.image_widget_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
layout = QVBoxLayout()
grid_row = grid_row()
layout.addWidget(grid_row)
btn = QPushButton('press')
btn.clicked.connect(lambda: grid_row.region_1.setText('[0,0,1920,1080]'))
layout.addWidget(btn)
widget.setLayout(layout)
scroll_area = QScrollArea()
scroll_area.setWidget(widget)
scroll_area.show()
sys.exit(app.exec_())
So currently, I've implemented events that allow me to drag images into the image_widget and click the push button to modify the two regions that are framed (format: [x1, y1, x2, y2]). The problem is that when I do that(e.g. region values go from 'null' to say '[20,20, 500, 500]', the image gets squished because now the labels are taking up more width.
I realize that some size policy needs to be set (and maybe other properties) but I don't know which property to use and on which widget. I want the image to remain the same. Maybe stretch out the width of each column for the grid_row?
To clarify, I want the label containing the pixmap to remain the same size (always 96*54) and fully displayed(not cropped or stretched) at all times.
I've provided the a simplified executable code to display my problem, the classes are the same as my code, I just only put grid_row inside the scroll_area and added a button to change one of the values of the region to simulate the situation. Can provide additional code if needed. Thanks in advance!
Wow sometimes the answer is really one extra line of code...
So the documentation mentions that QScrollArea by default honors the size of its widget. Which is why when I changed the region (to a value that's wider/ more text) the widget does not auto adjust.
I needed to add
scroll_area.setWidgetResizable(True)
to allow the widget to resize wider thus prompting the scroll bars to appear. This way my pixmap image doesn't get cropped from not having enough space.
The easiest way would be to add size constraints to the label before adding to the layout
self.image_widget_label.adjustSize()
self.image_widget_label.setFixedSize(self.image_widget_label.size())
self.image_widget_layout.addWidget(self.image_widget_label)
adjustSize would resize the label depending on the contents.
The more difficult way is to answer the questions :
"when I change the size of the overall window, how do I want this
particular item to behave? When the window is at its minimal size,
which items do I want hidden or out of view? When the window is full
size, where do I want empty spots?"
To answer these better read a bit on Qt Layout management

Categories

Resources