7

I'm making an app in Tkinter using multiple Text widgets, and I'm working on undo/redo functionality, which is triggered by a KeyPress event. (I'm not using the text widget's built-in undo stack because I have non-tkinter objects which can also be undone/redone and I want to keep everything in one stack)

Before I began, I read tutorials and documentation (e.g. http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm) which gave me the impression event.char would be empty or None if the key pressed was a 'special' key rather than an alphanumeric/punctuation key.

This is not actually the case. Pressing Backspace on my keyboard (I'm using a Macbook Air running El Capitan) returns an event.char of \x7f.

Reading more about Tcl/TK, I also learned the list of X keysyms are not consistent from platform to platform, and that e.g. on my Mac, the Shift keys apparently have a keysym code of 0. This means my application interpreted unprintable characters like Delete or the Shift key as printable characters, which is messing with my implementation.

I'll handle the common unprintable-key KeyPress events separately anyway, but I'm still concerned about unexpected keyboard event behaviour for two reasons: 1) my program is intended to be multi-platform, and 2) there is a high chance this program will be used by people using non-US keyboards.

My current solution, using this as a reference, is to check event.keysym_num to see if it is within the range of keysym numbers/keycodes used by printable characters.

Here is example code:

from tkinter import *
from tkinter import ttk

root = Tk()

textbox = Text(root, width=60, height=3)
textbox.grid(sticky=(N, S, E, W))

def KeyboardEvent(event):
    if event.keysym_num > 0 and event.keysym_num < 60000:
        print('This is a printable key. The character is: %r keysym: %r' % \
            (event.char, event.keysym_num))
    else:
        print('This key is unprintable. The character is: %r keysym: %r' % \
            (event.char, event.keysym_num))
textbox.bind('<KeyPress>', KeyboardEvent)
root.mainloop() 

My questions:

  1. Will my current method work, or are there non-printable keys that will still fall through?
  2. Is there a better way to tell non-printable key events apart from printable key events in Tkinter/Python? (I considered using unicodecategory or curses.ascii.isprint() but this check occurs every time the user presses a key so these seemed like overkill)
  3. Are there any more gotchas with KeyPress events, particularly inconsistent behaviour between Windows/Mac/Linux, that I should be aware of? (note: I'm already aware of issues with whitespace characters such as tabs and carriage returns)

I'm using Python 3 (installed with Homebrew) on a Mac. Thanks!

Gbgbgb
  • 73
  • 1
  • 5
  • 1
    what about `if len(repr(event.char)) == 3:` ? anything that is converted to an alt-code will have a longer representation and any non-character will be length 2 `''` – Tadhg McDonald-Jensen Jun 17 '16 at 18:30
  • 2
    Is important that you do this with key bindings, or would it work just as well if you could get a callback whenever something was inserted into or deleted from the widget? Undo is tricky if you are trying to account for different languages and keyboards. – Bryan Oakley Jun 17 '16 at 18:36
  • @TadhgMcDonald-Jensen I considered that but wasn't sure if it'd work. (I've used Python a fair bit but haven't been formally trained so wasn't too sure how repr worked) – Gbgbgb Jun 18 '16 at 05:55
  • @BryanOakley I got that impression and I can probably do it without key bindings, but this is my first major Tkinter app, a proof of concept and I'm working on it alone, so I went path-of-least-resistance for now. If it gets popular enough to have non-Latin language users, I'll hopefully have the experience (and time and money :D) by then to improve this or redevelop the whole thing as e.g. a web app. Thanks! – Gbgbgb Jun 18 '16 at 06:06
  • I don't understand your reply. I can give a solution that gives you a callback whenever anything is inserted or deleted; are you interested in that sort of solution? I don't know if that would be useful to your or not. – Bryan Oakley Jun 18 '16 at 11:25
  • @BryanOakley Please post your solution, even if I don't use it now it may be helpful later/to others, thank you – Gbgbgb Jun 20 '16 at 11:49

1 Answers1

3

Question 1 and 2 :

I believe your method works, but it seems simpler to just use the str.isprintable method, it returns a boolean indicating whether or not the argument string isprintable:

>>> "aéûβß\\".isprintable() #Works with various accents, special characters
True
>>> "\x16".isprintable()
False
>>> "\n".isprintable() #Excludes all other characters
False

the method using len(repr(event.char) == 3 could work, but it would for instance, also exclude \, which has a repr of 4 chars ('\\').

Question 3 :

Like you said, there are a bunch of gotcha (eg: tab's event char is "\t", returns is "\r"... I don't know where/if you can find an exhaustive list of these particularities, the only solution I ever had was try out the most common (eg pretty much every key and combinason Ctrl+key on my keyboard, using a program of course:

chars = dict()
def LogKey(event, chars = chars): #dict is mutable
    global char #get the actual value of char
    dict[char] = event.char #

root = Tk()
root.bind("<KeyPress>", LogKey)

for character in "abcde...123456...²&é\"'(-è_...)": #and so on
    char = character
    root.event_generate("<KeyPress-{}>".format(character))
    char = "<Control-{}>".format(character)
    root.event_generate(char)
    char = "<Control-Shift-{}>".format(character)
    ...
#Inspect the dictonary chars in the end

It's probably not the best method, but it should cover most cases, you can also expande to test multiple keys (eg Control-c-v)...

dlesbre
  • 357
  • 3
  • 9