How to add custom space between widgets in QVboxLayout? - python

I am trying to move last 2 widgets (btn1, btn2) apart from the rest of widgets in vbox layout. I use insertSpacing with index of the widget below but it moves both widgets down by 500. How can I move down btn1 by 20 and btn2 by 500 ?
filter_layout = QVBoxLayout()
data_filters = [label1, combo1, label2, combo2, label3, combo3, label4, combo4, label5, combo5, btn1, btn2]
[filter_layout.addWidget(wg) for wg in data_filters]
filter_layout.insertSpacing(10, 20)
filter_layout.insertSpacing(11, 500)
filter_layout.addStretch()

Qt layout managers use abstract items called QLayoutItem in order to represent an element that is managed by a layout.
Those items normally contain widgets, but they can also contain other layouts or spacers (QSpacerItem).
What happens in your case is that when you call the following line:
filter_layout.insertSpacing(10, 20)
you're inserting a spacer item between element at index 10 and 11, so the next insertion is actually just after that spacer you added, because it's like you added an empty widget between the last two elements.
As you probably had already figured out, knowing all this it's simple to find the solution: you either set the next spacer counting the index insertion, or you invert the insertion order.
filter_layout.insertSpacing(10, 20)
filter_layout.insertSpacing(12, 500)
# or, alternatively:
filter_layout.insertSpacing(11, 500)
filter_layout.insertSpacing(10, 20)

Related

Tkinter, update widgets real time if list is modified

Let say I've a list of widgets that are generated by tkinter uisng a loop (it's customtkinter in this case but since tkinter is more well known so I think it'd be better to make an example with it), each widgets lie in the same frame with different label text. Here is an example for the code:
x=0
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)
My question is, if the list that help generated those widgets change (in this case it's just a loop ranging from 0-100, might change the widgets text, list size..), what would be the best way for real time update the tkinter window contents?
Ps: I've tried to look for my answer from many places but as of right now, the best answer I can come up with is to update the whole frame with same grid but changed list content, I'll put it bellow. Is there any way better than this? Thank you
Like I said before, while the existing answer might work, it might be inefficient since you are destroying and creating new widgets each time there is a change. Instead of this, you could create a function that will check if there is a change and then if there is extra or less items, the changes will take place:
from tkinter import *
import random
root = Tk()
def fetch_changed_list():
"""Function that will change the list and return the new list"""
MAX = random.randint(5, 15)
# Create a list with random text and return it
items = [f'Button {x+1}' for x in range(MAX)]
return items
def calculate():
global items
# Fetch the new list
new_items = fetch_changed_list()
# Store the length of the current list and the new list
cur_len, new_len = len(items), len(new_items)
# If the length of new list is more than current list then
if new_len > cur_len:
diff = new_len - cur_len
# Change text of existing widgets
for idx, wid in enumerate(items_frame.winfo_children()):
wid.config(text=new_items[idx])
# Make the rest of the widgets required
for i in range(diff):
Button(items_frame, text=new_items[cur_len+i]).pack()
# If the length of current list is more than new list then
elif new_len < cur_len:
extra = cur_len - new_len
# Change the text for the existing widgets
for idx in range(new_len):
wid = items_frame.winfo_children()[idx]
wid.config(text=new_items[idx])
# Get the extra widgets that need to be removed
extra_wids = [wid for wid in items_frame.winfo_children()
[-1:-extra-1:-1]] # The indexing is a way to pick the last 'n' items from a list
# Remove the extra widgets
for wid in extra_wids:
wid.destroy()
# Also can shorten the last 2 steps into a single line using
# [wid.destroy() for wid in items_frame.winfo_children()[-1:-extra-1:-1]]
items = new_items # Update the value of the main list to be the new list
root.after(1000, calculate) # Repeat the function every 1000ms
items = [f'Button {x+1}' for x in range(8)] # List that will keep mutating
items_frame = Frame(root) # A parent with only the dynamic widgets
items_frame.pack()
for item in items:
Button(items_frame, text=item).pack()
root.after(1000, calculate)
root.mainloop()
The code is commented to make it understandable line by line. An important thing to note here is the items_frame, which makes it possible to get all the dynamically created widgets directly without having the need to store them to a list manually.
The function fetch_changed_list is the one that changes the list and returns it. If you don't want to repeat calculate every 1000ms (which is a good idea not to repeat infinitely), you could call the calculate function each time you change the list.
def change_list():
# Logic to change the list
...
calculate() # To make the changes
After calculating the time for function executions, I found this:
Widgets redrawn
Time before (in seconds)
Time after (in seconds)
400
0.04200148582458496
0.024012088775634766
350
0.70701003074646
0.21500921249389648
210
0.4723021984100342
0.3189823627471924
700
0.32096409797668457
0.04197263717651367
Where "before" is when destroying and recreating and "after" is only performing when change is needed.
So I've decided that if I want to click a button, that button should be able to update the list. Hence, I bind a non-related buttons in the widget to this function:
def sidebar_button_event(self):
global x
x=10
self.scrollable_frame.destroy()
self.after(0,self.update())
Which will then call for an update function that store the change value, and the update function will just simply overwrite the grid:
def update(self):
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)

