It's a simple program, the user is prompted to enter number of variables.
Then the user is prompted to enter coefficients and constants. That many equations will be there as many variables.
Once filled up, submit button is clicked and the solutions are displayed.
I've accomplished this much successfully. However I'm new to Tkinter. I would appreciate if someone can help me improve the user interface and the code in general. The code is here. No need to make any change to the solving algorithm. I'm not allowed to use numpy for this assignment :( I want to improve the interface and the design language, that's it.
def minor(matrix, i, j):
return [[matrix[r][c] for c in range(len(matrix[r])) if c != j]
for r in range(len(matrix)) if r != i]
def det(matrix):
if len(matrix) == len(matrix[0]) == 1:
return matrix[0][0]
return sum(matrix[0][i] * cofac(matrix, 0, i) for i in range(len(matrix[0])))
cofac = lambda matrix, i, j: (-1) ** ((i + j) % 2) * det(minor(matrix, i, j))
transpose = lambda matrix: [[matrix[r][c] for r in range(len(matrix))] for c in range(len(matrix[0]))]
def adj(matrix):
return transpose([[cofac(matrix, r, c) for c in range(len(matrix[r]))] for r in range(len(matrix))])
def div_and_store(a, d):
toPrint = []
for i in a:
toPrint.append([])
for j in i:
if j % d == 0:
toPrint[-1].append(f'{j//d}')
else:
h = hcf(j, d)
denominator = d//h
numerator = j//h
if denominator > 0:
toPrint[-1].append(f'{numerator}/{denominator}')
else:
toPrint[-1].append(f'{-numerator}/{-denominator}')
return toPrint
hcf = lambda x, y: y if x == 0 else hcf(y % x, x)
def product(A, B):
if len(A[0]) != len(B):
return
Bt = transpose(B)
return [[sum(a * b for a, b in zip(i, j)) for j in Bt] for i in A]
from tkinter import *
root = Tk()
root.resizable(width=False, height=False) # not resizable in both directions
root.title('Simultaneous liner equation Solver')
my_label = Label(root, text='How many variables?')
my_label.grid(row=0, column=0)
e = Entry(root, width=10, borderwidth=5)
def done():
global row, n
try:
A = []
S = []
for record in entries:
A.append([])
for i in range(0, len(record)-1):
entry = record[i].get()
if entry:
A[-1].append(int(entry))
else:
A[-1].append(0)
entry = record[-1].get()
S.append([int(entry)])
except ValueError:
new_label = Label(root, text='Invalid. Try again!')
new_label.grid(row=row, columnspan=n * 3, sticky='W')
new_label.after(1000, lambda: new_label.destroy())
return
for record in entries:
for entry in record:
entry['state'] = DISABLED
new_button['state'] = DISABLED
determinant = det(A)
if determinant == 0:
new_label = Label(root, text='No unique solution set!')
new_label.grid(row=row, columnspan=n * 3, sticky='W')
return
adjoin = adj(A)
solution = div_and_store(product(adjoin, S), determinant)
for i in range(n):
new_label = Label(root, text=chr(97+i) + ' = '+solution[i][0])
new_label.grid(row=row, columnspan=2, sticky='W')
row += 1
def submit():
try:
global n
n = int(e.get())
except ValueError:
label = Label(root, text="You're supposed to enter a number, Try again")
label.grid(row=1, column=0, columnspan=3)
label.after(1000, lambda: label.destroy())
return
if n < 2:
label = Label(root, text="At least two variables are required!")
label.grid(row=1, column=0, columnspan=3)
label.after(1000, lambda: label.destroy())
return
e['state'] = DISABLED
my_label.grid_forget()
my_button.grid_forget()
e.grid_forget()
global row, entries
row = 0
entries = []
for i in range(n):
Label(root, text='').grid(row=row, columnspan=3*n+1)
row += 1
entries.append([])
col = 0
for j in map(chr, range(97, 97+n)):
entry = Entry(root, width=5, borderwidth=2, justify='right')
entries[i].append(entry)
entry.grid(row=row, column=col)
col += 1
Label(root, text=j).grid(row=row, column=col, padx=5, sticky='W')
col += 1
if col == 3*n-1:
Label(root, text='=').grid(row=row, column=col)
col += 1
entry = Entry(root, width=5, borderwidth=2, justify='right')
entries[i].append(entry)
entry.grid(row=row, column=col)
else:
Label(root, text='+').grid(row=row, column=col)
col += 1
row += 1
Label(root, text='').grid(row=row)
row += 1
txt = 'May leave the blank empty if the coefficient is 0!'
Label(root, text=txt).grid(row=row, columnspan=3*n-1, sticky='W')
row += 1
global new_button
new_button = Button(root, text='Submit', command=done)
new_button.grid(row=row, column=3*n)
row += 1
my_button = Button(root, text='Submit', command=submit)
e.grid(row=0, column=2)
my_button.grid(row=0, column=3)
root.mainloop()
1 Answer 1
- Always put your imports at the top of the file
- Avoid importing splat
*
; either import specific symbols, or import the module (optionally with an alias) likeimport tkinter as tk
and refer to symbols within its namespace liketk.Button
. - Strongly consider using Sympy. It can natively do fractional matrix math, as well as more advanced stuff like thorough treatment of solution sets and degrees of freedom.
- PEP484 type-hint your method signatures.
- Expand your one-liner nested comprehensions, which are very difficult to read, into multi-line formatting with nested indentation
- Do not use lambdas when functions suffice.
- Variables like
toPrint
should beto_print
by PEP8 - Avoid globals like
root
,my_label
etc. being in the global namespace. Pass them around by function parameter or as a class. e
,my_button
andmy_label
are not good variable names.- Do not call
mainloop
from the global namespace; call it from a main function so that it's possible for other people to reuse or test your code in pieces - Your interface decision of having a "disposable program" that can only solve one system, after which all controls are disabled, is strange. I would instead expect
- do not have a Submit button at all
- do not disable anything upon solution
- update the output solution set whenever any field is edited, so long as the inputs are valid
- if the inputs are invalid, rather than temporarily showing an error message and then hiding it after a timer, immediately show an error message that persists until the inputs have been edited to be valid
- do not have a two-step UI that asks you for the number of parameters. Instead, have a one-step UI with a spinbox control that allows selection of the number of parameters, adjusting the appropriate input controls in real time.
- You currently reject float inputs. It's not necessary to do this - you can have a conversion stage e.g. from 3.7 to 37/10, and preserve your exact fractional math.
A light and partial refactor that touches on only a few of the above suggestions, particularly type hinting, is
from numbers import Real
from typing import List, Sequence
from tkinter import Button, Entry, Label, Tk, DISABLED
def minor(matrix: Sequence[Sequence[Real]], i: int, j: int) -> List[List[Real]]:
return [
[
matrix[r][c] for c in range(len(matrix[r]))
if c != j
]
for r in range(len(matrix)) if r != i
]
def det(matrix: Sequence[Sequence[Real]]) -> Real:
if len(matrix) == len(matrix[0]) == 1:
return matrix[0][0]
return sum(
matrix[0][i] * cofac(matrix, 0, i)
for i in range(len(matrix[0]))
)
def cofac(matrix: Sequence[Sequence[Real]], i: int, j: int) -> Real:
return (-1) ** ((i + j) % 2) * det(minor(matrix, i, j))
def transpose(matrix: Sequence[Sequence[Real]]) -> List[List[Real]]:
return [
[
matrix[r][c] for r in range(len(matrix))
] for c in range(len(matrix[0]))
]
def adj(matrix: Sequence[Sequence[Real]]) -> List[List[Real]]:
return transpose(
[
[
cofac(matrix, r, c) for c in range(len(matrix[r]))
] for r in range(len(matrix))
]
)
def div_and_store(a: Sequence[Sequence[Real]], d: Real) -> List[List[Real]]:
to_print = []
for i in a:
to_print.append([])
for j in i:
if j % d == 0:
to_print[-1].append(f'{j//d}')
else:
h = hcf(j, d)
denominator = d//h
numerator = j//h
if denominator > 0:
to_print[-1].append(f'{numerator}/{denominator}')
else:
to_print[-1].append(f'{-numerator}/{-denominator}')
return to_print
def hcf(x: Real, y: Real) -> Real:
return y if x == 0 else hcf(y % x, x)
def product(A: Sequence[Sequence[Real]], B: Sequence[Sequence[Real]]) -> List[List[Real]]:
if len(A[0]) != len(B):
raise ValueError()
Bt = transpose(B)
return [
[
sum(a * b for a, b in zip(i, j)) for j in Bt
] for i in A
]
def done() -> None:
global row, n
try:
A = []
S = []
for record in entries:
A.append([])
for i in range(0, len(record)-1):
entry = record[i].get()
if entry:
A[-1].append(int(entry))
else:
A[-1].append(0)
entry = record[-1].get()
S.append([int(entry)])
except ValueError:
new_label = Label(root, text='Invalid. Try again!')
new_label.grid(row=row, columnspan=n * 3, sticky='W')
new_label.after(1000, lambda: new_label.destroy())
return
for record in entries:
for entry in record:
entry['state'] = DISABLED
new_button['state'] = DISABLED
determinant = det(A)
if determinant == 0:
new_label = Label(root, text='No unique solution set!')
new_label.grid(row=row, columnspan=n * 3, sticky='W')
return
adjoin = adj(A)
solution = div_and_store(product(adjoin, S), determinant)
for i in range(n):
new_label = Label(root, text=chr(97+i) + ' = '+solution[i][0])
new_label.grid(row=row, columnspan=2, sticky='W')
row += 1
def submit() -> None:
try:
global n
n = int(e.get())
except ValueError:
label = Label(root, text="You're supposed to enter a number, Try again")
label.grid(row=1, column=0, columnspan=3)
label.after(1000, lambda: label.destroy())
return
if n < 2:
label = Label(root, text="At least two variables are required!")
label.grid(row=1, column=0, columnspan=3)
label.after(1000, lambda: label.destroy())
return
e['state'] = DISABLED
my_label.grid_forget()
my_button.grid_forget()
e.grid_forget()
global row, entries
row = 0
entries = []
for i in range(n):
Label(root, text='').grid(row=row, columnspan=3*n+1)
row += 1
entries.append([])
col = 0
for j in map(chr, range(97, 97+n)):
entry = Entry(root, width=5, borderwidth=2, justify='right')
entries[i].append(entry)
entry.grid(row=row, column=col)
col += 1
Label(root, text=j).grid(row=row, column=col, padx=5, sticky='W')
col += 1
if col == 3*n-1:
Label(root, text='=').grid(row=row, column=col)
col += 1
entry = Entry(root, width=5, borderwidth=2, justify='right')
entries[i].append(entry)
entry.grid(row=row, column=col)
else:
Label(root, text='+').grid(row=row, column=col)
col += 1
row += 1
Label(root, text='').grid(row=row)
row += 1
txt = 'May leave the blank empty if the coefficient is 0!'
Label(root, text=txt).grid(row=row, columnspan=3*n-1, sticky='W')
row += 1
global new_button
new_button = Button(root, text='Submit', command=done)
new_button.grid(row=row, column=3*n)
row += 1
root = Tk()
e = Entry(root, width=10, borderwidth=5)
my_label = Label(root, text='How many variables?')
my_button = Button(root, text='Submit', command=submit)
def main() -> None:
root.resizable(width=False, height=False) # not resizable in both directions
root.title('Simultaneous linear equation Solver')
my_label.grid(row=0, column=0)
e.grid(row=0, column=2)
my_button.grid(row=0, column=3)
root.mainloop()
if __name__ == '__main__':
main()
With all suggestions, a refactor looks like
from fractions import Fraction
from string import ascii_lowercase
from typing import List, Optional, Callable, Dict
from sympy import Matrix, linsolve, FiniteSet
import tkinter as tk
MAX_VARS = len(ascii_lowercase)
ComplainCB = Callable[[str, Optional[str]], None]
class UICell:
def __init__(
self, parent: tk.Widget, complain: ComplainCB, row: int, col: int,
justify: str = tk.LEFT,
) -> None:
self.complain = complain
self.var_name = f'cell_{row}_{col}'
# not DoubleVar - it needs to be Fraction-parseable
self.var = tk.StringVar(
master=parent,
name=self.var_name,
value='0',
)
self.trace_id = self.var.trace_add('write', self.changed)
self.entry = tk.Entry(
master=parent,
textvariable=self.var,
width=6,
justify=justify,
)
self.entry.grid(row=row, column=col)
self.value: Optional[Fraction] = Fraction(0)
def destroy(self) -> None:
self.var.trace_remove('write', self.trace_id)
self.entry.destroy()
def changed(self, name: str, index: str, mode: str) -> None:
try:
self.value = Fraction(self.var.get())
except ValueError:
self.value = None
if self.value is None:
complaint = 'Invalid number'
colour = '#FCD0D0'
else:
complaint = None
colour = 'white'
self.entry.configure(background=colour)
self.complain(self.var_name, complaint)
class UIVarCell(UICell):
def __init__(self, parent: tk.Widget, complain: ComplainCB, row: int, col: int, letter: str) -> None:
super().__init__(parent, complain, row, col, tk.RIGHT)
self.letter = letter
self.separator = tk.Label(master=parent)
self.separator.grid(sticky=tk.W, row=row, column=1 + col)
def destroy(self) -> None:
super().destroy()
self.separator.destroy()
def set_separator(self, rightmost: bool) -> None:
if rightmost:
sep = '='
else:
sep = '+'
self.separator.config(text=f'{self.letter} {sep}')
class UIVariable:
def __init__(self, parent: tk.Widget, complain: ComplainCB, index: int) -> None:
self.parent, self.complain, self.index = parent, complain, index
self.letter = ascii_lowercase[index]
self.cells: List[UIVarCell] = []
self.result_var = tk.StringVar(
master=parent,
value='',
name=f'result_{index}',
)
self.result_entry = tk.Entry(
master=parent,
state='readonly',
textvariable=self.result_var,
width=6,
justify=tk.RIGHT,
)
self.result_label = tk.Label(master=parent, text=f'={self.letter}')
self.result_entry.grid(sticky=tk.E, row=1 + MAX_VARS, column=2*index)
self.result_label.grid(sticky=tk.W, row=1 + MAX_VARS, column=2*index + 1)
for _ in range(index + 1):
self.grow()
def grow(self) -> None:
self.cells.append(UIVarCell(
parent=self.parent,
complain=self.complain,
row=1 + len(self.cells),
col=2*self.index,
letter=self.letter,
))
def shrink(self) -> None:
self.cells.pop().destroy()
def destroy(self) -> None:
for widget in (self.result_label, self.result_entry):
widget.destroy()
while self.cells:
self.shrink()
def set_result(self, s: str) -> None:
self.result_var.set(s)
class UIFrame:
def __init__(self, parent: tk.Tk):
self.root = root = tk.Frame(master=parent)
self.variables: List[UIVariable] = []
self.sums: List[UICell] = []
self.complaints: Dict[str, str] = {}
self.count = tk.IntVar(master=root, name='count', value=2)
self.count.trace_add('write', self.count_changed)
tk.Label(
master=root, text='Variables'
).grid(row=0, column=0, sticky=tk.E)
tk.Spinbox(
master=root,
from_=2,
to=MAX_VARS,
increment=1,
width=4, # characters
textvariable=self.count, # triggers a first call to count_changed
).grid(row=0, column=1, columnspan=2, sticky=tk.W)
self.error_label = tk.Label(master=root, text='', foreground='red')
# Row indices do not need to be contiguous, so choose one that will
# always be at the bottom - one for the spinbox, max vars, and one for
# the result row.
self.error_label.grid(row=1 + MAX_VARS + 1, column=0, columnspan=MAX_VARS + 1)
root.pack()
def count_changed(self, name: str, index: str, mode: str) -> None:
try:
requested = self.count.get()
except tk.TclError:
return
if 2 <= requested <= MAX_VARS:
for var in self.variables:
var.set_result('')
while len(self.variables) < requested:
self.grow()
while len(self.variables) > requested:
self.shrink()
def grow(self) -> None:
for var in self.variables:
var.grow()
for cell in var.cells:
cell.set_separator(rightmost=False)
var = UIVariable(
parent=self.root, complain=self.complain,
index=len(self.variables),
)
for cell in var.cells:
cell.set_separator(rightmost=True)
self.variables.append(var)
self.sums.append(UICell(
parent=self.root, complain=self.complain,
row=1 + len(self.sums), col=2*MAX_VARS,
))
def shrink(self) -> None:
self.sums.pop().destroy()
self.variables.pop().destroy()
for var in self.variables:
var.shrink()
for cell in self.variables[-1].cells:
cell.set_separator(rightmost=True)
def complain(self, var_name: str, complaint: Optional[str]) -> None:
if complaint is None:
self.complaints.pop(var_name, None)
else:
self.complaints[var_name] = complaint
self.error_text = ', '.join(self.complaints.values())
if not self.complaints:
self.solve()
@property
def error_text(self) -> str:
return self.error_label.cget('text')
@error_text.setter
def error_text(self, s: str) -> None:
self.error_label.configure(text=s)
@property
def matrix(self) -> Matrix:
return Matrix([
[c.value for c in var.cells]
for var in self.variables
]).T
@property
def sum_vector(self) -> Matrix:
return Matrix([c.value for c in self.sums])
def solve(self) -> None:
res_set = linsolve((self.matrix, self.sum_vector))
if not isinstance(res_set, FiniteSet):
self.error_text = 'No finite set of solutions'
elif len(res_set) < 1:
self.error_text = 'No solution found'
elif len(res_set) > 1:
self.error_text = 'Non-unique solution space'
else:
self.error_text = ''
result, = res_set
for var, res in zip(self.variables, result):
var.set_result(res)
return
for var in self.variables:
var.set_result('')
def main() -> None:
parent = tk.Tk()
parent.title('Simultaneous linear equation solver')
UIFrame(parent)
parent.mainloop()
if __name__ == '__main__':
main()
Error highlighting:
Underdetermined systems:
Systems with no solution:
Solution:
-
\$\begingroup\$ Your answer made me realize... I need to study tkinter in details before trying to do these kinda programs... Thanks a lot.... \$\endgroup\$Nothing special– Nothing special2021年09月17日 06:39:56 +00:00Commented Sep 17, 2021 at 6:39
-
1\$\begingroup\$ On the contrary! I think studying while you do these kinda programs is a great way to learn. Basically ask "it would be nice if I could do X. How is that done in tkinter?" \$\endgroup\$Reinderien– Reinderien2021年09月17日 13:40:59 +00:00Commented Sep 17, 2021 at 13:40