New to Tkinter. I've been at this for a few days now. I'm looking to pass the file path of an Mp4 video (retrieved using askopenfilename and a button) to another frame (where I grab the first frame and display it, so the user can select a ROI).
UPDATED! MINIMAL, VIABLE EXAMPLE: RUNNING THIS CODE, THE FILENAME CHOSEN DOES NOT DISPLAY ON THE SECOND FRAME (PROBLEM):
LARGE_FONT=("Verdana",12)
import tkinter as tk
from tkinter import filedialog
from tkinter import *
filename = ''
class HRDetectorApp(tk.Tk): #in brackets, what the class inherits
def __init__(self,*args,**kwargs): #this will always load when we run the program. self is implied args = unlimited vars. kwargs are keywords arguments (dictionaries)
tk.Tk.__init__(self,*args,**kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True) #pack into top, fill into entire top space, and expand will expand into the whole window. fill into the area you packed.
container.grid_rowconfigure(0, weight=1) #min size zero, weight is priority
container.grid_columnconfigure(0,weight=1)
self.frames = {}
self.shared_data = {"filename": tk.StringVar()}
for F in (StartPage, SelectROIPage):
frame = F(container,self)
self.frames[F] = frame
frame.grid(row=0,column=0, sticky="nsew") #sticky = alignment + stretch, north south east west
self.show_frame(StartPage)
self.title("Heart Rate Detection")
def show_frame(self,cont):
frame=self.frames[cont]
frame.tkraise()
def get_page(self, page_class):
return self.frames[page_class]
def openFile():
root = tk.Tk()
global filename
filename = filedialog.askopenfilename(title="Select an Mp4 Video", filetypes =(("Mp4 Files", "*.mp4"),))
root.update_idletasks()
print(filename)
class StartPage(tk.Frame):
def __init__(self,parent,controller):
self.controller = controller
#global filename
#filename = ""
tk.Frame.__init__(self,parent)
label = tk.Label(self,text="Start Page", font=LARGE_FONT)
label.pack(pady=10,padx=10)
self.filename = tk.StringVar()
chooseFileButton = tk.Button(self,text="Upload a Video",command=openFile)
chooseFileButton.pack()
goButton = tk.Button(self,text ="Click me after you've uploaded a video!", command = lambda: controller.show_frame(SelectROIPage))
goButton.pack()
class SelectROIPage(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self,parent)
label = tk.Label(self,text="Select a Region of Interest (R.O.I)", font=LARGE_FONT)
label.pack(pady=10,padx=10)
label = tk.Label(self, text = "selected file : " + filename)
label.pack()
app = HRDetectorApp()
app.mainloop()
How to reproduce?
Click "Upload a video" and select an MP4 file.
Click "Click me after you've uploaded a video"
For some reason, the variable doesn't update after calling askopenfilename. I've tried to use global variables, using root.update, nothing has worked (see my attempts commented out)
Any help is greatly appreciated, thank you!
ORIGINAL CODE : thanks for your suggestions to simplify it :)
LARGE_FONT=("Verdana",12)
import tkinter as tk
from tkinter import filedialog
from tkinter import *
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.widgets import RectangleSelector
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import cv2
import numpy as np
import sys
import time
from scipy import signal
import scipy.signal as signal
import selectinwindow
class HRDetectorApp(tk.Tk): #in brackets, what the class inherits
def __init__(self,*args,**kwargs): #this will always load when we run the program. self is implied args = unlimited vars. kwargs are keywords arguments (dictionaries)
tk.Tk.__init__(self,*args,**kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True) #pack into top, fill into entire top space, and expand will expand into the whole window. fill into the area you packed.
container.grid_rowconfigure(0, weight=1) #min size zero, weight is priority
container.grid_columnconfigure(0,weight=1)
self.frames = {}
self.shared_data = {"filename": tk.StringVar()}
for F in (StartPage, SelectROIPage):
frame = F(container,self)
self.frames[F] = frame
frame.grid(row=0,column=0, sticky="nsew") #sticky = alignment + stretch, north south east west
self.show_frame(StartPage)
self.title("Heart Rate Detection")
def show_frame(self,cont):
frame=self.frames[cont]
frame.tkraise()
def get_page(self, page_class):
return self.frames[page_class]
#
#def openFile():
# root = tk.Tk()
# global filename
# root.filename = filedialog.askopenfilename(title="Select an Mp4 Video", filetypes =(("Mp4 Files", "*.mp4"),))
# filename = root.filename
# root.update_idletasks()
# #name = root.filename
#
# print(filename)
class StartPage(tk.Frame):
def __init__(self,parent,controller):
self.controller = controller
# global filename
# filename = ""
tk.Frame.__init__(self,parent)
label = tk.Label(self,text="Start Page", font=LARGE_FONT)
label.pack(pady=10,padx=10)
self.filename = tk.StringVar()
chooseFileButton = tk.Button(self,text="Upload a Video",command=self.openFile)
chooseFileButton.pack()
goButton = tk.Button(self,text ="Click me after you've uploaded a video!", command = lambda: controller.show_frame(SelectROIPage))
goButton.pack()
def openFile(self):
#root = tk.Tk()
#global filename
#root.filename = filedialog.askopenfilename(title="Select an Mp4 Video", filetypes =(("Mp4 Files", "*.mp4"),))
# filename = root.filename
self.filename.set(filedialog.askopenfilename(title="Select an Mp4 Video", filetypes =(("Mp4 Files", "*.mp4"),)))
#root.update()
#if filename == "":
#root.after(1000,openFile(self))
#name = root.filename
print(self.filename.get())
**code to use rectangle selector for selecting ROI**
class SelectROIPage(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self,parent)
self.controller = controller
startpg = self.controller.get_page(StartPage)
file = startpg.filename.get() **THIS IS NOT UPDATING**
label = tk.Label(self,text="Select a Region of Interest (R.O.I)", font=LARGE_FONT)
label.pack(pady=10,padx=10)
#file=filename
**code to read image and display it (confirmed no errors here!)**
app = HRDetectorApp()
app.mainloop()
You are never updating the value in SelectROIPage. The page is instantiated once, and in that instantiation you initialize file to '' with file = startpg.filename.get(), because this occurs before the button has been clicked when you create HRDetectorApp at frame = F(container, self)
When you click the button, it doesn't create a new frame, thus does not call so it is not updating, and the value is an empty string. You must somehow update the value when the first button is clicked.
I see a few options:
Use the same variable in both SelectROIPage and StartPage.
Update the variable when the button is clicked by extending readyForROI (a bit kludgy in my opinion)
Update the variable when the ROI frame is shown using a binding (overkill, but see: How would I make a method which is run every time a frame is shown in tkinter)
If it were me, I would choose the first.
UPDATE
It's a bit trimmed but that's still not what I'd call a "minimum reproducible example".
I've edited your code a bit to accomplish what you're trying to do. I wanted to be able to point at a few other issues as well, so it's not quite minimal.
import tkinter as tk
from tkinter import filedialog
from tkinter import *
class HRDetectorApp(tk.Tk): #in brackets, what the class inherits
def __init__(self,*args,**kwargs): #this will always load when we run the program. self is implied args = unlimited vars. kwargs are keywords arguments (dictionaries)
tk.Tk.__init__(self,*args,**kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True) #pack into top, fill into entire top space, and expand will expand into the whole window. fill into the area you packed.
container.grid_rowconfigure(0, weight=1) #min size zero, weight is priority
container.grid_columnconfigure(0,weight=1)
self.frames = {}
self.shared_data = {"filename": tk.StringVar()}
for F in (StartPage, SelectROIPage):
frame = F(container,self)
self.frames[F] = frame
frame.grid(row=0,column=0, sticky="nsew") #sticky = alignment + stretch, north south east west
self.show_frame(StartPage)
def show_frame(self,cont):
frame=self.frames[cont]
frame.tkraise()
def get_page(self, page_class):
return self.frames[page_class]
class StartPage(tk.Frame):
def __init__(self, parent: tk.Widget, controller: HRDetectorApp):
tk.Frame.__init__(self,parent)
self.filename = controller.shared_data['filename'] # use the same object for filename here as in SelectROIPage
tk.Button(self,text="Click me to pick a file", command=self.openFile).pack()
tk.Button(self,text ="Click me after you've picked a file", command = lambda: controller.show_frame(SelectROIPage)).pack()
def openFile(self):
self.filename.set(filedialog.askopenfilename(title="Select a file"))
print('filename in StartPage: {}'.format(self.filename.get()))
class SelectROIPage(tk.Frame):
def __init__(self, parent: tk.Widget, controller: HRDetectorApp):
tk.Frame.__init__(self,parent)
self.filename = controller.shared_data['filename']
tk.Label(self, text = "selected file : ").pack() # text assigns a permanent value
tk.Label(self, textvariable=self.filename).pack()
app = HRDetectorApp()
app.mainloop()
So let's discuss this a little ...
First, you don't need to declare a variable global for this to work; that's more relevant to threading, i.e. a global variable can be accessed by all threads in the program.
You also instantiated a second root with root=tk.Tk(). It's important that you only ever have one root (instance of tk.Tk()) in your program.
You can call .grid() and .pack() directly after declaring a label if you don't need access to the widget in the future.
The actual issue you are having. What I meant was to provide a reference to the same variable in both classes. You were close with shared_data['filename'], but you never called or assigned it! Both classes have access to the variable (through controller), and thus they can access the value of the variable.
The benefit of using tkinter variables rather than pythong variables is callbacks and traces. If you use the text property of the label, it will assign a static string to the label. The second label iv'e created uses the textvariable property. When you assign a tkinter variable to this property, the text of the label will automatically update whenever the variable changes. You could get more complicated in what happens by using the .trace() method of tkinter variables to call a function whenever it is written.
Regarding minimum reproducible examples ... what I would have expected is a program which creates two frames, where clicking a button in one frame (StartPage) updates a string variable in the other frame (SelectROIPage), and , each with the same parent frame (HRDetectorApp). I wouldn't expect it to be more than 20-30 lines.
You can bind <Expose> event to the SelectROIPage class and update a label in the event callback (which is called when SelectROIPage is shown):
class SelectROIPage(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self,parent)
self.controller = controller
self.startpg = self.controller.get_page(StartPage)
tk.Label(self,text="Select a Region of Interest (R.O.I)", font=LARGE_FONT).pack(pady=10,padx=10)
self.label = tk.Label(self) # label used to show the selected filename
self.label.pack()
self.bind('<Expose>', self.on_expose)
def on_expose(self, event):
self.label.config(text='Selected file: '+self.startpg.filename.get())
In your methods you reference the global filename. But in the global scope you don't create filename.
If you were to do that (say just before app = HRDetectorApp()), it should work.
Update
Based on your MVE, this is what happens.
You are creating the SelectROIPage object in the HRDetectorApp.__init__ method. At that time, the global variable filename points to an empty string.
What I would suggest it to create a synthetic event named e.g. <<Update>>.
In the SelectROIPage class, you define an on_update method that sets the label text. In SelectROIPage.__init__, you bind the on_update method to the <<Update>> event.
In the handler for clicking the goButton in StartPage, you call the event_generate method on the SelectROIPage object to send it the message. (This requires that the StartPage has an attribute for the SelectROIPage object)
Related
I would like to create a contractible panel in a GUI, using the Python package tkinter.
My idea is to create a decorator for the tkinter.Frameclass, adding a nested frame and a "vertical button" which toggles the nested frame.
Sketch: (Edit: The gray box should say Parent of contractible panel)
I got it to toggle just fine, using the nested frame's grid_remove to hide it and then move the button to the left column (otherwise occupied by the frame).
Now I want to be able to use it like any other tkinter.Frame, but let it target the nested frame. Almost acting like a proxy for the nested frame. For example, adding a tkinter.Label (the green Child component in the sketch) to the decorator should add the label to the nested frame component (light yellow tk.Frame in the sketch) not the decorator itself (strong yellow ContractiblePanel in the sketch).
Minimal example: (omitting the toggling stuff and any "formatting"):
(Here's a published (runnable) Repl project)
import tkinter
class ContractiblePanel(tkinter.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._panel = tkinter.Frame(self)
self._toggle = tkinter.Button(self, text='<', command=self._toggle_panel)
self.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
def _toggle_panel(self):
# ...
if __name__ == '__main__':
root = tkinter.Tk()
root.geometry('128x128')
contractible_panel = ContractiblePanel(root)
Forwarding configuration calls is just overriding the config method I guess?
class ContractiblePanel(tkinter.Frame):
# ...
def config(self, **kwargs):
self._panel.config(**kwargs)
# ...
contractible_panel.config(background='blue')
But I would like to be able to add a child component into the nested panel frame by
label_in_panel = tkinter.Label(contractible_panel, text='yadayada')
How do I get the ContractiblePanel object to act like a proxy to its member _panel, when adding child components?
What other methods/use cases should I consider? I am quite new to tkinter and thus expect the current implementation to break some common practices when developing tkinter GUIs.
This is an interesting question. Unfortunately, tkinter really isn't designed to support what you want. I think it would be less complicated to simply expose the inner frame and add widgets to it.
That being said, I'll present one possible solution. It's not implemented as a python decorator, but rather a custom class.
The difficulty is that you want the instance of the custom class to represent the outer frame in one context (for example, when packing it in your UI) and the inner frame in another context (when adding child widgets to it)
The following solution solves this by making the instance be the inner frame, and then overriding pack,place, and grid so that they operates on the outer frame. This works fine, with an important exception: you cannot use this class directly inside a notebook or embedded in a text widget or canvas.
I've used colors and borders so it's easy to see the individual components, but you can remove the colors in production code, obviously. Also, I used a label instead of a button since I created the screenshot on OSX where the background color of a button can't be changed.
import tkinter as tk
class ContractiblePanel(tk.Frame):
def __init__(self, parent, **kwargs):
self._frame = tk.Frame(parent, **kwargs)
super().__init__(self._frame, bd=2, relief="solid", bg="#EFE4B0")
self._button = tk.Label(
self._frame, text="<", bg="#00A2E8", bd=2,
relief="solid", font=("Helvetica", 20), width=4
)
self._frame.grid_rowconfigure(0, weight=1)
self._frame.grid_columnconfigure(0, weight=1)
self._button.grid(row=0, column=1, sticky="ns", padx=4, pady=4)
super().grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
self._button.bind("<1>", lambda event: self.toggle())
def collapse(self):
super().grid_remove()
self._button.configure(text=">")
def expand(self):
super().grid()
self._button.configure(text="<")
def toggle(self):
self.collapse() if self.winfo_viewable() else self.expand()
def pack(self, **kwargs):
# override to call pack in the private frame
self._frame.pack(**kwargs)
def grid(self, **kwargs):
# override to call grid in the private frame
self._frame.grid(**kwargs)
def place(self, **kwargs):
# override to call place in the private frame
self._frame.place(**kwargs)
root = tk.Tk()
root.geometry("400x300")
cp = ContractiblePanel(root, bg="yellow", bd=2, relief="raised")
cp.pack(side="left", fill="y", padx=10, pady=10)
label = tk.Label(cp, text="Child component", background="#22B14C", height=3, bd=2, relief="solid")
label.pack(side="top", expand=True, padx=20, pady=20)
root.mainloop()
First of all it is kinda gross to use this code and it's very confusing. So I'm really not sure if you really want to take this route. However, it is possible to achieve it.
The basic idea is to have a wrapper and to pretend the wrapper is the actual object you can lie with __str__ and __repr__ about what the class really is. That is not what a proxy means.
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
You can do even more confusing things by delegate the lookup-chain to the _wrapped_frame inside the WrapperClass this enables you to call on the instance of WrapperFrame() methods like pack or every other method. It kinda works similar for inheritance with the difference that by referring to the object, you will point to different one.
I don't recommend using this code by the way.
import tkinter as tk
NONE = object()
#use an object here that there will no mistake
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
def __getattr__(self, name):
#when wrapper class has no attr name
#delegate the lookup chain to self.frame
inreturn = getattr(self._wrapped_frame, name, NONE)
if inreturn is NONE:
super().__getattribute__(name)
return inreturn
root = tk.Tk()
wrapped_frame = WrapperClass(root, bg='red', width=200, height=200)
root.mainloop()
My understanding is that the in_ keyword argument to pack/grid should allow me to specify the managing widget. I want to pack arbitrary widgets inside a Frame subclass, so I passed the widgets and packed them during intialization, but the widgets didn't appear (although space in the window appears to have been allocated...). If I create the widget internally using master which is root, there is no issue and the widgets are displayed as expected.
The following working example and its output demonstrate the issue:
import tkinter as tk
from tkinter import ttk
class ItemContainerExternal(ttk.Frame):
def __init__(self, master, input_label, input_object):
ttk.Frame.__init__(self, master)
self.label = input_label
self.label.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
self.input_object = input_object
self.input_object.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
def get(self):
return variable.get()
class ItemContainerInternal(ttk.Frame):
def __init__(self, master):
ttk.Frame.__init__(self, master)
ttk.Label(master, text='internal').pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
self.input_object = ttk.Entry(master)
self.input_object.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
def get(self):
return variable.get()
if __name__ == '__main__':
root = tk.Tk()
inputobj = ttk.Entry(root)
inputlabel = ttk.Label(root, text='external')
ItemContainerExternal(root, inputlabel, inputobj).grid(row=0, column=0)
ItemContainerInternal(root).grid(row=1, column=0)
root.mainloop()
The problem is that you're creating the entry and label before you're creating the frame, so they have a lower stacking order. That means the frame will be on top of the entry and label and thus, obscuring them from view.
A simple fix is to call lift() on the entry and label:
class ItemContainerExternal(tk.Frame):
def __init__(self, master, input_label, input_object):
...
self.input_object.lift()
self.label.lift()
The order in which widgets get created matters. Newer widgets are "on top of" previous widgets.
Call .lower() on the Frame after you create it, assuming it's created after all the widgets that you will pack into it. If not, you'll need to either call .lower() again on the Frame after creating a new widget to go inside it, or you'll have to raise the new widget via .lift() as per Bryan's answer.
I am having a problem Disabling Menu's in my Tkinter App. Only I want to show them only on certain pages, I have tried to disable them in the init function of my app, but that didn't work, I have tried to disable them in the show frames function of my app but that didn't work and I have tried to disable them through the start page class of my app, in that I have tried to use self.menubar , parent.menubar and controler.menubar; But nothing seems to work. I would just code them on each individual page but this is the only way I found to even show them on any page. Any help would be much appreciated.
class App(tk.Tk):
def __init__(self,*args,**kwargs):
tk.Tk.__init__(self,*args,**kwargs)
container = tk.Frame(self)
container.pack(side='top',fill='both',expand= True)
container.grid_rowconfigure(0,weight = 1)
container.grid_columnconfigure(0,weight = 1)
menubar = tk.Menu(container)
file = tk.Menu(menubar, tearoff = 0)
file.add_command(label='Exit', command = quit)
menubar.add_cascade(label='File',menu=file)
tk.Tk.config(self, menu=menubar)
self.frames = {}
for F in (StartPage):
frame = F(container,self)
self.frames[F] = frame
frame.grid(row=0,column=0,sticky='nsew')
page = F
self.show_frame(StartPage)
def show_frame(self,cont):
frame = self.frames[cont]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self,parent,controller):
tk.Frame.__init__(self,parent)
menubar.add_cascade(label='File',state=DISABLED)
label = ttk.Label(self, text='Start', font = LARGE_FONT).pack()
main = App()
main.mainloop()
This isn't a tkinter problem, it's how python works - to modify an object you need a reference to the object. This is true for tkinter widgets just as it is true for dictionaries, strings, or any other object in python.
In this case the object is part of the main application. You first need to save a reference to it:
class App(tk.Tk):
def __init__(self,*args,**kwargs):
...
self.menubar = tk.Menu(container)
...
In later code, you can now access this menu from the controller variable, which is a reference to the instance of App:
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent)
controller.menubar.entryconfigure('File',state="disabled")
However, that likely won't do what you want since that code runs when the program starts up. I'm guessing you want the code to run when the page is selected. To do that, follow the instructions in this answer to the question How would I make a method which is run every time a frame is shown in tkinter
1st lets clean up this to reflect the PEP8 standard more closely for readability reason.
Added in the imports as those should have been in your example.
We need to replace the parenthesis in (StartPage) with brackets like this [StartPage]. This is because a single value in parenthesis is treated like a single value without parenthesis so you are trying to iterate over an object and not a list.
You should be more specific on what you are configuring. Instead of calling tk.tk.config() do self.config(menu=self.menubar).
for your StartPage class you are not going to be able to do anything to the menubar for several reason. First you do not define the menubar as a class attribute in your main tk class. 2nd you are not properly calling your master and container so I have changed the argument names in the init to better reflect what we are working with so you can see what you need to call.
Lastly the crux if the issue for disabling the menu you need to use entryconfig()
See below cleaned up code:
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Tk):
def __init__(self):
super().__init__()
container = tk.Frame(self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.menubar = tk.Menu(container)
self.config(menu=self.menubar)
file = tk.Menu(self.menubar, tearoff=0)
file.add_command(label='Exit', command=quit)
self.menubar.add_cascade(label='File', menu=file)
self.frames = {}
for F in [StartPage]:
frame = F(self, container)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky='nsew')
page = F
self.show_frame(StartPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, master, container):
tk.Frame.__init__(self, container)
master.menubar.entryconfig('File', state='disabled')
ttk.Label(self, text='Start').pack()
main = App()
main.mainloop()
TkInter's frames are driving me crazy. My goal is to have an options frame where I can select some options, then press "Archive" and the TkInter window changes to showing the output from the rest of my script.
I cannot get this to size correctly - there appears to be some additional frame taking up space in the window.
import string
from tkinter import *
import tkinter as tk
import threading
def main(argv):
print("In Main")
for arg in argv:
print(arg)
class TextOut(tk.Text):
def write(self, s):
self.insert(tk.CURRENT, s)
self.see(tk.END)
def flush(self):
pass
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, frameClass):
# make new frame - for archive output
self._frame = frameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, newFrameClass):
# make new frame - for archive output
self._frame = newFrameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class OptionsFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test")
master.geometry("325x180")
self.selectedProject = None
self.initUI(master)
def initUI(self, master):
frame1 = Frame(master)
frame1.pack(fill=BOTH, expand=True)
self.label1 = Label(frame1, text="Select Project to Archive, then click Archive")
self.projectListbox = tk.Listbox(frame1, width=60, height=100)
self.projectListbox.bind("<<ProjectSelected>>", self.changeProject)
# create a vertical scrollbar for the listbox to the right of the listbox
self.yscroll = tk.Scrollbar(self.projectListbox,command=self.projectListbox.yview,orient=tk.VERTICAL)
self.projectListbox.configure(yscrollcommand=self.yscroll.set)
# Archive button
self.archiveBtn=tk.Button(frame1,text="Archive",command=self.ArchiveButtonClick)
# Do layout
self.label1.pack()
self.projectListbox.pack(fill="both", expand=True)
self.yscroll.pack(side="right", fill="y")
self.archiveBtn.pack(side="bottom", pady=10, expand=False)
choices = ["test 1", "test 2", "test 3", "test 4", "test 5", "test 6"]
# load listbox with sorted data
for item in choices:
self.projectListbox.insert(tk.END, item)
def getSelectedProject(self):
# get selected line index
index = self.projectListbox.curselection()[0]
# get the line's text
return self.projectListbox.get(index)
# on change dropdown value
def changeProject(self,*args):
self.selectedProject = self.getSelectedProject()
def ArchiveButtonClick(self):
# Switch to second frame - for running the archive
self.changeProject(None)
# Hide existing controls
self.label1.pack_forget()
self.projectListbox.pack_forget()
self.yscroll.pack_forget()
self.archiveBtn.pack_forget()
newFrame = self.master.change(ArchivingOutputFrame)
newFrame.args = [ "-n", self.selectedProject]
newFrame.start()
# Frame shown while archive task is running
class ArchivingOutputFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test Frame 2")
master.geometry("1000x600")
# Set up for standard output in window
self.var = tk.StringVar(self)
lbl = tk.Label(self, textvariable=self.var)
#lbl.grid(row=0, column=0)
lbl.pack(anchor="nw")
def start(self):
t = threading.Thread(target=self.process)
t.start()
def process(self):
main(self.args)
if __name__=="__main__":
# If command line options passed - skip the UI
if len(sys.argv) > 1:
main(sys.argv[1:])
else:
app=Mainframe()
text = TextOut(app)
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
Here is what I get in the UI; note this is showing the UI hierachy from Microsoft's Spy++ - there is a frame I didn't create (at least I don't think I did) that is at the bottom of the window and taking up half of the UI area; this is the yellow highlight. My options pane is thus squeezed into the top half.
Resize also doesn't work - if I resize the window, I get this:
When I click the button and the code to remove the options frame and put in the frame that is capturing stdout/stderr from the main script runs, I get this:
Now the extra space appears to be at the top!
Thanks for any ideas - I know I could switch to using the "Grid" UI layout engine, but this seems so simple - I'm not doing anything sophisticated here that shouldn't work with pack.
That was a lot of complicated code. It would be easier to debug if you provide a Minimal, Complete, and Verifiable example.
However; the bottom Frame is the TextOut() widget that you pack after Mainframe():
if __name__=="__main__":
app = Mainframe()
text = TextOut(app) # This one
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
You'll have an easier time debugging if you give each widget a bg colour and then give them all some padding so you can easier identify which widget is inside which widget.
I am working with tkinter and have set up a bare bones application of my project. My objective is to retrieve a value from tk.Entry() which lies within a tk.LabelFrame() (in this code referenced by the groupbox variable). The button finds the groupbox, and the code passes the compiler, too. I guess my question is: How do I access Widgets and their values in a LabelFrame?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.master.title("Application Title")
# Introduce LabelFrame
self.groupbox = tk.LabelFrame(self, text="Parameters")
self.groupbox.grid(row=0, column=1, padx=5, pady=5)
# Test Label & Entry Widget
label = tk.Label(self.groupbox, text="label=")
label.grid(row=0, column=0, sticky="W")
entry = tk.Entry(self.groupbox)
entry.insert(0, default_value)
entry.grid(row = 0, column=1)
# Compile Button
button = tk.Button(self.groupbox, text="Compile", command=self.compile)
button.grid(row=1, column=1)
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.groupbox.entry.get(1))
if __name__ == '__main__':
figure = Application()
figure.pack()
figure.mainloop()
I am doing this because I want to perform some calculations based on the tk.Entry() values triggered by a button click which is contained in the same LabelFrame() as suggested by the code snippet above (in the original code there are a lot more widgets but that's essentially the gist of my current problem).
Change entry to self.entry.
class Application(tk.Frame):
def __init__(self, master=None):
....
self.entry = tk.Entry(self.groupbox)
self.entry.insert(0, "default_value")
self.entry.grid(row = 0, column=1)
...
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.entry.get())