Python 3, Tkinter, trying to get 9 buttons on 3 different rows - python-3.x

Ive just started using tkinter and have this so far:
from tkinter import *
BoardValue = ["-","-","-","-","-","-","-","-","-"]
window = Tk()
window.title("Noughts And Crosses")
window.geometry("250x250")
for b in BoardValue[0:3]:
btn = Button(window, text=b, relief=GROOVE, width=2).pack(side=LEFT)
for b in BoardValue[3:6]:
btn = Button(window, text=b, relief=GROOVE, width=2).pack(side=LEFT)
for b in BoardValue[6:9]:
btn = Button(window, text=b, relief=GROOVE, width=2).pack(side=LEFT)
window.mainloop()
I want 3 buttons on 3 rows, how can I do this?

There are two common solutions: one using grid, and one using pack with subframes for each row. There are other solutions but these are the most common. Of these two, when creating a grid of widgets, grid is the most natural solution.
Note: in both of the examples below, you can create the buttons in three separate loops if you want, but since the buttons are identical I chose to use a single loop to make the code a little easier to maintain (for example, if you decide to change the relief, you only have to change it in one line of code rather than three).
Using grid
You can use the grid geometry manager to align things in rows and columns. Since you can compute the row and column with simple math, you can reduce all of that code down to something like this:
for i, b in enumerate(BoardValue):
row = int(i/3)
col = i%3
btn = Button(window, text=b, relief=GROOVE, width=2)
btn.grid(row=row, column=col, sticky="nsew")
You may want to add a weight to the rows and columns if you want them to grow and shrink equally when you resize the window.
grid has an advantage over pack when building a matrix like this, since each cell is guaranteed to be the same size.
Using pack
If you know that each button will be the same width, you can use pack in combination with subframes. Pack the subframes along the top, and the buttons along the left or right. Again, you can use a little math to know when a new row is starting.
It would look something like this:
for i, b in enumerate(BoardValue):
if i%3 == 0:
row_frame = Frame(window)
row_frame.pack(side="top")
btn = Button(row_frame, text=b, relief=GROOVE, width=2)
btn.pack(side="left")
Using pack may not be the right choice if buttons could be different sizes, since there are no actual columns using this method.

The simple answer is to put the buttons in frames so you can control the packing order. E.g.;
i=0
for f in range(3):
frame[f] = Frame(window).pack(side=TOP)
for b in range(3):
btn[i] = Button(frame[f], text=i, relief=GROOVE, width=2).pack(side=LEFT)
i+=1
I've put the button & frame objects into lists in case you need to access the button objects later; you were overwriting your handles in the original code.

Related

Is there a way to center both item s in a Tk Label

I'm making a program and I need a image and an entry field. However when I try to put them in the same column it defaults the to the top left no matter the value. I tried column as various values and it either goes to the top left if they are the same or one goes to the center-ish and one is on the left. is there something I'm missing. Any help would be greatly appreciated.
from tkinter import *
import tkinter as tk
from PIL import Image,ImageTk
pg3 = Tk()
img1 = ImageTk.PhotoImage(Image.open("download.png"))
pg3.attributes("-fullscreen", True)
tk.Label(pg3, image = img1, anchor = "c").grid(row=0, column = 1)
e1 = tk.Entry(pg3)
e1.grid(row=1, column=1)
pg3.mainloop()
Use pack.
BaseWidget.pack() automatically centers all your elements.
P.S. If you want to horizontally center a few elements, you can use a Frame to grid the elements and then pack the frame.
Another way will be using pg3.columnconfigure(1, weight=1). It is an alias of grid_columnconfigure.
def grid_columnconfigure(self, index, cnf={}, **kw):
"""Configure column INDEX of a grid.
Valid resources are minsize (minimum size of the column),
weight (how much does additional space propagate to this column)
and pad (how much space to let additionally)."""
So you can set the weight to 1 to center it.

Issues when freezing wx.Grid columns with FreezeTo: bad cell position in grid, and HOME keyboard key behavior

