Issues with position manipulation using QGridLayout addWidget(QWidget,x,y) - python

This is a really simple question I'm just new to PyQt5 and am a bit confused on how QGridLayout works...
def init_main_page(self):
layout = QGridLayout()
b1 = buttons.QPushButton("0",self.main_page)
b2 = buttons.QPushButton("1",self.main_page)
b3 = buttons.QPushButton("2",self.main_page)
layout.addWidget(b1,0,0)
layout.addWidget(b2,5,0)
layout.addWidget(b3,1,0)
self.main_page.setLayout(layout)
The problem I am having is that no matter how high I make the x and y arguments in addwidget(QWidget,x,y), it b1 b2 and b3 always remain equidistant from each other. I'm trying figure out how to manipulate the position of the buttons whilst maintaining a proportional setup (so avoiding QPushButton.move()) and from what I've seen, QGridLayout is the best way to do this.

Setting the coordinates of widgets in a grid layout by "skipping" rows or columns is almost useless, as those are grid coordinates that only tell the layout manager in which "slots" the widgets will be: since there is nothing in the rows 2 to 4, that space will not be used.
To achieve what you want you need to set stretch factors and, possibly, use minimum heights for the specified rows.
layout.addWidget(b1, 0, 0)
layout.addWidget(b3, 1, 0)
layout.addWidget(b2, 2, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)
But this might not be what you want, since it will place the third button in the vertical center of the row grid (leaving empty space at the bottom).
To avoid that there are two possible solutions:
add the widget by setting the alignment to the bottom:
layout.addWidget(b2, 2, 0, alignment=QtCore.Qt.AlignBottom)
add the last widget to the fourth row, and set stretch and minimum height for the third empty row.
layout.addWidget(b2, 3, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)

Related

QFontMetrics leave extra space between lines

I'm trying to draw multiple paragraphs of text with PySide6's QPainter and QFontMetrics. I want to draw them with the same spacing as they would have if I drew them all in a single block of text, but the line spacing isn't quite right.
In the following example, the font metrics say that the font's line spacing is 17. When I measure a single line of text, the bounding rectangle is indeed 17 pixels high. However, when I measure two lines of text, the bounding rectangle is 35 pixels high, not 34. Where does the extra pixel come from, and can I see it on some property of the font or the font metrics?
from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetrics(font)
print(metrics.lineSpacing()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A').height()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A\nB').height()) # 35 != 17 * 2
print(metrics.leading()) # 0
print(metrics.ascent()) # 14
print(metrics.descent()) # 3
By the way, it isn't always one extra pixel. If I make the font bigger, the extra space increases.
Update
I thought I had figured this out with musicamante's suggestion of switching from QFontMetrics to QFontMetricsF, but there's still a difference.
from PySide6.QtCore import QRectF
from PySide6.QtGui import QFont, QFontMetricsF
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetricsF(font)
print(metrics.height()) # 16.8125
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A').getCoords()) # (0.0, 0.0, 9.9375, 16.8125)
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A\nB').getCoords()) # (0.0, 0.0, 9.9375, 34.8125)
# Note the height of that rect doesn't match the next calculation.
print(metrics.height() + metrics.lineSpacing()) # 34.046875
# I can't see any combination of these numbers that makes 34.8125
print(metrics.lineSpacing()) # 17.234375
print(metrics.leading()) # 0.421875
print(metrics.ascent()) # 13.984375
print(metrics.descent()) # 2.828125
First of all, the height of a bounding rect of a font metrics doesn't depend on the characters used, but on the font specifications.
Two lines of text don't have the double of the height() of the bounding rect of a single line: instead, you have to consider the lineSpacing().
In practice, the height of a bounding rect is normally the sum of:
the height() multiplied the number of lines;
the leading() multiplied by the number of spaces between the lines (aka: number of lines - 1);
Or, similarly, the sum of:
the ascent();
the lineSpacing() multiplied by the number of spaces between lines;
the descent();
Note that, obviously, the number of lines depends on the input text and the given options, for instance, if word wrapping was enabled and any of the source lines didn't fit the given source rectangle.
Also consider that most fonts are vectorial, meaning that their coordinates and metrics are proportional and in floating point values. QFontMetrics, instead, works with integer values for simplicity and optimization reasons, so you might get inconsistent results caused by rounding in cases for which the point size doesn't give rounded values: non integer numbers are generally "floored" (like int() in python).
In your case, the leading is probably more than 0 (but still less than 1), so you don't get a proper sum of the aforementioned heights.
Specifically, QFontMetrics.boundingRect() returns a QRect resulting by the QRectF.toAlignedRect() of the computed formatted text, which is always "the smallest possible integer rectangle that completely contains this rectangle".
If you need to get precise coordinates, you need to use QFontMetricsF, which is the floating point counterpart of the default basic QFontMetrics.
That said, if you plan on drawing formatted text with QPainter, then consider using QTextDocument or, at least, QTextLayout, which is consistent with the standard Qt text drawing and is generally faster, more reliable and "simpler" (well, once you get to know it). While it might seem a bit too complex than required, it's actually what Qt does when calling boundingRect(), so if you need custom painting, the QTextLayout option is actually better, especially if you can combine it with some smart caching (see QPicture) to avoid the common python bottleneck.