Prevent ListBoxItems from being resized (specifically shrunk)

I have a Gtk Listbox to which I'm adding a large number of items (basically text labels). I've put the ListBox inside a ScrolledWindow but if I add too many items then the height of each item is reduced until the text on the label is no longer readable.
How can I prevent the ListBox items from being reduced in height as I add more of them?
The code I'm using to create the ListBox and add the items looks like this:
# Add the listbox
self.test_list_window = Gtk.ScrolledWindow();
self.test_list = Gtk.ListBox()
self.test_list.connect("row_activated", some_method)
self.test_list_window.add(self.test_list)
The adding of the items is done with this method (each ListBox item has a LHS and RHS label). I thought that the set_size_request would add a minimum size to the ListBox entries but it does not appear to do so (also setting a specific height in pixels feels like the wrong answer I just want to prevent the rows from shrinking).
def add_list_box_entry(self, lhs, rhs, lbox, set_min_size=False):
box = Gtk.Box()
if set_min_size:
box.set_size_request(10, 10)
box.pack_start(Gtk.Label(label=lhs, xalign=0), True, True, 1)
lab = Gtk.Label(label=f'({rhs})')
lab.set_halign(0.95)
box.pack_start(lab,False, True, 5)
lbox.add(box)
To resolve this behavior you need to call show_all() on the Listbox after adding the items.
If you don't there is a vertically tiny entry in the Listbox for each item you added. This can be clicked on but that doesn't display the widgets it contains and so can give the impression that the items have been scaled down from their normal height.

How to Find the number of rows and columns in QGridlayout in PyQt5?

