Win32 API. Урок 35. RichEdit Control: подстветка синтаксиса

Дата публикации 5 июн 2002

Win32 API. Урок 35. RichEdit Control: подстветка синтаксиса — Архив WASM.RU

  Вначале я хочу вас предупредить, что это сложная тема, не подходящая для начинающего. Это последний туториал из серии о RichEdit.

  Скачайте пример.

ТЕОРИЯ

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

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

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

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

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

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

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

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

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

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

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

  Конечно, этот путь не так прост: есть несколько вещей, которые нужно пофиксить, но вышеизложенный метод работает достаточно хорошо. Скорость отображения на экране более чем удовлетворительна.

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

  Метод, который я использовал, возможно, не самый лучший, но он работает хорошо. Я уверен, что вы сможете найти более быстрый путь. Как бы то ни было, вот он:

  Я создал массив на 256 двойных слов, заполненный нулями. Каждый dword соответствует возможному ASCII-символу. Я назвал массив ASMSyntaxArray. Например, 21-ый элемент представляет собой 20h (пробел). Я использовал массив для быстрого просмотра таблицы: например, если у меня есть слово "include", я извлекаю первый символ из слова и смотрю значение соответствующего элемента. Если оно равно 0, я знаю, что среди слов, которые нужно подсвечивать, нет таких, которые бы начинались с "i". Если элемент не равен нулю, он содержит указатель на структуру WORDINFO, в которой находится информацию о слове, которое должно быть подсвечено.

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

Код (Text):
  1.  
  2. WORDINFO struct
  3.    WordLen dd ?  ; длина слова: используется для быстрого сравнения
  4.    pszWord dd ?  ; указатель на слово
  5.    pColor dd ?   ; указатель на dword, содержащий цвет, которым
  6.                  ; нужно подсвечивать слово
  7.    NextLink dd ? ; указатель на следующую структуру WORDINFO
  8. WORDINFO ends
  9.  

  Как вы можете видеть, я использовал длину слова для убыстрения процесса сравнивания. Если первый символ слова совпадает, мы можем сравнить его длину с доступными словами. Каждый dword в ASMSyntaxArray содержит указатель на начало ассоциированного с ним массива WORDINFO. Например dword, который представляет символ "i", будет содержать указатель на связанный список слов, которые начинаются с "i". Поле pColor указывает на слово, содержащее цвет, которым будет подсвечиваться слово. pszWord указывает на слово, которое будет подсвечиваться, преобразованное к нижнему регистру.

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

  Список слов сохраняется в файле под названием "wordfile.txt", я получаю к нему доступ через API-функцию GetPrivateProfileString. Есть десять различных подсветок символов, начинающиеся с C1 до C10. Массив цветов называется ASMColorArray. Поле pColor каждой структуры WORDINFO указывает на один из элементов ASMColorArray. Поэтому можно легко изменять подсветку синтаксиса на лету: вам всего лишь нужно изменить dword в ASMColorArray и все слова, которые используют этот цвет, будут использовать новое значение.

ПРИМЕР

Код (Text):
  1.  
  2. Здесь исходник. Возмите его из архива ;)
  3.  

АНАЛИЗ

  Первое действие, совершаемое до вызова WinMain, это вызов FillHiliteInfo. Эта функция считывает содержимое wordfile.txt и обрабатывает его.

Код (Text):
  1.  
  2.   FillHiliteInfo proc uses edi
  3.      LOCAL buffer[1024]:BYTE
  4.      LOCAL pTemp:DWORD
  5.      LOCAL BlockSize:DWORD
  6.      invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
  7.  
  8.   Initialize ASMSyntaxArray to zero.
  9.      invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
  10.      invoke lstrlen,addr buffer
  11.      mov ecx,eax
  12.      dec ecx
  13.      lea edi,buffer
  14.      add edi,ecx
  15.      std
  16.      mov al,"\"
  17.      repne scasb
  18.      cld
  19.      inc edi
  20.      mov byte ptr [edi],0
  21.      invoke lstrcat,addr buffer,addr WordFileName
  22.  

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

Код (Text):
  1.  
  2.      invoke GetFileAttributes,addr buffer
  3.      .if eax!=-1
  4.  

  Я использую этот метод как быстрый путь для проверки, существует ли файл.

