I am trying to build an application with tkinter.
The layout works without OO principles, but I am struggling to understand how I should move it to OO.
The layout is as shown in the pic below. (1280x720px)
I have the following:
banner on top with username/welcome message, and logo rh corner, columnspan=8
menu bar with buttons on the left, split into 2 rows (row1: rowspan 6, row2: rowspan=4)
working area (white block) that has a Frame, which I'll add a notebook to, each menu button opening a different notebook page.
What is the best way to make this OO? (I am still learning, so very new to OO)
There is no straight translation possible, since everything depends on your needs.
If you create a simple programm you can just create the class and create every Button,Label,Frame... in the constructor. When created you have to choose one of the layout managers grid,pack or place. After that you create your functions and you are done. If you deal with bigger projects and have a big amount of Labels, Buttons etc.. you maybe want to create containers for each.
In your case you wont need a lot of functions and buttons, so you should maybe go with the basic approach:
from tkinter import *
class name_gui:
def __init__(self, top):
#specify main window
self.top = top
self.title = top.title("name_gui")
self.minsize = top.geometry("1280x720")
self.resizable = top.resizable(height=False,width=False)
#create buttons,Labels,Frames..
self.Button1 = Button(top,text="Button1",command=self.exa_fun)
self.Button2 = Button(top,text="Button2",command=self.exa_fun2)
#place them, choose grid/place/pack
self.Button1.place(relx=0.5,rely=0.5)
self.Button2.place(relx=0.5,rely=0.2)
#create your functions
def exa_fun(self):
pass
def exa_fun2(self):
pass
top = Tk()
exa_gui = name_gui(top)
top.mainloop()
Related
I am creating a Tkinter/Python3 application where the main window inherits from Notebook (i need tabs), and each tab should be a custom class inheriting from Frame (I would then dynamically use matplotlib to create custom graphs).
Unfortunately I don't seem to be able to have Notebook accept my custom Frames.
Following very reduced snippet of code:
#!/usr/bin/env python3
from tkinter import *
from tkinter.ttk import Notebook
class MyFrame1(Frame):
def __init__(self, master=None, mytext=""):
super().__init__(master)
self.create_widgets(mytext)
def create_widgets(self, mytext):
self.label = Label(self.master, text=mytext, anchor=W)
# this is not placed relative to the Frame, but to the
# master
# 1. How I get the relative coordinates inside the frame
# to be 10, 10 of the frame area?
self.label.place(x=10, y=10, width=128, height=24)
class MyNotebook(Notebook):
def __init__(self, master=None):
super().__init__(master)
self.create_widgets()
def create_widgets(self):
self.f1 = MyFrame1(self, "abc")
# once the UI is drawn, the label "def" seems to overlay
# "abc" even when "f1" is selected
# 2. Why is self.f2 always shown even when self.f1 is
# selected?
self.f2 = MyFrame1(self, "def")
self.add(self.f1, text="f1")
self.add(self.f2, text="f2")
# Without this command nothing gets drawn
# 3. Why is this? Is this equivalent of 'pack' but for
# pixel driven layout?
self.place(width=640, height=480)
def main():
root = Tk()
root.minsize(640, 480)
root.geometry("640x480")
app = MyNotebook(master=root)
# this works as intended the label is indeed placed
# in the frame at 10, 10
#app = MyFrame1(master=root, mytext="123abc")
app.mainloop()
return None
if __name__ == "__main__":
main()
As per comments I have the following main question: why aren't my custom instances of MyFrame1 properly displayed inside MyNotebook?
Sub questions:
How can I get relative coordinate areas of where the frame is located when place my elements (in this case a Label)?
Why even when self.f1 tab is selected in the UI, I can still see the content of self.f2 tab?
Is self.place required in order to show all sub-elements when not using pack?
If I dynamically create Tkinter elements after the MyNotebook is initialized, will those be bound to respective tabs?
Not sure what I'm doing wrong?
Thanks!
Not sure what I'm doing wrong?
Your create_widgets method needs to add widgets to self, not self.master.
How can I get relative coordinate areas of where the frame is located when place my elements (in this case a Label)?
I don't understand what you mean by this. When you use place, coordinates will be interpreted relative to the frame. However, I strongly advise against using place. Both pack and grid will trigger the frame to resize to fit its children which almost always results in a more responsive UI
Why even when self.f1 tab is selected in the UI, I can still see the content of self.f2 tab?
Because you added internal widgets to self.master instead of self.
Is self.place required in order to show all sub-elements when not using pack?
No. It is required to use a geometry manager but it doesn't have to be place. Usually, place is the least desirable geometry manager to use. pack and grid are almost always better choices except for some very specific situations.
If I dynamically create Tkinter elements after the MyNotebook is initialized, will those be bound to respective tabs?
They will be in whatever tab you put them in.
Finally, I would suggest that you remove self.place in create_widgets. Instead, call pack, place, or grid in the same block of code that creates an instance of that class.
It's a bad practice for a widget to add itself to another widget's layout. The code that creates the widget should be the code that adds the widget to the layout.
I have a UI with ttk.Notebook tabs, and would like to display the same widget in multiple tabs. This requires the widget to simultaneously have multiple parent frames, which doesn't seem possible. Is there some other way it can be done?
No, you cannot have a single widget in multiple places. A widget can only be in one place at a time.
I have since tinkered around with this problem, and have come up with this preliminary (and largely untested) solution to having a widget appear on multiple tabs of a ttk.Notebook.
Although the physical parent of a widget is immutably set when the widget is created, its geometric parent (which controls where it appears) CAN be changed using the .grid() in_ option. So in theory it should be possible to have the widget appear on multiple notebook tabs simply by changing its geometric parent whenever the user changes to a new tab (NotebookTabChanged)
The following code seems to work as advertised, although I haven't really tested it outside the toy code below:
import tkinter as tk
import tkinter.ttk as ttk
class myApp(tk.Tk):
def __init__(self):
super().__init__()
# Create the Notebook and bind the tab-select event
self.notebook = ttk.Notebook(self, width=500, height=200)
self.notebook.grid()
self.notebook.bind("<<NotebookTabChanged>>", self.select_tab)
# Create a status bar which displays the name of the currently selected tab
self.status = ttk.Label(self)
self.status.grid(sticky='w')
# Create three frames - one for each of three tabs - and store them in
# a dictionary with the tab names as keys
self.tabs = dict()
self.tabs['PAGE 1'] = ttk.Frame(self.notebook)
self.tabs['PAGE 2'] = ttk.Frame(self.notebook)
self.tabs['PAGE 3'] = ttk.Frame(self.notebook)
# Create the tabs in the notebook
for t in self.tabs.keys():
self.notebook.add(self.tabs[t], text=t, underline=0, sticky='nsew')
# Put a widget on the middle tab, just to have soemthing there
ttk.Label(self.tabs['PAGE 2'],text="A SIMPLE LABEL").grid(row=0,column=0)
# Create a button - this is the widget we wish to appear on all tabs
self.btn = tk.Button(self,text='PRESS ME!',command=self.button_pressed)
# This is the method called when the user selectes a new tab. It
# updates the status bar and moves the button to the new tab.
def select_tab(self, event):
id = self.notebook.select()
name = self.notebook.tab(id, "text")
text = f"--- {name} is currently selected ---"
self.status.config(text=text)
self.btn.grid(row=4,column=0,in_= self.tabs[name])
def button_pressed(self):
print('BUTTON PRESSED')
if __name__ == "__main__":
app = myApp()
app.mainloop()
Yes you can, example using two notebook tabs(aba1, aba2)
def widgets_with_icon(self):
aba=''
abas = (self.aba1, self.aba2)
for aba in abas:
self.tkimage3 = PhotoImage(file="images/salvar_100.png")
self.bt_image_save = tk.Button(aba, image=self.tkimage3, compound=tk.LEFT, bd=0, bg='#A9A9A9', activebackground='#A9A9A9', command=your_command)
self.bt_image_save.image = self.tkimage3 # reference to image not garbage collect
self.bt_image_save.place(relx=0.01, rely=0.01, relwidth=0.03, relheight=0.04)
self.bt_image_save (button with image only) will appear in aba1 and aba2.
I hope it can help.
I've been struggling with this for a while. I think I'm missing some simple piece of information and I hope you guys can help clear this up for me.
I'm trying to get tkinter to display different frames which I will eventually place widgets inside of. Here's what I did:
I've made a class that is supposed to initialize the window and make all the different frames the program will run.
I've made a separate class for each frame(I'm intending to have variables associated with the different classes when the program is done), and assigned a variable that will start that class up and make it run it's init function
I ended the StartUp class by telling it to tkraise() the frame I want displayed, and that's where things stop working correctly.
I set each frame to a different color, so when you run this program you will see that they split the screen space up instead of one being raised to the top. What am I missing?
One last point, I am purposely trying to spell everything out in my program, I learn better that way. I left it so I have to type tkinter.blah-blah-blah in front of each tkinter command so I can recognize them easily, and I decided not to have my classes inherit Frame or Tk or anything. I'm trying to understand what I'm doing.
import tkinter
class StartUp:
def __init__(self):
self.root = tkinter.Tk()
self.root.geometry('300x300')
self.container = tkinter.Frame(master=self.root, bg='blue')
self.container.pack(side='top', fill='both', expand=True)
self.page1 = Page1(self)
self.page2 = Page2(self)
self.page1.main_frame.tkraise()
class Page1():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='green')
self.main_frame.pack(side='top', fill='both', expand=True)
class Page2():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='yellow')
self.main_frame.pack(side='top', fill='both', expand=True)
boot_up = StartUp()
boot_up.root.mainloop()
When you do pack(side='top', ...), top doesn't refer to the top of the containing widget, it refers to the top of any empty space in the containing widget. Page initially takes up all of the space, and then when you pack Page2, it goes below Page1 rather than being layered on top of it.
If you are using the strategy of raising one window above another, you need to either use grid or place to layer the widgets on top of each other. The layering is something pack simply can't do.
Your other choice is to call pack_forget on the current window before calling pack on the new windowl
We have a functioning program that uses Tkinter as its GUI. Everything works fine however different branches of the code are now using different hardware which realistically need different buttons. Hence we'd like to have the main GUI import modules representing the buttons depending on what hardware is being used.
I've cut out some of the code below, I'm interested in removing the makemenu() function to a separate module, hence when it is called in the Application __init__ (self.makemenu(master)) I would like to make that a reference to a separate module. I've tried doing this and am having trouble. Is this even possible?
I'm a little confused on the parent structure, what needs to be passed to my button module, etc.? I know this is a poorly constructed question but if anyone is able to advise if this is possible and put my on the right track that would be great. For example if someone could show how to modify this code to have the buttons defined in a separate module I could figure out how to do the same in my module.
# Import necessary libraries
import sys
import os
import Tkinter as tk
class Application(tk.Frame):
##################################################################
## Final functions are designed to initialize the GUI and
## connect various mouse movements to useful functions.
##################################################################
def definevars(self):
'''Original definition of all of the key variables that
we need to keep track of while running the GUI
'''
self.disable = True
self.savimgstatus = 'off'
self.mode = 'Standby'
self.status = 'Not Ready'
def makemenu(self,master):
''' Function to create the main menu bar across
the top of the GUI.
'''
self.menubar = tk.Menu(master)
## Motor Submenu
motormenu = tk.Menu(self.menubar,tearoff=1)
motormenu.add_command(label='ALT',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('alt'))
motormenu.add_separator()
motormenu.add_command(label='AZ',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('az'))
self.menubar.add_cascade(label='Tracker Motors',menu=motormenu)
## Set the big menu as the main menu bar.
master.config(menu=self.menubar)
def __init__(self,tcpconn,DOME,TRACKERSTAGE, master=None):
'''Main function to initialize the GUI. Will scale
the size of the GUI to fit any size screen... to a
point. It will not allow it to be smaller than
600x800.
'''
self.buf = 1024
## Check resolution of screen. Make GUI 2/3rds of size
## unless that means under 600x800.
fh = round(master.winfo_screenheight()*2./3.)
fw = round(master.winfo_screenwidth()*2./3.)
if fh < 600: fh = 600
if fw < 800: fw = 800
print 'GUI resolution set to {0} x {1}'.format(fw,fh)
self.fw = fw
self.fh = fh
self.imwidth = int(0.45*self.fw)
self.imheight = int(0.45*self.fh)
self.imcentx = self.imwidth/2
self.imcenty = self.imheight/2this
## Initialize Frame
tk.Frame.__init__(self, master, height=fh,width=fw)
self.grid()
self.grid_propagate(0)
## Initialize Various variables.
self.definevars()
## Create buttons, etc.
self.createWidgets()
self.makemenu(master)
self.disableall()
## Main Loop function
self.checkoutput()
###################################################################
# Initialize GUI window.
root = tk.Tk()
root.title('Hardware') # window title
app = Application(master=root)
app.mainloop() # go into the main program loop
sys.exit()
If you want to move makemenu to a separate module, that should be pretty simple. However, you'll need to change a few things.
Since makemenu no longer has a reference to self (or has a different reference, if you implement it as a separate class), you need to replace calls like command=lambda: self.geterror('alt')) to be command=lambda: master.geterror('alt')).
The other thing I recommend is to remove the call to add the menu to the root. I believe that modules shouldn't have side effects like this -- the function should make a menu and return it, and let the caller decide how to use it, ie:
self.menubar=makemenu(master)
master.configure(menu=self.menubar)
Roughly speaking, this is a variation of the MVC (model/view/controller) architectural pattern where the Application instance is your controller (and also part of the view unless you make modules of all your UI code). The menu is part of the view, and forwards UI functions to the controller for execution.
Your application then looks something like this:
from makemenu import makemenu
class Application(...):
def __init__(...):
...
self.menubar = makemenu(master)
master.config(menu=self.menubar)
...
I have a canvas (for ttk.Frame not having xview,yview methods) holding frames inside of it, which I want to browse horizontally, as they are in the same row. Found some tutorials, and while they were dealing with different combination of widgets, I followed them, and tried to apply them to my problem, but with no success. I do see a scrollbar, but it doesn't do or respond to anything.
class App:
def __init__(self,db):
self.db = db
self.root = tkinter.Tk()
self.masterframe = ttk.Frame(self.root)
self.masterframe.grid()
self.mastercanvas = tkinter.Canvas(self.masterframe)
self.mastercanvas.grid()
self.scrollbar = ttk.Scrollbar(self.masterframe,orient="horizontal",
command = self.mastercanvas.xview)
self.scrollbar.grid()
self.mastercanvas.configure(xscrollcommand=self.scrollbar.set)
for i,e in enumerate(self.db.elements):
xf = XFrame(self,e)
xf.grid(row=0,column=i,sticky="n")
Edit:
class XFrame:
def __init__(self,app,x):
self.app = app
self.x = x
self.frame = ttk.Frame(self.app.mastercanvas)
self.set_up() # this sets frame's padding and populates it with widgets
Now, where ever I paste two lines of code here suggested* - at the end of the first init definition, or at the end of the second init definition - nothing new happens. I see my frames appearing as I intended them to appear - 3 of them. Part of the 4th. And an unfunctional scrollbar.
*
self.update_idletasks() # using self.root in first, self.app.root in second variant
self.mastercanvas.configure(scrollregion=self.mastercanvas.bbox("all"))
# in second variant reffered to as self.app.mastercanvas ...
You've got to configure the scrollregion attribute of the canvas so that it knows how much of the virtual canvas you want to scroll. This should do it:
# create everything that will be inside the canvas
...
# next, cause the window to be drawn, so the frame will auto-size
self.update_idletasks()
# now, tell the canvas to scroll everything that is inside it
self.mastercanvas.configure(scrollregion=self.mastercanvas.bbox("all"))
That being said, there's nothing in your canvas to be scrolled, and I'm not entirely sure what you were expecting to be in the canvas. If you are wanting to scroll through the instances of XFrame, they need to be children of some other frame, and that other frame needs to be an object on the canvas.