Вопрос или проблема
ПРОБЛЕМА:
Используя Python 2.7 и Tkinter. У меня есть прокручиваемый Text
виджет в одном Frame
и прокручиваемый Canvas
в другом Frame
.
Я включил выбор текста в Text
виджете, но не набор текста. Text
виджет прокручивается нормально, если текст выбран (Text
виджет имеет фокус) или если фокус на других виджетах.
Проблема возникает, когда текст выбран, Text
виджет имеет focus
, и перехватывает <MouseWheel>
события, применяя Event
к Text
виджету в дополнение к Canvas
виджету, который должен прокручиваться. Поведение перехвата Event
активными виджетами является характерным для Python 2.
(Обратите внимание, что в следующих фрагментах кода отсутствует стандартный код Tkinter, например, размещение виджетов, так как это не относится к вопросу.)
<– СНИП –>
import Tkinter as tk
class MouseWheelConf:
# Обратите внимание, что передача виджета и ползунка в эти процедуры
# позволяет использовать этот удобный класс для обоих виджетов
def _on_mouse_wheel(self, event, widget, scrollbar):
#### Прокрутка предоставленного виджета и ползунка с помощью события Mousewheel ####
def _bind_mouse_wheel(self, event, widget, scrollbar):
# Привязка прокрутки к событию Mousewheel:
self.bind_all('<MouseWheel>',
lambda e: self._on_mouse_wheel(e, widget, scrollbar))
def _unbind_mouse_wheel(self):
# Отвязка прокрутки от события Mousewheel:
self.unbind_all('<MouseWheel>')
class ScrollableText(MouseWheelConf):
def __init__(self, master=None, text_conf={}, frame_conf={}):
# Инициализация прокручиваемого Text фрейма:
tk.Frame.__init__(self, master, **frame_conf)
self.text = tk.Text(self, **text_conf)
# Настройка прокрутки:
self.scrollbar = tk.Scrollbar(
self, orient="vertical",
command=lambda *e: self.text.yview(*e))
self.scrollbar.pack(side="right", fill="y")
self.text.config(yscrollcommand=lambda *e: self.scrollbar.set(*e))
# Применение прокрутки при входе и удаление при выходе:
self.bind(
'<Enter>',
lambda e: self._bind_mouse_wheel(e, self.text, self.scrollbar))
self.bind('<Leave>', lambda e: self._unbind_mouse_wheel(e))
#### Разместите все виджеты здесь ####
class ScrollableCanvas(MouseWheelConf):
def __init__(self, master=None, canvas_conf={}, frame_conf={}, canvasframe_conf={}):
#### Здесь идет стандартный код прокручиваемого холста ####
<– СНИП –>
Примечание: Проблема отсутствует в Python 3, но этот код работает в Python 3.
РЕШЕНИЕ:
Текущий обходной путь заключается в том, чтобы добавить копию тега sel
(sel_copy
) для применения к выбранным участкам текста при <FocusOut>
Text
виджета, а затем, когда <<Leave>>
Text
виджета, применить фокус к родителю Text
виджета (Frame
) и немедленно применить тег sel_copy
к выбранному тексту (потеряя участки выбранного текста).
<– СНИП –>
class MouseWheelConf:
#### ПРЕДЫДУЩИЙ КОД ЗДЕСЬ ####
# Единственное изменение - метод отвязки:
def _unbind_mouse_wheel(self, event, widget):
#### ПРЕДЫДУЩИЙ КОД ЗДЕСЬ ####
# Смена фокуса на родителя Text при выходе:
if 'text' in widget._name:
# Если Text имеет фокус при выходе, сменить фокус на Frame (родитель)
# (это удаляет тег 'sel' из виджета Text!)
if self.focus_get() == widget:
widget.master.focus()
class ScrollableText(MouseWheelConf):
def __init__(self, master=None, text_conf={}, frame_conf={}):
#### ПРЕДЫДУЩИЙ КОД ЗДЕСЬ ####
# Изменение привязки Leave, чтобы включить виджет:
self.bind(
'<Leave>',
lambda e: self._unbind_mouse_wheel(e, self.text))
# Обходной путь:
self._create_sel_copy()
self.text.bind(
'<FocusOut>', lambda e: self._apply_sel_copy(self.text, e))
self.bind('<FocusOut>', lambda e: self._remove_sel_copy(self.text, e))
self.text.bind(
'<FocusIn>', lambda e: self._remove_sel_copy(self.text, e))
def _create_sel_copy(self):
#### Создание копии сел-тега с именем 'sel_copy' здесь ####
def _apply_sel_copy(self, widget, *event):
# Применение копии сел-тега, когда сел удален:
if widget.tag_ranges("sel"):
widget.tag_add("sel_copy", *widget.tag_ranges("sel"))
def _remove_sel_copy(self, widget, *event):
# Удаление копии сел-тега вручную:
widget.tag_remove("sel_copy", '1.0', 'end')
<– СНИП –>
ВОПРОС:
Есть ли лучший способ сделать это? Могу ли я перехватить Event
до того, как он достигнет фокусированного Text
виджета? Или эмуляция поведения, которое я хочу, это лучшее, что я могу сделать?
Ответ или решение
Для решения проблемы перехвата события прокрутки мыши (<MouseWheel>
) в требуемом контексте, возможно, существует более элегантное решение, нежели добавление копии тега sel
и обработка событий FocusOut
. Мы можем воспользоваться функцией перехвата событий в Tkinter, используя механизм обработки событий.
Подход к решению проблемы
Вместо изменения фокуса с Text
виджета на его родительский Frame
, что может вести к потере выделенного текста, вы можете перехватить события прокрутки, когда Text
виджет имеет фокус, и перенаправить их в нужное место.
Пример реализации
Вот более оптимизированный код, используя выделение событий <MouseWheel>
:
import Tkinter as tk
class MouseWheelConf:
def _on_mouse_wheel(self, event, widget):
# Прокрутка указанного виджета на основе события
widget.yview_scroll(-1 * (event.delta // 120), "units")
def _bind_mouse_wheel(self, event, text_widget, canvas_widget):
# Здесь мы проверяем фокус и перенаправляем событие
if self.focus_get() == text_widget:
# Перенаправляем событие на canvas
canvas_widget.event_generate(event.type, x=event.x, y=event.y)
else:
# Если другой виджет имеет фокус, обрабатываем как обычно
self._on_mouse_wheel(event, canvas_widget)
def _unbind_mouse_wheel(self):
self.unbind_all('<MouseWheel>')
class ScrollableText(MouseWheelConf):
def __init__(self, master=None, text_conf={}, frame_conf={}):
tk.Frame.__init__(self, master, **frame_conf)
self.text = tk.Text(self, **text_conf)
self.scrollbar = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
self.scrollbar.pack(side="right", fill="y")
self.text.config(yscrollcommand=self.scrollbar.set)
# Привязываем события
self.bind('<Enter>', lambda e: self._bind_mouse_wheel(e, self.text, master.canvas))
self.bind('<Leave>', lambda e: self._unbind_mouse_wheel())
class ScrollableCanvas(MouseWheelConf):
def __init__(self, master=None, canvas_conf={}, frame_conf={}, canvasframe_conf={}):
tk.Frame.__init__(self, master, **frame_conf)
self.canvas = tk.Canvas(self, **canvas_conf)
self.scrollbar = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.scrollbar.pack(side="right", fill="y")
self.canvas.config(yscrollcommand=self.scrollbar.set)
# Инициализация остальных компонентов canvas
# Пример создания основного приложения
if __name__ == '__main__':
root = tk.Tk()
main_frame = tk.Frame(root)
scrollable_text = ScrollableText(master=main_frame)
scrollable_canvas = ScrollableCanvas(master=main_frame)
scrollable_text.pack(side="left", fill="both", expand=True)
scrollable_canvas.pack(side="right", fill="both", expand=True)
main_frame.pack(fill="both", expand=True)
root.mainloop()
Описание решения
В этом решении мы используем агрегирование событий для проверки того, какой виджет имеет фокус. Если это Text
, событие <MouseWheel>
перенаправляется на Canvas
. Таким образом вы избегаете громоздкой манипуляции с выделением текста и его тэгами.
Заключение
Данный подход позволяет более дружелюбно и эффективно обрабатывать события прокрутки, не теряя выделение текста и избегая избыточных манипуляций с фокусом. Вы сможете легко адаптировать это решение под свои нужды, а также обеспечивать лучший пользовательский интерфейс в вашем приложении на Tkinter в Python 2.7.