Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Question] Is it possible for me to force update the values dictionary for my menubutton elements to None? #6734

Open
5 of 10 tasks
lilyanne12 opened this issue Apr 2, 2024 · 10 comments
Labels
Done - Install Dev Build (see docs for how) See https://docs.pysimplegui.com/en/latest/documentation/installing_licensing/upgrading/ Port - TK PySimpleGUI question Further information is requested

Comments

@lilyanne12
Copy link

Type of Issue (Enhancement, Error, Bug, Question)

Question


Operating System

Windows 11 Home, OS build: 22631.3296

PySimpleGUI Port (tkinter, Qt, Wx, Web)

tkinter


Versions

Version information can be obtained by calling sg.main_get_debug_data()
Or you can print each version shown in ()

Python Interpeter: C:\Users\lily-\anaconda3\python.exe
Python version: 3.11.7
Platform: Windows
Platform version: ('10', '10.0.22631', 'SP0', 'Multiprocessor Free')
Port: PySimpleGUI
tkinter version: 8.6.12
PySimpleGUI version: 5.0.4
PySimpleGUI filename: C:\Users\lily-\anaconda3\Lib\site-packages\PySimpleGUI\PySimpleGUI.py

Python version (sg.sys.version)

3.11.7 | packaged by Anaconda, Inc. | (main, Dec 15 2023, 18:05:47) [MSC v.1916 64 bit (AMD64)]

PySimpleGUI Version (sg.__version__)

'5.0.4'

GUI Version (tkinter (sg.tclversion_detailed), PySide2, WxPython, Remi)


Your Experience In Months or Years (optional)

Years Python programming experience
2/3 as a hobbyist

Years Programming experience overall
see above

Have used another Python GUI Framework? (tkinter, Qt, etc) (yes/no is fine)
No

Anything else you think would be helpful?
This is my first PySimpleGUI project!


Troubleshooting

These items may solve your problem. Please check those you've done by changing - [ ] to - [X]

  • Searched main docs for your problem PySimpleGUI Documenation
  • Looked for Demo Programs that are similar to your goal. It is recommend you use the Demo Browser! Demo Programs
  • None of your GUI code was generated by an AI algorithm like GPT
  • If not tkinter - looked for Demo Programs for specific port
  • For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • Run your program outside of your debugger (from a command line)
  • Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.com
  • Have upgraded to the latest release of PySimpleGUI on PyPI (lastest official version)
  • Tried running the Development Build. Your problem may have already been fixed but not released. Check Home Window for release notes and upgrading capability
  • For licensing questions please email license@PySimpleGUI.com

Detailed Description

I am building a GUI to allow the user to play the "Mastermind" logic game against the computer (see image below). The user inputs 5 colours in a row each time (using MenuButtons) and then when the "Submit Guess" button is clicked the users inputs are then checked against the predefined 5 colour solution and feedback given.

When users click the "Submit Guess" button a method is called which checks if any of the colours haven't been set (ie. if the items in the values dict for the keys in that row are equal to None) and notifies the user that this is an invalid guess. This works very well until a new game is started using the "New" button. The values dictionary for the MenuButtons are retained from the previous game so this method no longer detects an invalid submission and in fact if buttons are left blank it uses the values from the previous game.

I would like to know if there is a way to force update the values dictionary to have value = None for all these elements. So far I've tried:

  • Replacing the values using a for loop both inside and outside the class
  • Using .update() method (though I'm a little unclear on using this where I'm not updating a specific paramter)
  • Using window.refresh() but again I'm unclear where to use this

I'm sorry the code below is long - I couldn't find a way to make it run with only the problem parts in. Hopefully it might be possible to suggest a solution from the description above...? Also I'm sure some of the code is probably very GRIM! This is my first PySimpleGUI and I'm still reasonably new to Python so forgive the rough edges! :)

Code To Duplicate

A short program that isolates and demonstrates the problem (Do not paste your massive program, but instead 10-20 lines that clearly show the problem)

This pre-formatted code block is all set for you to paste in your bit of code:

import PySimpleGUI as sg
from string import ascii_uppercase
import random as rd
import re
import ast

