patternpythonMinor
Widget setup for Tkinter-based game
Viewed 0 times
tkintersetupgameforbasedwidget
Problem
I'm thinking of making a little strategic game using tkinter (player should interact with it using buttons). But my code is very repetitive. This is a mock-up of my program:
```
import tkinter as tk
import random
def multi(*args):
for func in args:
func()
def show_hide():
if not button["text"]:
button.configure(text="Perform action #1", bd=2, bg="white", command=activate_deactivate)
status_label["text"] = "Status: {}".format(status.get())
button_2.configure(text="Perform action #2", bd=2, bg="white", command=activate_deactivate_2)
status_label_2["text"] = "Status: {}".format(status_2.get())
else:
button.configure(text="", bd=0, bg="#F0F0F0", command=None)
status_label["text"] = ""
button_2.configure(text="", bd=0, bg="#F0F0F0", command=None)
status_label_2["text"] = ""
def activate_deactivate():
if status.get() == "Can be done":
status.set("To be done")
status_label.configure(text="Status: {}".format(status.get()), fg="blue")
else:
status.set("Can be done")
status_label.configure(text="Status: {}".format(status.get()), fg="black")
def activate_deactivate_2():
if status_2.get() == "Can be done":
status_2.set("To be done")
status_label_2.configure(text="Status: {}".format(status.get()), fg="blue")
else:
status_2.set("Can be done")
status_label_2.configure(text="Status: {}".format(status.get()), fg="black")
def step_forward():
if status.get() == "To be done":
button.configure(text="", bd=0, bg="#F0F0F0", state="disabled")
status_label["text"] = ""
result = random.choice(["success", "failure"])
if result == "success":
status.set("Accomplished")
status_label["fg"] = "green"
else:
status.set("Failed")
status_label["fg"] = "red"
else:
button.configure(text="", bd=0, bg="#F0F0F0", command=None)
s
```
import tkinter as tk
import random
def multi(*args):
for func in args:
func()
def show_hide():
if not button["text"]:
button.configure(text="Perform action #1", bd=2, bg="white", command=activate_deactivate)
status_label["text"] = "Status: {}".format(status.get())
button_2.configure(text="Perform action #2", bd=2, bg="white", command=activate_deactivate_2)
status_label_2["text"] = "Status: {}".format(status_2.get())
else:
button.configure(text="", bd=0, bg="#F0F0F0", command=None)
status_label["text"] = ""
button_2.configure(text="", bd=0, bg="#F0F0F0", command=None)
status_label_2["text"] = ""
def activate_deactivate():
if status.get() == "Can be done":
status.set("To be done")
status_label.configure(text="Status: {}".format(status.get()), fg="blue")
else:
status.set("Can be done")
status_label.configure(text="Status: {}".format(status.get()), fg="black")
def activate_deactivate_2():
if status_2.get() == "Can be done":
status_2.set("To be done")
status_label_2.configure(text="Status: {}".format(status.get()), fg="blue")
else:
status_2.set("Can be done")
status_label_2.configure(text="Status: {}".format(status.get()), fg="black")
def step_forward():
if status.get() == "To be done":
button.configure(text="", bd=0, bg="#F0F0F0", state="disabled")
status_label["text"] = ""
result = random.choice(["success", "failure"])
if result == "success":
status.set("Accomplished")
status_label["fg"] = "green"
else:
status.set("Failed")
status_label["fg"] = "red"
else:
button.configure(text="", bd=0, bg="#F0F0F0", command=None)
s
Solution
The main issues, with your code, are that:
But first, let's talk a bit about
First, you can remove the need for a
I also changed names, I feel these are more explicit.
Second, you should wrap your top-level code under an
Now onto simplifying your code.
There is at least 3 things you are trying to achieve all at once:
The first part is pretty straightforward: if you want several elements that behave like a single entity, you build a class to hold these elements together. Having done that, the second one comes naturally: you provide methods on your class so that "the outside" can interact with it. Managing states are thus nearly done:
In the following improvements, you’ll see that I provide several level of customization to be able to interact with the group of item both from the inside and the outside. Depending on your needs, you can merge some or detail even more:
```
import tkinter as tk
import random
class ButtonWithStatus:
def __init__(self, root, pack_side, frame_config=None, button_config=None, label_config=None):
if frame_config is None:
frame_config = {}
self._status_config = {} if label_config is None else label_config
self._button_config = {} if button_config is None else button_config
self._button_config['command'] = self.on_click
self.hidden = True
frame = tk.Frame(root, **frame_config)
frame.pack(side=pack_side)
self._hidden_background_color = frame.cget('bg')
# Build hidden button
parameters = self.hidden_parameters(self._button_config)
self.button = tk.Button(frame, **parameters)
self.button.grid(row=0, column=0)
# Build label with hidden text
parameters = self.hidden_parameters(self._status_config, False)
self.label = tk.Label(frame, **parameters)
self.label.grid(row=1, column=0)
def hidden_parameters(self, configuration, is_button=True):
parameters = configuration.copy()
parameters.update(text='', bg=self._hidden_background_color)
if is_button:
parameters['command'] = None
return parameters
def on_click(self):
raise NotImplementedError
def show_hide(self):
if self.hidden:
self.button.configure(**self._button_config)
self.label.configure(**self._status_config)
else:
parameters = self.hidden_parameters(self._button_config)
self.button.configure(**parameters)
parameters = self.hidden_parameters(self._status_config, False)
self.label.configure(**parameters)
self.hidden = not self.hidden
class ActivableButton(ButtonWithStatus):
STATUS = 'Status: {}'
def __init__(self, root, button_text, inactive_status, active_status):
super().__init__(root, 'left', {'padx': 10},
{'text': button_text, 'font': 'courier 20', 'bd': 0},
{'text': self.STATUS.format(inactive_status), 'font': 'courier 14'})
self.activated = False
self._active = active_status
self._inactive = inactive_status
def on_click(self):
self.activated = not self.activated
if self.activated:
self._status_config.update(text=self.STATUS.format(self._active), fg='blue')
else:
self._status_config.update(text=self.STATUS.format(self._inactive), fg='black')
self.label.configure(**self._status_config)
class ValidableActivableButton(ActivableButton):
def __init__(self, root, button_text):
super().__init__(root, button_text, 'Can be done', 'To be done')
self._done = False
def step_forward(self):
if self._done:
return
if self.activated:
self._button_config.update(text='', bd=0
- you do not factorize part of your code that are similar by correctly parametrizing your functions; and
- you do not manage any common state between entities that are related.
But first, let's talk a bit about
multi and __name__.First, you can remove the need for a
lambda by having multi return a callable that you will be able to provide to the command parameter. Writting something allong the lines of ..., command=multi(step_forward, step_forward_2)):def combine(*callables):
def runner():
for function in callables:
function()
return runnerI also changed names, I feel these are more explicit.
Second, you should wrap your top-level code under an
if __name__ == '__main__': clause. That way, if I want to use the building blocks you provide, I can import your file and build around it without having your UI pop up.Now onto simplifying your code.
There is at least 3 things you are trying to achieve all at once:
- Build logical blocks such as a button and a label in a frame;
- Interact with such blocks from "the outside"; I believe the show/hide should be more generic than the "next step" one, but not knowing what you want to achieve with such UI, I can be misleading;
- Manage states and control flow to have all your parts work together.
The first part is pretty straightforward: if you want several elements that behave like a single entity, you build a class to hold these elements together. Having done that, the second one comes naturally: you provide methods on your class so that "the outside" can interact with it. Managing states are thus nearly done:
combine fed by instance method is very helpful. You only need to set it as a command on UI elements once each of them has been built.In the following improvements, you’ll see that I provide several level of customization to be able to interact with the group of item both from the inside and the outside. Depending on your needs, you can merge some or detail even more:
```
import tkinter as tk
import random
class ButtonWithStatus:
def __init__(self, root, pack_side, frame_config=None, button_config=None, label_config=None):
if frame_config is None:
frame_config = {}
self._status_config = {} if label_config is None else label_config
self._button_config = {} if button_config is None else button_config
self._button_config['command'] = self.on_click
self.hidden = True
frame = tk.Frame(root, **frame_config)
frame.pack(side=pack_side)
self._hidden_background_color = frame.cget('bg')
# Build hidden button
parameters = self.hidden_parameters(self._button_config)
self.button = tk.Button(frame, **parameters)
self.button.grid(row=0, column=0)
# Build label with hidden text
parameters = self.hidden_parameters(self._status_config, False)
self.label = tk.Label(frame, **parameters)
self.label.grid(row=1, column=0)
def hidden_parameters(self, configuration, is_button=True):
parameters = configuration.copy()
parameters.update(text='', bg=self._hidden_background_color)
if is_button:
parameters['command'] = None
return parameters
def on_click(self):
raise NotImplementedError
def show_hide(self):
if self.hidden:
self.button.configure(**self._button_config)
self.label.configure(**self._status_config)
else:
parameters = self.hidden_parameters(self._button_config)
self.button.configure(**parameters)
parameters = self.hidden_parameters(self._status_config, False)
self.label.configure(**parameters)
self.hidden = not self.hidden
class ActivableButton(ButtonWithStatus):
STATUS = 'Status: {}'
def __init__(self, root, button_text, inactive_status, active_status):
super().__init__(root, 'left', {'padx': 10},
{'text': button_text, 'font': 'courier 20', 'bd': 0},
{'text': self.STATUS.format(inactive_status), 'font': 'courier 14'})
self.activated = False
self._active = active_status
self._inactive = inactive_status
def on_click(self):
self.activated = not self.activated
if self.activated:
self._status_config.update(text=self.STATUS.format(self._active), fg='blue')
else:
self._status_config.update(text=self.STATUS.format(self._inactive), fg='black')
self.label.configure(**self._status_config)
class ValidableActivableButton(ActivableButton):
def __init__(self, root, button_text):
super().__init__(root, button_text, 'Can be done', 'To be done')
self._done = False
def step_forward(self):
if self._done:
return
if self.activated:
self._button_config.update(text='', bd=0
Code Snippets
def combine(*callables):
def runner():
for function in callables:
function()
return runnerimport tkinter as tk
import random
class ButtonWithStatus:
def __init__(self, root, pack_side, frame_config=None, button_config=None, label_config=None):
if frame_config is None:
frame_config = {}
self._status_config = {} if label_config is None else label_config
self._button_config = {} if button_config is None else button_config
self._button_config['command'] = self.on_click
self.hidden = True
frame = tk.Frame(root, **frame_config)
frame.pack(side=pack_side)
self._hidden_background_color = frame.cget('bg')
# Build hidden button
parameters = self.hidden_parameters(self._button_config)
self.button = tk.Button(frame, **parameters)
self.button.grid(row=0, column=0)
# Build label with hidden text
parameters = self.hidden_parameters(self._status_config, False)
self.label = tk.Label(frame, **parameters)
self.label.grid(row=1, column=0)
def hidden_parameters(self, configuration, is_button=True):
parameters = configuration.copy()
parameters.update(text='', bg=self._hidden_background_color)
if is_button:
parameters['command'] = None
return parameters
def on_click(self):
raise NotImplementedError
def show_hide(self):
if self.hidden:
self.button.configure(**self._button_config)
self.label.configure(**self._status_config)
else:
parameters = self.hidden_parameters(self._button_config)
self.button.configure(**parameters)
parameters = self.hidden_parameters(self._status_config, False)
self.label.configure(**parameters)
self.hidden = not self.hidden
class ActivableButton(ButtonWithStatus):
STATUS = 'Status: {}'
def __init__(self, root, button_text, inactive_status, active_status):
super().__init__(root, 'left', {'padx': 10},
{'text': button_text, 'font': 'courier 20', 'bd': 0},
{'text': self.STATUS.format(inactive_status), 'font': 'courier 14'})
self.activated = False
self._active = active_status
self._inactive = inactive_status
def on_click(self):
self.activated = not self.activated
if self.activated:
self._status_config.update(text=self.STATUS.format(self._active), fg='blue')
else:
self._status_config.update(text=self.STATUS.format(self._inactive), fg='black')
self.label.configure(**self._status_config)
class ValidableActivableButton(ActivableButton):
def __init__(self, root, button_text):
super().__init__(root, button_text, 'Can be done', 'To be done')
self._done = False
def step_forward(self):
if self._done:
return
if self.activated:
self._button_config.update(text='', bd=0, bg='#F0F0F0', state='disabled')
if random.randintimport tkinter as tk
import random
class ButtonWithStatus:
def __init__(self, root, pack_side, frame_config=None, button_config=None, label_config=None):
if frame_config is None:
frame_config = {}
if button_config is None:
button_config = {}
if label_config is None:
label_config = {}
button_config['command'] = self.on_click
frame = tk.Frame(root, **frame_config)
frame.pack(side=pack_side)
self.button = tk.Button(frame, **button_config)
self.button.grid(row=0, column=0)
self.label = tk.Label(frame, **label_config)
self.label.grid(row=1, column=0)
# Hide label and button
self.button.grid_remove()
self.label.grid_remove()
self.hidden = True
def on_click(self):
raise NotImplementedError
def show_hide(self):
if self.hidden:
self.button.grid()
self.label.grid()
else:
self.button.grid_remove()
self.label.grid_remove()
self.hidden = not self.hidden
class ActivableButton(ButtonWithStatus):
STATUS = 'Status: {}'
def __init__(self, root, button_text, inactive_status, active_status):
super().__init__(root, 'left', {'padx': 10},
{'text': button_text, 'font': 'courier 20', 'bd': 0},
{'text': self.STATUS.format(inactive_status), 'font': 'courier 14'})
self.activated = False
self._done = False
self._active = active_status
self._inactive = inactive_status
def on_click(self):
self.activated = not self.activated
if self.activated:
self.label['text'] = self.STATUS.format(self._active)
self.label['fg'] = 'blue'
else:
self.label['text'] = self.STATUS.format(self._inactive)
self.label['fg'] = 'black'
def step_forward(self):
if self._done:
return
if self.activated:
self.button['text'] = ''
self.button['bd'] = 0
self.button['bg'] = '#F0F0F0'
self.button['state'] = 'disabled'
if random.randint(0, 1):
self.label['text'] = 'Accomplished'
self.label['fg'] = 'green'
else:
self.label['text'] = 'Failed'
self.label['fg'] = 'red'
self._done = True
def combine(*callables):
def runner():
for function in callables:
function()
return runner
if __name__ == '__main__':
root = tk.Tk()
main = tk.Button(root, text="Show/Hide", bg="white", font="courier 30")
main.pack()
frame = tk.Frame(root, pady=10)
frame.pack()
button1 = ActivableButton(frame, 'Perform action #1', 'Can be done', 'To be done')
button2 = ActivableButton(frame, 'Perform action #2', 'Can be done', 'To be done')
main['command'] = combine(button1.show_hide, button2.Context
StackExchange Code Review Q#146078, answer score: 4
Revisions (0)
No revisions yet.