tkinter: странный порядок операций в связанных событиях, как изменить? - PullRequest
0 голосов
/ 09 июня 2019

Это относится к Управление порядком обратных вызовов с помощью Tkinter , но это не совсем то же самое

У меня есть поле ввода (tk.Entry) и поле флажка (tk.Checkbutton). Я привязываюсь к FocusOut Entry, и флажок получает команду = для запуска, когда он отмечен. Они оба уволены, но ...

Во многих ситуациях пользователь вводит текст, и, зная, что я использую FocusOut как способ зафиксировать изменение текста, он не удосуживается нажать клавишу Return. Он просто заходит и нажимает на соответствующий флажок, будучи уверенным в том, что, конечно, изменение текста будет зафиксировано до изменения состояния флажка. И это имеет значение; как текстовое изменение, так и состояние флажка отправляются на сервер по мере их появления, который будет определять, является ли состояние флажка законным, на основе текста.

Проблема в том, «как они происходят». По какой-то причине tkinter решает сначала отключить изменение галочки, и только потом приступает к запуску FocusOut в текстовом поле. Я нахожусь на Ubuntu, но я (и мои пользователи) привыкли к приложениям Windows SDK, где это не произойдет. В результате сервер видит установленный флажок, когда текст (насколько ему было сказано) находится в плохом состоянии, принимаются пути ошибок, происходят злые вещи, пользователи скрежетают зубами, мне угрожают счетами стоматолога и т. Д.

Это не имеет смысла для меня. Флажок должен получить фокус для обработки клика; чтобы получить фокус, виджет Entry должен был отказаться от него. Но Ткинтер явно не согласен.

Я попытался установить фокус на галочку в обработчике команд галочки, но явно слишком поздно. В этот момент tkinter не остановится и не вызовет FocusOut. В теории я мог бы заставить обработчик команды checkmark дойти до текстового поля и сначала отправить этот контент, но по сложным причинам код галочки даже не знает, что текстовое поле существует, и изменить его было бы довольно сложно. Вероятно, я мог бы иметь очередь команд галочки через 50 мс после операции, чтобы отправить состояние галочки, но тогда я бегу против пользователей быстрыми пальцами.

На мой взгляд, это ошибка tkinter - кнопка-флажок получает фокус при нажатии, обрабатывает пробел как способ переключения, как и все остальное, что может фокусироваться. Мне кажется, что tkinter кратко утверждает, что фокус принадлежит двум виджетам, сообщая об этом в неправильном порядке. Но если предположить, что tkinter не изменится, и при определенных знаниях о том, что пользователи не собираются останавливаться и нажимать Return после редактирования каждого текстового поля, есть ли какой-нибудь умный обходной путь?

По запросу, код. Извините за длину, но я хотел сохранить все привязки на случай, если они имеют значение. Следуйте инструкциям на экране, и результат отобразится в строке заголовка (и на стандартном выводе).

"""See line 130 to enable the hackAround
Python 3.6.7 linux mint
"""

import tkinter as tk
from tkinter import ttk
from tkinter import font

myFont = None
zerowidth = 10
tkinterHandle = None
firstThingsFirst = False

#infrastructure, ignore
class About:
    def __init__(self, isA, info):
        self.typeIs = isA

#infrastructure, ignore
class Cmd:
    def __init__(self, name, about, text):
        self.about = about
        self.name = name
        self.text = text

#holds a widget, base class of specific widget types
class WidgetHolder: 
    def __init__(self, name, about):
        self.widgit = None
        self.name = name
        self.about = about
        self.parentW = self.about.widgit 

    def canTakeFocus(self):
        try: #apparently not everything has a 'state'.
            if self.widgit['state'] == "disabled":
                return False
        except: #sigh
            pass
        return isinstance(self.widgit, tk.Entry) or isinstance(self.widgit, ttk.Button)\
              or isinstance(self.widgit, ttk.Combobox) or isinstance(self.widgit, tk.Checkbutton)\
              or isinstance(self.widgit, ScrolledText)

    def set(self, t):
        self.apply(t)

    def doEnable(self, b):
        pass

    def setErrorColor(self):
        pass