class Mastermind:

    _active_row = 1
    
    # This sets the active_row variable as a property
    @property
    def active_row(self):
        return self.__class__._active_row
        
    # This enables active_row to be updated in any instance
    @active_row.setter
    def active_row(self, value):
        self.__class__._active_row = value
    
    def __init__(self) -> None:

        layout = [
            [
                self._build_answer_frame(),
            ],
            [
                self._build_pegs_frame(),
            ],
            [
                self._build_buttons_frame(),
            ],
        ]
        
        self._window = sg.Window(
            title = 'Mastermind',
            layout = layout,
            finalize = True,
        )
    # This frame contains the solution at the top (currently set to vsisible whilst building but will be invisible in final version)
    def _build_answer_frame(self):
        row_length = 5

        colours = ['red','orange','yellow','green','blue','white','brown','black']
        solution = rd.sample(colours,5)

        # Create 5 buttons of different colours, the text contains the colour which will be used later as an answer key
        answer_buttons = [
            [
        
            sg.Button(
                button_text=f'{solution[i],ascii_uppercase[i]}',
                key = f'-{ascii_uppercase[i]}-',
                button_color = solution[i],
                visible = True,
            )
            for i in range(0,row_length)
            ]
        ]
            
        return sg.Frame(
                        title = 'solution',
                        layout = answer_buttons,
                        expand_x = True,
                        element_justification = "center",
                    )
    # This frame includes the manubutton objects that users input their guesses, row numbers, and the button objects which will be updated with different colours to provide feedback
    def _build_pegs_frame(self):
        row_length = 5
        coords = []

        # Each button is defined by coordinates eg. A1
        for row in range(12,0,-1):
            group = []
            for col in ascii_uppercase[0:row_length]:
                group.append(str(row)+str(col))
            coords.append(group)

        buttons = []
        for coord_group in coords:
            group = []
            for coord in coord_group:

                 # Define coordinate parts to construct key
                coord_no = re.findall(r'\d+',coord)[0]
                coord_let = re.findall(r'[A-Z]',coord)[0]

                # Create keypeg buttons
                button = sg.Button(
                    button_text = '   ',
                    font = 'Any 10',
                    border_width = 1,
                    disabled = True,
                    button_color = (None, sg.theme_background_color()),
                    key = f'-KEYPEG-{coord_no}-{coord_let}-',
                    # pad = (5,11),
                )
                group.append(button)
            for coord in coord_group:

                # Define coordinate parts to construct key
                coord_no = re.findall(r'\d+',coord)[0]
                coord_let = re.findall(r'[A-Z]',coord)[0]

                # Add text element between each set of key and code pegs
                if coord == coord_group[0]:
                    if len(coord) == 3:
                        text = sg.Text(
                            text = coord[0:2],
                            size = (5),
                            justification = "center"
                        )
                    else:
                        text = sg.Text(
                            text = coord[0],
                            size = (5),
                            justification = "center"
                        )
                    group.append(text)
                    
                # Create codepeg menubuttens    
                button = sg.ButtonMenu(
                    button_text = '   ',
                    font = 'Any 20',
                    border_width = 1,
                    button_color = (None, sg.theme_background_color()),
                    menu_def = ['',['red','orange','yellow','green','blue','white','brown','black']],
                    key = f'-CODEPEG-{coord_no}-{coord_let}-',
                    # size = (5,1),
                    # pad = (8,8,4,4),
                )
                group.append(button)
            buttons.append(group)
        
        return sg.Column(
            layout = [
                [
                    sg.Frame(
                        title = 'pegs',
                        layout = buttons,
                        expand_y = True,
                        element_justification = "center",
                    ),
                    sg.Sizer(),
                ]
            ],
            element_justification = "center",
        )
                       
    # This frame contains the user event buttons
    def _build_buttons_frame(self):
        return sg.Frame(
            '',
            [
                [
                    sg.Sizer(h_pixels = 90),
                    sg.Button(
                        button_text = 'Submit Guess',
                        key = '-SUBMITGUESS-',
                        font = 'Any 20',
                    ),
                    sg.Sizer(h_pixels = 60),
                    sg.Button(
                        button_text = 'New',
                        key = '-NEW-',
                        font = 'Any 20',
                    ),
                    sg.Sizer(h_pixels = 60),
                    sg.Button(
                        button_text = 'Quit',
                        key = '-QUIT-',
                        font = 'Any 20',
                    ),
                    sg.Sizer(h_pixels = 90),
                ]
            ],
            font = 'Any 20',
        )


    # This function creates a new solution and updates the answer frame at the top
    def _reset_solution(self):

        row_length = 5

        colours = ['red','orange','yellow','green','blue','white','brown','black']
        new_solution = rd.sample(colours,5)
        
        for i in range (0,row_length):
            self._window[f'-{ascii_uppercase[i]}-'].update(text = f'{new_solution[i],ascii_uppercase[i]}')
            self._window[f'-{ascii_uppercase[i]}-'].update(button_color = new_solution[i])

    # This function is called when a new game is created. It resets the solution, resets the active row and removes previous coloured buttons
    def _new_game(self,values):
        self._reset_solution()
        self.active_row = 1
        
        # Reset all visual elements
        for key in values.keys():
            self._window[key].update(button_color= (None, sg.theme_background_color()))
            
    # This function updates the colour of the codepeg based on the user menubutton choice
    def _select_codepeg(self, event, values):
        self._window[event].update(button_color=values[event])

    # This function checks if a guess is valid
    def _valid_guess(self,values):

        active_row = self.active_row
        
        guess = []
        for key in dict.keys(values):
            if key.startswith(f'-CODEPEG-{active_row}-'):
                guess.append((values[key]))
        
        if None in guess:
            print('Invalid guess')
            return False
        else:
            print('Valid guess')
            return True
            
    # This function evaluates a guess and generates the feedback    
    def _evaluate_guess(self, event, values):

        active_row = self.active_row
        
        row_length = 5
        current_solution = []
        for i in range (0,row_length):
            colour = self._window[f'-{ascii_uppercase[i]}-'].get_text()
            current_solution.append(colour)
    
        # Convert string tuples to tuples
        current_solution = list(map(lambda x: ast.literal_eval(x), current_solution))

        # Get guess values
        guess = []
        for key in dict.keys(values):
            if key.startswith(f'-CODEPEG-{active_row}-'):
                guess.append((key,values[key]))

        print(guess) # currently in use for debugging
        print(current_solution) # currently in use for debugging
        
        # Evalute number of correct colours
        sol_colours = set(map(lambda x: x[0], current_solution))
        guess_colours = set(map(lambda x: x[1], guess))
        common_colours = len(sol_colours.intersection(guess_colours))

        # Evaluate the number of correct positions
        guess_positions = set(map(lambda x: x[0][9:].split("-")[1]+x[1], guess))
        sol_positions = set(map(lambda x: x[1]+x[0], current_solution))
        common_positions = len(sol_positions.intersection(guess_positions))
        
        black = ['black']*common_positions
        white = ['white']*(common_colours-common_positions)

        keypeg_colours = black+white
        if len(keypeg_colours) < 5:
            keypeg_colours.extend([(None, sg.theme_background_color())]*(5-len(keypeg_colours)))

        self.active_row += 1

     # This function reads the events from the elements and returns the data
    def read_event(self):
        event, values = self._window.read()
        event_id = event[0] if event is not None else None
        return event_id, event, values

    # This function details the response to different events
    def process_event(self, event, values):
        if event[:9] == '-CODEPEG-':
            self._select_codepeg(event, values)
        elif event == '-SUBMITGUESS-':
            if self._valid_guess(values) == True:                
                self._evaluate_guess(event, values)
        elif event == '-NEW-':
            self._new_game(values)
        elif event == '-QUIT-':
            self._window.close()
            
    # This function closes the window when X is pressed    
    def close(self):
        self._window.close()

