Why does my window layout change when I change my selection? - python

If you look at my two images here, you'll notice that switching between the two selections changes the size of the QListWidget on the right.
I have it laid out using a QGridLayout. Here is the relevant code snippet, with pn_list being the relevant widget, but you can see the full code here:
def initUI(self):
# Layouts
grid = QtGui.QGridLayout()
ov_main = QtGui.QHBoxLayout()
ov_col1 = QtGui.QFormLayout()
ov_col2 = QtGui.QVBoxLayout()
ov_custs = QtGui.QFormLayout()
# Main window widgets
self.pn_input = QtGui.QLineEdit()
overviewBox = QtGui.QGroupBox('Overview')
self.pn_list = MyListWidget()
# Main window layout
grid.addWidget(QtGui.QLabel('Part Number'), 1, 1)
grid.addWidget(self.pn_input, 1, 2)
grid.addWidget(overviewBox, 2, 1, 1, 2)
grid.addWidget(self.pn_list, 1, 3, 2, 1)
Update: Also, as a follow-on question, what can I do to prevent this from happening? Finally, I would like the "Description" field to wrap when the line-length exceeds a pre-determined character limit - I currently have my presenter truncating the "Description" when it exceeds this length. How would I have the QLabel wrap at a given character width?

Figured it out. On the MyListWidget (which extends QListWidget) I overwrote the sizeHint method to return the width, in pixels, that I want my widget to be:
class MyListWidget(QtGui.QListWidget):
def sizeHint(self):
hint = QtCore.QSize()
hint.setWidth(120)
return hint
Then, in my initUI method, I added a line to the #Main window layout section:
# Main window layout
grid.addWidget(QtGui.QLabel('Part Number'), 1, 1)
grid.addWidget(self.pn_input, 1, 2)
grid.addWidget(overviewBox, 2, 1, 1, 2)
grid.addWidget(self.pn_list, 1, 3, 2, 1)
This kept the widget on the right a constant width, however I still had problems with varying length descriptions messing things up. I added the following lines to my initUI method to similarly deal with the description QLabel widget and now all is well:
myQLabel.sizeHint = lambda: QtCore.QSize(150, -1)
myQLabel.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Preferred)
myQLabel.setWordWrap(True)
The lambda statement is essentially a quicker way of achieving what I did with the QListWidget. In the QListWidget case, I was already re-implementing the class in order to add a custom signal, so I added the sizeHint code there.

Related

How to make QGridLayout resize cells the exact same size?