wx.grid.Grid using too much space within wx.BoxSizer

I'm trying to display a table of items in wxpython using a wx.grid.Grid() inside a wx.BoxSizer. The table is large but I didn't want it to take up all the screen space so I put it in the boxsizer as shown in the code below.
def setOutputPanel(self):
self.outputBox = wx.grid.Grid(parent=self.pnl)
self.outputBox.CreateGrid(100, 100)
self.outputBox.DisableCellEditControl()
self.outputPanel.Add(self.outputBox, 1, wx.EXPAND)
self.graphControl = wx.Button(parent=self.pnl, label="close")
self.outputPanel.Add(self.graphControl, 0, wx.ALIGN_RIGHT)
self.outputText = wx.TextCtrl(parent=self.pnl, value="text control", style=wx.EXPAND)
self.outputPanel.Add(self.outputText, 1, wx.EXPAND)
The problem is that when I put the size for the outputText to 1
self.outputPanel.Add(self.outputText, 1, wx.EXPAND)
It doesn't show at all
link to TextCtrl#size1
I can get it to show if I set the size to 0.
self.outputPanel.Add(self.outputText, 0, wx.EXPAND)
But, it's far too small. I'd like it to be the same height at the table. link to TextCtrl#size0
So, after reading some more of the wxPython documentation I have found the solution.
If you go to this page, where they discuss some of the wxPython sizer features (the super class for wx.BoxSizer). On the page it says "Normally wx.Sizers will use wx.Window.GetEffectiveMinSize to determine what the minimal size of window items should be, and will use that size to calculate the layout." so I added the wx.FIXED_MINSIZE and now the grid doesn't have a minimum size.
This might seem like a bad idea as the grid can now shrink to a size that's too small. But, I've set the frame min size pretty large and the grid uses half the space, so I don't think It's going to be an issue.

PyQt5 QGridLayout sizing incorrect