if __name__ == '__main__':
    game = Mastermind()
    
    #Event loop
    while True:
        event_id, event, values = game.read_event()

        if event_id in {sg.WIN_CLOSED}:
            break
        game.process_event(event,values)
        
    game.close()

Screenshot, Sketch, or Drawing

image

Watcha Makin?

If you care to share something about your project, it would be awesome to hear what you're building.

See description above - this is only halfway finished but should be a fun game when it is done!

@jason990420
Copy link
Collaborator

I'm sorry the code below is long - I couldn't find a way to make it run with only the problem parts in.

I believe you can provide a short program that isolates and demonstrates the problem.

There's no method provide to reset the value of a MenuButton element for menu item chosen.

Try following code if it is about your question.

import PySimpleGUI as sg

menu_def = ['', ['red', 'orange', 'yellow', 'green', 'blue', 'white', 'brown', 'black']]

layout = [
    [sg.ButtonMenu("", menu_def=menu_def, expand_x=True, button_color=(None, sg.theme_background_color()), key=("BM", i)) for i in range(5)],
    [sg.Button(key) for key in ("Clear", "Check", "Quit")],
]
window = sg.Window("Title", layout)

while True:

    event, values = window.read()

    if event in (sg.WIN_CLOSED, "Quit"):
        break
    print(event, [value for key, value in values.items()])
    if event[0] == "BM":
        window[event].update(button_color=(None, values[event]))
    elif event == "Clear":
        for i in range(5):
            window[("BM", i)].update(button_color=(None, sg.theme_background_color()))
            window[("BM", i)].MenuItemChosen = None

