tkinter readonly combobox with autocomplete - python

I am trying to make a combobox in tkinter with python 3 that has autocomplete when the first letter is pressed, but is also readonly, so that only names in the list are selectable.
There's some old code which works well for autocomplete:
class AutocompleteCombobox(ttk.Combobox): #https://mail.python.org/pipermail/tkinter-discuss/2012-January/003041.html
def set_completion_list(self, completion_list):
"""Use our completion list as our drop down selection menu, arrows move through menu."""
self._completion_list = completion_list #sorted(completion_list, key=str.lower) # Work with a sorted list
self._hits = []
self._hit_index = 0
self.position = 0
self.bind('<KeyRelease>', self.handle_keyrelease)
self['values'] = self._completion_list # Setup our popup menu
def autocomplete(self, delta=0):
"""autocomplete the Combobox, delta may be 0/1/-1 to cycle through possible hits"""
if delta: # need to delete selection otherwise we would fix the current position
self.delete(self.position, END)
else: # set position to end so selection starts where textentry ended
self.position = len(self.get())
# collect hits
_hits = []
for element in self._completion_list:
if element.lower().startswith(self.get().lower()): # Match case insensitively
_hits.append(element)
# if we have a new hit list, keep this in mind
if _hits != self._hits:
self._hit_index = 0
self._hits=_hits
# only allow cycling if we are in a known hit list
if _hits == self._hits and self._hits:
self._hit_index = (self._hit_index + delta) % len(self._hits)
# now finally perform the auto completion
if self._hits:
self.delete(0,END)
self.insert(0,self._hits[self._hit_index])
self.select_range(self.position,END)
def handle_keyrelease(self, event):
"""event handler for the keyrelease event on this widget"""
if event.keysym == "BackSpace":
self.delete(self.index(INSERT), END)
self.position = self.index(END)
if event.keysym == "Left":
if self.position < self.index(END): # delete the selection
self.delete(self.position, END)
else:
self.position = self.position-1 # delete one character
self.delete(self.position, END)
if event.keysym == "Right":
self.position = self.index(END) # go to end (no selection)
if len(event.keysym) == 1:
self.autocomplete()
# No need for up/down, we'll jump to the popup
# list at the position of the autocompletion
But the challenge is that this does not work if you set the state to "readonly":
self.combo1 = AutocompleteCombobox(mainframe, width=12, state="readonly")
What I'd like is for the combobox to jump to the first name in the list that starts with a given letter if I press that letter, and remain blank if there isn't a valid name starting with that letter.
The other, related issue, is that the ComboboxSelected binding is not triggered when using the above autocomplete:
self.combo1.bind("<<ComboboxSelected>>", self.selected)
will trigger self.selected when you select using the usual methods (clicking on the drop down list, or pressing the up/down arrows), but not when you get an autocomplete.

Related

Autocompletion Combobox with <<ComboboxSelected>>

