1

I'm working on a little project and made a little on-screen keyboard as a tkinter Toplevel

my application is buildt like this:

  • Root-Window (Tk-Widget)
    • input 1 (Entry-Widget)
    • input 2 (Entry-Widget)
    • input 3 (Text-Widget)
    • on_screen-keyboard (Toplevel-Widget)

the Toplevel-Widget contains Buttons, with callbacks that should enter text in the entries (just like keyboard-Buttons)

What I want is a communication between children of the keyboard/the keyboard and the last active input-Widget. My Problem is, that I don't know, how to say the keyboard, to which input-Widget it should send the message.

import tkinter as tk
from tkinter import ttk

class MainWindow(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.active_input = tk.Variable(value=None)
        ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
        self.text = tk.StringVar(value="")
        self.input1 = ttk.Entry(self)
        self.input1.bind("<FocusIn>", lambda e: self.active_input.set(self.input1))
        self.input2 = ttk.Entry(self)
        self.input2.bind("<FocusIn>", lambda e: self.active_input.set(self.input2))
        self.input3 = tk.Text(self, height=3, width=15)
        self.input3.bind("<FocusIn>", lambda e: self.active_input.set(self.input3))
        self.input1.pack()
        self.input3.pack()
        self.input2.pack()

class Keyboard(tk.Toplevel):

    OPENED = False
    NAME = "- Keyboard -"
    NUM = [{"text":"1", "width":1},
           {"text":"2", "width":1},
           {"text":"3", "width":2}]
    CHAR= [{"text":"A", "width":1},
           {"text":"B", "width":1},
           {"text":"C", "width":2}]

    def __init__(self, master):
        if not Keyboard.OPENED:
            Keyboard.OPENED = True
            print("keyboard opened!")
            self.master = master
            tk.Toplevel.__init__(self, master)
            self.title(self.NAME)
            self.protocol("WM_DELETE_WINDOW", self.close)

            self.keyb_nb = ttk.Notebook(self)
            self.keyb_nb.pack()

            self.num_tab = ttk.Frame(self.keyb_nb)
            self.createPad(self.num_tab, Keyboard.NUM,2)
            self.keyb_nb.add(self.num_tab, text="123")

            self.char_tab = ttk.Frame(self.keyb_nb)
            self.createPad(self.char_tab, Keyboard.CHAR, 2)
            self.keyb_nb.add(self.char_tab, text="ABC")

    def createPad(self, master, pad:list, max_col):
        self.co_count = 0
        self.ro = 1
        for button in pad:
            button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=self.bclicked(button))
            if self.co_count >= max_col:
                self.ro = self.ro + 1
                self.co_count = 0
            button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
            self.co_count = self.co_count+button["width"]

    def bclicked(self, button:dict):
        """
        reciver = self.master.active_input #I think the Problem here is, that  the variable contains a string, not a widget
        reciever.focus_force()
        reciever.insert(index=tk.INSERT, string=button["text"])
        """
        pass

    def close(self):
        Keyboard.OPENED = False
        self.destroy()
        print("keyboard closed!")


root = MainWindow()
root.mainloop()

Here the init of the Mainwindow and the bclicked of the Keyboard class are important...

the code is debug-ready

I would prefer a solution, similar to the communication in the internet (sender=button, receiver-id, message), but very welcome every working solution

btw: I'm also looking for a solution, how I don't have to force the input to focus and the Toplevel stays an the highest layer of the screen (that if I focus the Tk-Widget/one of the inputs/the button, the keyboard will stay in front of it)