window.close()
('BM', 1) [None, 'yellow', None, None, None]
('BM', 3) [None, 'yellow', None, 'blue', None]
Clear [None, 'yellow', None, 'blue', None]    # The values is generated after event "Check", not after this event processed.
Check [None, None, None, None, None]

@lilyanne12
Copy link
Author

lilyanne12 commented Apr 3, 2024

Thank you, thank you for such a quick reply!! The .MenuItemChosen method is the one that I needed :) Just for future reference are these methods listed in the documentation anywhere? I may be being daft but I searched for the method above and couldn't find anything.

I think I took the guidance for issues where it said to "include your layout and window call" a little too literally, ha. I didn't realise I could post a piece of representative code instead. I'll do that next time.

I've never come across a package with such fantastic support, thank you to you and everyone that works on it.

@PySimpleGUI
Copy link
Owner

Thank you for the many kind words image It's very appreciated!

Looks like we need to add a "clear choice" feature to Button Menus, and maybe a "set choice" too. PySimpleGUI is certainly a work in progress that will never be "done". I didn't realize that the choice "stuck" in the values dictionary. Normal Menubars clear the value once the event has passed. Something tells me there's a reason for this, but I don't recall what it is just yet. Needs to simmer for a while I think to see if I can figure out why the difference.

The reason the member variable MenuItemChosen isn't documented is that it's an internal variable that wasn't designed to be manipulated by users. Until these new enhancements are added, it's fine to use workarounds like this. Jason's a master of these kinds of solutions (all kinds of solutions actually).

@PySimpleGUI
Copy link
Owner

Looking at the code, it looks like this may be a bug. There is a bit of commented out code that includes clearing the chosen item just as the Menu element does. It may have happened when the CustomTitlebar was added. If so, then it's a bug and I can safely revert it back to the old way.... more investigation....

@PySimpleGUI PySimpleGUI added the Done - Install Dev Build (see docs for how) See https://docs.pysimplegui.com/en/latest/documentation/installing_licensing/upgrading/ label Apr 3, 2024
@PySimpleGUI
Copy link
Owner

OK! Fixed in 5.0.4.2

ButtonMenu elements now operate the way that they used to and the same way that MenuBars work. When you get the event for a ButtonMenu element, the values dictionary has the value chosen. After the event is provided, the value the next time an event happens will be None.

Using @jason990420's excellent sample code, here's it running on 5.0.4.2

pycharm64_xG5WfSu3jJ.mp4

@jason990420 jason990420 added question Further information is requested Port - TK PySimpleGUI labels Apr 3, 2024
@jason990420
Copy link
Collaborator

jason990420 commented Apr 3, 2024

a method is called which checks if any of the colours haven't been set (ie. if the items in the values dict for the keys in that row are equal to None) and notifies the user that this is an invalid guess.

After fixed, user code won't work now.

IMO, programmer need to find another way to keep and check the state of each ButtonMenu element.
For example, it can be done by using an user_defined dict variable, like the dict-variable states here.

import PySimpleGUI as sg

menu_def = ['', ['red', 'orange', 'yellow', 'green', 'blue', 'white', 'brown', 'black']]

layout = [
    [sg.ButtonMenu("", menu_def=menu_def, expand_x=True, button_color=(None, sg.theme_background_color()), key=("BM", i)) for i in range(5)],
    [sg.Button(key) for key in ("Clear", "Check", "Quit")],
]
window = sg.Window("Title", layout)
states = {}

