I'm trying to build a more complex GUI for my app. I'm trying to place buttons using .grid() inside of a frame. However, I'm getting "AttributeError: object has no attribute 'tk'"
whenever I try to create a button with the frame as the root. I've seen people write GUI classes (i.e. class Frame(tk.Frame)), and that has created more problems with my code. How am I suppose to create buttons and place them within the frame without having to rewrite the majority of my classes from scratch?
When I root a button to the "master," it works fine. However, if it's rooted to the "action_frame" that's when I get the error.
calculator.py
# -*- coding: utf-8 -*-
import tkinter as tk
from math import *
from classes_GUI import ButtonBlock, LabelBlock, TextBlock, FrameBlock
from classes_calculator import ActionBlock
# Store input number
def storeInput(entry_text, result_text, action, array):
numb = 0.0
try:
numb = float(entry_text.retrieveTextInput())
except:
print('Please enter a valid number')
return
calc_action = ActionBlock(numb, action)
array.append(calc_action)
entry_text.clearText()
num = calc_action.returnNumber()
act = calc_action.returnAction()
input_texts = dict([
(1, ' + ' + str(num)),
(2, ' - ' + str(num)),
(3, ' * ' + str(num)),
(4, ' / ' + str(num)),
(5, ' + 1/' + str(num)),
(6, ' + ' + str(num) + '^2')
])
result_text.changeText(input_texts[act])
# Calculate result
def calcResult(entry_text, result_text, array):
result = 0.0
for calc in array:
action = calc.returnAction()
num = calc.returnNumber()
if action == 1:
result += num
elif action == 2:
result -= num
elif action == 3:
result *= num
elif action == 4:
result /= num
elif action == 5:
result += 1.0 / num
elif action == 6:
result += num ** 2
entry_text.clearText()
result_text.changeText(str(result), True)
# Create a new calculator instance
def exeCalc():
action_blocks = []
button_blocks = []
frame_blocks = []
label_blocks = []
text_blocks = []
# Create GUI
master = tk.Tk()
master.title('Calculator')
# Create frames
action_frame = FrameBlock(master, 30, 30, 1, 6)
frame_blocks.append(action_frame)
for f in frame_blocks:
f.createFrame()
# Create GUI labels
title_label = LabelBlock(master, 20, 2, 'n', 0, 0, 'center', 'Calculator')
label_blocks.append(title_label)
entry_label = LabelBlock(master, 20, 2, 'n', 1, 0, 'center', 'Enter:')
label_blocks.append(entry_label)
result_label = LabelBlock(master, 20, 2, 'n', 2, 0, 'center', 'Result:')
label_blocks.append(result_label)
for l in label_blocks:
l.createLabel()
# Create GUI text
entry_text = TextBlock(master, 20, 2, 1, 1, 'normal', '')
text_blocks.append(entry_text)
result_text = TextBlock(master, 20, 2, 2, 1, 'disabled', '0')
text_blocks.append(result_text)
for t in text_blocks:
t.createText()
# Create GUI buttons
close_button = ButtonBlock(master, 6, 2, 3, 0, 'Close',
lambda: master.destroy())
button_blocks.append(close_button)
add_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 0, '+',
lambda: storeInput(text_blocks[0], text_blocks[1], 1, action_blocks))
button_blocks.append(add_button)
subtract_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 1, '-',
lambda: storeInput(text_blocks[0], text_blocks[1], 2, action_blocks))
button_blocks.append(subtract_button)
multiply_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 2, 'x',
lambda: storeInput(text_blocks[0], text_blocks[1], 3, action_blocks))
button_blocks.append(multiply_button)
divide_button = ButtonBlock(frame_blocks[0], 4, 2, 1, 0, '/',
lambda: storeInput(text_blocks[0], text_blocks[1], 4, action_blocks))
button_blocks.append(divide_button)
fraction_button = ButtonBlock(frame_blocks[0], 4, 2, 1, 1, '1/x',
lambda: storeInput(text_blocks[0], text_blocks[1], 5,
action_blocks))
button_blocks.append(fraction_button)
square_block = ButtonBlock(frame_blocks[0], 4, 2, 1, 2, 'x^2',
lambda: storeInput(text_blocks[0], text_blocks[1], 6,
action_blocks))
button_blocks.append(square_block)
equal_button = ButtonBlock(frame_blocks[0], 4, 2, 2, 0, '=',
lambda: calcResult(text_blocks[0], text_blocks[1], action_blocks))
button_blocks.append(equal_button)
for b in button_blocks:
b.createButton()
master.mainloop()
classes_GUI.py
# -*- coding: utf-8 -*-
import tkinter as tk
# Create a base data block
class BaseBlock():
def __init__(self, root, width, height, txt):
self.root = root
self.width = width
self.height = height
self.txt = txt
# Create a inner data block
class InnerBlock(BaseBlock):
def __init__(self, root, width, height, row, column, txt):
super().__init__(root, width, height, txt)
self.g_row = row
self.g_column = column
# Create a data block for a button
class ButtonBlock(InnerBlock):
def __init__(self, root, width, height, row, column, txt, command=None):
super().__init__(root, width, height, row, column, txt)
self.command = command
def createButton(self):
button = tk.Button(self.root, text=self.txt, width=self.width,
height=self.height, command=self.command)
button.grid(row=self.g_row, column=self.g_column)
return button
# Create a frame data block
class FrameBlock(InnerBlock):
def __init__(self, root, width, height, row, column, txt=None):
super().__init__(root, width, height, row, column, txt)
def createFrame(self):
frame = tk.Frame(self.root, width=self.width, height=self.height)
frame.grid(row=self.g_row, column=self.g_column)
return frame
# Create a data block for a window
class LabelBlock(InnerBlock):
def __init__(self, root, width, height, anchor, row, column, justify, txt):
super().__init__(root, width, height, row, column, txt)
self.anchor = anchor
self.justify = justify
def createLabel(self):
label = tk.Label(self.root, width=self.width, height=self.height,
anchor=self.anchor, justify=self.justify, text=self.txt)
label.grid(row=self.g_row, column=self.g_column)
return label
# Create a data block for text
class TextBlock(InnerBlock):
def __init__(self, root, width, height, row, column, state, txt):
super().__init__(root, width, height, row, column, txt)
self.state = state
self.text = None
def createText(self):
self.text = tk.Text(self.root, width=self.width, height=self.height)
self.text.insert(tk.END, self.txt)
self.text.grid(row=self.g_row, column=self.g_column)
self.text.config(state=self.state)
return self.text
# Clear text
def clearText(self):
self.text.delete('1.0', 'end')
# Change text
def changeText(self, new_txt, clear=False):
self.text.config(state='normal')
if clear:
self.clearText()
self.text.insert(tk.END, new_txt)
self.text.config(state='disabled')
# Retrieve input from text box
def retrieveTextInput(self):
text_input = self.text.get('1.0', 'end')
return text_input
It helps if your base class actually extends a Frame
. ;)
class BaseBlock(tk.Frame):
def __init__(self, master, width, height, txt):
tk.Frame.__init__(self, master)
Really though, you are creating too many layers and managing everything strangely. The below would be better. All this inheriting at every step and creation functions is too obfuscated.
import tkinter as tk
from math import *
from dataclasses import asdict, dataclass
from typing import Callable
@dataclass
class Label_dc:
width: int = 20
height: int = 2
anchor: str = 'n'
justify: str = 'center'
text: str = ''
@dataclass
class Button_dc:
width: int = 4
height: int = 2
text: str = ''
command: Callable = None
@dataclass
class Text_dc:
width: int = 20
height: int = 2
state: str = 'normal'
#from classes_calculator import ActionBlock
class FrameBlock(tk.Frame):
def __init__(self, master, row, column, rowspan, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, rowspan=rowspan)
class ButtonBlock(tk.Button):
def __init__(self, master, row, column, **kwargs):
tk.Button.__init__(self, master, **asdict(Button_dc(**kwargs)))
self.grid(row=row, column=column)
class LabelBlock(tk.Label):
def __init__(self, master, row, column, **kwargs):
tk.Label.__init__(self, master, **asdict(Label_dc(**kwargs)))
self.grid(row=row, column=column)
class TextBlock(tk.Text):
def __init__(self, master, row, column, text='', **kwargs):
tk.Text.__init__(self, master, **asdict(Text_dc(**kwargs)))
self.grid(row=row, column=column)
self.insert('1.end', text)
# Clear text
def clearText(self):
self.delete('1.0', 'end')
# Change text
def changeText(self, new_txt, clear=False):
self.config(state='normal')
if clear:
self.clearText()
self.insert(tk.END, new_txt)
self.config(state='disabled')
# Retrieve input from text box
def retrieveTextInput(self):
return self.get('1.0', 'end')
class App(tk.Tk):
WIDTH = 800
HEIGHT = 600
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# Create GUI labels
LabelBlock(self, 0, 0, text='Calculator')
LabelBlock(self, 1, 0, text='Enter:')
LabelBlock(self, 2, 0, text='Result:')
# Create GUI text
text_blocks = {
'entry' : TextBlock(self, 1, 1),
'result': TextBlock(self, 2, 1, state='disabled', text='0'),
}
#can't use ButtonBlock for this one ~ self.destroy wont pickle properly
tk.Button(self, text='Close', width=6, height=2, command=self.destroy).grid(row=3, column=0)
action = []
# Create frames
frame = FrameBlock(self, 1, 3, 2, width=30, height=30)
# Create GUI buttons
ButtonBlock(frame, 0, 0, text='+', command=lambda: self.store(*text_blocks, 1, action))
ButtonBlock(frame, 0, 1, text='-', command=lambda: self.store(*text_blocks, 2, action))
ButtonBlock(frame, 0, 2, text='*', command=lambda: self.store(*text_blocks, 2, action))
ButtonBlock(frame, 1, 0, text='/', command=lambda: self.store(*text_blocks, 4, action))
ButtonBlock(frame, 1, 1, text='1/x', command=lambda: self.store(*text_blocks, 5, action))
ButtonBlock(frame, 1, 2, text='x^2', command=lambda: self.store(*text_blocks, 6, action))
ButtonBlock(frame, 2, 0, text='=', command=lambda: self.calc(*text_blocks, action))
def store(self, entry, result, action, array):
pass #remove this line
numb = 0.0
try:
numb = float(entry.retrieveTextInput())
except:
print('Please enter a valid number')
return
calc_action = ActionBlock(numb, action)
array.append(calc_action)
entry.clearText()
num = calc_action.returnNumber()
act = calc_action.returnAction()
input_texts = dict([
(1, ' + ' + str(num)),
(2, ' - ' + str(num)),
(3, ' * ' + str(num)),
(4, ' / ' + str(num)),
(5, ' + 1/' + str(num)),
(6, ' + ' + str(num) + '^2')
])
result.changeText(input_texts[act])
# Calculate result
def calc(self, entry, result, array):
pass #remove this line
r = 0.0
for calc in array:
action = calc.returnAction()
num = calc.returnNumber()
if action == 1:
result += num
elif action == 2:
result -= num
elif action == 3:
result *= num
elif action == 4:
result /= num
elif action == 5:
result += 1.0 / num
elif action == 6:
result += num ** 2
entry.clearText()
result.changeText(str(r), True)
if __name__ == '__main__':
app = App()
app.title("Calculator")
app.geometry(f'{App.WIDTH}x{App.HEIGHT}')
app.mainloop()
It's worth it to change what you have. Everything will be a lot cleaner and easier to manage. Also, I just did a nice chunk for you, and your way is never going to work. Once you use Frame
as the super
for your BaseBlock
, your Button
, Label
and Text
will all break. Lesson learned: don't tell a bunch of different types of widgets to ultimately extend the same thing.
If you are absolutely stuck on doing it your way ~ you can do it like this
class FrameBlock(InnerBlock):
def __init__(self, root, width, height, row, column, txt=None):
super().__init__(root, width, height, row, column, txt)
self.frame = tk.Frame(self.root, width=self.width, height=self.height)
self.frame.grid(row=self.g_row, column=self.g_column)
and then when you want to use it as the master
for the Button
use action_frame.frame
aside
Your method for calculating results is not going to work, at all. You aren't even considering operator precedence. Use eval()
. To show you just how far off you are ... This is what it takes to parse every imaginable math expression that python supports. Even if you stripped it down to just what your calculator supports it would still be bigger than your entire current application.
class Expression:
# Clean
__WHITE: str = '\\s'
__white: Pattern = re.compile(__WHITE)
__COMM: str = '#\\s.*$'
__comm: Pattern = re.compile(__COMM)
# Symbolic
__PARENS: str = '[\\)\\(]'
__parens: Pattern = re.compile(__PARENS)
__INFIX: str = '[%&+-]|[*/]{1,2}|<<|>>|\\||\\^'
__infix: Pattern = re.compile(__INFIX)
__TOKEN: str = 'STK([0-9]+)'
__token: Pattern = re.compile(__TOKEN)
__SYMBOLIC: str = f'{__PARENS}|{__INFIX}'
# Prefix
__INV: str = '([~]+|~u)?'
# Numeric
__HEX: str = '([-]?0x[0-9a-f]+)'
__hex: Pattern = re.compile(__HEX)
__IHEX: str = f'{__INV}{__HEX}'
__ihex: Pattern = re.compile(__IHEX)
__OHEX: str = f'^{__HEX}$'
__ohex: Pattern = re.compile(__OHEX)
__NUM: str = '([-]?[0-9]+(\\.[0-9]+)?)'
__num: Pattern = re.compile(__NUM)
__INUM: str = f'{__INV}{__NUM}'
__inum: Pattern = re.compile(__INUM)
__ONUM: str = f'^{__NUM}$'
__onum: Pattern = re.compile(__ONUM)
__NUMERIC: str = f'{__IHEX}|{__INUM}'
# Variable
__HYPER: str = 'acosh|asinh|atanh|cosh|sinh|tanh'
__TRIG: str = 'acos|asin|atan2|atan|cos|sin|tan|hypot|dist'
__THEORY: str = 'ceil|comb|fabs|factorial|floor|fmod|frexp|gcd|isqrt|ldexp|modf|perm|remainder|trunc'
__LOG: str = 'expm1|exp|log1p|log10|log2|log|pow|sqrt'
__ANGLE: str = 'degrees|radians'
__SPEC: str = 'erfc|erf|lgamma|gamma'
__FN: str = f'{__HYPER}|{__TRIG}|{__THEORY}|{__LOG}|{__ANGLE}|{__SPEC}'
__func: Pattern = re.compile(__FN)
__RAND: str = '(random|rand)'
__rand: Pattern = re.compile(__RAND)
__CONST: str = 'pi|e|tau|inf|' + __RAND
__const: Pattern = re.compile(__CONST)
__BITWISE: str = '<<|>>|\\||\\^|&'
__bitwise: Pattern = re.compile(__BITWISE)
__FN2: str = 'min|max|' + __RAND
__func2: Pattern = re.compile(__FN2)
__VARIABLE: str = f'{__FN}|{__FN2}|{__CONST}'
__SIMPLE: str = f'^({__INUM}+{__INFIX})+{__INUM}$'
__simple: Pattern = re.compile(__SIMPLE)
# Combo
__MATH: str = f'{__VARIABLE}|{__NUMERIC}|{__SYMBOLIC}|,|E|\\s'
__math: Pattern = re.compile(__MATH)
# Priorities
__P1: str = '[*/]{1,2}|%'
__P2: str = '[+-]'
__P3: str = '<<|>>|&'
__P4: str = '\\||\\^'
__priority: List[Pattern] = [re.compile(__P1), re.compile(__P2), re.compile(__P3), re.compile(__P4)]
def __init__(self):
self.value = math.nan
def evaluate(self, expr: str) -> float:
self.value = Expression.eval(expr)
return self.value
@staticmethod
def __hexrepl(m: Match[Union[str, bytes]]):
return str(int(m.group(0), 16))
@staticmethod
def eval(expr: str, fast: bool = False) -> float:
# Remove Whitespace, Comments, Convert Hash To Hex and Case To Lower
expr = Expression.__comm.sub("", expr)
expr = Expression.__white.sub("", expr)
expr = expr.replace('#', '0x').lower()
# Check If This Is Actual Math By Deleting Everything Math Related And Seeing If Anything Is Left
if len(re.sub(Expression.__math, "", expr)) > 0:
return math.nan
if fast:
return Expression.__fast(expr)
# Parse All Inversions Now ... invert(~) is the only "left side only" operator
expr = Expression.__parse_inversions(expr)
expr = Expression.__hex.sub(Expression.__hexrepl, expr)
# Check If This Is Solely A Number ~ If So, Parse Int And Return
if Expression.__onum.match(expr):
n = float(expr)
return int(n) if n % 1 == 0 else n
# We Got This Far. It Must Be Math
n = Expression.__parse(expr)
return int(n) if n % 1 == 0 else n
# Private Static Interfaces
@staticmethod
def __parse_inversions(expr: str) -> str:
match: Iterator[Match[Union[str, bytes]]] = Expression.__ihex.finditer(expr)
m: Match[Union[str, bytes]]
for m in match:
expr = Expression.__invert_expr(expr, m, 16)
match = Expression.__inum.finditer(expr)
for m in match:
expr = Expression.__invert_expr(expr, m, 10)
return expr
@staticmethod
def __invert_expr(expr: str, m: Match[Union[str, bytes]], b: int) -> str:
t1: str = m.group(1)
t2: str = m.group(2)
if t1:
if t1 == '~u':
n: int = Expression.__uinvert_num(int(t2, b))
else:
f: int = len(t1) % 2 == 1
n: int = -(int(t2, b) + 1) if f else int(t2, b)
expr = expr.replace(m.group(0), str(n))
return expr
@staticmethod
def __uinvert_num(num: float) -> int:
if num > 0:
x: int = int(math.log(num, 2.0) + 1)
i: int = 0
for i in range(0, x):
num = (num ^ (1 << i))
return num
@staticmethod
def __parse(expr: str) -> float:
exp_stack: List[str] = []
ops_stack: List[str] = []
res_stack: List[float] = []
tokens = Expression.__tokenize(expr)
# everything that can come before an operator
b1: str = f'{Expression.__HEX}|{Expression.__NUM}|{Expression.__CONST}|\\)'
c: Pattern = re.compile(b1)
# before an operator that is the rest of this expression
b2: str = f'{Expression.__NUM}E'
d: Pattern = re.compile(b2, re.I)
expr = tokens.expression[0::]
while len(expr):
m: Match[Union[str, bytes]] = Expression.__infix.search(expr)
if m:
op: str = m.group()
left: str = expr[0:m.span()[0]]
if re.search(c, left) and not re.search(d, left):
exp_stack.append(left)
ops_stack.append(op)
expr = expr.replace(f'{left}{op}', "")
else:
if len(left) == 0 or re.match(d, left):
right: str = expr[m.span()[1]::]
m = Expression.__infix.search(right)
if m:
left = f'{left}{op}'
op = m.group()
left = f'{left}{right[0:m.span()[0]]}'
exp_stack.append(left)
ops_stack.append(op)
expr = expr.replace(f'{left}{op}', "")
else:
exp_stack.append(expr)
expr = ""
else:
# Probably Not Even Possible In A Valid Math Expression
print("Expression.parse(expr:String): unexpected left side")
print("expression: ", expr)
print("left side: ", left)
print("operator: ", op)
print("exp_stack: ", exp_stack)
print("ops_stack: ", ops_stack)
else:
exp_stack.append(expr)
expr = ""
for r in range(len(exp_stack)):
m: Match[Union[str, bytes]] = Expression.__token.search(exp_stack[r])
inner: str = ""
if m:
i: int = int(m.group(1))
inner = tokens.stack[i]
res_stack.append(Expression.__parsetype(exp_stack[r], inner))
# Iterate Through Stacks Based On Priority and Do Assignments ~ ie... Calculate Everything
if len(ops_stack) > 0:
p: int = 0
for p in range(len(Expression.__priority)):
n: int = 0
while n < len(ops_stack) and len(ops_stack) > 0:
m: Match[Union[str, bytes]] = Expression.__priority[p].match(ops_stack[n])
if m is not None:
if not math.isnan(res_stack[n]) and not math.isnan(res_stack[n + 1]):
res_stack[n] = Expression.__value(res_stack[n], ops_stack[n], res_stack[n + 1])
res_stack.pop(n + 1)
ops_stack.pop(n)
else:
n += 1
return res_stack[0] if len(res_stack) == 1 else math.nan
@staticmethod
def __parsetype(expr: str, val: str = "") -> float:
fin: float = math.nan
if val != "":
tokens: Tokens_t = Expression.__tokenize(val)
csv: List[str] = tokens.expression.split(",")
a: float = 0
b: float = 0
f: str = ""
ln: int = len(csv)
if ln >= 1:
a = Expression.__parse(Expression.__detokenize(csv[0], tokens))
if ln == 2:
b = Expression.__parse(Expression.__detokenize(csv[1], tokens))
m: Match[Union[str, bytes]] = Expression.__func.match(expr)
m2: Match[Union[str, bytes]] = Expression.__func2.match(expr)
if m:
f = m.group()
fin = getattr(math, f)(a, b) if len(csv) == 2 else getattr(math, f)(a)
elif m2:
f = m2.group()
if ln == 2:
if f == 'min':
fin = min(a, b)
elif f == 'max':
fin = max(a, b)
elif ln == 1:
if Expression.__rand.match(f):
fin = random() * a
else:
fin = Expression.__parse(val)
else:
m: Match[Union[str, bytes]] = Expression.__const.match(expr)
c: Match[Union[str, bytes]] = Expression.__hex.match(expr)
if m:
cn: str = m.group()
fin = random() if Expression.__rand.match(cn) else getattr(math, cn)
elif c:
fin = int(c.group(), 16)
else:
fin = float(expr)
return fin
@staticmethod
def __tokenize(expr: str) -> Tokens_t:
c: int = 0
b: int = -1
e: int = -1
ex: str = expr[0::]
s: List[str] = []
m: Match[Union[str, bytes]]
p: Iterator[Match[Union[str, bytes]]] = Expression.__parens.finditer(ex)
for m in p:
if m.group() == "(":
c += 1
if b == -1:
b = m.span()[1]
elif m.group() == ")":
c -= 1
if c == 0 and b > -1:
e = m.span()[0]
if b != e:
s.append(expr[b:e])
ex = ex.replace(expr[b:e], f'STK{len(s) - 1}')
b = -1
return Tokens_t(ex, s) # Tokens_t ~ python equivalent to my c++ math parser
@staticmethod
def __detokenize(part: str, tokens: Tokens_t) -> str:
ex: str = part[0::]
m: Match[Union[str, bytes]]
p: Iterator[Match[Union[str, bytes]]] = Expression.__token.finditer(ex)
for m in p:
ex = ex.replace(m.group(0), tokens.stack[int(m.group(1))])
return ex
@staticmethod
def __fast(expr: str) -> float:
return eval(expr)
__ops: Dict[str, Callable] = {
'+': lambda x, y: x + y,
'-': lambda x, y: x - y,
'*': lambda x, y: x * y,
'/': lambda x, y: x / y,
'**': lambda x, y: x ** y,
'//': lambda x, y: x // y,
'>>': lambda x, y: x >> y,
'<<': lambda x, y: x << y,
'&': lambda x, y: x & y,
'|': lambda x, y: x | y,
'^': lambda x, y: x ^ y,
'%': lambda x, y: x % y,
}
@staticmethod
def __value(v1: float, oper: str, v2: float) -> float:
x: float = 0
try:
m: Match[Union[str, bytes]] = Expression.__bitwise.match(oper)
x = Expression.__ops[oper](v1, v2) if not m else Expression.__ops[oper](int(v1), int(v2))
except KeyError:
x = math.nan
return x