SUMMARY: how do I find out, which of the 3 input-widgets was active at last, when the keyboard-toplevel has already the focus?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
silvapuer
  • 33
  • 4
  • Does this answer your question? [How to call and close a virtual keyboard made by Tkinter using touchscreen display](https://stackoverflow.com/questions/60136473/how-to-call-and-close-a-virtual-keyboard-made-by-tkinter-using-touchscreen-displ) – stovfl Apr 07 '20 at 21:44
  • You should use `lambda` in `.bind(...)`. – acw1668 Apr 08 '20 at 04:06
  • im sorry, but this doesn't answer my question – silvapuer Apr 08 '20 at 13:49
  • and yes, usually i use lambda, i just forgott it while coding this example, because my code is more complex than the above – silvapuer Apr 08 '20 at 13:51

2 Answers2

1

I may made more changes than needed, but mainly focus on the method keyboard_triger() and pass_key_to_master(), this two use the idea that the variable master implements, having access to call methods out of scope. Olso the method set_focused_object() stores a reference to the last object beeng focused, note than it stores the widget and not the event, it's easyer than searching each time the object

import tkinter as tk
from tkinter import ttk

class MainWindow(tk.Tk):

    def keyboard_triger(self, key):
        # to identify wath object is just use
        # isinstance(self.active_input, ttk.Entry)
        self.active_input.insert(tk.END, key)

    def new_keyboard(self):
        Keyboard(self)

    def set_focused_object(self, event):
        self.active_input = event.widget

    def __init__(self):
        tk.Tk.__init__(self)
        self.active_input = None

        ttk.Button(self, text="Show Keyboard", command=self.new_keyboard).pack()

        self.input1 = ttk.Entry(self)
        self.input1.bind("<FocusIn>", self.set_focused_object)
        self.input1.pack()

        self.input2 = ttk.Entry(self)
        self.input2.bind("<FocusIn>", self.set_focused_object)
        self.input2.pack()

        self.input3 = tk.Text(self, height=3, width=15)
        self.input3.bind("<FocusIn>", self.set_focused_object)
        self.input3.pack()

class Keyboard(tk.Toplevel):

    def pass_key_to_master(self, key):
        self.master.keyboard_triger(key)

    def __init__(self, master):
        tk.Toplevel.__init__(self, master)

        self.master = master
        self.title('Keyboard')

        # this way of agruping keys stores the kwags
        # of the drawing method
        keys = {
            'A': {'x': 0, 'y': 0},
            'B': {'x': 20, 'y': 20},
            'C': {'x': 50, 'y': 50}
        }

        # expected structure
        # {string key: reference to the button}
        self.buttons = {}
        for i in keys:
            self.buttons[i] = tk.Button(  # i=i is required to make a instance
                self, text=i, command=lambda i=i: self.pass_key_to_master(i)
            )
            self.buttons[i].place(**keys[i])


if __name__ == '__main__':
    root = MainWindow()
    root.mainloop()

SrPanda
  • 854
  • 1
  • 5
  • 9
0

Your code maybe could have a better construction.(But I didn't revise your code construction.)

Followed by your code,I use a global variable.And fix some errors in your code.And it could work normally in my computer.

import tkinter as tk
from tkinter import ttk

class MainWindow(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.active_input = tk.Variable(value=None)
        ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
        global focusedWidget
        focusedWidget = None
        self.text = tk.StringVar(value="")
        self.input1 = ttk.Entry(self)
        self.input1.bind("<FocusIn>", self.getFocusWidget)
        self.input2 = ttk.Entry(self)
        self.input2.bind("<FocusIn>", self.getFocusWidget)
        self.input3 = tk.Text(self, height=3, width=15)
        self.input3.bind("<FocusIn>", self.getFocusWidget)
        self.input1.pack()
        self.input3.pack()
        self.input2.pack()

    def getFocusWidget(self,event): # this function could be a static function
        global focusedWidget
        focusedWidget = event.widget

class Keyboard(tk.Toplevel):

    OPENED = False
    NAME = "- Keyboard -"
    NUM = [{"text":"1", "width":1},
           {"text":"2", "width":1},
           {"text":"3", "width":2}]
    CHAR= [{"text":"A", "width":1},
           {"text":"B", "width":1},
           {"text":"C", "width":2}]

    def __init__(self, master):
        if not Keyboard.OPENED:
            Keyboard.OPENED = True
            print("keyboard opened!")
            self.master = master
            tk.Toplevel.__init__(self, master)
            self.title(self.NAME)
            self.protocol("WM_DELETE_WINDOW", self.close)

            self.keyb_nb = ttk.Notebook(self)
            self.keyb_nb.pack()

            self.num_tab = ttk.Frame(self.keyb_nb)
            self.createPad(self.num_tab, Keyboard.NUM,2)
            self.keyb_nb.add(self.num_tab, text="123")

            self.char_tab = ttk.Frame(self.keyb_nb)
            self.createPad(self.char_tab, Keyboard.CHAR, 2)
            self.keyb_nb.add(self.char_tab, text="ABC")

    def createPad(self, master, pad:list, max_col):
        self.co_count = 0
        self.ro = 1
        for button in pad:
            button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=lambda button=button:self.bclicked(button)) # this lambda expression has some errors.
            if self.co_count >= max_col:
                self.ro = self.ro + 1
                self.co_count = 0
            button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
            self.co_count = self.co_count+button["width"]

    def bclicked(self, button:dict):
        global focusedWidget

        """
        reciver = self.master.active_input #I think the Problem here is, that  the variable contains a string, not a widget
        reciever.focus_force()
        reciever.insert(index=tk.INSERT, string=button["text"])
        """
        if not focusedWidget: # If user hasn't click a entry or text widget.
            print("Please select a entry or text")
            return
        if focusedWidget.widgetName=='ttk::entry': # use if statement to check the type of selected entry.
            focusedWidget.insert(index=tk.INSERT,string=button["text"])
        else:
            focusedWidget.insert("end",button["text"])


    def close(self):
        Keyboard.OPENED = False
        self.destroy()
        print("keyboard closed!")


root = MainWindow()
root.mainloop()
jizhihaoSAMA
  • 12,336
  • 9
  • 27
  • 49
  • thank you. I found one more error: to use partial() instead of lambda in the button command will work better. therefore you have to import partial from functools – silvapuer Apr 08 '20 at 15:00
  • @silvapuer Yes,That's your error in your post code.Using lambda is okay.You can read it in [official document](https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result) – jizhihaoSAMA Apr 08 '20 at 15:04
  • thats realy great! Thank you – silvapuer Apr 08 '20 at 15:29
  • do u think, it's possible to solve this without a global variable? – silvapuer Apr 08 '20 at 15:30
  • @silvapuer Yes,alex's answer didn't use global variable. – jizhihaoSAMA Apr 08 '20 at 15:55
  • @silvapuer Sum up,to make small revision and avoid global variable,you can def `self.focusedWidget` variable in class `Mainwindow`.And call `bclicked` use `self.master.focusedWidget` in class `Keyboard`. – jizhihaoSAMA Apr 08 '20 at 16:13