Работа с текстом и курсором / tkinter 8

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Изменение иконки курсора

Tkinter позволяет менять внешний вид иконки курсора при наведении на виджет. Это поведение иногда включается по умолчанию, как, например, в случае с виджетом Entry, который показывает курсор текстового выделения.

Следующее приложение демонстрирует, как показывать курсор загрузки, а также курсор со знаком вопроса, который часто используется во вспомогательных меню.

Иконку можно поменять с помощью параметра cursor. В этом примере используется значение watch для демонстрации нативной иконки загрузки, а также question_arrow — для обычной стрелки со знаком вопроса:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо иконки курсора")
self.resizable(0, 0)
self.label = tk.Label(self, text="Нажмите для старта")
self.btn_launch = tk.Button(self, text="Старт !",
command=self.perform_action)
self.btn_help = tk.Button(self, text="Помощь",
cursor="question_arrow")

btn_opts = {"side": tk.LEFT, "expand": True, "fill": tk.X,
"ipadx": 30, "padx": 20, "pady": 5}
self.label.pack(pady=10)
self.btn_launch.pack(**btn_opts)
self.btn_help.pack(**btn_opts)

def perform_action(self):
self.btn_launch.config(state=tk.DISABLED)
self.btn_help.config(state=tk.DISABLED)
self.label.config(text="Запуск...")
self.after(3000, self.end_action)
self.config(cursor="watch")

def end_action(self):
self.btn_launch.config(state=tk.NORMAL)
self.btn_help.config(state=tk.NORMAL)
self.label.config(text="Готово!")
self.config(cursor="arrow")

def set_watch_cursor(self, widget):
widget._old_cursor = widget.cget("cursor")
widget.config(cursor="watch")
for w in widget.winfo_children():
self.set_watch_cursor(w)

def restore_cursor(self, widget):
widget.config(cursor=widget.old_cursor)
for w in widget.winfo_children():
self.restore_cursor(w)

if __name__ == "__main__":
app = App()
app.mainloop()

Полный список валидных значений для cursor( включая те, которые характерны для определенной ОС) можно посмотреть в официальной документации Tcl/TK на сайте https://www.tcl.tk/man/tcl/TkCmd/cursors.htm.

Как работает изменение курсора

Если виджет не определяет параметр cursor, он берет значение из родительского контейнера. Таким образом можно запросто задать нужную иконку для всех виджетов, определив значение на уровне root. Это делается с помощью вызова set_watch_cursor() внутри метода perform_action():


def perform_action(self):
self.config(cursor="watch")
# ...

Исключением здесь является кнопка Помощь, которая явно задает значение question_arrow для курсора. Это же можно сделать при создании экземпляра виджета:


self.btn_help = tk.Button(self, text="Помощь",
cursor="question_arrow")

Стоит также обратить внимание на то, что если нажать на кнопку Старт и разместить курсор над кнопкой Помощь до вызова запланированного метода, то значение будет help, а не watch. Это происходит из-за того что, если параметр cursor виджета задан, то у него более высокая приоритетность по сравнению со значением в родительском контейнере.

Чтобы избежать этого, можно сохранить текущее значение cursor и поменять его на watch, вернув позже. Функцию, которая будет выполнять эту операцию, можно вызывать рекурсивно в дочернем виджете, перебирая список winfo_children():


def perform_action(self):
self.set_watch_cursor(self)
# ...

def end_action(self):
self.restore_cursor(self)
# ...

def set_watch_cursor(self, widget):
widget._old_cursor = widget.cget("cursor")
widget.config(cursor="watch")
for w in widget.winfo_children():
self.set_watch_cursor(w)

def restore_cursor(self, widget):
widget.config(cursor=widget.old_cursor)
for w in widget.winfo_children():
self.restore_cursor(w)

В этом коде свойство _old_cursor было добавлено каждому виджету. При использовании такого же подхода важно помнить, что нельзя вызывать restore_cursor() до set_watch_cursor().

Виджет Text

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

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

Виджет Text

Помимо виджета Text это приложение содержит три кнопки, которые вызывают методы для очистки всего содержимого, вставки строки «Hello, world!» в месте, где сейчас находится курсор, и вывода выделения, сделанного с помощью мыши или клавиатуры:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо виджета Text")
self.resizable(0, 0)
self.text = tk.Text(self, width=50, height=10)
self.btn_clear = tk.Button(self, text="Очистить",
command=self.clear_text)
self.btn_insert = tk.Button(self, text="Вставить",
command=self.insert_text)
self.btn_print = tk.Button(self, text="Печать",
command=self.print_selection)
self.text.pack()
self.btn_clear.pack(side=tk.LEFT, expand=True, pady=10)
self.btn_insert.pack(side=tk.LEFT, expand=True, pady=10)
self.btn_print.pack(side=tk.LEFT, expand=True, pady=10)

def clear_text(self):
self.text.delete("1.0", tk.END)

def insert_text(self):
self.text.insert(tk.INSERT, "Hello, world")