while True:

    event, values = window.read()

    if event in (sg.WIN_CLOSED, "Quit"):
        break
    elif event[0] == "BM":
        window[event].update(button_color=(None, values[event]))
        states[event] = values[event]
    elif event == "Clear":
        for i in range(5):
            window[("BM", i)].update(button_color=(None, sg.theme_background_color()))
        states = {}
    print(len(states), event, states)

window.close()
1 ('BM', 1) {('BM', 1): 'yellow'}
2 ('BM', 2) {('BM', 1): 'yellow', ('BM', 2): 'blue'}
3 ('BM', 4) {('BM', 1): 'yellow', ('BM', 2): 'blue', ('BM', 4): 'brown'}
0 Clear {}

@PySimpleGUI
Copy link
Owner

IMO, programmer need to find another way to keep and check the state of each ButtonMenu element.

This is the same thoughts I was having when fixing it. The event and values when the choice is made is that something happened. It's up to the user's code to remember this is a way that makes sense to them. They would even be a simple list because the position never changes for each spot.

Menu choices are a 1-time kind of thing, like a button click. They come, they go... if it's important to you, you save it somewhere.

Sorry that the behavior changed in a way that may have been relied on. The new code really is how it was originally design and has worked for most of the PySimpleGUI lifetime.

I do appreciate that the issues was opened and led to finding this bug!

@PySimpleGUI
Copy link
Owner

PySimpleGUI commented Apr 4, 2024

Side note for @jason990420 ....image

I loved seeing this in your code:

    elif event[0] == "BM":

This Python construct of being able to reference a string with [0] or a tuple and then compare to a string makes the event processing of both events that are plain strings and events that are tuples to be mixed together without problems. I'm great that we don't have to look at the type of the event variable first as it adds complexity.

Here's a version of Jason's code modified to use a list to store the current state of the board.

import PySimpleGUI as sg

menu_def = ['', ['red', 'orange', 'yellow', 'green', 'blue', 'white', 'brown', 'black']]

layout = [
    [sg.ButtonMenu("", menu_def=menu_def, expand_x=True, button_color=(None, sg.theme_background_color()), key=("BM", i)) for i in range(5)],
    [sg.Button(key) for key in ("Clear", "Check", "Quit")],
]
window = sg.Window("Title", layout)

states = ['' for i in range(5)]     # start with cleared states

while True:

    event, values = window.read()

    if event in (sg.WIN_CLOSED, "Quit"):
        break
    elif event[0] == "BM":
        window[event].update(button_color=(None, values[event]))
        states[event[1]] = values[event]
    elif event == "Clear":
        for i in range(5):
            window[("BM", i)].update(button_color=(None, sg.theme_background_color()))
            states[i] = ''
    print(event, states)

window.close()

With sample output

('BM', 1) ['', 'yellow', '', '', '']
('BM', 0) ['orange', 'yellow', '', '', '']
('BM', 4) ['orange', 'yellow', '', '', 'white']
Check ['orange', 'yellow', '', '', 'white']
Check ['orange', 'yellow', '', '', 'white']
Clear ['', '', '', '', '']
('BM', 4) ['', '', '', '', 'blue']

@lilyanne12
Copy link
Author

lilyanne12 commented Apr 4, 2024

Hi both,

Wow, I've been without internet for a couple of days and you've done a lot! Firstly, glad this helped find a bug :)

Secondly, I agree, clearing after the event feels like a more sensible behaviour for the menu buttons. As suggested I'll modify my code to capture the current state of the button. Thank you for the suggestions above - particularly using a tuple for the button key, didn't know that was possible and now I can get rid of all my horrible string splitting in favour of nice Pythonic indexing :)

@PySimpleGUI
Copy link
Owner

I'm genuinely thrilled you're clearly enjoying using PySimpleGUI.

You can read more about using datatypes other than strings for keys here:
https://docs.pysimplegui.com/en/latest/documentation/module/keys/#tuples-and-other-data-types-as-keys

There are examples in the Demo Programs and Cookbook as well showing tuples as keys. It's a very useful construct as you've seen. I use tuples when a thread is sending events to the GUI. This enables me to use the first entry of the tuple to tell the event loop that the event is coming from the thread. The other entries in the tuple can carry any kind of data I want. Here's a Demo Program that shows this design pattern:
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Multithreaded_Write_Event_Value.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Done - Install Dev Build (see docs for how) See https://docs.pysimplegui.com/en/latest/documentation/installing_licensing/upgrading/ Port - TK PySimpleGUI question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants