Как сделать подсветку синтаксиса в tkinter

Я подозреваю что этот вопрос очень сложный (ну или невозможный). Но
я хотел бы узнать можно ли сделать подсветку синтаксиса в текстовом виджете ?


Ответы (2 шт):

Автор решения: insolor

Нужно делать лексический разбор (парсинг) текста - разбивать текст на подсвечиваемые куски (токены), в зависимости от типа токена по-разному выделять куски текста. Для простоты можно взять готовый парсер, например, из библиотеки pygments (ставится с помощью pip install pygments). Также можно использовать модуль tokenize из стандартной библиотеки python, если предполагается делать подсветку только для Python.

Ниже собранный на коленке пример с подсветкой ключевых слов и текстовых литералов (работает неоптимально, при каждом изменении удаляет всю подсветку, заново разбирает текст на токены, заново подсвечивает - в идеале нужно обрабатывать только видимые строки текста):

import tkinter as tk
from pygments.lexers import PythonLexer
from pygments.token import Token

lexer = PythonLexer()

root = tk.Tk()

text = tk.Text(root)
text.pack()

# Создаем теги с разными свойствами, которые будем присваивать соответствующим типам токенов
text.tag_config("keyword", foreground='blue')
text.tag_config("string_literal", foreground='red')


def get_text_coord(s: str, i: int):
    """
    Из индекса символа получить "координату" в виде "номер_строки_текста.номер_символа_в_строке"
    """
    for row_number, line in enumerate(s.splitlines(keepends=True), 1):
        if i < len(line):
            return f'{row_number}.{i}'
        
        i -= len(line)


# Прописываем соответствие типа токена тегу подсветки
token_type_to_tag = {
    Token.Keyword: "keyword",
    Token.Operator.Word: "keyword",
    Token.Name.Builtin: "keyword",
    Token.Literal.String.Single: "string_literal",
    Token.Literal.String.Double: "string_literal",
}


def on_edit(event):
    # Удалить все имеющиеся теги из текста
    for tag in text.tag_names():
        text.tag_remove(tag, 1.0, tk.END)
    
    # Разобрать текст на токены
    s = text.get(1.0, tk.END)
    tokens = lexer.get_tokens_unprocessed(s)
    
    for i, token_type, token in tokens:
        print(i, token_type, repr(token))  # Отладочный вывод - тут видно какие типы токенов выдаются
        j = i + len(token)
        if token_type in token_type_to_tag:
            text.tag_add(token_type_to_tag[token_type], get_text_coord(s, i), get_text_coord(s, j))

    # Сбросить флаг редактирования текста
    text.edit_modified(0)


text.bind('<<Modified>>', on_edit)


root.mainloop()

Скриншот

→ Ссылка
Автор решения: Nikson Twixson

Это наверно не лучший способ, но рабочий. Цвета конечно подобрал не очень.

import tkinter as tk


root = tk.Tk()

text = tk.Text(root)
text.pack()

# Тэги
text.tag_config("loops", foreground='#FF051D')
text.tag_config("imports", foreground='green')
text.tag_config("if_else", foreground='#8B00FF')
text.tag_config("brackets", foreground='#1F75FE')
text.tag_config("operators",foreground="#0095B6")
text.tag_config("basic", foreground='black')
text.tag_config("string", foreground='#CC5500')
text.tag_config("int", foreground='#6600FF')
text.tag_config("functions", foreground='#A9AD00')

# Словари с ключевыми словами
imports = ["import","from","as"]
loops = ["for","while","break","continue","range"]
if_else = ["if","elif","else","True","False"]
brackets = ["(",")","[","]","{","}"]
operators = ["in","=","==",">=","<=","!=","+=","-=",
"*=","/=","%=","//=","**=","|=","^=",">>=","<<=","&=","def"]
functions = ["print","input","int","str","bool","float"]

def checking():
    

    def replace_char(string,chars,separators):  
        for i in chars:
            string = string.replace(i,f"{separators}{i}{separators}")

        return string

    for i in text.tag_names(): # Очищаем тэги
        text.tag_remove(i,1.0,tk.END)
   
    a = text.get(1.0,tk.END)
    chars = [" ","\n","(",")","[","]","{","}",":",","]
    a = replace_char(a,chars,"⠀")
    a = a.split("⠀") 
    ''' Далее разбиваем строку по невидимым символам 
    (но этот вариант с невидимкой или другим любым символом не очень, 
    ведь его могут ввести в качестве строки в коде) и  ещё текст ткинтера его не различает'''

    text.delete(1.0,tk.END) # Очищаем текстовое поле
    
    # Проводим цикл по всем элементам списка
    for i in a:
        

        if i in loops: # Проверка по категориям 
            text.insert(tk.END,i,'loops')

        elif i in imports:   
            text.insert(tk.END,i,'imports') 

        elif i in if_else:   
            text.insert(tk.END,i,'if_else') 

        elif i in brackets:   
            text.insert(tk.END,i,'brackets')

        elif i in operators:   
            text.insert(tk.END,i,'operators') 

        elif i.startswith("'") and i.endswith("'") or i.startswith('"') and i.endswith('"'):
            text.insert(tk.END,i,'string') 

        elif i.isdigit():
            text.insert(tk.END,i,'int') 

        elif i in functions:
            text.insert(tk.END,i,'functions') 

        else:
            text.insert(tk.END,i,"basic")

    
    '''На самом деле этот способ неособо классный для больших текстов, 
    скорее просто для ознакомления, 
    для серьёзных проектов как было сказано выше, лучше tkcode'''


    


but = tk.Button(root,text="Проверка",command=checking).pack()


root.mainloop()

введите сюда описание изображения

→ Ссылка