Использование Scintilla в wxPython | jenyay.net

Использование Scintilla в wxPython

Содержание

Введение и общие слова

Эта статья представляет собой не руководство или документацию по использованию редактора Scintilla (который в wxPython называется wx.stc.StyledTextCtrl), а скорее набор рецептов, которые должны помочь в освоении этого, прямо скажем, сложного, но мощного компонента. Поэтому не обязательно читать все подряд, но в некоторых примерах будут встречаться участки кода, взятые из предыдущих примеров. Эта статья будет постепенно пополняться новыми разделами, поэтому следите за обновлениями на сайте, например, с помощью <a href="http://jenyay.net/rss.xml">RSS</a>.

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

Изначально компонент Scintilla бал написан на языке C++ и его официальная страница - http://www.scintilla.org/, но компонент оказался настолько мощным, что его портировали во многие библиотеки, предназначенных для создания GUI, в том числе и в wxPython, на примере которого и будет описание в этой статье.

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

Во-первых, это ссылка на официальную документацию на оригинальный компонент Scintilla - http://scintilla.sourceforge.net/ScintillaDoc.html.

И, во-вторых, на документацию по классу wx.stc.StyledTextCtrl, который представляет собой оболочку над Scintilla для библиотеки wxPython - http://www.yellowbrain.com/stc/index.html. Эта справка немного старая, но в 90% случаев она будет полезна, тем более, что и написана она понятным, хоть и английским, языком.

Эта статья рассчитана на использование Scintilla именно в wxPython, поэтому в дальнейшем я буду использовать названия Scintilla, и wx.stc.StyledTextCtrl как синонимы.

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

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import wx
import wx.stc


class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)  


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)

        self.SetTitle("Scintilla")

        sizer = wx.BoxSizer(wx.VERTICAL)       

        self.editor = MyEditor(self)
        sizer.Add (self.editor, 1, flag=wx.EXPAND)

        self.SetSizer(sizer)
        self.Layout()


if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = MyFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()
 

В этом исходнике у нас создается два класса: MyEditor и MyFrame. MyEditor представляет собой просто класс, производный от wx.stc.StyledTextCtrl, а класс MyFrame - окно, в котором на все окно растянут MyEditor.

Все дальнейшие игры над Scintilla будут происходить внутри MyEditor, а код главного окна меняться не будет, поэтому его в будущем я приводить не буду.

Если вы запустите этот пример, то увидите вот такое окно, куда можно вводить текст:

В этом примере wx.stc.StyledTextCtrl ведет себя как обычный многострочный wx.TextCtrl, но постепенно мы будем подключать и использовать специфические для Scintilla возможности.

Немного про внутреннюю структуру Scintilla

Прежде чем начать использовать продвинутые возможности Scintilla, надо разобраться в том, как он хранит настройки стилей и введенный текст. Стиль - это такая штука, которая указывает компоненту как нужно отображать тот или иной символ. К каждому введенному символу "прицеплен" какой-нибудь стиль и поэтому мы можем сделать так, например, чтобы в одном слове каждый символ имел свой цвет, размер шрифта или фон. А хранит Scintilla стили очень даже интересным способом, который, правда, иногда доставляет некоторые хлопоты. Итак, у компонента есть внутренний буфер, представляющий собой массив байт. Каждый символ хранится парами байт - один байт, представляющий собой символ и байт стиля, байт символа. Вы можете подумать, что Scintilla работает только с 8-битными кодировками, но на самом деле это не так. По умолчанию для wx.stc.StyledTextCtrl в юникодной сборке используется кодировка UTF-8, то есть на один символ может приходиться как один, так и несколько байт. Я пока не очень понял как это устроено на нижнем уровне архитектуры, в смысле для каждого байта символа есть соответствующий байт стиля (тогда часть байтом стиля оказывается лишними), или все-таки байт стиля идет для нескольких байт символа. Здесь пишут, что символы представлены как двухбайтовые ячейки. Но официальная документация ничего не говорит об этих двух байтах. На практике это все не так важно, потому что в таком "сыром" виде пользоваться буфером не придется, для всех основных операций есть более высокоуровневые методы.

В будущем могут понадобиться дополнительные функции для определения длины строки в байтах, а не в символах и для пересчета положения каретки в символах в положение каретки в байтах. Для этого мы сделаем два метода (подсмотренных у WikidPad):

import codecs

import wx
import wx.stc


class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)

        # Создаем кодировщик один раз в конструкторе,
        # чтобы не создавать его при каждой необходимости
        self.encoder = codecs.getencoder("utf-8")

    def calcByteLen(self, text):
        """Посчитать длину строки в байтах, а не в символах"""
        return len(self.encoder(text)[0])

    def calcBytePos (self, text, pos):
        """Преобразовать позицию в символах в позицию в байтах"""
        return len(self.encoder (text[: pos] )[0] )

В конструкторе мы получим и сохраним для дальнейшего использования объект-функцию, которая будет работать с текстом в кодировке UTF-8. Затем эта функция будет использоваться в приведенных методах calcByteLen и calcBytePos.

Определение положения каретки

Давайте в продолжение разговора о длине строк сделаем небольшой пример, который показывает один неприятный эффект, который должен быть знаком тем, кто уже работал с кодировкой UTF-8. Следующий пример будет писать в заголовке окна положение каретки в тексте.

class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)      

        self.Bind (wx.stc.EVT_STC_UPDATEUI, self.onPosChange)


    def onPosChange (self, event):
        # Получим текущую позицию каретки в байтах
        pos = self.GetCurrentPos()

        self.GetParent().SetTitle (str (pos))

Сначала несколько слов о методах класса wx.stc.StyledTextCtrl, который мы здесь используем. Во-первых, мы подписались на событие EVT_STC_UPDATEUI, которое происходит каждый раз, когда Scintilla должен обновить свой вид, например, когда изменяются стили или изменяется текст, но, кроме того, это событие вызывается и тогда, когда перемещается каретка.

В качестве параметра event для всех обработчиков событий передается экземпляр класса wx.stc.StyledTextEvent. Это довольно специфический класс, в том смысле, что он имеет очень много методов, но многие из этих методов работают только при обработке определенных событий. Видно, на такую архитектуру разработчикам пришлось пойти, чтобы в качестве параметра event всегда передавался экземпляр одного и того же класса.

Обработчик события очень простой. Мы используем встроенный метод GetCurrentPos(), который возвращает позицию курсора. Но позиция эта считается в "символьных" байтах, то есть сколько байт для представления символов, исключая байты стиля, расположено слева от каретки.

В начале каретка стоит в начале строки (пустой):

Введем один заведомо однобайтовый символ:

А теперь введем какую-нибудь русскую букву:

Как видите, символ слева от каретки один, а значение, которое вернул метод GetCurrentPos() - два.

Вот более жизненный пример:

Чтобы получить положение каретки в символах немного изменим обработчик событий:

class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)      

        self.Bind (wx.stc.EVT_STC_UPDATEUI, self.onPosChange)

    def onPosChange (self, event):
        # Получим текущую позицию каретки в байтах
        pos = self.GetCurrentPos()

        text_left = self.GetTextRange (0, pos)

        self.GetParent().SetTitle (str (len (text_left) ) )

Здесь мы воспользовались методом GetTextRange(), который возвращает строку текста с заданной позиции (0) до второй заданной границы (в качестве этого параметра мы использовали позицию каретки).

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

Из последнего скриншота видно, что текст представляет собой одну длинную строку без разбивки на строки, включающую переводы строки, состоящие, судя по всему, из двух символов (\r\n):

Подсвечиваем парные скобки

Рассмотрим теперь долее сложный пример. Научим класс wx.stc.StyledTextCtrl выделять парные скобки синим цветом, а те из них, для которых пары не нашлось, - красным. А за одно и научимся работать со стилями. Вот как выглядит код:

import codecs

import wx
import wx.stc