So I found this beautiful module which provides the abbility to autocomplete combobox out of provided list of values.
Yet, it does not seem to work with <<ComboboxSelected>> (the values just do not appear) event in bind method, can it be fixed?
Module itelf is below:
"""
tkentrycomplete.py
A Tkinter widget that features autocompletion.
Created by Mitja Martini on 2008-11-29.
Updated by Russell Adams, 2011/01/24 to support Python 3 and Combobox.
Updated by Dominic Kexel to use Tkinter and ttk instead of tkinter and tkinter.ttk
Licensed same as original (not specified?), or public domain, whichever is less restrictive.
"""
import sys
import os
import tkinter
import tkinter.ttk
__version__ = "1.1"
# I may have broken the unicode...
Tkinter_umlauts=['odiaeresis', 'adiaeresis', 'udiaeresis', 'Odiaeresis', 'Adiaeresis', 'Udiaeresis', 'ssharp']
class AutocompleteEntry(tkinter.Entry):
"""
Subclass of Tkinter.Entry that features autocompletion.
To enable autocompletion use set_completion_list(list) to define
a list of possible strings to hit.
To cycle through hits use down and up arrow keys.
"""
def set_completion_list(self, completion_list):
self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list
self._hits = []
self._hit_index = 0
self.position = 0
self.bind('<KeyRelease>', self.handle_keyrelease)
def autocomplete(self, delta=0):
"""autocomplete the Entry, delta may be 0/1/-1 to cycle through possible hits"""
if delta: # need to delete selection otherwise we would fix the current position
self.delete(self.position, tkinter.END)
else: # set position to end so selection starts where textentry ended
self.position = len(self.get())
# collect hits
_hits = []
for element in self._completion_list:
if element.lower().startswith(self.get().lower()): # Match case-insensitively
_hits.append(element)
# if we have a new hit list, keep this in mind
if _hits != self._hits:
self._hit_index = 0
self._hits=_hits
# only allow cycling if we are in a known hit list
if _hits == self._hits and self._hits:
self._hit_index = (self._hit_index + delta) % len(self._hits)
# now finally perform the auto completion
if self._hits:
self.delete(0,tkinter.END)
self.insert(0,self._hits[self._hit_index])
self.select_range(self.position,tkinter.END)
def handle_keyrelease(self, event):
"""event handler for the keyrelease event on this widget"""
if event.keysym == "BackSpace":
self.delete(self.index(tkinter.INSERT), tkinter.END)
self.position = self.index(tkinter.END)
if event.keysym == "Left":
if self.position < self.index(tkinter.END): # delete the selection
self.delete(self.position, tkinter.END)
else:
self.position = self.position-1 # delete one character
self.delete(self.position, tkinter.END)
if event.keysym == "Right":
self.position = self.index(tkinter.END) # go to end (no selection)
if event.keysym == "Down":
self.autocomplete(1) # cycle to next hit
if event.keysym == "Up":
self.autocomplete(-1) # cycle to previous hit
if len(event.keysym) == 1 or event.keysym in Tkinter_umlauts:
self.autocomplete()
class AutocompleteCombobox(tkinter.ttk.Combobox):
def set_completion_list(self, completion_list):
"""Use our completion list as our drop down selection menu, arrows move through menu."""
self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list
self._hits = []
self._hit_index = 0
self.position = 0
self.bind('<KeyRelease>', self.handle_keyrelease)
self['values'] = self._completion_list # Setup our popup menu
def autocomplete(self, delta=0):
"""autocomplete the Combobox, delta may be 0/1/-1 to cycle through possible hits"""
if delta: # need to delete selection otherwise we would fix the current position
self.delete(self.position, tkinter.END)
else: # set position to end so selection starts where textentry ended
self.position = len(self.get())
# collect hits
_hits = []
for element in self._completion_list:
if element.lower().startswith(self.get().lower()): # Match case insensitively
_hits.append(element)
# if we have a new hit list, keep this in mind
if _hits != self._hits:
self._hit_index = 0
self._hits=_hits
# only allow cycling if we are in a known hit list
if _hits == self._hits and self._hits:
self._hit_index = (self._hit_index + delta) % len(self._hits)
# now finally perform the auto completion
if self._hits:
self.delete(0,tkinter.END)
self.insert(0,self._hits[self._hit_index])
self.select_range(self.position,tkinter.END)
def handle_keyrelease(self, event):
"""event handler for the keyrelease event on this widget"""
if event.keysym == "BackSpace":
self.delete(self.index(tkinter.INSERT), tkinter.END)
self.position = self.index(tkinter.END)
if event.keysym == "Left":
if self.position < self.index(tkinter.END): # delete the selection
self.delete(self.position, tkinter.END)
else:
self.position = self.position-1 # delete one character
self.delete(self.position,tkinter.END)
if event.keysym == "Right":
self.position = self.index(tkinter.END) # go to end (no selection)
if len(event.keysym) == 1:
self.autocomplete()
# No need for up/down, we'll jump to the popup
# list at the position of the autocompletion
def test(test_list):
"""Run a mini application to test the AutocompleteEntry Widget."""
root = tkinter.Tk(className=' AutocompleteEntry demo')
entry = AutocompleteEntry(root)
entry.set_completion_list(test_list)
entry.pack()
entry.focus_set()
combo = AutocompleteCombobox(root)
combo.set_completion_list(test_list)
combo.pack()
combo.focus_set()
# I used a tiling WM with no controls, added a shortcut to quit
root.bind('<Control-Q>', lambda event=None: root.destroy())
root.bind('<Control-q>', lambda event=None: root.destroy())
root.mainloop()
if __name__ == '__main__':
test_list = ('apple', 'banana', 'CranBerry', 'dogwood', 'alpha', 'Acorn', 'Anise' )
test(test_list)
Found solution, just rename class Autocompletion Combobox to Combobox
Change the first part of the auto complete class to this.
You need to use a StringVar to trace any changes. <<ComboboxSelected>> will only be raised when the value is selected.
class AutocompleteCombobox(tkinter.ttk.Combobox):
def set_completion_list(self, completion_list):
"""Use our completion list as our drop down selection menu, arrows move through menu."""
self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list
self._hits = []
self._hit_index = 0
self.position = 0
self.set_val=tkinter.StringVar()
self.bind('<KeyRelease>', self.handle_keyrelease)
self.set_val.trace('w',self.change_values)
self['textvariable']=self.set_val
print(completion_list)
self['values'] = self._completion_list # Setup our popup menu
def change_values(self,*event):
self['values'] = self._hits

Curses Python don't clean window when back in main menu

