I want to set up a three-column table with Python and Tkinter. For this purpose, I implement each cell both as a canvas and as a label inside the canvas because I need the tags option of the canvas widget.
I want to spread each cell over the whole width of the column from left to right (so that click events can be recorded not only on the text but on the whole row). However, the following code does not do this:
The text is centered but the labels and canvases only include the text, not the whole width of the cell. I tried adding
sticky = Tkinter.N+Tkinter.S+Tkinter.E+Tkinter.W
on the labels and/or the canvases; however, with some combinations, the text is aligned left and each label and canvas only includes the text instead of the whole cell, with others the sticky attribute the text is again centered but the labels and canvases are still narrow.
This is my code:
import Tkinter
class Application(Tkinter.Frame):
def __init__(self, master=None):
Tkinter.Frame.__init__(self, master)
self.master.geometry("800x600")
self.grid()
list = [[1, 2, 3], [4, 5, 6], [7777, 8888, 9999]]
self.cells_canvas = []
self.cells_label = []
i = 0
for entry in list:
self.cells_canvas.append([Tkinter.Canvas(self), Tkinter.Canvas(self), Tkinter.Canvas(self)])
self.cells_label.append([None, None, None])
self.cells_label[i][0] = Tkinter.Label(self.cells_canvas[i][0])
self.cells_label[i][0]["text"] = entry[0]
self.cells_label[i][0].config(bg = "#A00")
self.cells_label[i][0].grid(row = 0, column = 0, columnspan = 1) # reference A
self.cells_canvas[i][0].grid(row = i, column = 0, columnspan = 1) # reference B
self.cells_label[i][1] = Tkinter.Label(self.cells_canvas[i][1])
self.cells_label[i][1]["text"] = entry[1]
self.cells_label[i][1].grid(row = 0, column = 0, columnspan = 1)
self.cells_canvas[i][1].grid(row = i, column = 1, columnspan = 1)
self.cells_label[i][2] = Tkinter.Label(self.cells_canvas[i][2])
self.cells_label[i][2]["text"] = entry[2]
self.cells_label[i][2].grid(row = 0, column = 0, columnspan = 1)
self.cells_canvas[i][2].grid(row = i, column = 2, columnspan = 1)
i = i+1
root = Tkinter.Tk()
app = Application()
app.mainloop()
With this code, only the text in column 0 but not the whole columns appears with red background.
With adding sticky = Tkinter.N+Tkinter.S+Tkinter.E+Tkinter.W at reference A nothing changes.
With adding that at code B, the text appears aligned left. Again, only the text has red beackground.
With that code at references A and B, the same.
When using grid, the sticky option is indeed how you get a label to fill its cell. The label widget has an anchor option which controls where the text appears within the label.
In addition, you have to make sure that in a given parent, the column(s) expand to fill any extra space in the parent. In your case, column 0 in the canvas is only as wide as its contents, so it doesn't fill the full width of the canvas.
You need to give column 0 in each canvas a positive weight so the column will expand to fill the space given to it.
self.cells_canvas[i][0].columnconfigure(0, weight=1)
self.cells_canvas[i][1].columnconfigure(1, weight=1)
self.cells_canvas[i][2].columnconfigure(2, weight=1)
You'll probably want to do a similar thing with each row. A very good rule of thumb when using a canvas is to always make sure at least one row and at least one column have a positive weight.
If you only have one widget in each canvas, pack is probably the better choice since you don't have to worry about color and rows.
Related
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)
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.
I have a frame with multiple child elements, that are placed in it using the grid() geometry manager.
How can I modify the code below to make the frame responsive?
content.grid(column=0, row=0, sticky='nwse')
userButt.grid(column=2, row=1, sticky='nwse')
favoButt.grid(column=3, row=1, sticky='nwse')
locaButt.grid(column=4, row=1, sticky='nwse')
histScal.grid(column=5, row=1, sticky='nwse')
As a rule of thumb, whenever you use grid you should always give at least one row and one column a non-zero weight so that tkinter knows where to allocate extra space. A weight of 0 (zero) is assigned by default.
The two most common cases are where you have a "hero" widget (eg: a text widget, canvas widget, etc) that should grow and shrink as necessary, or you want everything to resize equally. For the case where one widget gets all the extra space, give a weight just to the row and column where that widget is placed. If you want everything to resize equally, give each row and each column a weight.
Assuming that the parent of your widgets content, userButt, etc are root, you might do it like this:
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
In the above example, all extra space would go to row zero and column 0.
Suppose you have a window that is managed by grid system and you want to make it responsive knowing the total number of rows and column you used. Let's say the total number of rows =6 and the total number of columns =10 making the window responsive under grid management system can be done as follows.
n_rows =6
n_columns =10
for i in range(n_rows):
root.grid_rowconfigure(i, weight =1)
for i in range(n_columns):
root.grid_columnconfigure(i, weight =1)
You need to use grid_rowconfigure(<row number>,weight=<row weight>) and grid_columnconfigure to stretch rows/columns when container is being stretched. Also use grid(sticky=NSEW) attribute to stretch items inside a grid
from tkinter import *
root = Tk()
for i in range(10):
root.grid_rowconfigure(i, weight=1)
for j in range(10):
root.grid_columnconfigure(j, weight=1)
Button(root, text=f'Button {i}-{j}').grid(row=i, column=j, sticky=NSEW)
root.mainloop()
I have a Tkinter ttk.Scale in a GUI. What I want to do is add some labels on the top or bottom (or side if its vertical) that show the values the user is selecting through. I have a minimum working example shown below.
My problem comes from the fact that I cannot figure out how to get the Scale to line up with the labels. It is set to span all the columns of the labels, but the Scale widget comes with a length keyword that automatically forces it to be 100 pixels in size. I can't know, a priori, what to set that length to and I can't figure out how to tell it to fill its grid space.
Is there a way to tell the Scale to choose a length such that it fills its grid space?
import tkinter as tk
from tkinter import ttk
def show_values():
print (w1.get())
def scaleFunc(val):
scaleVal = float(w1.get())
if int(scaleVal) != scaleVal:
w1.set(round(float(val)))
root = tk.Tk()
for i,text in enumerate(['O','B','A','F','G','K','M','L']):
ttk.Label(root, text = text).grid(row=0,column=i,padx=10)
w1 = ttk.Scale(root, to=7, command=scaleFunc, length=None)
w1.grid(row = 1, column = 0, columnspan = 8,ipadx=0)
ttk.Button(root, text='Show', command=show_values).grid(row=2,column=0,columnspan=8)
root.mainloop()
I suppose I should have played around a bit more. It looks like the Scale widget listens to the sticky keyword in the grid manager.
w1 = ttk.Scale(root, to=7, command=scaleFunc, length=None)
w1.grid(row=1,column=0,columnspan=8,padx=(10,7),sticky='NSEW')
You can use either one of these solutions:
Solution 1: w1.grid(row = 1, column = 0, columnspan=8, sticky='ew')
Solution 2: w1 = ttk.Scale(root, to=7, command=scaleFunc, length=250)
The 1st solution is cleaner.
I am building a fairly complicated GUI in TKinter so naturally have been using the .grid function.
I have grouped some of my widgets into Frames to make them easier to handle, but for some reason when I use .grid with widgets in a frame, the sticky attribute does not seem to work - my widgets don't fill the whole frame.
Example:
f3 = tk.Frame(self, borderwidth=1, relief="ridge")
f3.grid(row=0, column=0, sticky="NS")
tk.Label(f3, text="Site List").grid(in_=f3, row=0, column=0)
self.sitelist = tk.Listbox(f3)
self.sitelist.grid(in_=f3, row=1, column=0, sticky="NS")
In the above code, the frame f3 fills the space in the 0,0 cell of my root widget, but the Listbox does not fill all the space in the Frame, even though I have asked it to be sticky "NS".
If put the Listbox in the root widget at 0,0 then it stretches and fills all the space fine. It just does not behave well if it is in the Frame.
Can anyone explain how to get around this?
I thought that using frames would simplify my layout, but it is not!!!
Cheers.
Chris
You need to give one or more columns and rows "weight". Typically you'll have exactly one row and one column with a weight of 1 (the default is zero). Any row or column with a weight will expand and contract when the containing widget or window is resized.
If more than one row or more than one column has a weight, the weight describes the proportion in which they expand. For example, if one column has a weight of 2 and another a weight of 1, the one with two will expand twice as much as the one with 1.
You can assign weight using the grid_rowconfigure and grid_columnconfigure options to the containing widget:
f3.grid_rowconfigure(0, weight=1)
f3.grid_columnconfigure(0, weight=1)
For a nice brief description of weights, see the section titled Handling Resize on the grid tutorial on the tkdocs.com website.
First of all you shouldn't grid the lable at the same time when you instantiate it. Then, you have to use Grid.rowconfigure to add 'weight' to the row and Grid.columnconfigure to add it to the column. The weight controls expansion and has to be non-zero to allow grid to expand widgets.
If you add the following lines to your code it should work:
Grid.columnconfigure(f3, 0, weight=1)
Grid.rowconfigure(f3, 0, weight=1)
Grid.rowconfigure(f3, 1, weight=1)
Hope it helps