I'm having some issues with QGridLayout in pyqt5. I'm trying to make a GUI that has a stack of buttons on one side, a table on the other side, and a plot that occupies the entire bottom of the window. This is the first program I've ever made, so I might have more issues than I know.
I've arranged the buttons within a QTableWidget, and the main QTableWidget contains several fields where users can enter data. I'd like the data entry table to be larger in size than the button table, but resizing it as in this answer doesn't seem to do anything. The button table is larger no matter the columnSpan entry I put in. What am I doing wrong?
Here are the relevant bits of code:
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(50, 50, 700, 1000)
self.home()
def home(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.button_table = QTableWidget(self)
self.layer_add = QPushButton("Add layer", self)
self.plotter = QPushButton("plot transmission", self)
self.layer_table = QTableWidget(self)
self.graphWidget = pg.PlotWidget(self)
self.grid = QGridLayout()
self.grid.setSpacing(10)
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3)
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
self.centralWidget().setLayout(self.grid)
self.show()
I doodled in what I'd ideally like to have happen... here's a picture of what it looks like with the above code
and in red what I'd like to have happen.
Edit: I don't understand why, if I set the QGridLayout columnSpan to be 1 for the table on the left and 3 for the table on the right, the left-hand table is still significantly wider. I am open to either learning how to fix that, understanding how to make the left-hand table auto-shrink to the size of the buttons within it, or an alternative layout suggestion. Thanks for any help!
While you answered your own question, it seems that you changed the behavior by removing the first table (moreover, changing the resize mode of the first column stretch doesn't have much to do with your issue). So I'm answering to your [edited] question, even if it's missing the part in which you added the buttons to the first table.
The main problem was that you were setting a column span too big for the second table:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3) # <- 3 columns!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
In the code above, you're telling the layout that the layer_table will have a column span of 3 columns. Even if you are not actually using three columns, by doing this the layout thinks that the second table will (probably) occupy more space than the first.
Normally, a QGridLayout will use the columnStretch property for that, but since by default the stretch is 0 for all columns and rows, it will use the span as a reference.
In fact, using the following:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 1) # <- 1 column!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 2)
is the same as this:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3) # <- 3 columns!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
self.grid.setColumnStretch(0, 1)
self.grid.setColumnStretch(1, 1)
In the first case, the column span is 1 (one widget, one column), and, since the two widgets are of the same type, they will use half of the available horizontal space. In the second, the column span of the right table is 3 (as in your code), but the stretch is 1 for both the first and second column, and 0 for the third and fourth, meaning that a widget that occupies the second, third and fourth column will have the same available space than a widget that occupies the first, thus obtaining the horizontal space equally divided between those two widgets.
col1 | col2 | col3 | col4
1 | 1 | 0 | 0
Since the second table occupies columns 2 to 4, it will have a stretch of 1 (1 + 0 + 0). Stretches are used by layouts to equally divide the space between widgets (considering their size hints, their minimum size hints, or their minimum/maximum size whenever they're set): the stretches are summed integer values, and then the layout uses the proportions between the sum and those values to resize widgets.
To ensure that the first table uses only the minimum space required to show its contents, you need to do the following:
set the sizeAdjustPolicy (which is a property of every QAbstractScrollArea descendant, including every item view) to AdjustToContents; this will make the table "tell" the layout that its size hint is based on its minimum contents;
set the resize mode of the horizontal header to adjust all of its sections (as in columns) to their contents;
set the horizontal size policy of the table to Maximum; the term "maximum" might be counterintuitive, but it means that the widget's size cannot be larger than its size hint; still, it could be shrunk if any other widget requires space (but not less than the minimumSizeHint) so, alternatively, you could use Fixed (meaning that it cannot even shrink), but it's usually better to allow widgets to be shrunk anyway if the layout is too crowded and the user requires to make the window smaller than it is;
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(50, 50, 700, 1000)
self.home()
def home(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.button_table = QTableWidget(self)
self.button_table.setRowCount(3)
self.button_table.setColumnCount(1)
# set the sizeHint of the table view (actually, its ancestor class,
# QAbstractScrollArea) to the minimum size required to show its contents
self.button_table.setSizeAdjustPolicy(self.button_table.AdjustToContents)
# set all sections of the horizontal headers to adjust themselves to
# their contents
self.button_table.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents)
# get the current sizePolicy and set it to Maximum, meaning that it will
# use its sizeHint as "maximum": it can expand, but there's no need for
# that, so if any other sibling widget requires more space, it can use it
policy = self.button_table.sizePolicy()
policy.setHorizontalPolicy(policy.Maximum)
# apply the changed policy
self.button_table.setSizePolicy(policy)
self.layer_add = QPushButton("Add layer", self)
self.plotter = QPushButton("plot transmission", self)
# I restored the following lines, which were missing in your edit
self.button_table.setCellWidget(0, 0, self.layer_add)
self.button_table.setCellWidget(1, 0, self.plotter)
self.layer_table = QTableWidget(self)
self.graphWidget = pg.PlotWidget(self)
self.grid = QGridLayout()
self.grid.setSpacing(10)
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 1)
self.grid.addWidget(self.graphWidget, 1, 0, 1, 2)
self.centralWidget().setLayout(self.grid)
As you can see, now the left table only uses the minimum required width, based on the horizontal header width (plus the vertical header width), which in turn is based on the sum of the maximum width of each column.
Ahh turns out that using setColumnStretch on column 1 fixed this problem. I also changed the left table to a QVBoxLayout and put it in using QGridLayout.addLayout, so everything looks better now
I still don't quite understand why the two tables were unequal widths on QGridLayout though, regardless of the number of columns selected.

Is there any way I could improve the speed of this Python Program?

I re-created the game Lights Out using Python and Tkinter and as far as I know there are no bugs but it is very slow especially if you set the grid size to be much higher then 10x10 (I have a slider in game that lets you do this.) I was just wondering if you had any ideas on how I could get it to run faster.
#Nicholas Eckstein
#Lights Out
#11/20/14
import random
import math
from tkinter import *
from tkinter import ttk
Lastx, lasty = 0,0
GridSize="410x520"
def reset():#Resets the grid size to the size set in the slider and randomizes cells.
global grid
global GridSize
gridMaker()
canvas.delete("all")#Clears the screen
ResetMin=math.trunc(len(grid)/3) ##Picks random amount of cells to switch states.
ResetMax=math.trunc(len(grid)/2) #Amount chosen is relative to the grid size.
ResetAmount=random.randint(ResetMin,ResetMax) ##(Random amount in between 1/2 and 1/3 of the cells.
iterate=0
while iterate<ResetAmount:#Picks random cells to switch states until iterate==ResetAmount
#cell=random.choice(grid)#All cells exist in a list of lists called grid.
#cell.pop(2) #A single list inside the Grid List is a cell.
#cell.append(1) #This Cell consists of 2 ranges and a state. [range(105, 125), range(5, 25), 0]
#iterate+=1 #The first range is the width of the cell, the second range is for the height, and the last number is for the state.
#The grid list looks something like this: [[range(105, 125), range(5, 25), 0], [range(125, 145), range(5, 25), 0], [range(145, 165), range(5, 25), 0]...]
cell=random.choice(grid)
cellx=cell[0][5]
celly=cell[1][5]
iterate+=1
CellSwitcher(cellx,celly)
GridSize=str((len(grid)/2)*20)+"x"+str(((len(grid)/2)*20)+110)#This sets the gridsize to the size determined by the slider
art()
def art():#Takes the information from the Grid list and "draws" the cells.
for cell in grid:
if cell[2]==1:
canvas.create_rectangle(cell[0][0],cell[1][0],cell[0][19],cell[1][19],fill="white")
canvas.create_rectangle(cell[0][0]+2,cell[1][0]+2,cell[0][19],cell[1][19],fill="black",outline="black")
else:
canvas.create_rectangle(cell[0][0],cell[1][0],cell[0][19],cell[1][19],fill="black")
canvas.create_rectangle(cell[0][0]+2,cell[1][0]+2,cell[0][19],cell[1][19],fill="white",outline="white")
def xy(event):#Takes the position of the mouse click
global lastx, lasty
lastx, lasty = event.x, event.y
CellSwitcher(lastx,lasty)
def CellSwitcher(lastx,lasty):#Switches the states of the cells neighboring the cell you clicked.
for coord in grid:
if lastx in coord[0] and lasty in coord[1]:
if coord[2]==0:
coord.pop(2)
coord.append(1)
else:
coord.pop(2)
coord.append(0)
if [coord[0],range(coord[1][0]+20,coord[1][19]+21),0] in grid: ####
grid[grid.index([coord[0],range(coord[1][0]+20,coord[1][19]+21),0])].pop(2) #
grid[grid.index([coord[0],range(coord[1][0]+20,coord[1][19]+21)])].append(1) #
elif [coord[0],range(coord[1][0]+20,coord[1][19]+21),1] in grid: # Switch Top Neighbor's state
grid[grid.index([coord[0],range(coord[1][0]+20,coord[1][19]+21),1])].pop(2) #
grid[grid.index([coord[0],range(coord[1][0]+20,coord[1][19]+21)])].append(0) #
####
if [coord[0],range(coord[1][0]-20,coord[1][19]-19),0] in grid: ####
grid[grid.index([coord[0],range(coord[1][0]-20,coord[1][19]-19),0])].pop(2) #
grid[grid.index([coord[0],range(coord[1][0]-20,coord[1][19]-19)])].append(1) #
elif [coord[0],range(coord[1][0]-20,coord[1][19]-19),1] in grid: # Switch Bottom Neighbor's state
grid[grid.index([coord[0],range(coord[1][0]-20,coord[1][19]-19),1])].pop(2) #
grid[grid.index([coord[0],range(coord[1][0]-20,coord[1][19]-19)])].append(0) #
####
if [range(coord[0][0]+20,coord[0][19]+21),coord[1],0] in grid: ####
grid[grid.index([range(coord[0][0]+20,coord[0][19]+21),coord[1],0])].pop(2) #
grid[grid.index([range(coord[0][0]+20,coord[0][19]+21),coord[1]])].append(1) #
elif [range(coord[0][0]+20,coord[0][19]+21),coord[1],1] in grid: # Switch Right Neighbor's state
grid[grid.index([range(coord[0][0]+20,coord[0][19]+21),coord[1],1])].pop(2) #
grid[grid.index([range(coord[0][0]+20,coord[0][19]+21),coord[1]])].append(0) #
####
if [range(coord[0][0]-20,coord[0][19]-19),coord[1],0] in grid: ####
grid[grid.index([range(coord[0][0]-20,coord[0][19]-19),coord[1],0])].pop(2) #
grid[grid.index([range(coord[0][0]-20,coord[0][19]-19),coord[1]])].append(1) #
elif [range(coord[0][0]-20,coord[0][19]-19),coord[1],1] in grid: # Switch Left Neighbor's state
grid[grid.index([range(coord[0][0]-20,coord[0][19]-19),coord[1],1])].pop(2) #
grid[grid.index([range(coord[0][0]-20,coord[0][19]-19),coord[1]])].append(0) #
####
art()
root = Tk()#Create the window
root.geometry(GridSize)#Set Window Size
root.resizable(0,0)#Stop people from resizing the window
root.title("Lights Out")
canvas = Canvas(root,background=root.cget('bg'))#Create the part of the window that draws the grid
canvas.bind("<Button-1>", xy)#Detect clicking and send coordinates of mouse
canvas.pack(fill=BOTH, expand=YES)#Resize canvas to window size and allign.
SizeLabel = Label(root,text="Grid Size")#Write the "reset" label
SizeLabel.pack()#Allign Label
Size = Scale(root,orient=HORIZONTAL,length=400,width=20,sliderlength=60,from_=1,to=20,tickinterval=1)#Create, orientate, and set the size of the slider
Size.set(10)#Set starting position for slider
Size.pack()#Allign Slider
Reset = Button(root,text ="Reset",command = reset)#Create the reset button
Reset.pack()#Allign the reset button
def gridMaker():#This function creates the grid list.
global grid
grid=[]
xCoord=205-(int(math.trunc(Size.get())/2)*20)#Centers the grid
yCoord=5
iterate=0
while yCoord<Size.get()*20:
grid.append([range(xCoord,xCoord+20),range(yCoord, yCoord+20),0])#Adds a cell to the grid list with the ranges based on what xCoord and yCoord are.
if Size.get()%2==1:#Tests to see if the grid size is odd or even
if xCoord<205+(int(math.trunc(Size.get())/2)*20):
xCoord+=20
else:
xCoord=205-(int(math.trunc(Size.get())/2)*20)
yCoord+=20
else:
if xCoord<205+(int(math.trunc(Size.get())/2)*20)-20:
xCoord+=20
else:
xCoord=205-(int(math.trunc(Size.get())/2)*20)
yCoord+=20
gridMaker()#Draws the grid
reset()#Adds Randomizes Cell States
root.mainloop()
Your CellSwitcher function iterates over all items in the cell, when it eventually only modifies nine cells (itself and its 8 neighbors), right? Why iterate over every single cell? If you know the cell that was clicked (eg: row 3, column 2) you can easily compute the neighboring cells. So, part of the answer is to remove the iteration over all of the cells and replace it with a direct lookup of the clicked-on cell and its neighbors.
Also, your reset function calls CellSwitcher which seems like overkill. If you're randomly setting the color of each cell, why go through CellSwitcher, since it changes the colors of all its neighbors?
Perhaps the biggest culprit is that you are recreating all of the canvas objects on each call to CellSwitcher, without deleting any of the old objects. There's no reason to do that -- create all of the canvas objects just once and then change them with the itemconfig method of the canvas.
The canvas has performance problems when you have lots of items. In your case, after the GUI first comes up you've already created 9800 canvas items. Click on a single cell and the canvas now has 10,200 items. And so on. The canvas can pretty easily handle thousands of items, even tens of thousands. However, when I move the slider to 20 you end up creating a whopping 125,600 objects on the canvas which will definitely cause the canvas to under-perform.