class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)

        # Создаем кодировщик один раз в конструкторе,
        # чтобы не создавать его при каждой необходимости
        self.encoder = codecs.getencoder("utf-8")
        #print self.encoder

        # Стиль по умолчанию будет 14-ым шрифтом
        self.StyleSetSpec (wx.stc.STC_STYLE_DEFAULT, "size:%d" % 14)

        # Стиль для парных скобок - синий цвет текста
        self.StyleSetSpec (wx.stc.STC_STYLE_BRACELIGHT,  "fore:#0000FF")

        # Стиль для непарных скобок (ошибочных) - красный цвет текста
        self.StyleSetSpec (wx.stc.STC_STYLE_BRACEBAD,    "fore:#FF0000")

        # Подпишемся на обработчик, где будем проверять парность скобок
        self.Bind (wx.stc.EVT_STC_UPDATEUI, self.onCheckBrace)


    def calcByteLen(self, text):
        """Посчитать длину строки в байтах, а не в символах"""
        return len(self.encoder(text)[0])

    def calcBytePos (self, text, pos):
        """Преобразовать позицию в символах в позицию в байтах"""
        return len(self.encoder (text[: pos] )[0] )


    def onCheckBrace (self, event):
        # Получим текущую позицию каретки в байтах
        pos = self.GetCurrentPos()

        # Получим набранный текст и посчитаем его длину
        text = self.GetText()
        text_len = self.calcByteLen (text)

        # Получим текст, расположенные слева от каретки
        text_left = u""
        if pos> 0:
            text_left = self.GetTextRange (0, pos)

        # Получим текст, расположенный справа от каретки
        text_right = u""
        if pos < text_len:
            text_right = self.GetTextRange (pos, text_len)

        # Проверим есть ли слева скобка (круглая, квадратная или фигурная)
        if len(text_left) > 0 and text_left[-1] in "{}()[]":
            # Попытаемся найти парную скобку
            match = self.BraceMatch(pos - 1)

            # нашли парную скобку
            if match != wx.stc.STC_INVALID_POSITION:
                # Подсветим обе
                self.BraceHighlight(pos - 1, match)
                return
            else:
                # Иначе подсветим первую скобку как ошибочную
                self.BraceBadLight(pos - 1)
                return
        else:
            # В этой позиции не скобка, отключим подсветку
            self.BraceBadLight(wx.stc.STC_INVALID_POSITION)

        # Если не нашли скобку слева, проверим есть ли она справа
        if len(text_right) > 0 and text_right[0] in "{}()[]":
            match = self.BraceMatch(pos)

            # нашли парную скобку
            if match != wx.stc.STC_INVALID_POSITION:
                # Подсветим обе
                self.BraceHighlight(pos, match)
                return
            else:
                # Иначе подсветим первую скобку как ошибочную
                self.BraceBadLight(pos)
                return
        else:
            # В этой позиции не скобка, отключим подсветку
            self.BraceBadLight(wx.stc.STC_INVALID_POSITION)

А вот скриншот работы этого класса:

Итак, что же мы сделали? Во-первых, мы изменили три стиля:

  • wx.stc.STC_STYLE_DEFAULT - стиль по умолчанию для всего текста, если не был применен другой стиль.
  • wx.stc.STC_STYLE_BRACELIGHT - стиль для подсветки парных скобок.
  • wx.stc.STC_STYLE_BRACEBAD - стиль для подсветки скобок, для которых не нашлось пары.

Как вы уже поняли, стиль изменяется с помощью метода StyleSetSpec(). В качестве первого параметра метод принимает идентификатор стиля (его номер), а в качестве второго - строку, описывающую внешний вид символов, к которым будет применен этот стиль. Задание внешнего вида стиля напоминает задание параметров в CSS. Параметры должны разделяться запятой. В данном мы установили для стиля по умолчанию размер шрифта 14, для парных скобок синий цвет символов, а для непарных скобок - красный цвет символов. А в принципе, возможны следующие параметры стиля:

  • bold - Шрифт должен быть полужирным
  • italic - Шрифт должен быть курсивным
  • fore:#RRGGBB - задание цвета символов
  • back:#RRGGBB - задание цвета фона
  • face:название шрифта - задание начертания шрифта
  • size:размер шрифта - задание размера шрифта
  • eol - использовать заполнение цветом фона последнего символа в строке до конца строки
  • underline - шрифт должен быть с подчеркиванием.

Что интересно, метод StyleSetSpec() есть только в классе wx.stc.StyledTextCtrl, но не в оригинальном классе Scintilla, в котором для каждого параметра стиля предусмотрен свой метод. У wx.stc.StyledTextCtrl тоже есть аналогичные методы, например, StyleSetSize(), StyleSetBold() и т.п.

После установки стиля мы подписываемся на событие wx.stc.EVT_STC_UPDATEUI, которое нам уже знакомо по предыдущему примеру.