6 years ago they proposed an excellent solution on this issue, but my changes led to an unexpected result, the window does not clear when I call the submenu function and the main menu, help me understand what is wrong.
import curses
from curses import panel
from test_print import get_menu_list, get_timings, time_to_seconds, GetPageChoise,\
main_menu_items, list_of_themes_end, text_discription_get
page = GetPageChoise()
class Menu(object):
def __init__(self, items, stdscreen):
self.window = stdscreen.subwin(5,2)
self.window.keypad(1)
self.panel = panel.new_panel(self.window)
self.panel.hide()
panel.update_panels()
self.position = 0
self.items = items
self.items.append(('exit','exit'))
def navigate(self, n):
self.position += n
if self.position < 0:
self.position = 0
elif self.position >= len(self.items):
self.position = len(self.items)-1
def display(self):
self.panel.top()
self.panel.show()
self.window.clear()
while True:
self.window.refresh()
curses.doupdate()
for index, item in enumerate(self.items):
if index == self.position:
mode = curses.A_REVERSE
else:
mode = curses.A_NORMAL
msg = '%d. %s' % (index, item[0])
self.window.addstr(10+ index, 1, msg, mode)
key = self.window.getch()
if key in [curses.KEY_ENTER, ord('\n')]:
if self.position == len(self.items)-1:
break
else:
self.items[self.position][1]()
elif key == curses.KEY_UP:
self.navigate(-1)
elif key == curses.KEY_DOWN:
self.navigate(1)
self.window.clear()
self.panel.hide()
panel.update_panels()
curses.doupdate()
class SubMenu(object):
def __init__(self, items, stdscreen):
self.window = stdscreen.subwin(5,2)
self.window.keypad(1)
self.panel = panel.new_panel(self.window)
self.panel.hide()
panel.update_panels()
self.position = 0
self.items = items
self.items.append(('exit','exit'))
def navigate(self, n):
self.position += n
if self.position < 0:
self.position = 0
elif self.position >= len(self.items):
self.position = len(self.items)-1
def display_sub(self):
while True:
self.window.refresh()
curses.doupdate()
for index, item in enumerate(self.items):
if index == self.position:
mode = curses.A_REVERSE
else:
mode = curses.A_NORMAL
next = 1
for disript in text_discription_get():
self.window.addstr(next, 1, disript)
next +=1
msg = '%d. %s' % (index, item[0])
self.window.addstr(10+ index, 1, msg, mode)
key = self.window.getch()
if key in [curses.KEY_ENTER, ord('\n')]:
if self.position == len(self.items)-1:
break
else:
self.items[self.position][1]()
elif key == curses.KEY_UP:
self.navigate(-1)
elif key == curses.KEY_DOWN:
self.navigate(1)
class MyApp(object):
def __init__(self, stdscreen):
self.screen = stdscreen
curses.curs_set(0)
submenu_items = [
('beep', curses.beep),
('flash', curses.flash)
]
submenu = SubMenu(sub_menu_items, self.screen) #Вывел конкретный подкаст, нужно изменить на выбор подкастов.
main_menu_items = [
('beep', curses.beep),
('flash', curses.flash),
('submenu', submenu.display_sub)
]
main_menu = Menu(main_menu_items, self.screen)
main_menu.display()
if __name__ == '__main__':
curses.wrapper(MyApp)
#Вывел меню, сделал выбор, нужно вывести Discription, сделать вызов плеера по выбору темы.
You can see how it looks at me at the GIF, the fact is that the proposed solution does not fit, since the submenu should display new content each time, depending on the choice.
gif how work this
Inserting a clear() after the function that shows the submenu worked for me:
class Menu(object):
...
def display(self):
...
if key in [curses.KEY_ENTER, ord('\n')]:
if self.position == len(self.items)-1:
break
else:
self.items[self.position][1]()
self.window.clear() # This will clear the main menu window every time an item is selected
You should also clear the screen before showing another window because the same thing could happen if your submenu was smaller than your main menu.
Alternatively, you could clear the window at the start of every loop, as you are already redrawing the list after each key press. This way you can update the items in the menu, and will not see parts of the old menu, as you would with your current program.

Z Ordering Controls, collision detection on top and bottom of control

