API и FreeBasic. (RichEdit Control: подстветка синтаксиса)

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

И так:

Подсветка синтаксиса - это предмет жарких дискуссий между создателями текстовых редакторов. Лучший метод (на мой взгляд) - это создать собственный edit control. Именно этот метод применяется во многих коммерческих приложениях. Тем не менее для тех из нас, у кого нет времени на создание подобного контрола, лучшим вариантом будет приспособить существующий контрол к нашим нуждам.

Давайте посмотрим, как может нам помочь RichEdit в реализации цветовой подсветки. Я должен сказать, что следующий метод "неверен": я всего лишь продемонстрирую ловушку, в которую угодили многие. RichEdit предоставляет сообщение EM_SETCHARFORMAT, которое позволяет вам менять цвет текста. На первый взгляд это именно то, что нам нужно (я знаю, потому что я стал одной из жертв этого подхода). Тем не менее, более детальное исследование покажет вам, что у данного сообщения есть несколько недостатков:

EM_SETCHARFORMAT работает либо со всем текстом сразу, либо только с тем, который сейчас выделен. Если вам потребуется изменить цвет определенного слова, вам сначала придется выделить его.

EM_SETCHARFORMAT очень медленна.

У нее есть проблемы с позицией курсора в контроле RichEdit.

Из всего вышеизложенного вы можете сделать вывод, что использование EM_SETCHARFORMAT - неправильный выбор. Я покажу вам "относительно верный" выбор.

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

Как это сделать? Ответ прост:

Субклассируйте контрол RichEdit и обрабатывайте сообщение WM_PAINT внутри вашей оконной процедуры.

Когда она встречает сообщение WM_PAINT, вызывается оригинальная оконная функция, которая обновляет экран как обычно.

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

Пример ниже дает принцип подсвечивания слов, но в нем реализован абсолютно неправильный поиск ключевых слов. То есть в примере будут подсвечиваться 5 слов в установленной последовательности ("function","sub","end","goto","len") и раскладке букв. Если слова в тексте будут встречаться в другой последовательности, то подсветка для последних слетит. Для правильного поиска нужно посимвольное чтение, которое адаптировать с примера , насыщенного исключительно ассемблерными опкодами затруднительно, а реализовывать заново пока нет нужды. Более того, для тех кто будет осуществлять подсветку в своем редакторе, реализация поиска, предложенная автором будет тоже не в тему. В любом случае придется писать свою, но принцип самой подсветки будет полезен.

#INCLUDE "windows.bi"