#Holds an Entry
class SingleLineInputOnAPage(WidgetHolder):
    def __init__(self, c):
        WidgetHolder.__init__(self, c.name, c.about)
        self.txt = tk.StringVar()
        vcmd = (self.parentW.register(self.onValidate), '%P')

        #key, not mouse or something?
        self.widgit = tk.Entry(self.parentW,   validate="key",
                          validatecommand=vcmd, textvariable=self.txt, font=myFont, width=0)
        self.savedText = c.text
        self.apply(c.text)
        self.widgit.bind("<Return>", (lambda event: self.timeToSend()))
        self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.doNot()))
        self.widgit.bind("<Tab>", (lambda event: self.doNot()))
        self.widgit.bind("<Button>", (lambda event: self.click(event)))
        self.widgit.bind("<FocusIn>", (lambda event: self.save()))
        self.widgit.bind("<FocusOut>", (lambda event: self.timeToSendMaybe()))
        self.widgit.bind("<Escape>", (lambda event: self.gotEsc()))
        self.widgit.bind("<Control-s>", (lambda event: self.timeToSend()))
        self.widgit.bind("<Control-a>", (lambda event: self.selectAll()))

    def gotEsc(self):
        pass
    def doNot(self):
        print("Reproduing the problem does not involve tab!")

    def save(self):
        self.savedText = self.txt.get()

    def timeToSendMaybe(self):
        self.timeToSend()

    def click(self, event):
        pass

    def setErrorColor(self):
        self.widgit.config(background="#ff5000")

    def selectAll(self):
        self.widgit.select_range(0, 'end')
        return 'break'

    def timeToSend(self):
        self.savedText = self.txt.get()
        global firstThingsFirst
        firstThingsFirst = True
        print("Ideally this happens first. Entry text would be sent now:", self.savedText)
        return 'break'

    def apply(self, t):
        self.savedText = t
        self.txt.set(t)

    def onValidate(self, P):
      return True

#Holds a checkbox
class CheckboxOnAPage(WidgetHolder):
    def __init__(self, c, cmd):
        WidgetHolder.__init__(self, c.name, c.about)
        if cmd == None:
            cmd = self.fire
        self.value = tk.IntVar()
        self.widgit = tk.Checkbutton(self.parentW, text="", variable=self.value, command=self.fire)
        self.apply(c.text)
        #self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.timeToSendMaybeAndPrev()))
        #self.widgit.bind("<Tab>", (lambda event: self.timeToSendMaybeAndNext()))

    def apply(self, t):
        pass

    def fire(self):
        self.widgit.focus()
        v = "X"[0:self.value.get()]

        #to get things in a better order, set to True
        if False:
          global tkinterHandle #clumsy, is there a way to get  self.widgit from the tk instance? 
          tkinterHandle.after(30, lambda sendthis=v: self.push(sendthis))
        else:
          self.push(v)

    def push(self, v):
        print("Checkmark ", v, " would be sent now. Ideally this does not happen first")
        global firstThingsFirst
        if firstThingsFirst:
          tkinterHandle.title("In order")
        else:
          tkinterHandle.title("Out of order!")


class CharSheet(tk.Tk):
    def __init__(self, argThing, *args, **kwargs):
        global myFont
        global zerowidth
        global tkinterHandle 

        tk.Tk.__init__(self, *args, **kwargs)
        self.geometry("360x80")
        self.title("Out of order?")

        myFont = tk.font.Font(family="Courier", size=10)
        zerowidth=myFont.measure("0")
        tkinterHandle = self
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand = True)

        abt2 = About("C", "")
        abt2.widgit = container
        cmd2 = Cmd("b", abt2, "give me focus; then click checkbox")
        w2 = SingleLineInputOnAPage(cmd2)
        w2.widgit.pack()

        abt = About("C", "")
        abt.widgit = container
        cmd = Cmd("a", abt, "")
        w1 = CheckboxOnAPage(cmd, None)
        w1.widgit.pack()

CharSheet(None).mainloop()

1 Ответ

0 голосов
/ 12 июня 2019

На мой взгляд, это ошибка tkinter - при нажатии кнопка проверки получает фокус,

Нет, это не ошибка tkinter.Tkinter работает как задумано.Стандартная кнопка не получает фокус при нажатии.Однако кнопка ttk делает это.

Мне кажется, что tkinter кратко утверждает, что фокус принадлежит двум виджетам, сообщая об этом в неправильном порядке.

Это неверная оценка.Невозможно, чтобы фокус был одновременно на нескольких виджетах.

Проблема заключается в том, что вы явно устанавливаете фокус на кнопку-флажок, но не даете циклу событий tkinter шанс обработать<FocusOut> событие, прежде чем продолжить.Поэтому, даже если вы думаете, что основное внимание уделяется кнопке проверки, это не так.Фокус не может измениться, пока tkinter не сможет обработать запрос на изменение фокуса.