Код (Text):
  1.  
  2. mov BlockSize,1024*10
  3. invoke HeapAlloc,hMainHeap,0,BlockSize
  4. mov pTemp,eax
  5.  

  Резервируем блок памяти, чтобы сохранять слова. По умолчанию - 10 килобайт. Память занимаем из кучи по умолчанию.

Код (Text):
  1.  
  2. @@:
  3.   invoke GetPrivateProfileString,addr ASMSection,
  4.          addr C1Key,addr ZeroString,pTemp,
  5.          BlockSize,addr buffer
  6.   .if eax!=0
  7.  

  Я использовал GetPrivateProfileString, чтобы получить содержимое каждого ключевого слова в wordfile.txt, начиная с C1 и до C10.

Код (Text):
  1.  
  2. inc eax
  3. .if eax==BlockSize ; the buffer is too small
  4.  add BlockSize,1024*10
  5.  invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
  6.  mov pTemp,eax
  7. jmp @B
  8. .endif
  9.  

  Проверяем, достаточно ли велик блок памяти. Если это не так, мы увеличиваем размер на 10k, пока он не окажется достаточно велик.

Код (Text):
  1.  
  2. mov edx,offset ASMColorArray
  3. invoke ParseBuffer,hMainHeap,
  4.        pTemp,eax,edx,addr ASMSyntaxArray
  5.  

  Передаем слова, хэндл на блок памяти, размер данных, считанных из wordfile.txt, адрес dword с цветом, который будет использоваться для подсветки слов и адреса ASMSyntaxArray.

  Теперь давайте посмотрим, что делает ParseBuffer. В сущности, эта функция принимает буфер, содержащий слова, которые должны быть выделены, парсит их на отдельные слова и сохраняет каждое из них в массиве структур WORDINFO, к которому можно легко получить доступ из ASMSyntaxArray.

Код (Text):
  1.  
  2.  ParseBuffer proc uses edi esi hHeap:DWORD,
  3.                   pBuffer:DWORD, nSize:DWORD,
  4.                   ArrayOffset:DWORD,pArray:DWORD
  5.   LOCAL buffer[128]:BYTE
  6.   LOCAL InProgress:DWORD
  7.   mov InProgress,FALSE
  8.  

  InProgress - это флаг, который я использовал, чтобы суметь определить, начался ли процесс сканирования. Если значение равно FALSE, vs

Код (Text):
  1.  
  2. lea esi,buffer
  3. mov edi,pBuffer
  4. invoke CharLower,edi
  5.  

  esi указывает на наш буфер, который будет содержать слово, взятое нами из списка слов. edi указывает на строку-список слов. Чтобы упростить поиск, мы приводим все символы к нижнему регистру.

Код (Text):
  1.  
  2.      mov ecx,nSize
  3.   SearchLoop:
  4.      or ecx,ecx
  5.      jz Finished
  6.      cmp byte ptr [edi]," "
  7.      je EndOfWord
  8.      cmp byte ptr [edi],9   ; tab
  9.      je EndOfWord
  10.  

  Сканируем весь список слов в буфере, ориентируясь по пробелам, по которым определяем конец или начало слова.

Код (Text):
  1.  
  2.      mov InProgress,TRUE
  3.      mov al,byte ptr [edi]
  4.      mov byte ptr [esi],al
  5.      inc esi
  6.   SkipIt:
  7.      inc edi
  8.      dec ecx
  9.      jmp SearchLoop
  10.  

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

Код (Text):
  1.  
  2.   EndOfWord:
  3.      cmp InProgress,TRUE
  4.      je WordFound
  5.      jmp SkipIt
  6.  

  Если пробел был найден, то мы проверяем значение в InProgress. Если значение равно TRUE, мы можем предположить, что был достигнут конец слова и мы можем поместить то, что у нас получилось в наш буфер (на который указывает esi) в структуру WORDINFO. Если значение равно FALSE, мы продолжамем сканирование дальше.

Код (Text):
  1.  
  2.   WordFound:
  3.      mov byte ptr [esi],0
  4.      push ecx
  5.      invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
  6.  

  Если был достигнут конец слова, мы прибавляем 0 к буферу, чтобы сделать из слова строку формата ASCIIZ. Затем мы занимаем для этого слова блок памяти из кучи размером в WORDINFO.

