HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Widget setup for Tkinter-based game

Submitted by: @import:stackexchange-codereview··
0
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

Solution

The main issues, with your code, are that:

  • 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 runner


I 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 runner
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, bg='#F0F0F0', state='disabled')
            if random.randint
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 = {}
        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.