def print_selection(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
content = self.text.get(*selection)
print(content)

if __name__ == "__main__":
app = App()
app.mainloop()

Как работает текстовый виджет

Изначально виджет Text пустой и имеет ширину на 50 символов, а высоту на 10 строк. Помимо добавления возможности для пользователей вводить любой текст, разберем также методы каждой кнопки, чтобы понимать, как взаимодействовать с виджетом.

Метод delete(start, end) удаляет содержимое с индексами от start до end. Если второй параметр не указан, то удаляются только символы с индексом start.

В этом примере будем удалять весь текст от индекса 1.0 (нулевая колонка первой строчки) до tk.END, которая ссылается на последний символ:


def clear_text(self):
self.text.delete("1.0", tk.END)

Метод insert(index, text) вставляет выбранный текст в положении index. Вызываем его с помощью индекса INDEX, который соответствует позиции курсора:


def insert_text(self):
self.text.insert(tk.INSERT, "Hello, world")

Метод tag_ranges(tag) возвращает кортеж с первым и последним индексами всех диапазонов конкретного tag. Здесь был использован тег tk.SEL, который указывает на текущую позицию. Если ничего не было выбрано, то вызов вернет пустой кортеж. Этот метод объединен с get(start, end), который возвращает текст в заданном диапазоне:

Поскольку тег SEL соответствует лишь одному диапазону, его можно с легкостью извлечь с помощью метода get.

Добавление HTML тегов в виджет Text

В этом разделе разберем, как настраивать поведение последовательности символов с проставленными тегами в виджете Text.

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

Для демонстрации принципов работы с тегами, создадим виджет Text, который симулирует добавление гиперссылок. При нажатии по таким в браузере по умолчанию будет открываться выбранный URL.

Например, если пользователь введет следующий текст, то python.org можно отметить тегом как гиперссылку:

Добавление HTML тегов

Определим тег «link», который представляет собой кликабельную гиперссылку. Этот тег будет добавляться к текущему выбранному тексту с помощью кнопки, а клик мышью запустит событие для открытия ссылки в браузере:


import tkinter as tk
import webbrowser

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо HTML тегов")
self.text = tk.Text(self, width=50, height=10)
self.btn_link = tk.Button(self, text="Добавить ссылку",
command=self.add_hyperlink)

self.text.tag_config("link", foreground="blue", underline=1)
self.text.tag_bind("link", "", self.open_link)
self.text.tag_bind("link", "",
lambda _: self.text.config(cursor="hand2"))
self.text.tag_bind("link", "",
lambda e: self.text.config(cursor=""))

self.text.pack()
self.btn_link.pack(expand=True)

def add_hyperlink(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.text.tag_add("link", *selection)

def open_link(self, event):
position = "@{},{} + 1c".format(event.x, event.y)
index = self.text.index(position)
prevrange = self.text.tag_prevrange("link", index)
url = self.text.get(*prevrange)
webbrowser.open(url)

if __name__ == "__main__":
app = App()
app.mainloop()

Как работает добавление тега

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


def __init__(self):
super().__init__()
self.title("Демо HTML тегов")
self.text = tk.Text(self, width=50, height=10)
self.btn_link = tk.Button(self, text="Добавить ссылку",
command=self.add_hyperlink)

self.text.tag_config("link", foreground="blue", underline=1)
self.text.tag_bind("link", "", self.open_link)
self.text.tag_bind("link", "",
lambda _: self.text.config(cursor="hand2"))
self.text.tag_bind("link", "",
lambda e: self.text.config(cursor=""))

Внутри метода open_link поменяем положение клика на соответствующую строку и колонку с помощью метода index класса Text:


position = "@{},{} + 1c".format(event.x, event.y)
index = self.text.index(position)
prevrange = self.text.tag_prevrange("link", index)

Стоит обратить внимание на то, что положение, соответствующее индексу, по которому был совершен клик, — «@x,y», но оно сдвинут на один символ. Это сделано из-за того, что tag_prevrange возвращает предшествующий диапазон конкретного индекса. В таком случае он бы не возвращал текущий диапазон при клике по первому символу.

Наконец, получаем текст из диапазона и открываем его с помощью браузера по умолчанию, используя для этого функцию open из модуля webbrowser:


url = self.text.get(*prevrange)
webbrowser.open(url)

Поскольку функция webbrowser.open не проверяет, является ли URL валидным, то приложение можно улучшить, добавив базовую валидацию гиперссылки. Например, можно использовать функцию urlparse, чтобы убедиться, что у ссылки есть сетевое положение:

Хотя этот подход не является идеальным, он подойдет на первых этапах, чтобы отбрасывать невалидные ссылки.

В целом, можно использовать теги для создания сложных программ, основанных на тексте: например, IDE с подсветкой синтаксиса. На самом деле, IDLE, которая идет в составе Python, основана на Tkinter.

Обучение с трудоустройством

Профессия Python-разработчик / Skillbox

Профессия Python-разработчик / Skillbox

7 500 3 750 ₽/мес.
Факультет Python-разработки / GeekBrains

Факультет Python-разработки / GeekBrains

4 990 ₽/мес.
Факультет Аналитики Big Data / GeekBrains

Факультет Аналитики Big Data / GeekBrains

7 490 ₽/мес.
Профессия Data Scientist / Skillbox

Профессия Data Scientist / Skillbox

8 167 4 083 ₽/мес.

Тест на знание python

Какой будет результат выполнения этого кода?
Что выведет этот код?
Что выведет этот код?
Что выведет этот код?
Что выведет этот код?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.