Context
I have put in place column freezing in a wx.grid.Grid, using FreezeTo method.
def __init__(self, parent):
# relevant lines
self.grid = wx.grid.Grid(self.sbox_grid, size=(1000, 800))
self.grid.CreateGrid(self.row_number, self.col_number)
self.grid.FreezeTo(0, self.frozen_column_number)
The freezing by itself works well, as soon as I keep the standard label renderer (*).
The first few columns I have frozen always stay visible, and moving the horizontal scrollbar by hand is also ok.
(*) I was initially using the GridWithLabelRenderersMixin of wx.lib.mixins.gridlabelrenderer, but it totally breaks consistency between column label width and column width. Anyway I can deal with the standard renderer, so it is not really a problem.
I faced several issues, now all solved and detailed below.
Capture the cell position for frozen columns: cells or labels (SOLVED)
For cells, the window can be captured with GetFrozenColGridWindow.
So mouseover can be done simply with:
if widget == self.grid.GetFrozenColGridWindow():
(x, y) = self.grid.CalcUnscrolledPosition(event.GetX(), event.GetY())
row = self.grid.YToRow(y)
col = self.grid.XToCol(x)
# do whatever your want with row, col
For labels, the window exists but is NOT accessible with a method.
With a GetChildren on the grid, I have found that it is the last of the list (corresponding to the latest defined).
So it is not very reliable, but a relatively good placeholder for the missing GetGridFrozenColLabelWindow method.
wlist = self.grid.GetChildren()
frozen_col_label_window = wlist[-1]
if widget == frozen_col_label_window:
x = event.GetX()
y = event.GetY()
col = self.grid.XToCol(x, y)
# do stuff with col
Mouse position from non-frozen columns (labels or cells) is shifted (SOLVED)
The effective position for non-frozen columns labels or cells is shifted from the total width of all the frozen columns.
This one is easily handled by a shift in position, computed before calls to YToRow or XToCol methods.
The following code shows the position corrections:
class Report(wx.Panel):
def _freeze_x_shit(self):
"""Returns the horizontal position offset induced by columns freeze"""
offset = 0
for col in range(self.frozen_column_number):
offset += self.grid.GetColSize(col)
return offset
def on_mouse_over(self, event):
widget = event.GetEventObject()
# grid header
if widget == self.grid.GetGridColLabelWindow():
x = event.GetX()
y = event.GetY()
x += self._freeze_x_shit() # <-- position correction here
col = self.grid.XToCol(x, y)
# do whatever grid processing using col value
# grid cells
elif widget == self.grid.GetGridWindow():
(x, y) = self.grid.CalcUnscrolledPosition(event.GetX(), event.GetY())
x += self._freeze_x_shit() # <-- and also here
row = self.grid.YToRow(y)
col = self.grid.XToCol(x)
# do whatever grid cell processing using row and col values
event.Skip()
HOME keyboard key not working as intended (SOLVED)
I generally use the HOME key to immediately go at the utmost left of the grid, and the END key to go far right. This is the normal behavior with a non-frozen grid.
The END key does its jobs, but not the HOME key.
When pushing HOME on any grid cell, I got two effects:
the selected cell becomes the first column: this is OK
but the scrollbar position is not changed at all: I would expect the scroll position to be fully left
I have corrected it by a simple remapping using EVT_KEY_DOWN event:
def __init__(self, parent):
self.grid.Bind(wx.EVT_KEY_DOWN, self.on_key_event)
def on_key_event(self, event):
"""Remap the HOME key so it scrolls the grid to the left, as it did without the frozen columns
:param event: wx.EVT_KEY_DOWN event on the grid
:return:
"""
key_code = event.GetKeyCode()
if key_code == wx.WXK_HOME:
self.grid.Scroll(0, -1)
event.Skip()
my 3 issues concerning column Freeze in a grid are now solved. I have edited my initial post with my solutions.

How to access cells in a simple grid Layout (Tkinter)

My grid Layout looks like this :
height = 5
width = 5
for i in range(height): #Rows
for j in range(width): #Columns
b = Entry(root, text="")
b.grid(row=i, column=j)
How do I access each cell? to write in it, or to acces the data that has been written in it while the programm is running?
Because the variable b is only the 25th cell.
You can query the parent window to get all of the slaves, and you can use grid_info() on a widget that is managed by grid to find out what row and what column it is in.
That's pretty cumbersome, however. The simplest thing is to store the widgets in a data structure which lets you easily access the widgets. A dictionary works well for this, with the keys being a tuple of the row and column.
For example:
widgets = {}
for i in range(height): #Rows
for j in range(width): #Columns
b = Entry(root, text="")
b.grid(row=i, column=j)
widgets[(i,j)] = b
With that, you can reference any widget by row and column. For example:
widgets[(2,3)].insert("end", "Hello, world")

Is filling a grid with images so I could control location a good practice?