How to find the number of rows and columns in QGridlayout ?, In my code, I have Buttons arranged in QGridLayout. Now I need to find out the total number of columns and the total number of rows.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Widget(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("QGridlayout")
self.btn1 = QPushButton("Button_1")
self.btn2 = QPushButton("Button_2")
self.btn3 = QPushButton("Button_3")
self.btn4 = QPushButton("Button_4")
self.btn4.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.MinimumExpanding)
self.btn5 = QPushButton("Button_5")
self.btn6 = QPushButton("Button_6")
self.btn7 = QPushButton("Button_7")
self.btn8 = QPushButton("Button_8")
self.btn9 = QPushButton("Button_9")
self.gl = QGridLayout()
self.gl.addWidget(self.btn1,1,0,1,1,Qt.AlignCenter)
self.gl.addWidget(self.btn2,0,1,1,1)
self.gl.addWidget(self.btn3,0,2,1,1)
self.gl.addWidget(self.btn4,0,3,2,1)
self.gl.addWidget(self.btn5,1,0,1,2)
self.gl.addWidget(self.btn6,2,0,1,3)
self.gl.addWidget(self.btn7,3,0,1,4)
self.gl.addWidget(self.btn8,1,2,1,1)
self.gl.setRowStretch(4,1)
self.gl.setColumnStretch(2,1)
self.gl.setSpacing(1)
self.setLayout(self.gl)
print(self.gl.count())
# print(self.gl.rowcount())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
For basic usage, rowCount() and columnCount() will suffice, as already said in other answers.
Be aware, though, that the above will only be reliable for the following conditions:
layout items (widgets, nested layouts, spacers) are always added continuously: for instance, grid layout allows adding an item at row 0 and another at row 10, even if they only occupy one row and no other item occupies the inner rows and even if no row spanning occurs in the middle;
removal of items will never clear the row or column count, even if there is no item at or after the "last" row or column;
rowCount() and columnCount() will always return a number equal or greater than 1, even if the layout is empty;
considering the above, dynamic insertion of new items between existing ones can become quite messy;
To explain the above, consider the following example:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
app = QApplication([])
window = QWidget()
layout = QGridLayout(window)
def addRow():
row = layout.rowCount()
label = QLabel(str(row))
label.setStyleSheet('border: 1px solid black;')
label.setMinimumSize(100, 30)
label.setAlignment(Qt.AlignCenter)
layout.addWidget(label)
def removeRow():
item = layout.itemAt(layout.count() - 1)
widget = item.widget()
if widget:
widget.deleteLater()
layout.removeItem(item)
for r in range(4):
addRow()
window.show()
QTimer.singleShot(1000, removeRow)
QTimer.singleShot(2000, addRow)
QTimer.singleShot(3000, removeRow)
QTimer.singleShot(4000, addRow)
app.exec()
The steps are the following:
create the first 4 labels; note that the label text shows the rowCount() before adding it to the layout: the text of the first label is "1", even if, at that moment, the layout is empty; the second label shows again "1", since the layout now does have an item at row 1;
remove the last label ("3") and the layout item corresponding to it; the layout now theoretically has only 3 rows; the layout item removal is actually pointless for this, since it won't change the result (I only added it for explanation purposes);
add another label; the new label displays "4", not "3";
remove the "4" label; the layout theoretically has only 3 rows again;
add a further label, which displays "5";
While counter-intuitive, this behavior is done just for performance, simplicity and consistency reasons: the row/column count is static, and always based on the greatest row/column index (plus possible spanning if greater than 1). While adding/removing items within the same function call, the grid size must be consistent (and as fast as possible). Also, the computation of layout managers is possibly quite important for performance, as each layout has to query its own child items, ask about their size hints and policies and then make complex computations to set their geometries, and all this becomes recursive for nested layouts.
If you want a more dynamic approach you can still implement it, for example with this simple (but costly) function:
def gridLayoutRealRowCount(layout):
rows = 0
for i in range(layout.count()):
row, _, span, _ = layout.getItemPosition(i)
rows = max(1, row + span)
return rows
Now you can replace the row = layout.rowCount() used before with the above function:
def addRow():
row = gridLayoutRealRowCount(layout)
# ...
And you'll get a more consistent result: the labels will always show "0", "1", "2" (and eventually "3"), even after removing the last item.
You can also make a "shim" (or "pseudo-polyfill") if you create a monkey patch after the very first Qt import; assuming all your Qt imports are consistent, you can use it from anywhere:
def gridLayoutRealRowCount(layout):
rows = 0
for i in range(layout.count()):
row, _, span, _ = layout.getItemPosition(i)
rows = max(1, row + span)
return rows
def gridLayoutRealColumnCount(layout):
columns = 0
for i in range(layout.count()):
_, column, _, span = layout.getItemPosition(i)
columns = max(1, columns + span)
return columns
QGridLayout.getRowCount = gridLayoutRealRowCount
QGridLayout.getColumnCount = gridLayoutRealColumnCount
Which allows the following:
def addRow():
row = layout.getRowCount()
# ...
If you require high performance and you're not interested on a "rigid" grid layout, you should consider other options, like a QFormLayout or a nested vertical+horizontal layout.
For the number of columns: QGridLayout.columnCount()
For the number of rows: QGridLayout.rowCount()
num_rows = self.gl.rowCount()
num_columns = self.gl.columnCount()
QGridLayout has methods rowCount() and columnCount() that you can use to get the rows and columns respectively. I see you commented out a call to rowcount, I am guessing you may just need to change that to rowCount

How to limit the rightest Gtk.TreeViewColumn's width inside a Gtk.ScrolledWindow?

