I have been creating a GUI application for university using Tkinter and python. I am however having a problem where when the application first loads, or when i make the window smaller, the only widget visible is the Plotter (extends canvas) widget. If i expand the window however, the others become visible.
This is my code:
from assign2_support import *
import tkinter as tk
from tkinter import *
from tkinter.messagebox import *
import random
from tkinter.filedialog import askopenfilename
def get_station_name(filename):
temp1 = list(filename.split("/"))
temp = list((temp1[len(temp1) - 1]).split("."))
return temp[0]
def isInDict(value, dic):
if value in dic:
return True
else:
return False
#TemperaturePlotApp class
class TemperaturePlotApp(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.stations = TemperatureData()
self.color = ['#f90909', '#ffa405', '#c0c203', '#1abd04', '#058096', '#042ee1',
'#d30af1','#ec06b3']
self.selected = dict()
self.usedColors = dict()
self.master.title("Max Temperature")
self.button = tk.Button(self, text="File", command=self.load_file, width=10)
self.button.pack(side = 'top', anchor = tk.W)
self.plotter = Plotter(self,width=850, height=400, bg="white", highlightthickness=0)
self.plotter.pack(fill='both', expand=tk.YES)
self.plotter.bind("<B1-Motion>", self.onPlotClicked)
self.plotter.bind("<Button 1>", self.onPlotClicked)
# tag all of the drawn widgets TODO delete
self.plotter.addtag_all("all")
self.df = DataFrame(self)
self.df.pack(fill = tk.X, anchor = tk.N, pady = 10)
self.sf = SelectionFrame(self)
self.sf.pack(fill = tk.X, anchor = tk.N)
self.pack(fill = 'both', side = 'left', expand = tk.YES)
def loadStation(self, stationName):
self.stations.load_data(stationName + ".txt")
def onPlotClicked(self, event):
x = event.x
year = self.ct.get_year(x)
self.df.setYear(year)
try:
self.plotter.delete(self.l)
except:
pass
self.l = self.plotter.create_line(x, 0, x, self.plotter.winfo_height(), fill = "black")
for s in self.stations.get_stations():
if self.selected[s] == True:
temp = self.stations.get_data()[s].get_temp(int(year))
print(temp)
self.df.setDatumText(s, temp)
def plotData(self):
self.plotter.delete(tk.ALL)
minY, maxY, minT, maxT = self.stations.get_ranges()
self.ct = CoordinateTranslator(self.plotter.winfo_width(),self.plotter.winfo_height(), minY, maxY, minT, maxT)
self.i = 0
data = self.stations.get_data()
for s in self.stations.get_stations():
firstRun = True
if s in self.usedColors:
pass
else:
self.usedColors[s] = random.choice(self.color)
if self.sf.isCheckButton(s) == False:
self.sf.addCheckButton(s, self.usedColors[s], lambda: self.toggleCheckButton(s))
self.selected[s] = self.stations.is_selected(self.i)
if self.selected[s] == True:
if self.df.isInDataFrameLabels(s) == False:
self.df.addDatum("", self.usedColors[s], s)
if self.df.isHidden(s) == True:
self.df.showDatum(s)
for d in data[s].get_data_points():
if firstRun:
self.lastX, self.lastY = self.ct.temperature_coords(d[0], d[1])
firstRun = False
else:
x, y = self.ct.temperature_coords(d[0], d[1])
self.plotter.create_line(self.lastX, self.lastY, x, y, fill = self.usedColors[s])
self.lastX = x
self.lastY = y
else:
self.df.hideDatum(s)
self.i = self.i + 1
def toggleCheckButton(self, stationName):
if self.selected[stationName] == True:
self.selected[stationName] = False
else:
self.selected[stationName] = True
self.plotData()
def load_file(self):
fname = askopenfilename(filetypes=([("Text files","*.txt")]))
if fname:
fn = get_station_name(fname)
self.loadStation(fn)
self.plotData()
try:
print(fname) # TODO Delete
except:
showinfo("Failed to read file", "failed to read file: " + fname)
return
# Start DataFrame class
class DataFrame(tk.Frame):
def __init__(self,parent, *args,**kwargs):
tk.Frame.__init__(self, parent,*args,**kwargs)
self.lb = dict()
self.l = tk.Label(self, text="Data for ")
self.l.pack(side = 'left')
self.year = tk.Label(self, text="")
self.year.pack(side = 'left')
self.hidden = dict()
def addDatum(self, txt, color, stationName):
l1 = tk.Label(self, text=txt, fg = color)
self.lb[stationName] = l1
l1.pack(side = 'left')
self.hidden[stationName] = False
def setDatumText(self, stationName, txt):
self.lb[stationName].configure(text = txt)
def hideDatum(self, stationName):
self.lb[stationName].pack_forget()
self.hidden[stationName] = True
def showDatum(self, stationName):
self.lb[stationName].pack(side = 'left')
self.hidden[stationName] = False
def isHidden(self, stationName):
return self.hidden[stationName]
def setYear(self, year):
self.year.configure(text = str(year) + ":")
def getDataFrameLabels(self):
return self.lb
def isInDataFrameLabels(self,stationName):
return isInDict(stationName, self.lb)
# Start SelectionFrame Class
class SelectionFrame(tk.Frame):
def __init__(self,parent,*args,**kwargs):
tk.Frame.__init__(self, parent,*args,**kwargs)
self.cb = dict()
self.l = tk.Label(self, text="Station Selection: ").pack(side = 'left')
def addCheckButton(self, text, color, com):
c = tk.Checkbutton(self, text = text, fg = color, activeforeground = color, command = com)
self.cb[text] = c
c.select()
c.pack(side = 'left')
def getCheckButtons(self):
return self.cb
def isCheckButton(self, stationName):
if stationName in self.cb:
return True
else:
return False
# Start Plotter Class
class Plotter(tk.Canvas):
def __init__(self, parent,*args,**kwargs):
Canvas.__init__(self,parent,**kwargs)
self.bind("<Configure>", self.on_resize)
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
def on_resize(self,event):
# determine the ratio of old width/height to new width/height
wscale = float(event.width)/self.width
hscale = float(event.height)/self.height
self.width = event.width
self.height = event.height
# resize the canvas
self.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
#Begin TemperatureData class
class TemperatureData:
def __init__(self):
self._data = dict()
self._stationNames = list()
self._stationsSelected = list()
def load_data(self, filename):
station_name = get_station_name(filename)
self._stationNames.append(station_name)
self._stationsSelected.append(True)
station = Station(filename)
self._data[station_name] = station
def get_data(self):
return self._data
def toggle_selected(self, i):
if self._stationsSelected[i] == True:
self._stationsSelected[i] = False
else:
self._stationsSelected[i] = True
def is_selected(self, i):
return self._stationsSelected[i]
def get_stations(self):
return self._stationNames
def get_ranges(self):
min_year = None
max_year = None
min_temp = None
max_temp = None
for k, v in self._data.items():
if min_year == None or max_year == None or min_temp == None or max_temp == None:
min_year, max_year = v.get_year_range()
min_temp, max_temp = v.get_temp_range()
else:
t_min_year, t_max_year = v.get_year_range()
t_min_temp, t_max_temp = v.get_temp_range()
min_year = min(min_year, t_min_year)
max_year = max(max_year, t_max_year)
min_temp = min(min_temp, t_min_temp)
max_temp = max(max_temp, t_max_temp)
return (min_year, max_year, min_temp, max_temp)
#End TemperatureData class
# My support
def load_stations(stations_file):
"""Return the list of station names
load_stations() -> list(str)
"""
fd = open(stations_file, "r")
stations = []
for line in fd:
line = line.strip()
if not line:
continue
stations.append(line)
fd.close()
return stations
##################################################
# !!!!!! Do not change (or add to) the code below !!!!!
###################################################
def main():
root = tk.Tk()
app = TemperaturePlotApp(root)
app.pack()
root.geometry("800x400")
root.mainloop()
if __name__ == '__main__':
main()
If someone wouldnt mind pointing out to me why this is happening, i would much appreciate it, as the assignment is due in 4 hours, and i have no idea what to do.
EDIT: assign2_support.py file code:
#
# Support for assignment 2
#
# Imports for use in your assignment
import tkinter as tk
import os.path
from tkinter import filedialog
from tkinter import messagebox
# colours for drawing lines and text
COLOURS = ['#f90909', '#ffa405', '#c0c203', '#1abd04', '#058096', '#042ee1',
'#d30af1','#ec06b3']
def load_data_points(filename):
"""Return the data contained in the given file.
load_data_points(str) -> dict(int:float)
"""
fd = open(filename, 'r')
data = {}
for line in fd:
parts = line.split(',')
data[int(parts[0])] = float(parts[1])
return data
class FileExtensionException(Exception):
pass
class Station(object):
"""A class for storing yearly average temperature data for a given station
"""
def __init__(self, stationfile):
""" Constructor: Station(str)"""
self._data = load_data_points(stationfile)
keys = self._data.keys()
self._min_year = min(keys)
self._max_year = max(keys)
temps = self._data.values()
self._min_temp = min(temps)
self._max_temp = max(temps)
base = os.path.basename(stationfile)
if not base.endswith('.txt'):
raise(FileExtensionException())
self._name = base.replace(".txt", "")
def get_temp(self, year):
"""Return the temperature average for the given year.
get_temp(int) -> float
"""
return self._data.get(year)
def get_data_points(self):
"""Return the data as a list of points in year order
get_data_points() -> list((int, float))
"""
return [(year, self._data[year]) for year in sorted(self._data.keys())]
def get_year_range(self):
""" Return the range of years in the data
get_year_range() -> (int, int)
"""
return (self._min_year, self._max_year)
def get_temp_range(self):
"""Return the range of temperatures in the data
get_temp_range() -> (float, float)
"""
return (self._min_temp, self._max_temp)
def get_name(self):
return self._name
def __repr__(self):
return "Station({0})".format(self._name)
class CoordinateTranslator(object):
"""A class which manages translation of data values into (x, y) coordinates.
The application manages real-world data (year, temp), but the Canvas
drawings require (x, y) coordinates. This class
converts between the two.
"""
def __init__(self, width, height, min_year, max_year, min_temp, max_temp):
"""
Create a CoordinateTranslator with the given canvas width/height,
the smallest and largest years and
the smallest and largest temperatures
Constructor: CoordinateTranslator(int, int, int, int, float, float)
"""
self._min_year = min_year
self._max_year = max_year
self._min_temp = min_temp
self._max_temp = max_temp
self.resize(width, height)
def resize(self, width, height):
"""Adjust the scaling factors to account for a new width/height.
After the Canvas resizes, call this method to fix the scaling.
"""
self._xscale = (self._max_year - self._min_year) / width
self._yscale = (self._max_temp - self._min_temp) / height
self._width = width
self._height = height
def temperature_coords(self, year, temperature):
"""Given a year and a temperature,
return (x, y) coordinates to plot.
temperature_coords(int, float) -> (float, float)
"""
return ((year - self._min_year)/ self._xscale,
self._height - (temperature - self._min_temp) / self._yscale)
def get_year(self, x):
"""Given an x coordinate on the Canvas, return the year that it
corresponds to.
get_year(float) -> int
"""
return int(x * self._xscale + 0.5) + self._min_year
## CSSE7030
def best_fit(points):
"""Given points are a list of (x,y) points ordered by x
this function computes the best line fit over that range and
returns the coords of end points of the line.
best_fit(list((floatt, float)) -> ((float, float), (float, float))
"""
count = len(points)
if count == 0:
# needed to avoid division by zero
# return something that will not appear on screen if drawn
return ((-1,-1), (-1, -1))
x_values = [x for x, _ in points]
y_values = [y for _, y in points]
sum_x = sum(x_values)
sum_y = sum(y_values)
sum_x2 = sum(x**2 for x in x_values)
sum_y2 = sum(y**2 for y in y_values)
sum_xy = sum(x*y for x,y in points)
x_mean = sum_x/count
y_mean = sum_y/count
slope = (sum_xy - sum_x * y_mean) / (sum_x2 - sum_x * x_mean)
y_inter = y_mean - slope * x_mean
return ((x_values[0], slope * x_values[0] + y_inter),
(x_values[-1], slope * x_values[-1] + y_inter))
Thanks heaps Corey :)
You are creating a canvas with a requested size of 850x400. You are fixing the window size to be 800x400. Because there's not enough room in the window to fit everything in, Tkinter has to start reducing widgets, or removing widgets from view. It won't attempt to reduce a widget below its requested size, so it's not going to shrink your canvas. So, it's next option is to start hiding widgets from view.
When tkinter has to start hiding part or all of a widget from view, it starts with the widget last in the "packing list" -- the last widget to have called
pack(...)
. Thus, if you pack the canvas last, before the bottom frame, it will be the one that starts getting shrunk below its requested size.A simple fix is to remove the width and height attributes of the canvas, and also remove the binding on
<Configure>
. This lets tkinter decide the size of the canvas, which when set up properly, means that it will grow and shrink to fit the available space.You can also save packing of the canvas to the very last, which makes it the first widget to start getting "chopped off" when there isn't enough room.
For the complete description of the packing algorithm see http://tcl.tk/man/tcl8.5/TkCmd/pack.htm#M26