Потихоньку начал писать собственный редактор для написания программ под ARM на языке ассемблера, и решил начать с самого простого: сделать разбор текста при редактировании.
И тут я нашел небольшие такие грабельки :-)

Итак вопрос:
Есть редактор RichEdit в который мы ввели текст:

Курсор стоит вначале строки перед "9", RichText.SelStart := 12

Как в программе узнать символ на котором стоит курсор?

Если ваш опыт подсказывает конструкцию наподобие:
   with RichEdit do
        textChar:=Text [SelStart]; 

— то ваш опыт не верен!

И если вам интересно — то правильный ответ можно увидеть под катом…


Итак обещанный правильный ответ:
для того чтобы прочитать символ на котором стоит курсор — нужно читать символ в RichEdit.Text с номером 16!
   with RichEdit do
        textChar:=Text [16]; 


Число взятое непонятно откуда? — ну не скажите! все имеет логическое объяснение, просто нужно один раз разобраться!

Первое.
Нумерация символов в свойстве RichEdit.SelStart идет с нуля. То есть символ "0" когда перед ним стоит курсор будет иметь индекс RichEdit.SelStart = 0
Согласитесь — это очень просто!
Одновременно, для того чтобы прочитать этот символ нам нужно читать символ в строке в позиции = 1:
      textChar := RichEdit.Text[1]

А все потому, что свойство RichEdit.Text имеет тип String, а в типе String — нулевой символ хранит длину строки, и только после этого идет собственно текст: таким образом первый символ в строке (у нас это "0") имеет номер "1".

Запоминайте!
Нумерация символов в свойстве RichEdit.SelStart начинается с нуля (первый символ имеет номер =0), а нумерация символов в строке (свойство RichEdit.Text, тип String) идет с единицы (первый символ имеет номер =1)


Второе.
Документация нам указывает, что свойство SelStart указывает на номер символа в тексте. И тут необходимо вспомнить про символ генерируемый при переводе строки (нажатии на клавишу «Enter») — в текст добавляются 2 байта (символа) идущих подряд: 0x0D и 0x0A.
Ниже описание данное этим символам в Википедии:
Возврат каретки (англ. carriage return, CR) — управляющий символ ASCII (0x0D, 1310, '\r'), при выводе которого курсор перемещается к левому краю поля, не переходя на другую строку. Этот управляющий символ вводится клавишей «Enter». Будучи записан в файле, в отдельности рассматривается как перевод строки только в системах Macintosh.

Подача строки (от англ. line feed, LF — «подача [бумаги] на строку») — управляющий символ ASCII (0x0A, 10 в десятичной системе счисления, '\n'), при выводе которого курсор перемещается на следующую строку. В случае принтера это означает сдвиг бумаги вверх, в случае дисплея — сдвиг курсора вниз, если ещё осталось место, и прокрутку текста вверх, если курсор находился на нижней строке. Возвращается ли при этом курсор к левому краю или нет, зависит от реализации.


Так вот SelStart считает эти два байта (возврат каретки — 0х0D, перевод строки — 0x0A ) — одним символом!

Таким образом, символ "4" в нашем редакторе будет иметь номер SelStart = 5!


А вот в свойстве RichEdit.Text у него будет номер 7!!!

Для того чтобы все стало совсем понятным я нарисовал табличку (первый столбец — индекс, второй SelStart — соответствие индекса SelStart символу, третий CharPos — соответствие Text[индекс] символу):


Такие вот особенности адресации символов у редактора RichEdit.
Возможно именно из-за этого, многие, начиная делать подсветку кода или разбор строк в RichEdit — вскоре забрасывают это занятие и смотрят на различные сторонние компоненты…

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

Буквально за 5 минут я написал две функции которые преобразуют SelStart в CharPos и обратно:
// Определим номер символа в (.Text) по позиции курсора (SelStart)
function SelStartToCPOS(worktext:string; SelStart:integer):integer;
var
  i:integer;
begin
   result:=1;
   i:=SelStart;
   while i>0 do
      begin
         if (worktext[result]>=' ') or (worktext[result]=#$09) then result:=result+1
            else  result:=result+2;
         i:=i-1;
      end;
end;

// Определим позицию символа (SelStart) по номеру символа в строке (.Text)
function CPOSToSelStart(worktext:string; cpos:integer):integer;
var
  i:integer;
begin
   result:=0;
   i:=1;
   while i<>cpos do
      begin
         if (i<length(worktext)) and ((worktext[i]<>#$09) and (worktext[i]<' ')) then i:=i+1;
         i:=i+1;
         result:=result+1;
      end;
end;


Теперь, при помощи этих двух процедур, вы сможете узнать на каком символе в тексте редактора RichEdit стоит курсор:
    with RichEdit do 
    Ch:= Text [ SelStartToCPOS (Text, SelStart) ];


И наоборот, зная позицию символа в тексте RichEdit.Text сможете произвести изменение его атрибутов:
   // в переменной cpos - индекс символа в строке редактора
    with RichEdit do 
         begin
              SelStart := CPOSToSelStart(Text, cpos);
              SelLength := 1;
              SelAttributes.Color:=clBlue;
         end;


PostScriptum's:
  1. Процедуры которые я привел — несомненно нуждаются в оптимизации, но я уверен — с этим вы справитесь :-)
  2. Если вы считаете, что лучше использовать другой компонент вместо RichEdit — то я не буду с вами спорить, я всего лишь люблю разобраться до конца в том, что работает не так, как я ожидаю

Комментарии (24)