Whenever I resize the window (a QDialog), Reference Viewer and Selected Viewer (subclasses of QScrollArea) should have the exact same size at all time, even after a resize event.
However, once out of twice, I get a size 1 pixel smaller for the Selected Viewer (QScrollArea widget on the right). By once out of twice, I mean every odd pixel count.
It seems that the QGridLayout is forcing the right-most panel to that smaller size, probably due to rounding down the value of the space still available.
I use a QGridLayout because I need the toolbar to stay aligned in the center between the panels and it works well.
Here is a screencast demonstrating the problem: you can see the scrollbar showing up every-time the Selected Viewer (panel on the right) is resized one pixel shorter in width compared to the panel on the left.
Here is mostly what I'm doing:
verticalLayout = QVBoxLayout(self)
verticalLayout.setSpacing(0)
verticalLayout.setContentsMargins(0, 0, 0, 0)
gridLayout = QGridLayout()
# Minimum width for the toolbar in the middle:
gridLayout.setColumnMinimumWidth(1, 30)
gridLayout.setColumnStretch(0,1)
gridLayout.setColumnStretch(1,0)
gridLayout.setColumnStretch(2,1)
gridLayout.setSpacing(3)
selectedImageViewer = ScrollAreaImageViewer(self)
gridLayout.addWidget(selectedImageViewer, 0, 0, 3, 1)
verticalToolBar = QToolBar(self)
verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
gridLayout.addWidget(verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
referenceImageViewer = ScrollAreaImageViewer(self)
gridLayout.addWidget(referenceImageViewer, 0, 2, 3, 1)
verticalLayout.addLayout(gridLayout)
I add another widget below in the QVBoxLayout but it's irrelevant here.
I have tried adding spacers but it doesn't seem to change anything:
gridLayout.addItem(QSpacerItem(5,0, QSizePolicy.Minimum), 1, 3, 1, 1, Qt.Alignment(Qt.AlignCenter))
Is there a way to ensure both Viewers get the same size without using resize() on them on every resizeEvent()?
Or should this actually be considered a bug in Qt?
I have tried the following which works around the scrollbar flickering issue:
def resizeEvent(self, event):
self.gridLayout.setColumnMinimumWidth(0, self.selectedImageViewer.size().width())
self.gridLayout.setColumnMinimumWidth(2, self.selectedImageViewer.size().width())
But the sizes still differ by one pixel once out of twice.
Edit: here is a minimal reproducible example
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtWidgets import (QDialog, QLayout, QVBoxLayout,
QLabel, QSizePolicy, QToolBar, QGridLayout,
QWidget, QApplication )
class MyWidget(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.label = QLabel(self)
def resizeEvent(self, event):
self.label.setText(f"{self.size()}")
class MyDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.setMinimumSize(QSize(500, 100))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout = QGridLayout()
self.gridLayout.setColumnMinimumWidth(1, 30)
self.gridLayout.setColumnStretch(0,1)
self.gridLayout.setColumnStretch(1,0)
self.gridLayout.setColumnStretch(2,1)
self.gridLayout.setSpacing(3)
self.selectedImageViewer = MyWidget(self)
self.gridLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1)
self.verticalToolBar = QToolBar(self)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
self.gridLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.referenceImageViewer = MyWidget(self)
self.gridLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
self.verticalLayout.addLayout(self.gridLayout)
def main():
app = QApplication([()])
window = QWidget()
dialog = MyDialog(window)
dialog.show()
return app.exec()
if __name__ == "__main__":
main()
I assume that problem is with ScrollArea being used for referenceImageViewer, so in each resize event actual referenceImageViewer is trying to add a horizontal scrollbar to itself.
As a solution you can
Set referenceImageViewer's adjust policy to (QAbstractScrollArea.AdjustIgnored or QAbstractScrollArea.AdjustToContents).
Try to use Widgets instead of ScrollAreaImageViewer instances in gridLayout, and then adding ScrollAreaImageViewer inside that widgets.
Edited.
There must be difference between width of 1st and 3rd widgets as long
as ToolBar's width is fixed. E.g when window width is 501 and toolbar
width is fixed at 20 auto alignment can't equally divide remaining 481
pixels in a half..
As a solution your toolbar must be resizable too.
For reducing ToolBar width changes you can increase 1st and 3rd column
stretch in GridLayout for example to value 8, and set 2nd column
stretch to 1, so layout will automatically adjust width of each
column.

PyQt5: A Label Within A Label?

I'd like to make part of the text of a label clickable, like an inline hyperlink on a website. I know how to make an individual label clickable, but I'm not sure how to only make part of the label clickable and still maintain a consistent format.
I've placed the code for my first attempt below and included an image of the output.
The two issues I see are the noticeable space between the labels (which even a QStretchItem at the end doesn't fix) and the issues with word wrapping.
Any help would be greatly appreciated. Thank you!
from PyQt5.QtWidgets import *
app = QApplication([])
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle('Title')
self.setGeometry(1200, 200, 350, 500)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# Dummy list to print
place_list = { '2000': 'An event happened.',
'2005': 'An event at {this place} happened long ago.',
'2010': 'Another event happened at {a different place}, but it was not fun.' }
# Initialize Grid of Notes
grid = QGridLayout()
# Create Headers for each column
grid.addWidget(QLabel('Date'), 0, 0)
grid.addWidget(QLabel('Note'), 0, 1)
index = 1
# Iterate through each entry in place_list
for year in place_list:
# Add index of entry (by year)
grid.addWidget(QLabel(year), index, 0)
# Get text of entry
note = place_list[year]
# Look for "{}" to indicate link
if '{' in note:
# Get location of link within the entry
start = note.find('{')
end = note.find('}')
# Create a label for the text before the link
lab_1 = QLabel(note[:start])
lab_1.setWordWrap(True)
# Create a label for the link
# NOTE: It's a QLabel for formatting purposes only
lab_2 = QLabel(note[start+1:end])
lab_2.setWordWrap(True)
# Create a label for the text after the link
lab_3 = QLabel(note[end+1:])
lab_3.setWordWrap(True)
# Combine the labels in one layout
note_lab = QHBoxLayout()
note_lab.addWidget(lab_1)
note_lab.addWidget(lab_2)
note_lab.addWidget(lab_3)
# Add the layout as the entry
grid.addLayout(note_lab, index, 1)
else:
# Create the label for the whole entry if no link indicator is found
note_lab = QLabel(note)
note_lab.setWordWrap(True)
grid.addWidget(note_lab, index, 1)
# Go to next row in grid
index += 1
self.layout.addLayout(grid)
window = MainWindow()
window.show()
app.exec_()
The best solution I believe is to subclass QLabel and override the mousePressEvent method.
def mousePressEvent(event):
# event.pos() or .x() and .y() to find the position of the click.
If you create a QRect in the area that you want in the initialization of your custom QLabel, you can easily check if the click is inside the rectangle by using the QRect.contains() method as well.
Other useful methods for this would be mouseReleaseEvent and mouseDoubleClickEvent.
And in general, when you are adding/changing functionality to widgets, look to subclass first.

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

How to create list-like gui in python with gtk3?

I am trying to create this kind of list-like user interface gui (without Glade) within my InputPage object, that can be seen throughout gnome3 user interface:
and all I have is this (note that I would only like to add the buttons + and - with the same style they appear in the first image, I would like to keep columns of my list just as it is):
for some reason buttons are wider than expected and the toolbar doesnt fill the horizontal space.
here is my code:
class InputPage(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self)
self.grid = Gtk.Grid()
self.grid.set_column_homogeneous(True)
self.grid.set_row_homogeneous(True)
self.add(self.grid)
#Creating the ListStore model
self.software_liststore = Gtk.ListStore(str, str, int)
for software_ref in software_list:
self.software_liststore.append(list(software_ref))
#creating the treeview, making it use the filter as a model, and adding the columns
self.treeview = Gtk.TreeView()
for i, column_title in enumerate(["Name", "Frequency", "Amplitude"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
self.treeview.append_column(column)
#button.connect("clicked", self.on_selection_button_clicked)
#setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 10, 7)
#Toolbar
list_add = Gtk.Button()
list_add.add(Gtk.Image(icon_name='list-add-symbolic', visible=True))
list_insert_object = Gtk.Button()
list_insert_object.add(Gtk.Image(icon_name='insert-object-symbolic', visible=True))
list_remove = Gtk.Button()
list_remove.add(Gtk.Image(icon_name='list-remove-symbolic', visible=True))
self.toolbar = Gtk.ButtonBox(spacing=5)
self.toolbar.get_style_context().add_class('inline-toolbar')
self.toolbar.add(list_add)
self.toolbar.add(list_remove)
self.toolbar.add(list_insert_object)
self.toolbar.set_hexpand(True)
self.grid.attach(self.toolbar, 0,7,4,1)
#self.grid.attach_next_to(self.toolbar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1)
# for i, button in enumerate(self.buttons[1:]):
#self.grid.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
self.scrollable_treelist.add(self.treeview)
self.show_all()
What you see on the bottom of the first screenshot is a standard GtkToolbar with the inline-toolbar style class.
A GtkToolbar does not necessarily need to be on the top of a window. You can place them anywhere, just like regular widgets. All widgets have a style context that defines how the widget looks; each style context has one or more classes applied to it. These classes are CSS classes; styling a widget is just like saying class="classname" in HTML.
Here is one possible way to do this:
Create a vertical GtkBox with no spacing.
Place the GtkTreeView as the first child of the GtkBox.
Create a GtkToolbar and place it as the second child of the GtkBox.
Call the get_style_context() method on the GtkToolbar.
Call the add_class() method of the style context, passing the string "inline-toolbar". (I believe there is a symbolic constant for this; I do not know what this is in Python.)
Recreate your buttons as GtkToolButtons (NOT regular GtkButtons!) that are children of the GtkToolbar.
If that doesn't result in the buttons looking connected, you can add the "linked" class to the toolbar in the same way.
Good luck!

Categories

Resources