Код (Text):
  1.  
  2.      push esi
  3.      mov esi,eax
  4.      assume esi:ptr WORDINFO
  5.      invoke lstrlen,addr buffer
  6.      mov [esi].WordLen,eax
  7.  

  Мы получаем длину слова в нашем буфере и сохраняем ее в поле WordLen структуры WORDINFO, чтобы использовать в дальнейшем при быстром сравнении.

Код (Text):
  1.  
  2.      push ArrayOffset
  3.      pop [esi].pColor
  4.  

  Сохраняем адрес dword'а, который содержит цвет для подсветки слова, в поле pColor.

Код (Text):
  1.  
  2.      inc eax
  3.      invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
  4.      mov [esi].pszWord,eax
  5.      mov edx,eax
  6.      invoke lstrcpy,edx,addr buffer
  7.  

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

Код (Text):
  1.  
  2.      mov eax,pArray
  3.      movzx edx,byte ptr [buffer]
  4.      shl edx,2    ; multiply by 4
  5.      add eax,edx
  6.  

  pArray содержит адрес ASMSyntaxArray. Нам нужно перейти к dword'у, у которого тот же индекс, что и у значения первого символа слова. Поэтому мы помещаем первый символ в edx, а затем умножаем edx на 4 (так как каждый элемент в ASMSyntaxArray равен 4 байтам), а затем добавляем получившееся смещение к адресу ASMSyntaxArray. Теперь адрес нужного dword'а находится в eax.

Код (Text):
  1.  
  2.      .if dword ptr [eax]==0
  3.         mov dword ptr [eax],esi
  4.      .else
  5.         push dword ptr [eax]
  6.         pop [esi].NextLink
  7.         mov dword ptr [eax],esi
  8.      .endif
  9.  

  Проверяем значение dword'а. Если оно равно 0, это означает, что в настоящее время ключевых слов, которые начинаются с этого символа, нет. Затем мы помещаем адрес текущей структуры WORDINFO в этот dword.

  Если значение dword'а не равно 0, это означает, что есть по крайней мере одно ключевое слово, которое начинается с этого символа. Затем мы вставляем эту структуру WORDINFO в голову связанного списка и обновляем его поле NextLink, чтобы оно указывало на структуру WORDINFO.

Код (Text):
  1.  
  2.      pop esi
  3.      pop ecx
  4.      lea esi,buffer
  5.      mov InProgress,FALSE
  6.      jmp SkipIt
  7.  

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

Код (Text):
  1.  
  2. invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,
  3.        TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
  4. invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
  5. .if eax==0       ; means this message is not processed
  6.   mov RichEditVersion,2
  7. .else
  8.  mov RichEditVersion,3
  9.  invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,
  10.         SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
  11. .endif
  12.  

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

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

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

Код (Text):
  1.  
  2.   NewRichEditProc proc hWnd:DWORD, uMsg:DWORD,
  3.                   wParam:DWORD, lParam:DWORD
  4.      .......
  5.      .......
  6.      .if uMsg==WM_PAINT
  7.         push edi
  8.         push esi
  9.         invoke HideCaret,hWnd
  10.         invoke CallWindowProc,OldWndProc,hWnd,
  11.                uMsg,wParam,lParam
  12.         push eax
  13.  

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

Код (Text):
  1.  
  2. mov edi,offset ASMSyntaxArray
  3. invoke GetDC,hWnd
  4. mov hdc,eax
  5. invoke SetBkMode,hdc,TRANSPARENT
  6.  

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

Код (Text):
  1.  
  2. invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
  3. invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
  4. invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
  5. invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
  6.  

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

Код (Text):
  1.  
  2. mov txtrange.chrg.cpMin,eax
  3. mov FirstChar,eax
  4. invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
  5. mov txtrange.chrg.cpMax,eax
  6.  

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

Код (Text):
  1.  
  2.         push rect.left
  3.         pop RealRect.left
  4.         push rect.top
  5.         pop RealRect.top
  6.         push rect.right
  7.         pop RealRect.right
  8.         push rect.bottom
  9.         pop RealRect.bottom
  10.         invoke CreateRectRgn,RealRect.left,RealRect.top,
  11.                RealRect.right,RealRect.bottom
  12.         mov hRgn,eax
  13.         invoke SelectObject,hdc,hRgn
  14.         mov hOldRgn,eax
  15.  

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

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