I cannot prevent the rightest column of a Gtk.TreeView to expand.
As the real Gtk.TreeView may display a greater number of rows, making it usually somewhat greater than the screen's height, it is embedded in a Gtk.ScrolledWindow. This is required. Without it, attaching an empty grid at the right of the treeview, expanding itself horizontally, would fix the problem. Based on this idea, I've tried a workaround that introduces another difficulty (see below).
I have built a minimal working example from the example from https://python-gtk-3-tutorial.readthedocs.io/en/latest/treeview.html#filtering, without filtering nor buttons; and the columns are 80 px wide at least (this works) and their content is horizontally centered. This last detail makes the horizontal expansion of the rightest column visible. In the original example, it does expand too, but as everything is left aligned, this is not really visible. I'd liked to keep the columns' content centered, without seeing the rightest expanded.
This example is minimal, but contains some helping features: you'll find clickable column titles, that will display some information about the clicked column in the console; a remove button (works fine, remove the selected rows) and a paste button that allows to paste new rows from a selection (e.g. from selected lines from a spreadsheet, but there's nothing to check the data are correct, if you paste something that does not convert to int, it will simply crash).
Workaround
A workaround I've tried consist of gathering both the treeview and a horizontally expanding empty right grid at its right inside a grid that would be put inside the Gtk.ScrolledWindow. It works, but causes other subtle problems: in some situations, the treeview does not get refreshed (it happens after a while), yet nothing prevents the main loop to refresh the view (there's no other processing in the background, for instance). To experiment this workaround: comment and uncomment the lines as described in the code below; run the program via python script.py (if you need to install pygobject in a venv, see here), notice the rightest column does not expand to the right any longer, select the 3 first rows and press "remove", then from a spread sheet, select 3 lines of dummy integers as shown below and then press "paste". Scroll down to the last rows: you'll see most of the time that the 3 pasted lines do not show up, even if it is possible to scroll over the last row. Maybe one of them will show up after some time, then another... (or simply select a row, and they'll show up). Strangely, it happens if one has just removed as many lines as one wants to paste after the removal (3 removed, 3 pasted; or 4 removed, 4 pasted etc.).
Example spreadsheet selection:
Question
So, I'd prefer to avoid the workaround (I'm afraid I may find other situations triggering a bad refreshing of the treeview), that I could not fix itself (for instance, setting self.scrollable_treelist.set_propagate_natural_height(True) proved useless, maybe I'm not using it correctly though?) and only attach the treeview itself directly in the Gtk.ScrolledWindow. How to prevent the rightest column to expand, then?
(I've tried to use a fair amount of setters and properties of the cell renderers, the treeview, the treeview columns, the scrolled window, to no avail. Some of them are still in the code below.)
Any solution using and fixing the workaround above would be accepted though.
In any case, the treeview may be scrolled, and lines may be added and removed from it without any refreshing problem.
Source Code
import gi
try:
gi.require_version('Gtk', '3.0')
except ValueError:
raise
else:
from gi.repository import Gtk, Gdk
# ints to feed the store
data_list = [(i, 2 * i, 3 * i, 4 * i, 5 * i) for i in range(40)]
class AppWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Treeview Columns Size Demo")
self.set_border_width(10)
# Setting up the self.grid in which the elements are to be positioned
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.store = Gtk.ListStore(int, int, int, int, int)
for data_ref in data_list:
self.store.append(list(data_ref))
# creating the treeview and adding the columns
self.treeview = Gtk.TreeView(model=self.store)
rend = Gtk.CellRendererText()
rend.set_alignment(0.5, 0.5)
for i, column_title in enumerate([f'nĂ—{p}' for p in [1, 2, 3, 4, 5]]):
column = Gtk.TreeViewColumn(column_title, rend, text=i)
column.set_min_width(80)
# column.set_max_width(80)
# column.set_fixed_width(80)
# column.set_sizing(Gtk.TreeViewColumnSizing(1))
column.set_alignment(0.5)
column.set_clickable(True)
column.connect('clicked', self.on_column_clicked)
self.treeview.append_column(column)
self.treeview.set_hexpand(False)
self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
# Put the treeview in a scrolled window
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
self.scrollable_treelist.add(self.treeview)
# WORKAROUND
# Alternatively, embed the treeview inside a grid containing an
# empty grid to the right of the treeview
# To try it: comment out the previous line; uncomment next lines
# scrolled_grid = Gtk.Grid()
# empty_grid = Gtk.Grid()
# empty_grid.set_hexpand(True)
# scrolled_grid.attach(self.treeview, 0, 0, 8, 10)
# scrolled_grid.attach_next_to(empty_grid, self.treeview,
# Gtk.PositionType.RIGHT, 1, 1)
# self.scrollable_treelist.add(scrolled_grid)
# self.scrollable_treelist.set_propagate_natural_height(True)
# Buttons
self.remove_button = Gtk.Button(label='Remove')
self.remove_button.connect('clicked', self.on_remove_clicked)
self.paste_button = Gtk.Button(label='Paste')
self.paste_button.connect('clicked', self.on_paste_clicked)
self.grid.attach_next_to(self.remove_button, self.scrollable_treelist,
Gtk.PositionType.TOP, 1, 1)
self.grid.attach_next_to(self.paste_button, self.remove_button,
Gtk.PositionType.RIGHT, 1, 1)
self.set_default_size(800, 500)
self.show_all()
# Clipboard (to insert several rows)
self.clip = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
self.clip2 = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
def on_column_clicked(self, col):
print(f'col.get_sizing()={col.get_sizing()}')
print(f'col.get_expand()={col.get_expand()}')
print(f'col.get_width()={col.get_width()}')
print(f'col.get_min_width()={col.get_min_width()}')
print(f'col.get_max_width()={col.get_max_width()}')
print(f'col.get_fixed_width()={col.get_fixed_width()}')
def on_remove_clicked(self, widget):
model, paths = self.treeview.get_selection().get_selected_rows()
refs = []
for path in paths:
refs.append(Gtk.TreeRowReference.new(model, path))
for ref in refs:
path = ref.get_path()
treeiter = model.get_iter(path)
model.remove(treeiter)
# print(f'AFTER REMOVAL, REMAINING ROWS={[str(r[0]) for r in model]}')
def on_paste_clicked(self, widget):
text = self.clip.wait_for_text()
if text is None:
text = self.clip2.wait_for_text()
if text is not None:
lines = text.split('\n') # separate the lines
lines = [tuple(L.split('\t')) for L in lines] # convert to tuples
print(f'PASTE LINES={lines}')
for line in lines:
if len(line) == 5:
line = tuple(int(value) for value in line)
self.store.append(line)
win = AppWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

is it possible to transfer row from qtablewidget to another qtablewidget?

I have a row with text data in some cells and other cells have widgets in it, Can I transfer these cells in the row, so I mean the whole row to another qtablewidget without having to break the row into small data then rebuild it again into the other qtablewidget?
Note: please don't ask me for minimal pro-- because I'm only asking before doing it so I be aware what's waiting for me.
There is no function like that for QTableWidget (the only similar function only exists for QStandardItemModel: takeRow()).
In order to remove items you need to use insertRow() on the target table, takeItem() for each column in the source row, and setItem() on the target, and finally removeRow().
def moveRow(self, row):
targetRow = self.target.rowCount()
self.target.insertRow(targetRow)
for column in range(self.source.columnCount()):
item = self.source.takeItem(row, column)
if item:
self.target.setItem(targetRow, column, item)
self.source.removeRow(row)
Unfortunately, this won't let you do anything for cell widgets. When a widget is set on a cell in an item view, the view will take complete and definitive ownership on the widget: even if you try to use removeWidget or setCellWidget with another widget, the previous one will be deleted internally by Qt (see the sources for QAbstractItemView.setIndexWidget(), which is called for both removeWidget and setCellWidget).
The only solution would be to check if the cell has a widget, create a new instance of the same class and copy its properties.
A possible workaround is to add cell widgets using a container widget with a layout, add the actual widgets to that layout, and then create a new container and set the layout for it:
# add a container for the actual widget
container = QtWidgets.QWidget()
contLayout = QtWidgets.QVBoxLayout(container)
# layout usually add some margins to their widgets, let's remove them
contLayout.setContentsMargins(0, 0, 0, 0)
testButton = QtWidgets.QPushButton('test')
contLayout.addWidget(testButton)
self.source.setCellWidget(0, 0, container)
def moveRow(self, row):
# ...
widget = self.source.cellWidget(row, column)
if widget and widget.layout() is not None:
newContainer = QtWidgets.QWidget()
newContainer.setLayout(widget.layout())
self.target.setCellWidget(targetRow, column, newContainer)
self.source.removeRow(row)

Categories