I'm trying to locate a few images on a grid, but when I try to do something like
panel = Label(root, image = img)
panel.grid(row = a, column = b)
where a,b are values that should bring the image to the center of the grid, I always get the image on the top left corner, because empty columns/rows don't fill the space. So, using a friend's advice, I added a blank image and did something like
a = 0
b = 0
for img in ls2: # ls2 is a list containing instances of the blank image
a += 1
if a == 11:
a = 1
b += 1
panel = Label(root, image = img)
panel.grid(row = a, column = b)
Now, when I'm trying to locate a new image on row = x, column = y it goes as I wanted and this is my current solution. My question is, is this a good way to enable me use the whole grid?
No, it's not a good way. There is no need to create invisible widgets. It's hard to say what the good way is, however, because "use the whole grid" is somewhat vague.
If by "use the whole grid" you mean want to create a grid where all rows are the same height, and all of the columns are the same width, and the grid fills its containing window, the right solution is to create uniform rows columns using rowconfigure and columnconfigure.
For example, if you want to create a 4x4 grid of equal sized cells, you could do something like this:
import tkinter as tk
root = tk.Tk()
for row in range(4):
root.grid_rowconfigure(row, uniform="default", weight=1)
for column in range(4):
root.grid_columnconfigure(column, uniform="default", weight=1)
label_2_2 = tk.Label(root, text="row 2, column 2", borderwidth=2, relief="groove")
label_1_0 = tk.Label(root, text="row1, column 0", borderwidth=2, relief="groove")
label_1_0.grid(row=1, column=0)
label_2_2.grid(row=2, column=2)
root.mainloop()
The uniform option takes any arbitrary string. Every row (or column) with the same value will have the same dimensions.
The weight option tells grid how to allocate any extra space. By giving each row and each column an identical non-zero weight, all extra space will be apportioned equally, causing the rows and columns to grow or shrink to fit the window.
Just found the place geometry manager. It is more explicit and lets me control the exact locations of my images inside the widget.
panel.place(x=100, y=100) gives pixel precision location which was what I looked for in the first place.

PyQt / Qt Designer: keeping elements together

I'm putting together a widget in PyQt's Qt Designer, but struggling to make elements that should stay as close to each other as possible do so after applying a grid layout.
For example, in this layout, I've set the question mark (which acts as the tooltip) to be 2 px from the right hand edge of each of the input elements:
When I apply a grid layout, these spaces massively (and inconsistently across all 3 lines) increase:
I've tried applying a horizontal layout to the rows of elements, but even this causes the elements to spread out more horizontally in an inconsistent manner.
Am I missing something obvious? Is there a way to force elements to stay a certain distance for neighbouring elements?
EDIT / UPDATE
Thanks to three_pineapples' suggestion, I've put some horizontal-spacers in (although I'm not sure if I've done it in the way he intended?)
This has solved it to some extent, but:
the gap between elements and the question mark is now consistent, but 6 px on all rows!
the gap between other elements is not consistent (the gap between the QLineEdit and the File Browse button is 12 px, not the 5 that I intended here)
the height of the spacers are the same height as the rows, but QtDesigner seems to have added in a weird orphaned row above the file select row. Although I'm sure it's nothing to be concerned about I do prefer to understand how things work and why it's doing it...
Have you tried calling setSpacing(0) on the QGridLayout to reduce the spacing between items? (there's also setHorizontalSpacing(x) and setVerticalSpacing(y) if you prefer). Also, setContentsMargins(0, 0, 0, 0) would eliminate the outer margins on the grid, which I think is not what you're looking for here but might be related or useful for someone stumbling upon this.
Snippet:
from PyQt4 import QtGui
class MyClass(QtGui.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
p1 = QtGui.QPushButton('One', self)
p2 = QtGui.QPushButton('Two', self)
p3 = QtGui.QPushButton('Three', self)
p4 = QtGui.QPushButton('Four', self)
grid = QtGui.QGridLayout(self)
grid.addWidget(p1, 0, 0)
grid.addWidget(p2, 0, 1)
grid.addWidget(p3, 1, 0)
grid.addWidget(p4, 1, 1)
grid.setContentsMargins(0, 0, 0, 0) # this seems to be the outer padding of the grid
grid.setHorizontalSpacing(0) # I think this is what you're looking for
grid.setVerticalSpacing(0)
# grid.setSpacing(0) # this would set vertical & horizontal spacing at once
self.setLayout(grid)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = MyClass()
window.show()
sys.exit(app.exec_())
I'm not used to using QtDesigner but there should be ways of setting these properties there, if you prefer.

Resources