Код (Text):
  1.  
  2. mov ecx,BufferSize
  3. lea esi,buffer
  4. .while ecx>0
  5.  mov al,byte ptr [esi]
  6.  .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || \
  7.      al=="+" || al=="-" || al=="*" || al=="&" || al=="<" \
  8.      || al==">" || al=="=" || al=="(" || al==")" || al=="{" \
  9.      || al=="}" || al=="[" || al=="]" || al=="^" || al==":" \
  10.      || al==9
  11.   mov byte ptr [esi],0
  12. .endif
  13. dec ecx
  14. inc esi
  15. .endw
  16.  

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

Код (Text):
  1.  
  2. lea esi,buffer
  3. mov ecx,BufferSize
  4. .while ecx>0
  5.  mov al,byte ptr [esi]
  6. .if al!=0
  7.  

  Ищем в буфере первый символ, не равный NULL, т.е. первый символ слова.

Код (Text):
  1.  
  2. push ecx
  3. invoke lstrlen,esi
  4. push eax
  5. mov edx,eax
  6.  

  Получаем длину слова и помещаем его в edx.

Код (Text):
  1.  
  2. movzx eax,byte ptr [esi]
  3. .if al>="A" && al<="Z"
  4.    sub al,"A"
  5.    add al,"a"
  6. .endif
  7.  

  Конвертируем символ в нижний регистр (если он в верхнем).

Код (Text):
  1.  
  2. shl eax,2
  3. add eax,edi     ; edi содержит указатель на массив указателей
  4.                 ; на структуры WORDINFO
  5. .if dword ptr [eax]!=0
  6.  

  После этого мы переходим к соответствующему dword'у в ASMSyntaxArray и проверяем равно ли его значение 0. Если это так, мы можем перейти к следующему слову.

Код (Text):
  1.  
  2. mov eax,dword ptr [eax]
  3. assume eax:ptr WORDINFO
  4. .while eax!=0
  5.     .if edx==[eax].WordLen
  6.  

  Если значение dword'а не равно нулю, он указывает на связанный список структур WORDINFO. Мы делаем пробег по связанному списку, сравнивая длину слова в нашем буфере с длиной слова в структуре WORDINFO. Это быстрый тест, проводимый до полноценного сравнения слов, что должно сохранить несколько тактов.

Код (Text):
  1.  
  2. pushad
  3. invoke lstrcmpi,[eax].pszWord,esi
  4. .if eax==0
  5.  

  Если длины обоих слов равны, мы переходим к сравнению слов с помощью lstrcmpi.

Код (Text):
  1.  
  2. popad
  3. mov ecx,esi
  4. lea edx,buffer
  5. sub ecx,edx
  6. add ecx,FirstChar
  7.  

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

Код (Text):
  1.  
  2. pushad
  3. .if RichEditVersion==3
  4.    invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
  5. .else
  6.   invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
  7.   mov ecx,eax
  8.   and ecx,0FFFFh
  9.   mov rect.left,ecx
  10.   shr eax,16
  11.   mov rect.top,eax
  12. .endif
  13. popad
  14.  

  Узнав индекс первого символа слова, которое должно быть подсвечено, мы переходим к получению его координат с помощью сообщения EM_POSFROMCHAR. Тем не менее это сообщение интерпретируется различным образом в RichEdit 2.0 или 3.0. В RichEdit 2.0 wParam содержит индес символа, а lParam не используется. Сообщение возвращается в eax. В RichEdit 3.0 wParam - это указатель на структуру POINT, которая будет заполнена координатой, а lParam содержит индекс символа.

  Как вы можете видеть, передача неверных аргументов EM_POSFROMCHAR может привести к непредсказуемым результатам. Вот почему я должен определить версию RichEdit.

Код (Text):
  1.  
  2. mov edx,[eax].pColor
  3. invoke SetTextColor,hdc,dword ptr [edx]
  4. invoke DrawText,hdc,esi,-1,addr rect,0
  5.  

  Как только мы получили координату, с которой надо начинать, мы устанавливаем цвет текста, который указан в структуре WORDINFO.

  В заключение я хочу сказать, что этот метод можно улучшить различными способами. Например я получаю весь текст, который начинается от первой до последней видимой линии. Если линии очень длинные, качество может пострадать из-за обработки невидимых слов. Вы можете оптимизировать это, получая видимый текст полинейно. Также можно улучшить алгоритм поиска. Поймите меня правильно: подсветка синтаксиса, используемая в этом примере, быстрая, но она может быть быстрее. :smile3: © Iczelion, пер. Aquila


0 1.505
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532