Capture keys with TKinter with this scenario

Viewed 472

I would either like to capture all key strokes, or associate a key stroke to a button. At this time, there is no user input in this game other than the user clicking buttons. I would like to assign a single keyboard letter to each button. I was also playing with pynput, but since the program is already using TKinter, seems like I should be able to accomplish it with its features.

I can either have an on_press method in the main Game class that then calls the appropriate function for each key (same as user click the key), or perhaps there is a better way.

Most of the examples I've seen deal with the object created from tkinter class, but in this case, it's removed from my main program several levels.

This is a game I got from GitHub, and adapting to my preferences. So I'm trying to change it as little as possibly structurally.

In Graphics.py, I see this code:

class GraphWin(tk.Canvas):

    def __init__(self, title="Graphics Window", width=200, height=200):
        master = tk.Toplevel(_root)
        master.protocol("WM_DELETE_WINDOW", self.close)
        tk.Canvas.__init__(self, master, width=width, height=height)
        self.master.title(title)
        self.pack()
        master.resizable(0,0)
        self.foreground = "black"
        self.items = []
        self.mouseX = None
        self.mouseY = None
        self.bind("<Button-1>", self._onClick)  #original code
        self.height = height
        self.width = width
        self._mouseCallback = None
        self.trans = None

    def _onClick(self, e):
        self.mouseX = e.x
        self.mouseY = e.y
        if self._mouseCallback:
            self._mouseCallback(Point(e.x, e.y)) 

The main program is basically something like this:

def main():
    # first number is width, second is height
    screenWidth = 800
    screenHeight = 500
    mainWindow = GraphWin("Game", screenWidth, screenHeight)
    game = game(mainWindow)
    mainWindow.bind('h', game.on_press())  #<---- I added this 
    #listener = keyboard.Listener(on_press=game.on_press, on_release=game.on_release)
    #listener.start()
    game.go()
    #listener.join()
    mainWindow.close()

if __name__ == '__main__':
    main()

I added a test function in the Game class, and it currently is not firing.

def on_press(self):
    #print("key=" + str(key))
    print( "on_press")
    #if key == keyboard.KeyCode(char='h'):
    #    self.hit()

Buttons are setup like this:

def __init__( self, win ): 
    # First set up screen
    self.win = win
    win.setBackground("dark green")
    xmin = 0.0
    xmax = 160.0
    ymax = 220.0
    win.setCoords( 0.0, 0.0, xmax, ymax )
    self.engine = MouseTrap( win )

then later...

self.play_button = Button( win, Point(bs*8.5,by), bw, bh, 'Play')
self.play_button.setRun( self.play )
self.engine.registerButton( self.play_button )

And finally, the Button code is in guiengine.py

class Button:

    """A button is a labeled rectangle in a window.
    It is activated or deactivated with the activate()
    and deactivate() methods. The clicked(p) method
    returns true if the button is active and p is inside it."""

    def __init__(self, win, center, width, height, label):
        """ Creates a rectangular button, eg:
        qb = Button(myWin, Point(30,25), 20, 10, 'Quit') """ 

        self.runDef = False
        self.setUp( win, center, width, height, label )

    def setUp(self, win, center, width, height, label):
        """ set most of the Button data - not in init to make easier
        for child class methods inheriting from Button.
        If called from child class with own run(), set self.runDef""" 

        w,h = width/2.0, height/2.0
        x,y = center.getX(), center.getY()
        self.xmax, self.xmin = x+w, x-w
        self.ymax, self.ymin = y+h, y-h
        p1 = Point(self.xmin, self.ymin)
        p2 = Point(self.xmax, self.ymax)
        self.rect = Rectangle(p1,p2)
        self.rect.setFill('lightgray')
        self.rect.draw(win)
        self.label = Text(center, label)
        self.label.draw(win)
        self.deactivate()

    def clicked(self, p):
        "Returns true if button active and p is inside"
        return self.active and \
               self.xmin <= p.getX() <= self.xmax and \
               self.ymin <= p.getY() <= self.ymax

    def getLabel(self):
        "Returns the label string of this button."
        return self.label.getText()

    def activate(self):
        "Sets this button to 'active'."
        self.label.setFill('black')
        self.rect.setWidth(2)
        self.active = True

    def deactivate(self):
        "Sets this button to 'inactive'."
        self.label.setFill('darkgrey')
        self.rect.setWidth(1)
        self.active = False

    def setRun( self, function ):
        "set a function to be the mouse click event handler"
        self.runDef = True
        self.runfunction = function

    def run( self ):
       """The default event handler.  It either runs the handler function
       set in setRun() or it raises an exception."""
       if self.runDef:
           return self.runfunction()
       else:
           #Neal change for Python3
           #raise RuntimeError, 'Button run() method not defined'
           raise RuntimeError ('Button run() method not defined')
           return False  # exit program on error