В обработчике этого события осуществляется поиск парных скобок слева и справа от каретки, а затем либо подсвечиваются парные скобки с помощью метода BraceHighlight(), либо символ скобки помечается как ошибочный (не парный) с помощью метода BraceBadLight(). BraceHighlight() принимает два параметра, которые обозначают положение двух парных скобок, а метод BraceBadLight() приниамет позицию одной не парной скобки. Поиск парных скобок тоже встроен в wx.stc.StyledTextCtrl, осуществляется он с помощью вызова метода BraceMatch(), который принимает позицию скобки (однйо из ( ) [ ] { } < > ), а возвращает позицию парной ей скобки или wx.stc.STC_INVALID_POSITION, если такой скобки не нашлось. Остальной алгоритм поиска скобок, думаю, понятен из комментариев.

Делаем автоматическое закрытие скобок

В продолжение темы скобок, двайте сделаем так, чтобы редактор сам закрывал только что набранные открывающиеся скобки, то есть, допустим, пользователь пишет sin(, а после открывающейся скобки "(" автоматически должна появиться закрывающаяся ")". Для большей убедительности научим редактор автоматически закрывать не только круглые "( )", но и квадратные "[ ]" и фигурные "{ }" скобки.

Код здесь будет довольно простой:

class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)

        # Сделаем по умолчанию 14-й шрифт
        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "size:%d" % 14)

        # Обработчик события "Добавление символа"
        self.Bind (wx.stc.EVT_STC_CHARADDED, self.onCharAdded)


    def onCharAdded (self, event):
        # Получим код нажатой клавиши
        key_val = event.GetKey()

        # Нас не интересуют нажатые клавиши с кодом больше 127
        if key_val > 127:
            return

        # Получим символ нажатой клавиши
        key = chr (key_val)

        # Варианты открывающихся скобок
        open = "{(["

        # Пары закрывающихся скобок к открывающимся
        close = "})]"

        keyindex = open.find (key)

        if keyindex != -1:
            pos = self.GetCurrentPos()
            text = self.GetText()

            self.AddText(close[keyindex] )

            # Установим каретку перед закрывающейся скобкой
            self.GotoPos (pos)

В конструкторе класса мы подписываемся на событие wx.stc.EVT_STC_CHARADDED, которое срабатывает, когда в редактор вводится новый символ.

В обработчике этого события мы можем узнать код введенного символа с помощью метода GetKey() класса wx.stc.StyledTextEvent. Для простоты обработки мы преобразуем код этого символа в строку с помощью встроенной функции chr() (предварительно убедившить, что код введенного символа не больше 127).

В остальном обработчик должен быть понятен. Обратите внимание на метод AddText, он добавляет строку в то место, где стоит каретка. Из дальнейшего кода хочется также обратить внимание на использование метода GotoPos(), с помощью которого мы устанавливаем каретку перед открывающейся скобкой. Без вызова этого метода после добавления закрывающейся скобки каретка переместилась бы на следующую за ней позицию, а нам нужно, чтобы она осталась внутри скобок, ведь есть вероятность того, что пользователь захочет ввести не только пустые скобки, но и какой-то текст перед ними. В результате, если пользователь напишет "sin(", то будет добавлена закрывающаяся скобка, но каретка останется перед ней "sin(|)".

Раскрашиваем введенный текст

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

Вот что мы должны получить в результате:

И сам исходник:

