I am creating a simple application in python using tkinter, which includes a menu from which I can pick different options and move to new menus. I am using the answer given by Steven Vascellaro here to destroy the frames as I move between them. In an earlier test version of the program I was able to give the buttons my custom font and have it display correctly, but when I add the
master, which switches between the different frames, the font no longer works, only making the text on the buttons slightly larger.
The version of the code that works correctly is this:
import tkinter as tk
from tkinter.font import Font, nametofont
class MainMenu(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.grid()
self.createWidgets()
def createWidgets(self):
global myFont
top=self.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1, pad=50)
self.columnconfigure(0, weight=1)
self.resume = tk.Button(self, text='Continue', height=2, width=10, font=myFont, command=self.quit)
self.library = tk.Button(self, text='Library', height=2, width=10, command=self.quit)
self.resume.grid(row=1, column=0,sticky=tk.N+tk.E+tk.W)
self.library.grid(row=3, column=0,sticky=tk.E+tk.W)
root = tk.Tk()
global myFont
fontCheck = open("Options.txt","r")
for line in fontCheck:
if "Font" in line:
tempLine = line.strip()
fontDetails = tempLine.split(",")
print(fontDetails)
myFont = Font(family=fontDetails[1], size=int(fontDetails[2]), weight="bold")
app = MainMenu()
app.mainloop()
root.destroy() `
Producing a menu that looks like this
But when I add this master section it no longer works:
import tkinter as tk
from tkinter.font import Font, nametofont
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = None
self.switch_frame(MainMenu)
def switch_frame(self, frame_class):
new_frame = frame_class(self)
if self._frame is not None:
self._frame.destroy()
self._frame = new_frame
self._frame.grid()
class MainMenu(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.grid()
self.createWidgets()
def createWidgets(self):
global myFont
top=self.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1, pad=50)
self.columnconfigure(0, weight=1)
self.resume = tk.Button(self, text='Continue', height=2, width=10, font=myFont, command=self.quit)
self.library = tk.Button(self, text='Library', height=2, width=10, command=self.quit)
self.resume.grid(row=1, column=0,sticky=tk.N+tk.E+tk.W)
self.library.grid(row=3, column=0,sticky=tk.E+tk.W)
root = tk.Tk()
global myFont
fontCheck = open("Options.txt","r")
for line in fontCheck:
if "Font" in line:
tempLine = line.strip()
fontDetails = tempLine.split(",")
print(fontDetails)
myFont = Font(family=fontDetails[1], size=int(fontDetails[2]), weight="bold")
app = Application()
app.mainloop()
root.destroy()
It creates a menu that looks like this
I would love if someone could explain why the font is not working correctly across the frames and explain how I can fix this issue.
You're creating 2 instances of tk.Tk: One where you set up the font, and one for your application. These two instances don't share the font. The solution would be to set up the Font inside your Application class (as a method, probably, and at initialization most likely).
class Application(tk.Tk):
def __init__(self, *args, fontfile = None, **kw):
super().__init__(*args, **kw)
if fontfile is None: fontfile = "Options.txt"
self._frame = None
self.fontfile = fontfile
self.setupFont()
self.switch_frame(MainMenu)
def setupFont(self):
global myFont
with open(self.fontfile,"r")
for line in fontCheck:
if "Font" in line:
tempLine = line.strip()
fontDetails = tempLine.split(",")
print(fontDetails)
myFont = Font(family=fontDetails[1], size=int(fontDetails[2]), weight="bold")
A few other Notes:
I don't particularly like the use of global; either having it as an attribute of Application or as a ttk.Style would be preferable (in my opinion).
You may want to consider using a options file with a predefined structure (recommend json) that can be read from in a more explicit fashion.
Related
I want to make a lot of notebook tabs, and I want to put them in canvas and to add a horizontal scrollbar so that I can scroll trough them.
I set the canvas size, but canvas size keep changing when I add new tab. Also, scrollbar does not work, can you tell me what am I doing wrong?
The program does not show me any error. This is the code:
from tkinter import *
from tkinter import ttk
myApp = Tk()
myApp.title(" Program ")
myApp.geometry("900x500")
CanvasTabs = Canvas(myApp, width=50, height=50)
CanvasTabs.grid(row=0,column=0)
tabs = ttk.Notebook(CanvasTabs, width=100, height=100)
tab1 = ttk.Frame(tabs)
tabs.add(tab1,text=" Tab 1 ")
tab2 = ttk.Frame(tabs)
tabs.add(tab2,text=" Tab 2 ")
tab3 = ttk.Frame(tabs)
tabs.add(tab3,text=" Tab 3 ")
tab4 = ttk.Frame(tabs)
tabs.add(tab4,text=" Tab 4 ")
hbar=Scrollbar(CanvasTabs,orient=HORIZONTAL)
hbar.pack(side=TOP,fill=X)
hbar.config(command=CanvasTabs.xview)
CanvasTabs.config(xscrollcommand=hbar.set)
tabs.pack(expand=1, fill="both")
myApp.mainloop()
I code a widget to fix the problem. Here is a real solution:
https://github.com/muhammeteminturgut/ttkScrollableNotebook
# -*- coding: utf-8 -*-
# Copyright (c) Muhammet Emin TURGUT 2020
# For license see LICENSE
from tkinter import *
from tkinter import ttk
class ScrollableNotebook(ttk.Frame):
def __init__(self,parent,*args,**kwargs):
ttk.Frame.__init__(self, parent, *args)
self.xLocation = 0
self.notebookContent = ttk.Notebook(self,**kwargs)
self.notebookContent.pack(fill="both", expand=True)
self.notebookTab = ttk.Notebook(self,**kwargs)
self.notebookTab.bind("<<NotebookTabChanged>>",self._tabChanger)
slideFrame = ttk.Frame(self)
slideFrame.place(relx=1.0, x=0, y=1, anchor=NE)
leftArrow = ttk.Label(slideFrame, text="\u25c0")
leftArrow.bind("<1>",self._leftSlide)
leftArrow.pack(side=LEFT)
rightArrow = ttk.Label(slideFrame, text=" \u25b6")
rightArrow.bind("<1>",self._rightSlide)
rightArrow.pack(side=RIGHT)
self.notebookContent.bind( "<Configure>", self._resetSlide)
def _tabChanger(self,event):
self.notebookContent.select(self.notebookTab.index("current"))
def _rightSlide(self,event):
if self.notebookTab.winfo_width()>self.notebookContent.winfo_width()-30:
if (self.notebookContent.winfo_width()-(self.notebookTab.winfo_width()+self.notebookTab.winfo_x()))<=35:
self.xLocation-=20
self.notebookTab.place(x=self.xLocation,y=0)
def _leftSlide(self,event):
if not self.notebookTab.winfo_x()== 0:
self.xLocation+=20
self.notebookTab.place(x=self.xLocation,y=0)
def _resetSlide(self,event):
self.notebookTab.place(x=0,y=0)
self.xLocation = 0
def add(self,frame,**kwargs):
if len(self.notebookTab.winfo_children())!=0:
self.notebookContent.add(frame, text="",state="hidden")
else:
self.notebookContent.add(frame, text="")
self.notebookTab.add(ttk.Frame(self.notebookTab),**kwargs)
def forget(self,tab_id):
self.notebookContent.forget(tab_id)
self.notebookTab.forget(tab_id)
def hide(self,tab_id):
self.notebookContent.hide(tab_id)
self.notebookTab.hide(tab_id)
def identify(self,x, y):
return self.notebookTab.identify(x,y)
def index(self,tab_id):
return self.notebookTab.index(tab_id)
def insert(self,pos,frame, **kwargs):
self.notebookContent.insert(pos,frame, **kwargs)
self.notebookTab.insert(pos,frame,**kwargs)
def select(self,tab_id):
self.notebookContent.select(tab_id)
self.notebookTab.select(tab_id)
def tab(self,tab_id, option=None, **kwargs):
return self.notebookTab.tab(tab_id, option=None, **kwargs)
def tabs(self):
return self.notebookContent.tabs()
def enable_traversal(self):
self.notebookContent.enable_traversal()
self.notebookTab.enable_traversal()
Taking Bryan's example on this post and modifying it to include your Notebook code we get a functioning scrollbar that will allow you to scroll over your Notebook widget if it exceeds the limit of the window.
Bryan's example uses the pack() geometry manager however I personally find grid() easier to visualize so I replace pack with grid() in my example.
UPDATE:
import tkinter as tk
import tkinter.ttk as ttk
class Example(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, borderwidth=0)
self.frame = tk.Frame(self.canvas)
self.vsb = tk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
self.vsb.grid(row=1, column=0, sticky="nsew")
self.canvas.configure(xscrollcommand=self.vsb.set)
self.canvas.grid(row=0, column=0, sticky="nsew")
self.canvas.create_window((3,2), window=self.frame, anchor="nw", tags="self.frame")
self.frame.bind("<Configure>", self.frame_configure)
self.populate()
def populate(self):
tabs = ttk.Notebook(self.frame, width=100, height=100)
for tab in range(50):
tabs.add(ttk.Frame(tabs), text=" Tab {} ".format(tab))
tabs.grid(row=0, column=0, sticky="ew")
def frame_configure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
app = Example()
app.mainloop()
Updated results:
Per your request in the comments here is a Non-OOP example:
import tkinter as tk
import tkinter.ttk as ttk
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0)
frame = tk.Frame(canvas)
vsb = tk.Scrollbar(root, orient="horizontal", command=canvas.xview)
vsb.grid(row=1, column=0, sticky="nsew")
canvas.configure(xscrollcommand=vsb.set)
canvas.grid(row=0, column=0, sticky="nsew")
canvas.create_window((3,2), window=frame, anchor="nw", tags="frame")
tabs = ttk.Notebook(frame, width=100, height=100)
for tab in range(50):
tabs.add(ttk.Frame(tabs), text=" Tab {} ".format(tab))
tabs.grid(row=0, column=0, sticky="ew")
def frame_configure(event):
global canvas
canvas.configure(scrollregion=canvas.bbox("all"))
frame.bind("<Configure>", frame_configure)
root.mainloop()
I'm working in Python 3.5 and TKinteer. Inside of a text widget, I have created a context menu that appears when the user right-clicks. However, when I try and create the commands I want (cut, copy, paste), the commands seem to have no effect.
The relevant code is as follows:
from tkinter import *
class Application:
def __init__(self,master):
self.master = master
self.initUI()
def initUI(self):
root.title("Simple Text Editor")
scrollBar = Scrollbar(root)
self.textPad = Text(root, width=100, height=100, wrap='word',
yscrollcommand=scrollBar.set,
borderwidth=0, highlightthickness=0)
scrollBar.config(command=self.textPad.yview)
scrollBar.pack(side='right', fill='y')
self.textPad.pack(side='left', fill='both', expand=True)
class PopupMenu:
def __init__(self, master, *args, **kwargs):
self.popup_menu = Menu(root, tearoff=0)
self.popup_menu.add_command(label="Cut",
command=lambda: app.textPad.event_generate('<Control-x>'))
self.popup_menu.add_command(label="Copy",
command=lambda: app.textPad.event_generate('<Control-c>'))
self.popup_menu.add_command(label="Paste",
command=lambda: app.textPad.event_generate('<Control-v>'))
app.textPad.bind("<Button-3>", self.popup)
self.popup_menu.bind("<FocusOut>",self.popupFocusOut)
def popup(self, event):
self.popup_menu.post(event.x_root, event.y_root)
self.popup_menu.focus_set()
def popupFocusOut(self, event=None):
self.popup_menu.unpost()
root = Tk()
app = Application(root)
popupMenu = PopupMenu(root)
root.mainloop()
You don't want to generate <Control-x>, etc. Instead, generate the virtual events <<Cut>>, <<Copy>> and <<Paste>>.
I've looked at other question with a similar title and have read the answers, however nothing has worked for me. I am trying to make a simple app with a listbox + scroll bar with two buttons below it all within a group box. I've used pyqt but this is my first time using tkinter:
import tkinter as tk
class InputWindow(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.initialize()
def initialize(self):
# Group box to contain the widgets
self.input = tk.LabelFrame(self, text="Input Files")
# Listbox with scrollbar to the side
self.listbox = tk.Listbox(self.input)
self.scrollbar = tk.Scrollbar(self.listbox, orient=tk.VERTICAL)
self.listbox.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.listbox.yview)
self.listbox.grid(row=0, column=0, columnspan=2)
self.add_btn = tk.Button(self.input, text="Add...")
self.add_btn.grid(row=1, column=0)
self.remove_btn = tk.Button(self.input, text="Remove")
self.remove_btn.grid(row=1, column=1)
if __name__ == "__main__":
root = tk.Tk()
app = InputWindow(root)
root.mainloop()
This is more or less what I want but in tkinter:
What am I doing wrong/how can this be done?
You're forgetting two things:
To pack (or grid or place) app
To pack (or grid or place) input
You're program with the required statements:
import tkinter as tk
class InputWindow(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.initialize()
def initialize(self):
# Group box to contain the widgets
self.input = tk.LabelFrame(self, text="Input Files")
# Listbox with scrollbar to the side
self.listbox = tk.Listbox(self.input)
self.scrollbar = tk.Scrollbar(self.listbox, orient=tk.VERTICAL)
self.listbox.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.listbox.yview)
self.listbox.grid(row=0, column=0, columnspan=2)
self.add_btn = tk.Button(self.input, text="Add...")
self.add_btn.grid(row=1, column=0)
self.remove_btn = tk.Button(self.input, text="Remove")
self.remove_btn.grid(row=1, column=1)
self.input.pack(expand=1, fill="both") # Do not forget to pack!
if __name__ == "__main__":
root = tk.Tk()
app = InputWindow(root)
app.pack(expand=1, fill="both") # packing!
root.mainloop()
I want to create a custom widget in tkinter such that when instantiated, displays a label and an entry box. Example I created a class named entry and call as.. entry ('name', master ) and this would display a label with text as main along side an entry box.
I have succeeded in doing that but my problem is with the geometry managers. they all seem to mess up everything
Your widget should subclass Frame. Within the frame you can use any geometry manager you want without affecting any other code. It's important that the widget class does not call grid, pack or place on itself -- that's the job of the function that creates the widget. Every widget, or function that creates a widget, should only ever worry about laying out its children.
Here's an example that creates a couple of different custom widgets. Each uses a different geometry manager to illustrate that they don't interfere with each other:
try:
# python 3.x
import tkinter as tk
except ImportError:
# python 2.x
import Tkinter as tk
class CustomWidget(tk.Frame):
def __init__(self, parent, label, default=""):
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text=label, anchor="w")
self.entry = tk.Entry(self)
self.entry.insert(0, default)
self.label.pack(side="top", fill="x")
self.entry.pack(side="bottom", fill="x", padx=4)
def get(self):
return self.entry.get()
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.label = tk.Label(self)
self.e1 = CustomWidget(self, "First Name:", "Inigo")
self.e2 = CustomWidget(self, "Last Name:", "Montoya")
self.submitButton = tk.Button(self, text="Submit", command=self.submit)
self.e1.grid(row=0, column=0, sticky="ew")
self.e2.grid(row=1, column=0, sticky="ew")
self.label.grid(row=2, column=0, sticky="ew")
self.submitButton.grid(row=4, column=0)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
def submit(self):
first = self.e1.get()
last = self.e2.get()
self.label.configure(text="Hello, %s %s" % (first, last))
if __name__ == "__main__":
root = tk.Tk()
Example(root).place(x=0, y=0, relwidth=1, relheight=1)
root.mainloop()
I agree with Mr. Oakley. You should subclass frame to do your job.
The simplest way to do what you want is to create a module with the following code:
# AnnotatedEntry.py
def AnnotatedEntry(master, name="An annoted entry box"):
'''
As a little extra, name is a keyword-argument, which defaults to "An annotated
entry box."
'''
import tkinter as tk
overlord = tk.Frame(master, height=5, width=40)
labeller = tk.Label(overlord, text=name, font="Times 14 bold")
labeller.grid(sticky='new')
inputter = tk.Entry(overlord, font="Times 14 bold")
inputter.grid(sticky='sew', pady=(10,0))
return overlord
This would be used as follows:
# Main program
import tkinter
import AnnotatedEntry
root = tkinter.Tk()
hold = AnnotatedEntry.AnnotatedEntry(root, name="Hello, world!")
hold.grid()
I hereby affirm, on my Scout Honor, that this code has been fully tested, and is guaranteed to work in Python 3.7.4. That being said, there is currently no method for returning the data contained in the Entry; you will have to work that out for yourself.
Based on #Bryan Oakley answer, I do have some modification. I know it's out of topic somehow. This is how to return a value from the widget and it only allows integer up to some number of digits that the user must entered.
#create a global value
global tbVal
tbVal = 0
class CustomWidget(tk.Frame):
def __init__(self, parent, nDigits):
tk.Frame.__init__(self, parent)
self.entry = tk.Entry(self)
self.entry.pack(side="bottom", fill="x", padx=4)
self.entry.configure(validate='all',validatecommand=windows.register(self.sbValidate),'%P','%W',nDigits))
def get(self):
return self.entry.get()
def sbValidate(self, userInput, widget, nDigits):
global tbVal
tbVal = userInput
if userInput == '':
return True
if '.' in userInput or ' ' in userInput:
return False
n = len(userInput)
if n > int(nDigits):
return False
try:
val = int(float(userInput))
except ValueError:
return False
return val
class Example(tk.Frame):
def __init__(self, parent, nDigitsLimit):
tk.Frame.__init__(self, parent)
self.e1 = CustomWidget(self, nDigitsLimit)
self.e1.grid(row=0, column=0, sticky="ew")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
def btnStartClick():
print(tbVal)
nDigitsLimit = 8
tbTest = ttk.Entry(Example(windows, nDigitsLimit).place(x=20, y=20, relwidth=0.25, relheight=0.05))
btnStart = tk.Button(frame, text='Start', command=btnStartClick)
btnStart.place(relx=0.50, rely=0.50)
This is the front end I developed for my application using Tkinter:
from Tkinter import *
class Example(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.title("Simple")
self.pack(fill=BOTH, expand=1)
frame = Frame(self, relief="flat", borderwidth=1)
label=Label(frame,text="Scope:")
label.pack(side="left", fill=None, expand=False)
var = StringVar()
var.set("today")
list = OptionMenu(frame, var, "today","yesterday","this week","last week","this month","last month")
list.pack(side="left", fill=None, expand=False)
fetchButton = Button(frame, text="Fetch",command=self.handle(var))
fetchButton.pack(side="left", fill=None, expand=False)
frame.grid(row=1,column=1,pady=4,padx=5,sticky=W)
area = Text(self,height=15,width=60)
area.grid(row=2,column=1,rowspan=1,pady=4,padx=5)
scroll = Scrollbar(self)
scroll.pack(side=RIGHT, fill=Y)
area.config(yscrollcommand=scroll.set)
scroll.config(command=area.yview)
scroll.grid(row=2, column=2, sticky='nsew')
quitButton = Button(self, text="Cancel",command=self.quit)
quitButton.grid(pady=4,padx=5,sticky=W,row=3, column=1)
root = Tk()
app = Example(root)
root.mainloop()
Where exactly do I have to put the handle() method so it can write repeatedly to the text widget? When I put handle() within the Example class and use self.area.insert(), it shows an error saying
Example instance has no attribute 'area'
Please help out.
You need to pass the function object to the Button instance, not a function call. i.e.
fetchButton = Button(frame, text="Fetch",command=self.handle)
To make the handle work in the context of the rest of the code:
from Tkinter import *
class Example(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.parent.title("Simple")
self.pack(fill=BOTH, expand=1)
self.init_ui()
def init_ui(self):
self.frame = Frame(self, relief="flat", borderwidth=1)
self.frame.grid(row=1,column=1,pady=4,padx=5,sticky=W)
self.label=Label(self.frame,text="Scope:")
self.label.pack(side="left", fill=None, expand=False)
self.var = StringVar()
self.var.set("today")
self.list = OptionMenu(self.frame, self.var, "today","yesterday",
"this week","last week","this month",
"last month")
self.list.pack(side="left", fill=None, expand=False)
self.fetchButton = Button(self.frame, text="Fetch",command=self.handle)
self.fetchButton.pack(side="left", fill=None, expand=False)
self.area = Text(self,height=15,width=60)
self.area.grid(row=2,column=1,rowspan=1,pady=4,padx=5)
self.scroll = Scrollbar(self)
self.scroll.pack(side=RIGHT, fill=Y)
self.area.config(yscrollcommand=self.scroll.set)
self.scroll.config(command=self.area.yview)
self.scroll.grid(row=2, column=2, sticky='nsew')
self.quitButton = Button(self, text="Cancel",command=self.quit)
self.quitButton.grid(pady=4,padx=5,sticky=W,row=3, column=1)
def handle(self):
self.area.delete(1.0, END)
self.area.insert(CURRENT,self.var.get())
if __name__ == "__main__":
root = Tk()
app = Example(root)
root.mainloop()
Declaring your widgets as attributes will save you a lot of pain an suffering as your application expands. Also keeping references to everything in Tk can stop some unwanted garbage collection, particularly with images in Label instances.
It is also worth noting that using grid and pack interchangeably is likely to lead to bugs later on.