#INCLUDE "win/richedit.bi"
Var hlib = LoadLibrary ("RICHED20.DLL")
Dim msg As MSG 'структурированная переменная MSG
Dim As WNDCLASSEX wc 'структурированная переменная WNDCLASSEX
Dim As String NameClass="MyClass" ' переменная имени класса
Dim As HINSTANCE Hinst=GetModuleHandle(0) ' хендл модуля
Dim Shared As Long OldWndProc,richeditversion
Dim Shared As ZString*20 ArrayWord(4) =>{"function","sub","end","goto","len"}
' функция сабкласса
Function childproc(hwnd As HWND, Umsg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer
    Select Case umsg
        Case WM_PAINT
            Dim As HDC hdc
            Dim As RECT rect,realRect
            Dim txtrange As TEXTRANGE
            Dim As ZString*(1024*10) buffer
            Dim As HRGN hRgn,hOldRgn
            HideCaret(hwnd)
            CallWindowProc(OldWndProc,hWnd,uMsg,wParam,lParam)
            hdc  = GetDC(hWnd)
            SetBkMode(hdc,TRANSPARENT)
            SendMessage(hWnd,EM_GETRECT,0,@rect)
            Var fi=SendMessage(hWnd,EM_CHARFROMPOS,0,@rect)
            Var L = SendMessage(hWnd,EM_LINEFROMCHAR,fi,0)
            fi = SendMessage(hWnd,EM_LINEINDEX,L,0)
            txtrange.chrg.cpMin = fi
            txtrange.chrg.cpMax = SendMessage(hWnd,EM_CHARFROMPOS,0,@rect.right)+20 '+20 много,по идее надо выставлять по самому длинному слову
            realRect = Type(rect.left,rect.top,rect.right,rect.bottom)
            hRgn =  CreateRectRgn(RealRect.left,RealRect.top,RealRect.right,RealRect.bottom)
            hOldRgn =  SelectObject(hdc,hRgn)
            SetTextColor(hdc,&hFF0000)
            txtrange.lpstrText = @buffer
            SendMessage(hWnd,EM_GETTEXTRANGE,0,@txtrange)
            Dim As Integer find =1, posfind=1
            CharLower(buffer)
            Do
                For i As Integer = 0 To 4
                    find = Instr(posfind,buffer,ArrayWord(i))
                    If find>0 Then
                        SendMessage(hWnd,EM_POSFROMCHAR,@rect,find-1)
                        DrawText(hdc,@ArrayWord(i),-1,@rect,0)
                        posfind+=(Len(ArrayWord(i))-1)
                        Exit For
                    Endif
                Next
            Loop Until find=0
            SelectObject(hdc,hOldRgn)
            DeleteObject(hRgn)
            ShowCaret(hwnd)
            DeleteDC(hdc)
        Case Else
            Return CallWindowProc(OldWndProc,hWnd,uMsg,wParam,lParam)
    End Select
End Function

' функция класса
Function wndproc(hwnd As HWND, msg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer
    Static childEdit As HWND
    Select Case msg
        Case WM_DESTROY
            PostQuitMessage(0)
        Case WM_CREATE
            childEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "RICHEDIT20A", 0,_
            WS_VISIBLE Or WS_CHILD Or WS_HSCROLL Or WS_VSCROLL  Or ES_MULTILINE Or ES_AUTOHSCROLL Or ES_AUTOVSCROLL,_
            10,10,200,200, hwnd, Cast(HMENU,1), 0, 0)
            SendMessage(childEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK)
            If SendMessage(childEdit,EM_GETTYPOGRAPHYOPTIONS,1,1)= 0 Then
                richeditversion = 2
            Else
                richeditversion = 3
                SendMessage(childEdit,EM_SETEDITSTYLE,1,1)
            Endif
            OldWndProc = SetWindowLong(childEdit,GWL_WNDPROC,@childproc)
    End Select
    Return DefWindowProc(hwnd,msg,wparam,lparam)
End Function
' Заполнение структуры WNDCLASSEX
With wc
    .cbSize=SizeOf(WNDCLASSEX)
    .style=CS_HREDRAW Or CS_VREDRAW
    .lpfnWndProc=@wndproc
    .hInstance=Hinst
    .hIcon=LoadIcon(0,IDI_WINLOGO)
    .hCursor=LoadCursor(0,IDC_ARROW)
    .hbrBackground=Cast(HBRUSH,COLOR_WINDOWFRAME)
    .lpszClassName=StrPtr(NameClass)
    .hIconSm=.hIcon
End With
' Регистрация класса окна
If RegisterClassEx(@wc)=0 Then
    Print "Register error, press any key"
    Sleep
    End
Endif
'Создание окна
CreateWindowEx(0,NameClass,"RichEdit",_
WS_VISIBLE Or WS_OVERLAPPEDWINDOW,100,100,240,260,0,0,Hinst,0)
' Цикл сообщений
While GetMessage(@msg,0,0,0)
    TranslateMessage(@msg)
    DispatchMessage(@msg)
Wend

FreeLibrary(hlib)


После того, как контрол RichEdit создан, нам нужно определить его версию. Этот шаг необходим, так как поведение EM_POSFROMCHAR отличается в зависимости от версии RichEdit, а EM_POSFROMCHAR жизненно важна для нашей процедуры подсветки. Я не видел документированного способа определения версии RichEdit, поэтому я пошел окольным путем. Я устанавливаю опцию, которая свойственна версии 3.0 и немедленно возвращаю его значение. Если я могу получить значение, я предполагаю, что версия этого контрола 3.0.

Если вы используете контрол RichEdit версии 3.0, вы можете заметить, что обновление цвета фонт на больших файлах занимает довольно много времени. Похоже, что эта проблема существует только в версии 3.0. Я нашел способ обойти это, заставив конрол эмулировать поведение контрола edit, послав сообщение EM_SETEDITSTYLE.

После того, как мы получили информацию о версии контрола, мы переходим к сабклассированию контрола RichEdit. Сейчас мы рассмотрим новую процедуру для обработки сообщений.

Function childproc(hwnd As HWND, Umsg As Uinteger,_
    wparam As WPARAM, lparam As LPARAM) As Integer
    Select Case umsg
        Case WM_PAINT
            Dim As HDC hdc
            Dim As RECT rect,realRect
            Dim txtrange As TEXTRANGE
            Dim As ZString*(1024*10) buffer
            Dim As HRGN hRgn,hOldRgn
            HideCaret(hwnd)
            CallWindowProc(OldWndProc,hWnd,uMsg,wParam,lParam)


Мы обрабатываем сообщение WM_PAINT. Во-первых, мы прячем курсор, чтобы избежать уродливых артефактов после подсветки. Затем мы передаем сообщение оригинальной процедуре richedit, чтобы оно обновило окно. Когда CallWindowProc возвращает управление, текст обновляется согласно его обычному цвету/бэкграунду. Теперь мы можем сделать подсветку синтаксиса.

hdc  = GetDC(hWnd)
SetBkMode(hdc,TRANSPARENT)

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

SendMessage(hWnd,EM_GETRECT,0,@rect)
Var fi=SendMessage(hWnd,EM_CHARFROMPOS,0,@rect)
Var L = SendMessage(hWnd,EM_LINEFROMCHAR,fi,0)
fi = SendMessage(hWnd,EM_LINEINDEX,L,0)

Мы хотим получить видимый текст, поэтому сначала нам требуется узнать размеры области, которую необходимо форматировать, послав ему EM_GETRECT. Затем мы получаем индес ближайшего к левому верхнему углу символа с помощью сообщения EM_CHARFROMPOS. Как только мы получаем индекс первого символа, мы начинаем делать цветовую подсветку, начиная с этой позиции. Но эффект может быть не так хорош, как если бы начали с первого символа линии, в которой находится символ. Вот почему мне нужно было получить номер линии, в которой находится первый видимый символ, с помощью сообщения EM_LINEFROMCHAR. Чтобы получить первый символ этой линии, я посылаю сообщение EM_LINEINDEX.

txtrange.chrg.cpMin = fi
txtrange.chrg.cpMax = SendMessage(hWnd,EM_CHARFROMPOS,0,@rect.right)+20 '+20 много,по идее надо выставлять по самому длинному слову


Затем мы получаем последний видимый символ, посылая ему EM_CHARFROMPOS, передавая нижний правый угол форматируемой области в lParam.

realRect = Type(rect.left,rect.top,rect.right,rect.bottom)
hRgn =  CreateRectRgn(RealRect.left,RealRect.top,RealRect.right,RealRect.bottom)
hOldRgn =  SelectObject(hdc,hRgn)


Во время подсветки синтаксиса, я заметил один побочный эффект этого метода: если у контрола richedit'а есть отступ (который вы можете указать, послав сообщение EM_SETMARGINS контролу RichEdit), DrawText пишет поверх него. Поэтому мне требуется создать функцией CreateRectRgn ограничительный регион, в который будут выводиться результат выполнения функций GDI.

(Нет в примере, но для теории неплохо знать) Затем нам требуется подсветить коментарии и убрать их с нашего пути. Когда подсвечены все комментарии, мы замещаем их нулями в нашем буфере, чтобы слова к комментарии не обрабатывались позже.

SetTextColor(hdc,&hFF0000)
txtrange.lpstrText = @buffer
SendMessage(hWnd,EM_GETTEXTRANGE,0,@txtrange)


Устанавливаем цвет подсветки слов . В структуру TEXTRANGE отправляем указатель на наш буфер и переписываем наш буфер с помощью сообщения EM_GETTEXTRANGE. Буфер в итоге будет состоять только из видимого объема текста с небольшим запасом (вправо на 20 символов), который мы добавляли в сообщении EM_CHARFROMPOS.

CharLower(buffer)
Do
    For i As Integer = 0 To 4
        find = Instr(posfind,buffer,ArrayWord(i))
        If find>0 Then
            SendMessage(hWnd,EM_POSFROMCHAR,@rect,find-1)
            DrawText(hdc,@ArrayWord(i),-1,@rect,0)
            posfind+=(Len(ArrayWord(i))-1)
            Exit For
        Endif
    Next
Loop Until find=0

Далее идет непутный поиск ключевого слова в буфере и как только хоть одно из слов будет найдено, с помощью сообщения EM_POSFROMCHAR получаем его позицию и с помощью DrawText отрисовываем.

Перевод на русский с оригинала: Aquila, адаптация материалов под FreeBasic: Станислав Будинов

содержание | назад