I'm working on creating controls in pygame (button and label are the only ones done, as well as setting a tooltip for the control). I'm drawing the controls in the correct Z order but I'm trying to detect that the mouse is not over another control, so it only activates the one that's visible.
This, somewhat, works if you remove test.set_on_bottom(btnButton7), it will only trigger button7 even if the mouse is over one of the buttons below it, but if you click on one of the buttons below button7 and move the mouse over button7 it still thinks that the original button is being clicked (when it shouldn't)
Been wrapping my brain around this problem for a few days now and I just can't see to figure it out.
(Also, I just thought about it this morning but I should have turned each of the controls into a class instead of an id, so this will get reworked later)
The code is too long to post, here's the pastebin link
All of the processing of setting the states and triggering the messages is done in the process_events method
def process_events(self):
button = pygame.mouse.get_pressed()
mouse_pos = pygame.mouse.get_pos()
for control_id in reversed(self.__z_order):
state = self.__control_list[control_id]['state']
if state in ('deleted', 'disabled') or not self.__control_list[control_id]['draw']:
continue
if self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
# left mouse is pressed
if button[0]:
# current state of this control is hot and left mouse is pressed on it
if state == 'hot':
for x in self.__z_order[0:control_id - 1]:
if self.__control_list[x]['state'] == 'pressed':
self.__control_list[x]['state'] = 'normal'
can_change = True
for x in self.__z_order[control_id + 1:]:
if self.__control_list[x]['state'] == 'pressed':
can_change = False
break
# change the state to pressed
if can_change:
self.__control_list[control_id]['state'] = 'pressed'
self.__control_list[control_id]['mouse_pos_lclick'] = None
self.__control_list[control_id]['mouse_pos_ldown'] = mouse_pos
if self.__control_list[control_id]['on_hover_called']:
self.__draw_tip = None
if (time.clock() - self.__control_list[control_id][
'dbl_timer'] >= self.__dbl_click_delay) and \
(time.clock() - self.__control_list[control_id]['timer'] <= self.__dbl_click_speed):
if self.__event_mode:
if self.__control_list[control_id]['on_dbl_lclick']:
self.__control_list[control_id]['on_dbl_lclick']()
else:
self.__messages.append(self.Message(control_id, PGC_LBUTTONDBLCLK))
# print('Double click', self.__control_list[control_id]['text'])
self.__control_list[control_id]['dbl_timer'] = time.clock()
self.__control_list[control_id]['timer'] = -1
break
# go through the controls from top to bottom first
for control_id in reversed(self.__z_order):
state = self.__control_list[control_id]['state']
if state in ('deleted', 'disabled') or not self.__control_list[control_id]['draw']:
continue
# check if the mouse is over this control
if self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
# left mouse is not down
if not button[0]:
# state is currently pressed
if state == 'pressed':
# check if there's a timer initiated for this control
# this prevents 2 clicks + a double click message
if self.__control_list[control_id]['timer'] >= 0:
self.__control_list[control_id]['dbl_timer'] = -1
self.__control_list[control_id]['timer'] = time.clock()
# if the event mode
if self.__event_mode:
# call the function if there is one
if self.__control_list[control_id]['on_lclick']:
self.__control_list[control_id]['on_lclick']()
else:
# post the message to the messages queue
self.__messages.append(self.Message(control_id, PGC_LBUTTONUP))
# print('Click', self.__control_list[control_id]['text'])
# the timer is < 0 (should be -1), double click just happened
else:
# reset the timer to 0 so clicking can happen again
self.__control_list[control_id]['timer'] = 0
# go through all of the ids below this control
for x in self.__z_order[0:control_id - 1]:
# set all the hot controls to normal
if self.__control_list[x]['state'] == 'hot':
self.__control_list[x]['state'] = 'normal'
can_change = True
# go through all the controls on top of this control
for x in self.__z_order[control_id + 1:]:
# something else is on top of this and it's already hot, can't change this control
if self.__control_list[x]['state'] == 'hot':
can_change = False
break
if can_change:
self.__control_list[control_id]['state'] = 'hot'
self.__control_list[control_id]['mouse_pos_lclick'] = mouse_pos
self.__control_list[control_id]['mouse_pos_ldown'] = None
# state is not currently hot (but we're hovering over this control)
elif state != 'hot':
# check for any other contorls
for x in self.__z_order[0:control_id - 1]:
if self.__control_list[x]['state'] == 'hot':
self.__control_list[x]['state'] = 'normal'
can_change = True
for x in self.__z_order[control_id + 1:]:
if self.__control_list[x]['state'] == 'hot':
can_change = False
break
# change the state to hot
if can_change:
self.__control_list[control_id]['state'] = 'hot'
self.__control_list[control_id]['mouse_pos_hover'] = mouse_pos
# used to start a tooltip (needs work)
self.__control_list[control_id]['mouse_pos_rect'] = pygame.Rect(mouse_pos[0] - 7,
mouse_pos[1] - 7,
mouse_pos[0] + 7,
mouse_pos[1] + 7)
# state is currently 'hot'
else:
# timer for on_hover hasn't been initialized
if self.__control_list[control_id]['timer_on_hover'] == 0:
self.__control_list[control_id]['timer_on_hover'] = time.clock()
# mouse is in the area
if self.__control_list[control_id]['mouse_pos_rect'].collidepoint(mouse_pos):
# if the on_hover hasn't been triggered and there is a timer for the on_hover
if not self.__control_list[control_id]['on_hover_called'] and self.__control_list[control_id]['timer_on_hover']:
# if the mouse has been in the hover area for 1.5 seconds or more
if time.clock() - self.__control_list[control_id]['timer_on_hover'] >= 1.5:
# trigger the hover
self.__control_list[control_id]['on_hover_called'] = True
# on_hover is a function call, call the function
if self.__control_list[control_id]['on_hover']['type'] == 'function':
self.__control_list[control_id]['on_hover']['func'](self.__control_list[control_id]['on_hover']['args'])
# on_hover is a tip, set the self.__draw_tip variable to the tip we need
else:
self.__draw_tip = self.__control_list[control_id]['on_hover'].copy()
self.__draw_tip['rect'].x = mouse_pos[0]
self.__draw_tip['rect'].y = mouse_pos[1]
# mouse is not in the control rect and the state is not currently normal
elif state != 'normal':
# set it to normal
self.__control_list[control_id]['state'] = 'normal'
# clear the on_hover stuff
if self.__control_list[control_id]['on_hover_called']:
self.__control_list[control_id]['on_hover_called'] = False
self.__draw_tip = None
if self.__control_list[control_id]['timer_on_hover']:
self.__control_list[control_id]['timer_on_hover'] = 0
(Will select as the correct answer tomorrow when the 24hr timeout is over)
Ended up figuring it out. I was way over complicating it.
Go through the list of controls in reverse, the first one that the .collidepoint succeeded on, save that control and break the loop
Go through all of the controls and one that's not disabled and isn't currently normal, set to normal
Work with the control that has been saved as needed
def process_events(self):
button = pygame.mouse.get_pressed()
mouse_pos = pygame.mouse.get_pos()
top_id = -1
for control_id in reversed(self.__z_order):
if self.__control_list[control_id]['state'] != 'disabled' and \
self.__control_list[control_id]['draw'] and \
self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
top_id = control_id
break
if top_id != -1:
# go through all of the controls
for control_id in self.__z_order:
# skip the top most control and any that are disabled/deleted
if self.__control_list[control_id]['state'] != 'disabled' and \
self.__control_list[control_id]['state'] != 'normal' and \
control_id != top_id:
# set it to normal
self.__control_list[control_id]['state'] = 'normal'
# clear the on_hover stuff
if self.__control_list[control_id]['on_hover_called']:
self.__control_list[control_id]['on_hover_called'] = False
self.__draw_tip = None
if self.__control_list[control_id]['timer_on_hover']:
self.__control_list[control_id]['timer_on_hover'] = 0
else:
for control_id in self.__z_order:
if self.__control_list[control_id]['state'] != 'disabled':
# set it to normal
self.__control_list[control_id]['state'] = 'normal'
# clear the on_hover stuff
if self.__control_list[control_id]['on_hover_called']:
self.__control_list[control_id]['on_hover_called'] = False
self.__draw_tip = None
if self.__control_list[control_id]['timer_on_hover']:
self.__control_list[control_id]['timer_on_hover'] = 0
return
if button[0]:
# current state of this control is hot and left mouse is pressed on it
if self.__control_list[top_id]['state'] == 'hot':
self.__control_list[top_id]['state'] = 'pressed'
self.__control_list[top_id]['mouse_pos_lclick'] = None
self.__control_list[top_id]['mouse_pos_ldown'] = mouse_pos
if self.__control_list[top_id]['on_hover_called']:
self.__draw_tip = None
if (time.clock() - self.__control_list[top_id][
'dbl_timer'] >= self.__dbl_click_delay) and \
(time.clock() - self.__control_list[top_id]['timer'] <= self.__dbl_click_speed):
if self.__event_mode:
if self.__control_list[top_id]['on_dbl_lclick']:
self.__control_list[top_id]['on_dbl_lclick']()
else:
self.__messages.append(self.Message(top_id, PGC_LBUTTONDBLCLK))
# print('Double click', self.__control_list[top_id]['text'])
self.__control_list[top_id]['dbl_timer'] = time.clock()
self.__control_list[top_id]['timer'] = -1
elif not button[0]:
# state is currently pressed
if self.__control_list[top_id]['state'] == 'pressed':
# check if there's a timer initiated for this control
# this prevents 2 clicks + a double click message
if self.__control_list[top_id]['timer'] >= 0:
self.__control_list[top_id]['dbl_timer'] = -1
self.__control_list[top_id]['timer'] = time.clock()
# if the event mode
if self.__event_mode:
# call the function if there is one
if self.__control_list[top_id]['on_lclick']:
self.__control_list[top_id]['on_lclick']()
else:
# post the message to the messages queue
self.__messages.append(self.Message(top_id, PGC_LBUTTONUP))
# print('Click', self.__control_list[top_id]['text'])
# the timer is < 0 (should be -1), double click just happened
else:
# reset the timer to 0 so clicking can happen again
self.__control_list[top_id]['timer'] = 0
# go through all of the ids below this control
for x in self.__z_order[0:top_id - 1]:
# set all the hot controls to normal
if self.__control_list[x]['state'] == 'hot':
self.__control_list[x]['state'] = 'normal'
can_change = True
# go through all the controls on top of this control
for x in self.__z_order[top_id + 1:]:
# something else is on top of this and it's already hot, can't change this control
if self.__control_list[x]['state'] == 'hot':
can_change = False
break
if can_change:
self.__control_list[top_id]['state'] = 'hot'
self.__control_list[top_id]['mouse_pos_lclick'] = mouse_pos
self.__control_list[top_id]['mouse_pos_ldown'] = None
# state is not currently hot (but we're hovering over this control)
elif self.__control_list[top_id]['state'] != 'hot':
self.__control_list[top_id]['state'] = 'hot'
self.__control_list[top_id]['mouse_pos_hover'] = mouse_pos
# used to start a tooltip (needs work)
self.__control_list[top_id]['mouse_pos_rect'] = pygame.Rect(mouse_pos[0] - 7,
mouse_pos[1] - 7,
mouse_pos[0] + 7,
mouse_pos[1] + 7)
# state is currently 'hot'
else:
# timer for on_hover hasn't been initialized
if self.__control_list[top_id]['timer_on_hover'] == 0:
self.__control_list[top_id]['timer_on_hover'] = time.clock()
# mouse is in the area
if self.__control_list[top_id]['mouse_pos_rect'].collidepoint(mouse_pos):
# if the on_hover hasn't been triggered and there is a timer for the on_hover
if not self.__control_list[top_id]['on_hover_called'] and \
self.__control_list[top_id]['timer_on_hover']:
# if the mouse has been in the hover area for 1.5 seconds or more
if time.clock() - self.__control_list[top_id]['timer_on_hover'] >= 1.5:
# trigger the hover
self.__control_list[top_id]['on_hover_called'] = True
# on_hover is a function call, call the function
if self.__control_list[top_id]['on_hover']['type'] == 'function':
self.__control_list[top_id]['on_hover']['func'] \
(self.__control_list[top_id]['on_hover']['args'])
# on_hover is a tip, set the self.__draw_tip variable to the tip we need
else:
self.__draw_tip = self.__control_list[top_id]['on_hover'].copy()
self.__draw_tip['rect'].x = mouse_pos[0]
self.__draw_tip['rect'].y = mouse_pos[1]

Reusing a Tkinter window for a game of Tic Tac Toe

I've written a program (listed below) which plays Tic Tic Toe with a Tkinter GUI. If I invoke it like this:
root = tk.Tk()
root.title("Tic Tac Toe")
player1 = QPlayer(mark="X")
player2 = QPlayer(mark="O")
human_player = HumanPlayer(mark="X")
player2.epsilon = 0 # For playing the actual match, disable exploratory moves
game = Game(root, player1=human_player, player2=player2)
game.play()
root.mainloop()
it works as expected and the HumanPlayer can play against player2, which is a computer player (specifically, a QPlayer). The figure below shows how the HumanPlayer (with mark "X") easily wins.
In order to improve the performance of the QPlayer, I'd like to 'train' it by allowing it to play against an instance of itself before playing against the human player. I've tried modifying the above code as follows:
root = tk.Tk()
root.title("Tic Tac Toe")
player1 = QPlayer(mark="X")
player2 = QPlayer(mark="O")
for _ in range(1): # Play a couple of training games
training_game = Game(root, player1, player2)
training_game.play()
training_game.reset()
human_player = HumanPlayer(mark="X")
player2.epsilon = 0 # For playing the actual match, disable exploratory moves
game = Game(root, player1=human_player, player2=player2)
game.play()
root.mainloop()
What I then find, however, is that the Tkinter window contains two Tic Tac Toe boards (depicted below), and the buttons of the second board are unresponsive.
In the above code, the reset() method is the same one as used in the callback of the "Reset" button, which usually makes the board blank again to start over. I don't understand why I'm seeing two boards (of which one is unresponsive) instead of a single, responsive board?
For reference, the full code of the Tic Tac Toe program is listed below (with the 'offensive' lines of code commented out):
import numpy as np
import Tkinter as tk
import copy
class Game:
def __init__(self, master, player1, player2, Q_learn=None, Q={}, alpha=0.3, gamma=0.9):
frame = tk.Frame()
frame.grid()
self.master = master
self.player1 = player1
self.player2 = player2
self.current_player = player1
self.other_player = player2
self.empty_text = ""
self.board = Board()
self.buttons = [[None for _ in range(3)] for _ in range(3)]
for i in range(3):
for j in range(3):
self.buttons[i][j] = tk.Button(frame, height=3, width=3, text=self.empty_text, command=lambda i=i, j=j: self.callback(self.buttons[i][j]))
self.buttons[i][j].grid(row=i, column=j)
self.reset_button = tk.Button(text="Reset", command=self.reset)
self.reset_button.grid(row=3)
self.Q_learn = Q_learn
self.Q_learn_or_not()
if self.Q_learn:
self.Q = Q
self.alpha = alpha # Learning rate
self.gamma = gamma # Discount rate
self.share_Q_with_players()
def Q_learn_or_not(self): # If either player is a QPlayer, turn on Q-learning
if self.Q_learn is None:
if isinstance(self.player1, QPlayer) or isinstance(self.player2, QPlayer):
self.Q_learn = True
def share_Q_with_players(self): # The action value table Q is shared with the QPlayers to help them make their move decisions
if isinstance(self.player1, QPlayer):
self.player1.Q = self.Q
if isinstance(self.player2, QPlayer):
self.player2.Q = self.Q
def callback(self, button):
if self.board.over():
pass # Do nothing if the game is already over
else:
if isinstance(self.current_player, HumanPlayer) and isinstance(self.other_player, HumanPlayer):
if self.empty(button):
move = self.get_move(button)
self.handle_move(move)
elif isinstance(self.current_player, HumanPlayer) and isinstance(self.other_player, ComputerPlayer):
computer_player = self.other_player
if self.empty(button):
human_move = self.get_move(button)
self.handle_move(human_move)
if not self.board.over(): # Trigger the computer's next move
computer_move = computer_player.get_move(self.board)
self.handle_move(computer_move)
def empty(self, button):
return button["text"] == self.empty_text
def get_move(self, button):
info = button.grid_info()
move = (info["row"], info["column"]) # Get move coordinates from the button's metadata
return move
def handle_move(self, move):
try:
if self.Q_learn:
self.learn_Q(move)
i, j = move # Get row and column number of the corresponding button
self.buttons[i][j].configure(text=self.current_player.mark) # Change the label on the button to the current player's mark
self.board.place_mark(move, self.current_player.mark) # Update the board
if self.board.over():
self.declare_outcome()
else:
self.switch_players()
except:
print "There was an error handling the move."
pass # This might occur if no moves are available and the game is already over
def declare_outcome(self):
if self.board.winner() is None:
print "Cat's game."
else:
print "The game is over. The player with mark %s won!" % self.current_player.mark
def reset(self):
print "Resetting..."
for i in range(3):
for j in range(3):
self.buttons[i][j].configure(text=self.empty_text)
self.board = Board(grid=np.ones((3,3))*np.nan)
self.current_player = self.player1
self.other_player = self.player2
# np.random.seed(seed=0) # Set the random seed to zero to see the Q-learning 'in action' or for debugging purposes
self.play()
def switch_players(self):
if self.current_player == self.player1:
self.current_player = self.player2
self.other_player = self.player1
else:
self.current_player = self.player1
self.other_player = self.player2
def play(self):
if isinstance(self.player1, HumanPlayer) and isinstance(self.player2, HumanPlayer):
pass # For human vs. human, play relies on the callback from button presses
elif isinstance(self.player1, HumanPlayer) and isinstance(self.player2, ComputerPlayer):
pass
elif isinstance(self.player1, ComputerPlayer) and isinstance(self.player2, HumanPlayer):
first_computer_move = player1.get_move(self.board) # If player 1 is a computer, it needs to be triggered to make the first move.
self.handle_move(first_computer_move)
elif isinstance(self.player1, ComputerPlayer) and isinstance(self.player2, ComputerPlayer):
while not self.board.over(): # Make the two computer players play against each other without button presses
move = self.current_player.get_move(self.board)
self.handle_move(move)
def learn_Q(self, move): # If Q-learning is toggled on, "learn_Q" should be called after receiving a move from an instance of Player and before implementing the move (using Board's "place_mark" method)
state_key = QPlayer.make_and_maybe_add_key(self.board, self.current_player.mark, self.Q)
next_board = self.board.get_next_board(move, self.current_player.mark)
reward = next_board.give_reward()
next_state_key = QPlayer.make_and_maybe_add_key(next_board, self.other_player.mark, self.Q)
if next_board.over():
expected = reward
else:
next_Qs = self.Q[next_state_key] # The Q values represent the expected future reward for player X for each available move in the next state (after the move has been made)
if self.current_player.mark == "X":
expected = reward + (self.gamma * min(next_Qs.values())) # If the current player is X, the next player is O, and the move with the minimum Q value should be chosen according to our "sign convention"
elif self.current_player.mark == "O":
expected = reward + (self.gamma * max(next_Qs.values())) # If the current player is O, the next player is X, and the move with the maximum Q vlue should be chosen
change = self.alpha * (expected - self.Q[state_key][move])
self.Q[state_key][move] += change
class Board:
def __init__(self, grid=np.ones((3,3))*np.nan):
self.grid = grid
def winner(self):
rows = [self.grid[i,:] for i in range(3)]
cols = [self.grid[:,j] for j in range(3)]
diag = [np.array([self.grid[i,i] for i in range(3)])]
cross_diag = [np.array([self.grid[2-i,i] for i in range(3)])]
lanes = np.concatenate((rows, cols, diag, cross_diag)) # A "lane" is defined as a row, column, diagonal, or cross-diagonal
any_lane = lambda x: any([np.array_equal(lane, x) for lane in lanes]) # Returns true if any lane is equal to the input argument "x"
if any_lane(np.ones(3)):
return "X"
elif any_lane(np.zeros(3)):
return "O"
def over(self): # The game is over if there is a winner or if no squares remain empty (cat's game)
return (not np.any(np.isnan(self.grid))) or (self.winner() is not None)
def place_mark(self, move, mark): # Place a mark on the board
num = Board.mark2num(mark)
self.grid[tuple(move)] = num
#staticmethod
def mark2num(mark): # Convert's a player's mark to a number to be inserted in the Numpy array representing the board. The mark must be either "X" or "O".
d = {"X": 1, "O": 0}
return d[mark]
def available_moves(self):
return [(i,j) for i in range(3) for j in range(3) if np.isnan(self.grid[i][j])]
def get_next_board(self, move, mark):
next_board = copy.deepcopy(self)
next_board.place_mark(move, mark)
return next_board
def make_key(self, mark): # For Q-learning, returns a 10-character string representing the state of the board and the player whose turn it is
fill_value = 9
filled_grid = copy.deepcopy(self.grid)
np.place(filled_grid, np.isnan(filled_grid), fill_value)
return "".join(map(str, (map(int, filled_grid.flatten())))) + mark
def give_reward(self): # Assign a reward for the player with mark X in the current board position.
if self.over():
if self.winner() is not None:
if self.winner() == "X":
return 1.0 # Player X won -> positive reward
elif self.winner() == "O":
return -1.0 # Player O won -> negative reward
else:
return 0.5 # A smaller positive reward for cat's game
else:
return 0.0 # No reward if the game is not yet finished
class Player(object):
def __init__(self, mark):
self.mark = mark
self.get_opponent_mark()
def get_opponent_mark(self):
if self.mark == 'X':
self.opponent_mark = 'O'
elif self.mark == 'O':
self.opponent_mark = 'X'
else:
print "The player's mark must be either 'X' or 'O'."
class HumanPlayer(Player):
def __init__(self, mark):
super(HumanPlayer, self).__init__(mark=mark)
class ComputerPlayer(Player):
def __init__(self, mark):
super(ComputerPlayer, self).__init__(mark=mark)
class RandomPlayer(ComputerPlayer):
def __init__(self, mark):
super(RandomPlayer, self).__init__(mark=mark)
#staticmethod
def get_move(board):
moves = board.available_moves()
if moves: # If "moves" is not an empty list (as it would be if cat's game were reached)
return moves[np.random.choice(len(moves))] # Apply random selection to the index, as otherwise it will be seen as a 2D array
class THandPlayer(ComputerPlayer):
def __init__(self, mark):
super(THandPlayer, self).__init__(mark=mark)
def get_move(self, board):
moves = board.available_moves()
if moves:
for move in moves:
if THandPlayer.next_move_winner(board, move, self.mark):
return move
elif THandPlayer.next_move_winner(board, move, self.opponent_mark):
return move
else:
return RandomPlayer.get_move(board)
#staticmethod
def next_move_winner(board, move, mark):
return board.get_next_board(move, mark).winner() == mark
class QPlayer(ComputerPlayer):
def __init__(self, mark, Q={}, epsilon=0.2):
super(QPlayer, self).__init__(mark=mark)
self.Q = Q
self.epsilon = epsilon
def get_move(self, board):
if np.random.uniform() < self.epsilon: # With probability epsilon, choose a move at random ("epsilon-greedy" exploration)
return RandomPlayer.get_move(board)
else:
state_key = QPlayer.make_and_maybe_add_key(board, self.mark, self.Q)
Qs = self.Q[state_key]
if self.mark == "X":
return QPlayer.stochastic_argminmax(Qs, max)
elif self.mark == "O":
return QPlayer.stochastic_argminmax(Qs, min)
#staticmethod
def make_and_maybe_add_key(board, mark, Q): # Make a dictionary key for the current state (board + player turn) and if Q does not yet have it, add it to Q
state_key = board.make_key(mark)
if Q.get(state_key) is None:
moves = board.available_moves()
Q[state_key] = {move: 0.0 for move in moves} # The available moves in each state are initially given a default value of zero
return state_key
#staticmethod
def stochastic_argminmax(Qs, min_or_max): # Determines either the argmin or argmax of the array Qs such that if there are 'ties', one is chosen at random
min_or_maxQ = min_or_max(Qs.values())
if Qs.values().count(min_or_maxQ) > 1: # If there is more than one move corresponding to the maximum Q-value, choose one at random
best_options = [move for move in Qs.keys() if Qs[move] == min_or_maxQ]
move = best_options[np.random.choice(len(best_options))]
else:
move = min_or_max(Qs, key=Qs.get)
return move
root = tk.Tk()
root.title("Tic Tac Toe")
player1 = QPlayer(mark="X")
player2 = QPlayer(mark="O")
# for _ in range(1): # Play a couple of training games
# training_game = Game(root, player1, player2)
# training_game.play()
# training_game.reset()
human_player = HumanPlayer(mark="X")
player2.epsilon = 0 # For playing the actual match, disable exploratory moves
game = Game(root, player1=human_player, player2=player2)
game.play()
root.mainloop()
It looks like you only need to create the board one time as the reset method resets it for the new players. Each type you create a Game instance, you create a new Tk frame so you either need to destroy the old one or you can reuse the windows by not creating a new Game instance each time.
A minor change to the main code at the bottom of the file seems to fix this:
player1 = QPlayer(mark="X")
player2 = QPlayer(mark="O")
game = Game(root, player1, player2)
for _ in range(1): # Play a couple of training games
game.play()
game.reset()
human_player = HumanPlayer(mark="X")
player2.epsilon = 0 # For playing the actual match, disable exploratory moves
game.player1 = human_player
game.player2 = player2
game.play()
I've noticed in this code that if you were to use it in python 3.2.3 or similar editions all of the print statements would need to be enclosed by brackets, and you'd need to add tkinter in the program by importing it.

How to call a function that is bound to key press only once

I am making a game and what I am trying to do is when moving the character around check if the character location is close to any object location, at which point i want to trigger my dialogue window to pop up. The problem I am having is that since the "checking if character location is close to object location" is binded to my key_press, it runs this a thousand times and calls the function multiple times when holding the key down. I am unable to come up with a solution to this. What I tried to do was add a counter outside the function and then add to it when the function is called and say if the counter is < 2 then proceed. Also, I'm not even sure if the actual code to check if the widget locations are near each other is correct.
The code in question is all the way at the bottom 'for i in listofwidgets':
class MoveableImage(Image):
def __init__(self, **kwargs):
super(MoveableImage, self).__init__(**kwargs)
self._keyboard = Window.request_keyboard(None, self)
if not self._keyboard:
return
self._keyboard.bind(on_key_down=self.on_keyboard_down)
self._keyboard.bind(on_key_up=self.on_keyboard_up)
self.y = (Window.height/2.1)
self.app = App.get_running_app()
def on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == 'left':
self.source = 'selectionscreen/left.zip'
if self.x < (Window.width * .25):
if any(self.collide_widget(i) for i in listofwidgets):
bglayout.x +=0
else:
bglayout.x += 4
else:
if any(self.collide_widget(i) for i in listofwidgets):
self.x -=0
else:
self.x -=6
for i in listofwidget:
if i.x - self.x < 10: #if image x location and self x location are within 100 pixels
calldialoguebox()
counter=0
def calldialoguebox(*args):
counter+=1
if counter < 2:
Clock.schedule_interval(typenewmessage.example, .5) #types message in dialogue box word by word every interval
else:
pass

Categories