Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TKinter GUI, how do I get Frames to correct size?

So here's the issue. I am programming a GUI using Python's TKinter, and I need the window to be a specific size. The release version of the program will have the screen settings as root.attributes("-fullscreen", True) so that it's fullscreen and the user won't be able to access any menus. Said screen will be an 800x480 tablet screen.

Obviously I am programming on a screen much bigger than 800x480, so when I create my tkinter window I am setting root.minsize("800x480") to simulate the environment the program will be in.

As for what is on the GUI, I'll have a series of Buttons and other things. The window itself is going to be split into two Frames: A Button Frame, and a Visual Frame. The Button Frame, as the name implies will be a Frame consisting entirely of Buttons for user input, whereas the Visual Frame, again obviously, will just contain visual outputs for the user.

So here is my problem. I am currently working on the Button Frame, and I am having an issue where the Frame is not sized properly. Since I don't have the programming done for the Visual Frame yet, I've just been creating two Button Frames and placing them into the root window. Both the Frames should take up the whole screen, but they aren't. Here is my code:

import tkinter as tk
from tkinter import ttk

class ButtonManager(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.config(background = 'green')
        self.columnconfigure(0, weight = 1)
        self.rowconfigure(0, weight = 1)

        lblFrame = LabelFrame(self, controller)
        lblFrame.grid(column = 0, row = 0, sticky = "nsew")
        lblFrame.tkraise()

        btnFrame = ButtonFrame(self, controller)
        btnFrame.grid(column = 0, row = 1, sticky = "nsew")
        btnFrame.tkraise()

        for child in self.winfo_children():
            child.grid_configure(padx = 5, pady = 5)

class LabelFrame(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.config(background = 'blue')

        lblTitleBar = ttk.Label(self, text = 'TITLE BAR', background = 'grey', font = ("Arial", 20, "bold"))
        lblTitleBar.grid(column = 1, row = 1, columnspan = 4, sticky = "nsew")

        lblTextBar = ttk.Label(self, text = 'test text', background = 'grey', font = ("Arial", 16, "bold"))
        lblTextBar.grid(column = 1, row = 2, columnspan = 4, sticky = "nsew")

class ButtonFrame(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.config(background = 'red')

        btn1 = ttk.Button(self, text = '1', command = lambda : print("1"))
        btn1.grid(column = 1, row = 1, columnspan = 2, rowspan = 2, sticky = "nsew")

        #Not gonna type out the code for all the buttons, but I have 2 columns, and 6 rows.  
        #The buttons on the 4th and 6th row span the columns of both rows.
        #In total there are 10 buttons

        for child in self.winfo_children():
            child.grid_configure(padx = 5, pady = 5)

class Interface(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        container = tk.Frame(self)
        container.grid(column = 0, row = 0, sticky = "nsew")
        container.grid_rowconfigure(0, weight = 1)
        container.grid_columnconfigure(0, weight = 1)

        bMan1 = ButtonManager(container, self)
        bMan1.grid(column = 0, row = 0, sticky = "nsew")
        bMan2 = ButtonManager(container, self)
        bMan2.grid(column = 1, row = 0, sticky = "nsew")

interface = Interface()
interface.minsize(800, 480)
interface.mainloop()

As I said above my issue is that I need each of the ButtonManager objects to take up half the screen each width wise. However I instead get 2 small-ish boxes and a large grey area.

The random colours are for testing purposes btw :P

EDIT: Had a few copy/paste errors in my code, and it should work as a single file now. Apologies.

like image 872
Skitzafreak Avatar asked Jun 29 '17 16:06

Skitzafreak


1 Answers

You are trying to solve too many problems at once. When first starting out with tkinter, or when laying out a new GUI for the first time, it really helps to be methodical and only solve one layout problem at a time.

Step 1 - The main window

You've elected to create a "container" for everything in your GUI. So, the first step is to get that working before trying to get anything else to work. If it's too small then it will cause everything inside of it to be too small, and that's part of the problem with your original code.

Since it's the only widget directly in the root, I recommend using pack. You can use grid, but that takes three lines of code instead of one, since with grid you have to remember to configure the weight of at least one row and column.

Start with the following (and only the following):

class Interface(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        container = tk.Frame(self, background="bisque")
        container.pack(fill="both", expand=True)

interface = Interface()
interface.minsize(800, 480)
interface.mainloop()

How does it look? It looks good to me -- the blue completely fills the 800x480 window. We now no longer have to worry about the container.

If you want to use grid, remove the one line that calls pack, and replace it with these three lines:

self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
container.grid(row=0, column=0, sticky="nsew")

Step 2 - Children of container

It looks like the container will contain two children, right? A button frame on the left and a main area on the right. For now let's use a couple of empty frames, and get them working before continuing.

You didn't specify the size of these areas, so in this example the column on the left will be 1/3 the screen size, and the right will be 2/3. If you prefer, you could use a fixed pixel width for the left, and let the right take up the rest.

If you need a very specific width, you could use place here, but for now we'll stick with using grid.

Add the following to the bottom of Interface.__init__:

    bman = ButtonManager(container, controller=self)
    main = Main(container, controller=self)

    container.grid_columnconfigure(0, weight=1)
    container.grid_columnconfigure(1, weight=2)
    container.grid_rowconfigure(0, weight=1)

    bman.grid(row=0, column=0, sticky="nsew")
    main.grid(row=0, column=1, sticky="nsew")

We also need to define stubs for ButtonManager and Main:

class ButtonManager(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.configure(background="green")

class Main(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.configure(background="yellow")

That's enough coding. Stop and run the code, and make sure that the pink window takes up 1/3 of the blue area, and the yellow window takes up the other 2/3.

This might be a good time to play around with different values for the columnconfigure attributes (minsize, weight, pad), or switch to using place to see how it works.

Step 3 - The button manager

We just need to continue this process, going one level deeper. The nice thing is, we only have to focus on what's happening inside the button manager. Unless we add a widget that is too large to fit, we can't change the overall layout of the window as a whole.

It looks like ButtonManager is made up of a title and an area for buttons. So, let's add that. Since there are only two widgets in this frame, we can again save a couple lines of code and some headache by using pack, since pack excels at stacking things top-to-bottom.

In the following code I have the label stick to the top, and let the button frame fill the rest of the window.

Change ButtonManager to look like this:

class ButtonManager(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.config(background = 'green')

        lblFrame = LabelFrame(self, controller)
        btnFrame = ButtonFrame(self, controller)

        lblFrame.pack(side="top", fill="x", expand=False)
        btnFrame.pack(side="top", fill="both", expand=True)

Step 4: the LabelFrame

This is just a frame with a couple of labels. Again, since it has only a couple widgets, pack will save a little coding. You can use grid if you want, just remember that you have to configure the weight of rows and columns.

class LabelFrame(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.configure(background="blue")

        lblTitleBar = ttk.Label(self, text = 'TITLE BAR', background = 'grey', font = ("Arial", 20, "bold"))
        lblTextBar = ttk.Label(self, text = 'test text', background = 'grey', font = ("Arial", 16, "bold"))

        lblTitleBar.pack(side="top", fill="x")
        lblTextBar.pack(side="top", fill="x")

Step 5 - ButtonFrame

Finally, the button frame. There will be a grid of buttons. By now the process should be familiar - create the widgets, and use grid or pack to lay them out. In this case grid makes the most sense.

class ButtonFrame(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.configure(background="red")

        for row in range(7):
            self.grid_rowconfigure(row, weight=1)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        b1 = tk.Button(self, text="Button 1")
        b2 = tk.Button(self, text="Button 2")
        b3 = tk.Button(self, text="Button 3")
        b4 = tk.Button(self, text="Button 4")
        b5 = tk.Button(self, text="Button 5")
        b6 = tk.Button(self, text="Button 6")
        b7 = tk.Button(self, text="Button 7")
        b8 = tk.Button(self, text="Button 8")
        b9 = tk.Button(self, text="Button 9")
        b10 = tk.Button(self, text="Button 10")

        b1.grid(row=0, column=0, sticky="nsew")
        b2.grid(row=0, column=1, sticky="nsew")
        b3.grid(row=1, column=0, sticky="nsew")
        b4.grid(row=1, column=1, sticky="nsew")
        b5.grid(row=2, column=0, sticky="nsew")
        b6.grid(row=2, column=1, sticky="nsew")
        b7.grid(row=3, column=0, columnspan=2, sticky="nsew")
        b8.grid(row=4, column=0, sticky="nsew")
        b9.grid(row=4, column=1, sticky="nsew")
        b10.grid(row=5, column=0, columnspan=2, sticky="nsew")

Summary

If you take a methodical approach to your GUI layout, problems become much easier to solve since you're only solving for one area at a time. if you've followed all of the above closely, you'll end up with a GUI that works quite well even if you grow or shrink the window.

I know you have a requirement for a very specific size, but it's good to get in the habit of writing guis that responsive to changes in fonts, resolutions, and window sizes.

like image 75
Bryan Oakley Avatar answered Oct 13 '22 12:10

Bryan Oakley