How do I change the size of a GTK container widget?

I'm getting myself thoroughly confused about how Gtk controls widget sizes inside a container such as a GtkBox. Please can someone guide me through the intricacies for this question?
I have a GtkBox containing two widgets - in the example these are just two GtkButtons.
The first widget should just fill the space available inside the GtkBox container.
The second widget I want to force always to be a physical square - thus as the square enlarges, the first widget will shrink in terms of width.
Some example code will help here:
from gi.repository import Gtk
def on_check_resize(window):
boxallocation = mainbox.get_allocation()
width = 0
height = 0
if boxallocation.height >= boxallocation.width:
width = boxallocation.height
height = width
if boxallocation.width >= boxallocation.height:
width = boxallocation.width
height = width
secondbtn_allocation = secondbtn.get_allocation()
secondbtn_allocation.width = width
secondbtn_allocation.height = height
secondbtn.set_allocation(secondbtn_allocation)
win = Gtk.Window(title="Containers Demo")
win.set_border_width(10)
win.connect("delete-event", Gtk.main_quit)
mainbox = Gtk.Box()
firstbtn = Gtk.Button(label="just fills the space")
mainbox.pack_start(firstbtn, True, True, 0)
secondbtn = Gtk.Button(label="square")
mainbox.pack_start(secondbtn, False, True, 1)
win.add(mainbox)
win.connect("check_resize", on_check_resize)
win.show_all()
initial = firstbtn.get_allocation()
initial.height = initial.width
firstbtn.set_size_request(initial.width, initial.height)
Gtk.main()
When you expand the window - the GtkBox will likewise expand. That's good. The first button similarly also expands. Good also. However, the second button never is square even though I'm using the set_allocation method for the GtkWidget.
I cannot use the set_size_request (or could I?) because when I shrink the window, the window cannot resize due to the altered minimum size of the second button.
The reason why I'm trying to figure out how containers manage spacing is that I'm looking to eventually implement something like this iTunes example:
i.e. you can see the cover is always square - but the music details will shrink or expand to depending upon the space available.
Instead of Gtk.Box, use Gtk.Grid and set the hexpand and vexpand properties on the buttons that you pack into the grid.
Also, consider using a Gtk.AspectFrame for the square button.

Categories