Extra code requested:

class Rectangle(_BBox):

    def __init__(self, p1, p2):
        _BBox.__init__(self, p1, p2)

    def _draw(self, canvas, options):
        p1 = self.p1
        p2 = self.p2
        x1,y1 = canvas.toScreen(p1.x,p1.y)
        x2,y2 = canvas.toScreen(p2.x,p2.y)
        return canvas.create_rectangle(x1,y1,x2,y2,options)

    def clone(self):
        other = Rectangle(self.p1, self.p2)
        other.config = self.config
        return other

class Point(GraphicsObject):
    def __init__(self, x, y):
        GraphicsObject.__init__(self, ["outline", "fill"])
        self.setFill = self.setOutline
        self.x = x
        self.y = y

    def _draw(self, canvas, options):
        x,y = canvas.toScreen(self.x,self.y)
        return canvas.create_rectangle(x,y,x+1,y+1,options)

    def _move(self, dx, dy):
        self.x = self.x + dx
        self.y = self.y + dy

    def clone(self):
        other = Point(self.x,self.y)
        other.config = self.config
        return other

    def getX(self): return self.x
    def getY(self): return self.y

Update

Some of the notes I put in the comments: It is using this: http://mcsp.wartburg.edu/zelle/python/graphics.py John Zelle's graphic.py.

http://mcsp.wartburg.edu/zelle/python/graphics/graphics.pdf - See class _BBox(GraphicsObject): for common methods.

I see class GraphWin has an anykey - where it captures the keys. But how would I get that back in my main program, especially as an event that would fire as soon as the user typed it?

Do I need to write my own listener - see also python graphics win.getKey() function?…. That post has a while loop waiting on the keys. I'm not sure where I would put in such a while loop, and how that would trigger into the "Game" class to fire an event. Do I need to write my own listener?

2 Answers

The reason the on_press() command not firing is due to the fact that the .bind() call is bound to an instance of Canvas. This means the canvas widget must have the focus for the keypress to register.

Use bind_all instead of bind.

Alternatives to fixing this:

  1. Using mainWindow.bind_all("h", hit) - to bind the letter h directly to the "hit" button handler function (just make sure the hit function has the signature as follows: def hit(self, event='')

  2. Using mainWindow.bind_all("h", game.on_press) - binds the keypress to the whole application

  3. Using root.bind("h", game.on_press) - binds the keypress to the root window (maybe a toplevel is more accurate here depending on if there are multiple windows)

Related to catching any key, there are some examples over here about doing that, using the "<Key>" event specifier: https://tkinterexamples.com/events/keyboard

whenever you want to combine your mouse and keyboard input with widgets I strongly suggest you to use the built-in .bind() method. .bind() can have two values:

  1. Type of input
  2. Name of callback function

An example:

entry_name.bind("<Return>", function_name_here)

This will call the function_name_here() function whenever the Return or the Enter key on your keyboard is pressed. This method is available for almost all tk widgets. You can also state key combinations as well as mouse controls.(Ctrl-K, Left-click etc.). If you want to bind multiple key strokes to a certain callback function, then just bind the both with same callback function like this:

entry_name.bind("<Return>", function_name_here)
entry_name.bind("<MouseButton-1>", function_name_here)

This will allow you to call the function when either of Return or Left-click of the mouse are pressed.

Related