Быстрое, хакерское решение - вызвать update после изменения фокуса, что дает tkinter возможность обрабатыватьвсе ожидающие события.Однако вызов update может быть сложным, если есть ожидающие события, которые могут вызвать повторный вызов обработчика.Мое личное правило - никогда не вызывать update, если в этом нет особой необходимости.

Если ваша основная цель состоит в том, чтобы переключить фокус на кнопку-флажок (и, таким образом, принудительно обработать привязку виджета ввода <FocusOut> дощелкнув по кнопке), вы можете создать привязку к классу виджета (Checkbutton), который обрабатывается перед обработчиком кнопки.Это, или используйте кнопку ttk, которая автоматически меняет фокус.

Правильный минимальный воспроизводимый пример

Я не собираюсь показывать вам, как изменить ваш код, потому что он слишком сложен для обслуживаниякак хороший минимальный пример.Вместо этого вот программа, которая иллюстрирует проблему.

Начните с этого базового кода, который служит минимальным воспроизводимым примером для проблемы, которую вы наблюдаете.Обратите внимание, что когда вы нажимаете на запись и затем нажимаете на кнопку-флажок, порядок, который отображается в текстовом виджете, - это сначала щелчок кнопки-флажка, а затем изменение фокуса.

import tkinter as tk
from tkinter import ttk

class Example():
    def __init__(self):
        self.root = tk.Tk()
        self.entry = tk.Entry(self.root)
        self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
        self.text = tk.Text(self.root, height=8, width=80, background="bisque")
        self.vsb = tk.Scrollbar(self.root, command=self.text.yview)
        self.text.configure(yscrollcommand=self.vsb.set)

        self.entry.pack(side="top", fill="x")
        self.cb.pack(side="top", anchor="w")
        self.vsb.pack(side="right", fill="y")
        self.text.pack(side="bottom", fill="both", expand=True)

        self.entry.insert(0, "click here, then click on the checkbutton")

        self.entry.bind("<FocusOut>", self.handle_focus_out)
        self.entry.focus_set()

    def handle_focus_out(self, event):
        self.text.insert("end", "received entry <FocusOut>\n")
        self.text.see("end")

    def handle_checkbutton(self):
        self.cb.focus_set()
        self.text.insert("end", "received checkbutton command\n")
        self.text.see("end")

e = Example()
tk.mainloop()

Решение 1: вызовите обновление дозаставить tkinter обработать изменение фокуса

В этом решении добавьте вызов к self.root.update() сразу после изменения фокуса.Это даст tkinter возможность обработать изменение фокуса, прежде чем продолжить работу с остальной частью кода.

def handle_checkbutton(self):
    self.cb.focus_set()
    self.root.update()
    self.text.insert("end", "received checkbutton command\n")
    self.text.see("end")

Решение 2: используйте кнопку ttk checkbox

вместо вызова update,Вы можете использовать кнопку проверки ТТК.Он имеет встроенное поведение установки фокуса на кнопку при нажатии.

Сначала измените обработчик, чтобы удалить код, управляющий фокусом:

def handle_checkbutton(self):
    self.text.insert("end", "received checkbutton command\n")
    self.text.see("end")

Затем используйте ttk.Checkbutton вместо tk.Checkbutton:

self.cb = ttk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")

Преимущество здесь в том, что вам не нужно писать код для управления фокусом.

Решение 3: добавить обработку фокуса в класс tk Checkbutton

Третье решение - добавить привязку к классу checkbutton, чтобы имитировать поведение кражи фокуса кнопки ttk.Преимущество здесь в том, что вам нужно установить привязку только один раз, и она будет применяться ко всем кнопкам в вашем пользовательском интерфейсе.

Сначала добавьте следующий обработчик:

def set_cb_focus(self, event):
    self.text.insert("end", "setting focus via class binding\n")
    self.text.see("end")
    event.widget.focus_set()

Затем добавьтекласс привязки в __init__.Обратите внимание, что встроенное поведение кнопки проверки также выполняется с помощью привязок классов.Чтобы не удалять это поведение, нам нужно включить add=True в команду bind, которая добавляет новое поведение, а не заменяет встроенное поведение.

self.root.bind_class("Checkbutton", "<ButtonPress-1>", self.set_cb_focus)

Наконец, переключите код обратно на tk.Checkbutton вместо ttk.Checkbutton:

self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...