So, I am building a tkinter GUI program which has a matplotlib graph on one of the pages. From various popups, the user can add, delete, and modify the traces on the graph.
What I had been doing up till now is using the draw() method on the FigureCanvasTkAgg object to update the graphic whenever something is changed. I have read in several other posts that this redraws the entire figure, and makes the rendering very slow. Additionally, in my specific case, it draws a lot of tick labels on the axes, seemingly one for every point in the source, so, in addition to taking forever, it also makes the graph absolutely unreadable. It also does not plot the data correctly.
I came across some posts suggesting not to use the draw method, as it redraws the entire figure, but that doesn't really explain why so many tick labels are being drawn, or why the data isn't being plotted as expected. I've looked into using animate, but I'm not sure that that's what I need, because the graph doesn't have to update constantly, only when it's changed by the user.
I'm using Python 3.7.2, and matplotlib 3.1.1. The data I'm plotting is spectral, and only has about 4000 points. I've also tested it with very small datasets (10-15 points) and this doesn't seem to happen. I've also tried setting the tick labels with axis.locator_params(nbins=10), but it doesn't prevent all those labels from being drawn.
Any idea what's happening here, and how I can prevent it? Here's the graphic it produces, and the relevant parts of my code. Sorry if I'm missing anything; I'm new to SO and I'm still a relatively inexperienced programmer.
The offensive graph:
class App(tk.Tk): ###The controller of all pages, & control of operations
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)
self.frames = {}
self.dfs = {} #contains dataframes loaded from csv/made by the user
self.spectra = {} #contains spectrum objects
self.plots = {} #contains Figure objects. Each figure can have exactly one axis
for F in (HomePage, SpectraPage, GraphPage, MakeSpectrumPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(HomePage)
def show_frame(self, cont):
self.frames[cont].tkraise()
def load(self, filename):
df = pd.read_csv(filename, header=None)
if any(df.iloc[0].apply(lambda x: isinstance(x, str))):
df = df[1:].reset_index(drop=True).rename(columns=df.iloc[0])
else:
names=[]
for i in range(len(df.columns)):
names.append("w%i" %i)
df.columns = names
self.dfs[filename] = df
self.frames[MakeSpectrumPage].makeFileList()
def get_dfs(self):
return self.dfs
def get_spectra(self):
return self.spectra
def get_plots(self):
return self.plots
def make_spectrum(self, name, df, x, y):
spectrum = Spectrum(name, df, x, y)
self.spectra[name] = spectrum
self.frames[SpectraPage].insertItems()
def make_plot(self, name):
fig = Figure(dpi=100)
fig.suptitle(name)
axis = fig.add_subplot(111, xlim=(4000, 500), ylim=(0,1))
axis.locator_params(nbins=10)
self.plots[name] = fig
def graph(self, axis, spectrum, **kwargs): #plot spectrum obj
axis.plot(spectrum.xdata, spectrum.ydata, label=spectrum.name, **kwargs)
class GraphPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller=controller
self.grid_columnconfigure(0,weight=1)
self.controller.make_plot('Plot 1')
self.showFigure(self.controller.get_plots()['Plot 1'])
self.makeGraphTray()
def showFigure(self, fig):
self.canvas = FigureCanvasTkAgg(fig, self)
self.canvas.draw()
self.canvas._tkcanvas.grid(row=1, column=1, rowspan=2, columnspan=2)
self.makeToolbar()
def makeToolbar(self):
self.toolbar_frame = tk.Frame(self)
self.toolbar_frame.grid(row=3,column=1, columnspan=2)
self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
self.toolbar.update()
def makeGraphTray(self):
#make container for graph/trace modification buttons
self.graphTray = tk.Frame(self, width=50)
self.graphTray.grid(row=1, column=3, sticky='nsew')
addTraceButton = ttk.Button(self.graphTray, text="Add Trace", command=lambda:self.raisePopup(NewTracePopup))
addTraceButton.grid(row=0, column=0, sticky='nsew')
def raisePopup(self, Popup):
popup = Popup(self)
class Spectrum: #Objects of this class are two-column structures.
def __init__(self, name, sourcedf, x, y):
self.xdata = sourcedf[x]
self.ydata = sourcedf[y]
self.df = pd.concat([self.xdata, self.ydata], axis=1)
self.name = name
class ConditionalPopup(tk.Toplevel):
#parent class of OK/Cancel popups where OK is disabled until all fields are filled
def __init__(self, master, title, **kwargs):##param **kwargs the Variables traced by the widgets in the popup
if all(isinstance(kwarg, tk.Variable) for kwarg in kwargs.values()):
super().__init__(master)
self.__dict__.update(kwargs)
self.master = master
self.vars = kwargs #dictionary of kwargs
self.wm_title(title)
self.widgetFrame = tk.Frame(self)
self.widgetFrame.grid(row=0, column=0)
self.traceVars()
self.makeWidgets(self.widgetFrame)
self.placeWidgets()
else: raise TypeError
def traceVars(self):
for var in self.vars.values():
var.trace('w', self.activateOK)
def activateOK(self, *args):
if all(self.vars[key].get() for (key, value) in self.vars.items()):
self.okButton.configure(state='normal')
else:
self.okButton.configure(state='disabled')
def makeWidgets(self, frame):
self.okButton = ttk.Button(self, text="OK", state='disabled', command=self.okPressed)
self.cancelButton = ttk.Button(self, text="Cancel", command=self.destroy)
def placeWidgets(self):
self.okButton.grid(row=1, column=0, padx=2.5, pady=10, sticky='e')
self.cancelButton.grid(row=1, column=1, padx=2.5, pady=10, sticky='w')
def okPressed(self, *args):
self.destroy()
class NewTracePopup(ConditionalPopup):
#a popup that enables adding a trace to a chosen plot
def __init__(self, master):
super().__init__(master, "Add trace to plot",
plotVar=tk.StringVar(),
spectrumVar=tk.StringVar(),
colorVar=tk.StringVar(),
linewidthVar=tk.StringVar())
def activateOK(self, *args):
if self.linewidthVar.get().isdecimal():
super().activateOK(*args)
def makeWidgets(self, frame):
self.plotLabel = tk.Label(frame, text="Plot:")
self.plotCombobox = ttk.Combobox(frame, state='readonly', values=list(self.master.controller.get_plots().keys()), textvariable=self.plotVar)
self.spectrumLabel = tk.Label(frame, text="Spectrum:")
self.spectrumCombobox = ttk.Combobox(frame, state='readonly', values=list(self.master.controller.get_spectra().keys()), textvariable=self.spectrumVar)
self.cLabel = tk.Label(frame, text="Colour:")
self.cCombobox = ttk.Combobox(frame, state='readonly', values=list(mcolors.BASE_COLORS)+list(mcolors.TABLEAU_COLORS), textvariable=self.colorVar)
self.mLabel = tk.Label(frame, text="Line width:")
self.mEntry = ttk.Entry(frame, textvariable=self.linewidthVar)
super().makeWidgets(frame)
def placeWidgets(self):
self.plotLabel.grid(row=0, column=0, padx=10, pady=10, sticky='e')
self.plotCombobox.grid(row=0, column=1, padx=10, pady=10, sticky='w')
self.spectrumLabel.grid(row=1, column=0, padx=10, pady=10, sticky='e')
self.spectrumCombobox.grid(row=1, column=1, padx=10, pady=10, sticky='w')
self.cLabel.grid(row=2, column=0, padx=10, pady=10, sticky='e')
self.cCombobox.grid(row=2, column=1, padx=10, pady=10, sticky='w')
self.mLabel.grid(row=3, column=0, padx=10, pady=10, sticky='e')
self.mEntry.grid(row=3, column=1, padx=10, pady=10, sticky='w')
super().placeWidgets()
def okPressed(self, *args):
self.master.controller.graph(self.master.controller.get_plots()[self.plotVar.get()].axes[0],
self.master.controller.get_spectra()[self.spectrumVar.get()],
color=self.colorVar.get(),
linewidth=float(self.linewidthVar.get()))
self.master.canvas.draw()
super().okPressed()
Related
I am building my first GUI using tkinter and have come up against some problems. To make the code more modular, I am using an object-oriented approach, as seen in the code below. The basic idea is that I have defined classes for the DataFrame, MetaFrame and SaveFrame, which are all instantiated within the OptionsFrame, which then is instantiated within the MainWindow.
import tkinter as tk
from tkinter import ttk
class DataFrame(ttk.Frame):
def __init__(self, main, *args, **kwargs):
super().__init__(main, *args, **kwargs)
# data frame elements
self.data_label = ttk.Label(self, text="Add Data:")
self.labelled_tweets_label = ttk.Label(self, text="Labelled-Tweets: ")
self.labelled_tweets_button = ttk.Button(self, text="Browse")
self.places_label = ttk.Label(self, text="Places: ")
self.places_button = ttk.Button(self, text="Browse")
self.plots_label = ttk.Label(self, text="Plots Path: ")
self.plots_button = ttk.Button(self, text="Browse")
self.submit_button = ttk.Button(self, text="Submit")
# data frame layout
self.data_label.grid(row=0, column=0, columnspan=2, pady=10)
self.labelled_tweets_label.grid(row=1, column=0)
self.labelled_tweets_button.grid(row=1, column=1)
self.places_label.grid(row=2, column=0)
self.places_button.grid(row=2, column=1)
self.plots_label.grid(row=3, column=0)
self.plots_button.grid(row=3, column=1)
self.submit_button.grid(row=4, column=0, columnspan=2, pady=10)
class MetaFrame(ttk.Frame):
...
class SaveFrame(ttk.Frame):
...
class OptionsFrame(ttk.Frame):
def __init__(self, main, *args, **kwargs):
super().__init__(main, *args, **kwargs)
# options frame components
self.data_frame = DataFrame(self)
self.horiz1 = ttk.Separator(self, orient="horizontal")
self.meta_frame = MetaFrame(self)
self.horiz2 = ttk.Separator(self, orient="horizontal")
self.save_frame = SaveFrame(self)
# options frame layout
self.data_frame.grid(row=0, column=0)
self.horiz1.grid(row=1, column=0, sticky="ew", pady=30)
self.meta_frame.grid(row=2, column=0)
self.horiz2.grid(row=3, column=0, sticky="ew", pady=30)
self.save_frame.grid(row=4, column=0, sticky="s")
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.geometry("800x600")
self.resizable(False, False)
# configuration
self.columnconfigure(index=0, weight=1)
self.columnconfigure(index=1, weight=2)
# main frames
self.options_frame = OptionsFrame(self, width=400, height=600, borderwidth=1)
self.vert = ttk.Separator(self, orient="vertical")
# main layout
self.options_frame.grid(row=0, column=0)
self.vert.grid(row=0, column=1, sticky="ns")
def main():
root = MainWindow()
root.mainloop()
The layout can be seen in the following image.
This is the basic layout I want within the OptionsFrame. My confusion lies with creating filedialog methods for the three file browsing buttons within the DataFrame. I understand how to use the filedialog class to return the path to a given file, but then this value is restricted to be in the scope of the DataFrame.
I have a back-end that is already developed which requires these file paths, so ideally I would like to access them from the main() function. How is this possible?
Thanks
I am using a library called tkintertable in python which essentially gives me the capability to efficiently add tables to my Tkinter application. I am trying to setup 6 different tables with 6 charts to go with each table in this frame(my home page essentially) but am relitvely new to tkinter so having some trouble organizing it, currently my code looks like the following:
import tkinter as tk
from tkintertable import TableCanvas, TableModel
class MainApplication(tk.Tk):
"""Main application class"""
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self)
self._frame = None
self.switch_frame(TickerInput, None)
def switch_frame(self, frame_class, ticker):
"""Destroys current frame and replaces it with a new one."""
if ticker is not None:
new_frame = frame_class(self, ticker)
else:
new_frame = frame_class(self)
if self._frame is not None:
self._frame.destroy()
self._frame = new_frame
self._frame.grid()
class TickerInput(tk.Frame):
"""Ticker input page that allows input of ticker and redirects to main indicator page"""
def __init__(self, master):
tk.Frame.__init__(self, master, background="#212020")
# Centers the frame
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
ticker_label = tk.Label(self, text="Enter ticker..")
ticker_label.grid(row=0, column=1, columnspan=2)
ticker_input = tk.Entry(self, width=35)
ticker_input.grid(row=0, column=3)
button = tk.Button(self, text="Search", command=lambda: master.switch_frame(Indicator, ticker_input.get()))
button.grid(row=1, column=1, columnspan=3)
class Indicator(tk.Frame):
"""Indicator page that shows the indicators and whether its a buy or sell"""
def __init__(self, master, ticker):
tk.Frame.__init__(self, master)
self.ticker = ticker
self.master.columnconfigure(0, weight=0)
self.master.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
button = tk.Button(self, text="Go Back", command=lambda: master.switch_frame(TickerInput, None))
button.grid(column=0, row=0)
ticker_label = tk.Label(self, text=("Current Ticker: " + self.ticker))
ticker_label.grid(column=1, row=0)
tableOne = Table(self)
tableOne.grid(column=0, row=1)
tableOne.createTable()
# tableTwo = Table(self)
# tableTwo.createTable()
class Table(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
def createTable(self):
data = {'rec1': {'col1': 99.88, 'col2': 108.79, 'label': 'rec1'},
'rec2': {'col1': 99.88, 'col2': 108.79, 'label': 'rec2'}
}
model = TableModel()
table = TableCanvas(self.master, model=model, data=data)
table.show()
return table
if __name__ == "__main__":
app = MainApplication()
app.title("Indicator Trading Confirmation Tool")
app.geometry("1920x1080")
app.config(background="#121212")
app.mainloop()
and here is the GUI
Why is my Go Back Button stuck on the table if I have it on a different row and have my weight set to 1 which should seperate them 50% correct? And also what is the best way to organize this both grid/code related to make sure that the GUI has 6 equal tables/charts, 3 on the left and 3 on the right? Thank you!
Okay, lets go step by step,
having replaced your Table class with just a colored Frame there is nothing overlapping for me, so this is probably just due to the Layout of the table itself - quick fix for that could be adding some padding
the weight option is used for distribution of extra space when resizing the window, for more information you can take a look here, there is also a nice visualization found here
in order to use weight for controlling actual used width or height of your widgets you might want to take a look at the sticky option of grid
see for a very nice overview you can also check this which is very detailed for both
for your general layout you can just go the easy route of combining weight and sticky options like this:
self.l1 = tk.Frame(self, background="red")
self.l2 = tk.Frame(self, background="orange")
self.l3 = tk.Frame(self, background="green")
self.l4 = tk.Frame(self, background="blue")
self.l5 = tk.Frame(self, background="brown")
self.l6 = tk.Frame(self, background="yellow")
self.rowconfigure(0, weight = 1)
self.rowconfigure(1, weight = 1)
self.rowconfigure(2, weight = 1)
self.columnconfigure(0, weight = 1)
self.columnconfigure(1, weight = 1)
self.l1.grid(row=0, column=0, sticky="nsew")
self.l2.grid(row=1, column=0, sticky="nsew")
self.l3.grid(row=2, column=0, sticky="nsew")
self.l4.grid(row=0, column=1, sticky="nsew")
self.l5.grid(row=1, column=1, sticky="nsew")
self.l6.grid(row=2, column=1, sticky="nsew")
for a 50:50 separation in width and 33:33:33 in height.
If you then later on want to add something like a menubar within your frame you might want to bundle your table frames within another Frame in order to have them resize equally
I'm trying to display two frames in a way such that one frame has a button to display the other frame and vice versa. I'm trying to use tkinter frame function of tkraise(). But they are getting simultaneously displayed over each other. Please you can ignore the import board file because it has some helper functions that I'm using to display some data on the GUI.
Here is my code:
import tkinter as tk
import tkinter.scrolledtext as st
from board import *
FONT = ("Helvetica", 20)
attributes = {'padx': 5, 'pady': 5}
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('DAS')
self.geometry('500x325')
container = tk.Frame(self)
container.pack(fill=tk.BOTH, expand=True)
self.frames = {}
self.frames[0] = Display(container, self)
self.frames[0].grid(row=0, column=0)
self.frames[1] = DataStored(container, self)
self.frames[1].grid(row=0, column=0)
self.show_frame(1)
def show_frame(self, frame):
frame = self.frames[frame]
frame.tkraise()
class Display(tk.Frame):
def __init__(self, container, controller):
super().__init__(container)
name = tk.Label(self, text='Data Acquistion System', font=FONT)
name.pack(fill='x')
left_wrapper = tk.Frame(self)
left_wrapper.pack(side='left')
right_wrapper = tk.Frame(self)
right_wrapper.pack(side='left')
port_label = tk.Label(left_wrapper, text='Port:', font=FONT)
port_label.grid(row=0, column=0)
port_option = tk.StringVar(left_wrapper, value='None')
ports_availiable = get_arduino_ports()
port_value = tk.OptionMenu(left_wrapper, port_option, *ports_availiable)
port_value.grid(row=0, column=1)
baud_label = tk.Label(left_wrapper, text='Baudrate:', font=FONT)
baud_label.grid(row=1, column=0)
baud_value = tk.Entry(left_wrapper)
baud_value.grid(row=1, column=1)
baud_value.insert(0, 9600)
connect_button = tk.Button(left_wrapper, text='Connect', command=lambda: connect_to_port(port_option.get(), baud_value.get()))
connect_button.grid(row=2, column=1)
playback_button = tk.Button(left_wrapper, text='Playback', command=lambda: controller.show_frame(1))
playback_button.grid(row=3, column=1)
data_display = st.ScrolledText(right_wrapper)
data_display.pack()
class DataStored(tk.Frame):
def __init__(self, container, controller):
super().__init__(container)
tk.Label(self, text='Playback Data').pack(fill=tk.X)
top_frame = tk.Frame(self)
top_frame.pack(fill=tk.X)
bottom_frame = tk.Frame(self)
bottom_frame.pack(fill=tk.X)
canvas = st.ScrolledText(top_frame)
canvas.pack()
update = tk.Button(bottom_frame, text='Update', command=self.update_playback)
update.grid(row=0, column=0)
back = tk.Button(bottom_frame, text='Back', command=lambda: controller.show_frame(0))
back.grid(row=0, column=1)
def update_playback(self):
with open("data.txt", "r") as file:
data = file.read()
self.data.insert(tk.END, data)
self.data.config(state="disabled")
if __name__ == "__main__":
window = App()
window.mainloop()
There is a simple solution by changing the way in which you use your show_frame function:
def show_frame(self, frame_num):
for frame in self.frames.values():
frame.grid_remove()
frame = self.frames[frame_num]
frame.grid()
The reason tkraise does not work is that your two frames are of different sizes, if they were the same rise you would be entirely right with your code. Because they are of different sizes you can still see the bigger frame when looking at the smaller one.
grid_remove removes a frame from being loaded on the window, and then the following grid command places the frame back where it was before, removing any issues with overlap and such.
As the two frames have different sizes, you need to add sticky="nsew" in .grid(...) to make both frames to occupy the available space:
self.frames[0] = Display(container, self)
self.frames[0].grid(row=0, column=0, sticky="nsew")
self.frames[1] = DataStored(container, self)
self.frames[1].grid(row=0, column=0, sticky="nsew")
sorry for the simple question. Relatively new to python and especially Object Oriented and Class systems. Basically, I want my GUI to have a couple of rows across the top of the window (across full width). Beneath, I want 4 equal sized columns. I don't want this to impact my plots (on a different page). Any help is much appreciated. I cannot workout how to use tkinter to get this to work.
Thanks
__author__ = "dev"
# https://pythonprogramming.net/object-oriented-programming-crash-course-tkinter/?completed=/tkinter-depth-tutorial-making-actual-program/
import tkinter as tk
from tkinter import ttk
import yfinance as yf
import datetime as dt
import matplotlib
from matplotlib import style
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
LargeFont = ("Verdana", 16)
style.use("ggplot")
def yfinance_get(index):
tkr = yf.Ticker(index)
today = dt.datetime.today().isoformat()
m_ago = dt.datetime.today() - dt.timedelta(days=30)
h_px = tkr.history(period="1d", start=m_ago, end=today[:10])
return h_px
class DailySumm(tk.Tk):
def __init__(self, *args, **kwargs): # initialise - runs as soon as the class is called
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self) # frame creates the window of our GUI
container.pack(side="top", expand=True) # pack is similar to grid, but less accurate
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
# creates an empty dictionary called named self.frames, this will hold the diff "pages" in the Gui
for f in (StartPage, Equities): # for each page defined, this tuple needs to be updated to include
# this loop will add the pages to the self.frames dict
frame = f(container, self)
self.frames[f] = frame # enters a new entry into the dictionary, the StartPage page
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(StartPage) # this calls the show_frame method with cont = StartPage
def show_frame(self, cont):
frame = self.frames[cont] # cont = dict key to frames (dict)
frame.tkraise() # this raises the "frame" to the front of the window (i.e. shows it to the user)
class StartPage(tk.Frame): # this creates the StartPage page which inherits the tk.Frame functionality (ie imports)
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent) # parent is the parent class, in this case DailySumm
label = tk.Label(self, text="News and Market Data", font=LargeFont).grid(row=0, column=0, columnspan=4)
label2 = tk.Label(self, text="Equities", font=LargeFont).grid(row=1, column=0, padx=10)
label3 = tk.Label(self, text="Credit", font=LargeFont).grid(row=1, column=1, padx=10)
label4 = tk.Label(self, text="Currencies", font=LargeFont).grid(row=1, column=2, padx=10)
label5 = tk.Label(self, text="Commodities", font=LargeFont).grid(row=1, column=3, padx=10)
button_equ = ttk.Button(self, text="Equities",
command=lambda: controller.show_frame(Equities)).grid(row=2, column=0)
# lambda stops the function being called on inception, only when the button is pressed
class Equities(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Equities", font=LargeFont)
label.pack(pady=10, padx=10)
button_home = ttk.Button(self, text="Back to Home",
command=lambda: controller.show_frame(StartPage))
button_home.pack()
index = [['FTSE 100', '^FTSE'], ['S&P 500', '^GSPC'], ["Nikkei 225", "^N225"], ["MSCI EM", "EEM"]]
fig = Figure(dpi=100)
for i in range(0, 3+1):
ax = fig.add_subplot(2, 2, i+1)
plot_data = yfinance_get(index[i][1])
ax.plot(plot_data['Close'], color='r', label=index[i][0])
ax.grid(which="major", color='k', linestyle='-.', linewidth=0.3)
ax.legend(loc=2)
ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MONDAY))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d-%m"))
canvas = FigureCanvasTkAgg(fig, self)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
app = DailySumm()
app.geometry("2560x1600")
app.mainloop()
One way to do this is to create a frame for the rows at the top, and another frame to hold the four columns below. You can use pack to add these frames to the window, and use grid in the lower frame to manage the columns. To get the columns to be the same size you can use the uniform option for each column.
Here's an example of the technique:
import tkinter as tk
root = tk.Tk()
top_frame = tk.Frame(bd=1, relief="raised", height=50)
bottom_frame = tk.Frame(bd=1, relief="raised")
top_frame.pack(side="top", fill="x")
bottom_frame.pack(side="bottom", fill="both", expand=True)
# add labels to the columns so that we can visualize the columns
labels = (
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 1", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 2", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 3", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 4", height=8, width=10)
)
# add the labels to the columns.
labels[0].grid(row=0, column=0, sticky="nsew", ipadx=4)
labels[1].grid(row=0, column=1, sticky="nsew", ipadx=4)
labels[2].grid(row=0, column=2, sticky="nsew", ipadx=4)
labels[3].grid(row=0, column=3, sticky="nsew", ipadx=4)
# allocate all extra horizontal space to each column evenly
# the critical piece is both the weight and uniform options
bottom_frame.grid_columnconfigure(0, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(1, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(2, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(3, weight=1, uniform="column")
# allocate all extra vertical space to row 0
bottom_frame.grid_rowconfigure(0, weight=1)
root.mainloop()
I'm trying to create a custom frame in tkinter, Python v2.7. I have done this just fine once (a frame with a scrollbar), but my second attempt isn't working. I compare it to the Frame that does work, and I can't understand what I have done differently.
What I want is a frame that has a little separator line underneath it, so I'm creating a "normal" frame, a thin frame to use as a separator under it, and a bigFrame to hold it.
Everything I create in the class works, except the frame itself. Hopefully my comments explain what is and isn't showing.
from Tkinter import *
class FunFrame(Frame):
def __init__(self, master, lbl, **kwargs):
self.bigFrame = Frame(master)
Frame.__init__(self, self.bigFrame, width=280, height=200, bg="red", **kwargs)
self.grid(row=0, column=0, pady=3) #this is in bigFrame, and doesn't display
#however the padding is still respected
self.separator = Frame(self.bigFrame, height=2, bd=1, width=280, relief = SUNKEN)
self.separator.grid(row=1, column=0) #this is in bigFrame, and displays
self.l = Label(self, text=lbl) #this is in self and doesn't display
self.l.grid(row=0, column=0)
def grid(self, **kwargs):
self.bigFrame.grid(**kwargs)
if __name__ == "__main__":
root=Tk()
Frame1=FunFrame(root, "hello")
Frame2=FunFrame(root, "world")
Frame1.grid(row=0, column=0)
Frame2.grid(row=1, column=0)
root.mainloop()
If you call self.grid in __init__, it calls your own grid, not Tkinter's version.
Try following (renamed grid to grid_):
from Tkinter import *
class FunFrame(Frame):
def __init__(self, master, lbl, **kwargs):
self.bigFrame = Frame(master)
Frame.__init__(self, self.bigFrame, width=280, height=200, bg="red", **kwargs)
self.grid(row=0, column=0, pady=3)
self.separator = Frame(self.bigFrame, height=2, bd=1, width=280, relief=SUNKEN)
self.separator.grid(row=1, column=0)
self.l = Label(self, text=lbl)
self.l.grid(row=0, column=0)
def grid_(self, **kwargs): ######## grid -> grid_
self.bigFrame.grid(**kwargs)
if __name__ == "__main__":
root=Tk()
Frame1 = FunFrame(root, "hello")
Frame2 = FunFrame(root, "world")
Frame1.grid_(row=0, column=0) ######## grid -> grid_
Frame2.grid_(row=1, column=0) ######## grid -> grid_
root.mainloop()
I'd rather code as follow (if '....' was used to represent hierarchy visually):
from Tkinter import *
class FunFrame(Frame):
def __init__(self, master, lbl, **kwargs):
Frame.__init__(self, master)
if 'inside outer frame (self)':
innerFrame = Frame(self, width=280, height=200, bg="red", **kwargs)
innerFrame.grid(row=0, column=0, pady=3)
if 'inside inner frame':
self.l = Label(innerFrame, text=lbl)
self.l.grid(row=0, column=0)
separator = Frame(self, height=2, bd=1, width=280, relief=SUNKEN)
separator.grid(row=1, column=0)
if __name__ == "__main__":
root = Tk()
Frame1 = FunFrame(root, "hello")
Frame2 = FunFrame(root, "world")
Frame1.grid(row=0, column=0)
Frame2.grid(row=1, column=0)
root.mainloop()