class MyEditor (wx.stc.StyledTextCtrl):
    def __init__ (self, parent, id = wx.ID_ANY, \
            pos = wx.DefaultPosition, \
            size = wx.DefaultSize,\
            style = 0,\
            name = "editor"):
        wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style, name)

        self.encoder = codecs.getencoder("utf-8")

        # Определение стилей
        self.style0 = 0
        self.style_blue = 1
        self.style_red = 2
        self.style_green = 3

        # Стиль для текста по умолчанию
        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "size:%d" % 14)

        # Стили для раскрашенных слов
        self.StyleSetSpec (self.style_red, "fore:#FF0000")
        self.StyleSetSpec (self.style_blue, "fore:#0000FF")
        self.StyleSetSpec (self.style_green, "fore:#00FF00")

        # Подпишемся на событие, срабатывающее, когда требуется установить стиль
        self.Bind (wx.stc.EVT_STC_STYLENEEDED, self.onStyleNeeded)


    def calcByteLen(self, text):
        """Посчитать длину строки в байтах, а не в символах"""
        return len(self.encoder(text)[0])

    def calcBytePos (self, text, pos):
        """Преобразовать позицию в символах в позицию в байтах"""
        return len(self.encoder (text[: pos] )[0] )


    def ColorizeWord (self, styled_text, style):
        """Раскрасить в тексте все слова styled_text стилем style"""
        text = self.GetText()

        # Ищем все вхождения слова
        pos = text.find (styled_text)      
        while pos != -1:
            nextsym = text[pos + len (styled_text): pos + len (styled_text) + 1]
            prevsym = text[pos - 1: pos]

            if (pos == 0 or prevsym.isspace()) and (pos == len (text) - len(styled_text) or nextsym.isspace()):

                # Нас интересует позиция в байтах, а не в символах
                bytepos = self.calcBytePos (text, pos)

                # Находим длину искомой строки в байтах
                text_byte_len = self.calcByteLen (styled_text)

                # Применим стиль
                self.StartStyling (bytepos, 0xff)
                self.SetStyling (text_byte_len, style)

            pos = text.find (styled_text, pos + len (styled_text) )    

    def onStyleNeeded (self, event):
        text = self.GetText()

        # Сначала ко всему тексту применим стиль по умолчанию
        self.StartStyling (0, 0xff)
        self.SetStyling ( self.calcByteLen (text), self.style0)

        # Раскрасим слова с использованием так называемых индикаторов
        self.ColorizeWord (u"синий", self.style_blue)
        self.ColorizeWord (u"красный", self.style_red)
        self.ColorizeWord (u"зеленый", self.style_green)

Теперь некоторые пояснения к коду. В конструкторе класса мы создаем четыре стиля, один из них - стиль, который будет использоваться по умолчанию и три стиля, для которых буквы будут красного, синего и зеленого цветов. Номера стилей не должны превшать значения 31, так как под номер стиля по умолчанию отвидится 5 бит в байте стиля. Оставшиеся три бита используются для индикаторов. Это соотношение 5-3 можно изменить с помощью метода SetStyleBits(), но мы этого делать не будем. Настраиваем созданные стили с помощью уже знакомого нам метода StyleSetSpec(). Индикаторы представляют собой различные подчеркивания, которые применяются поверх стиля. Наиболее типичный пример использования индикаторов - подчеркивание красной ломаной линией ошибочных слов в программе или синей линией - предупреждения. Про индикаторы мы поговорим как-нибудь в другой раз.

Затем мы подписываемся на событие wx.stc.EVT_STC_STYLENEEDED, оно вызывается, когда требуется установить стили для введенного текста. В обработчике этого события мы сначала для всего текста применим стиль по умолчанию. Для этого будем использовать два метода - StartStyling(), первый параметр которого задается начальное смещение в байтах (не в символах!), начиная с которого будем применять стиль. Второй параметр задает битовую маску, которая определяет биты стиля, на которые надо обращать внимание. С помощью этого параметра мы можем сделать так, чтобы при изменении стиля менялся только сам стиль, но не индикаторные биты, или наоборот. В данном случае мы устанавливаем флаг 0xFF, то есть все биты будут играть значение, хотя индикаторы мы использовать не будем.

С помощью вызова метода SetStyling() мы применяем нужный стиль. Первый параметр на какое количество байт (опять же не символов!) нужно применить стиль, в качестве второго параметра передается тот самый "стилевой" байт, который задает и номер стиля (по умолчанию 5 бит) и 3 бита для индикаторов.

Раскраска отдельных слов вынесена в метод ColorizeWord(), который принимает в качестве первого параметра (не считая self) слово, которое надо раскрасить, а в качестве второго параметра - байт стиля. Внутри этого метода используются все те же методы StartStyling() и SetStyling().

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 4.7/5. Всего 38 голос(а, ов)



Ponyatov 21.05.2018 - 13:26

а как применить парсер языка для раскраски ?

есть простенький скриптовый язык (конфиги и макросы), как прикрутить его к раскраске но при этом сохранить функционал для интерпретатора?

Сергей 30.10.2018 - 14:14

Очень полезная статья. Как раз столкнулся с проблемой визуального сдвига пометки найденной при поиске строки в тексте. О причине догадывался, но в статье все подано в готовом виде, - просто бери и используй. Спасибо огромное! Ресурсов на эту тему реально очень мало.


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 655