Немного о TRichEdit

Класс TRichEdit

  • Property Свойства
    • SelLength , SelStart Выделение текста

  • Methods Методы
    • FindText Поиск в тексте по образцу, в указанном диапазоне.
    • Lines.Add Добавляет строку в конец текста.

SelStart, SelLength

В библиотеке компонентов Delphi по поводу этих методов имеются следующие описания:

В файле Source\RTL\WIN\richedit.pas описание типа

type
TCharRange = record
cpMin : Longint;
cpMax : LongInt;

end;

Этот тип нужен для передачи границ выделенной части текста. Для Windows выделяемая часть текста указывается так, cpMin - первый символ, с которого начинается выделение (т.е. этот символ входит в выделение), а cpMax - первый символ не выделенного текста, сразу за выделением (т.е. этот символ не входит в выделение).

В файле Source\VCL\stdctrls.pas

TCustomEdit = class(TWinControl)
    public
property SelLength : Integer read GetSelLength write SetSelLength;
property SelStart : Integer read GetSelStart write SetSelStart;

Здесь определяются свойства SelStart и SelLength.

В файле Source\VCL\comctrls.pas

function TCustomRichEdit.GetSelStart: Integer;
var
CharRange : TCharRange;
begin
SendMessage(Handle, EM_EXGETSEL, 0, Longint(@CharRange));
Result := CharRange.cpMin;

end;

procedure TCustomRichEdit.SetSelStart(Value: Integer);

var
CharRange : TCharRange;
begin
CharRange.cpMin := Value;
CharRange.cpMax := Value;
SendMessage(Handle, EM_EXSETSEL, 0, Longint(@CharRange));

end;

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

function TCustomRichEdit.GetSelLength: Integer;
var
CharRange : TCharRange;
begin
SendMessage(Handle, EM_EXGETSEL, 0, Longint(@CharRange));
Result := CharRange.cpMax - CharRange.cpMin;

end;

procedure TCustomRichEdit.SetSelLength(Value: Integer);

var
CharRange : TCharRange;
begin
SendMessage(Handle, EM_EXGETSEL, 0, Longint(@CharRange));
CharRange.cpMax := CharRange.cpMin + Value;
SendMessage(Handle, EM_EXSETSEL, 0, Longint(@CharRange));
SendMessage(Handle, EM_SCROLLCARET, 0, 0);

end;

При установке SelLength приходится читать текущее положение курсора (первое сообщение - EM_EXGETSEL), изменять значение конца выделения (cpMax := cpMin + Value) и снова устанавливать курсор туда же (второе сообщение - EM_EXSETSEL), но с выделением текста. При этом компонент опять перерисует изображение, что бы стало видно выделение. И, наконец, третье сообщение EM_SCROLLCARET заставляет компонент произвести прокрутку изображения так, чтобы курсор стал виден.


FindText

Метод FindText ищет строку-образец в указанном диапазоне текста.

Метод возвращает номер позиции искомого образца в тексте (от начала текста, с 0), если образец не найден, то возвращает -1 (минус один).

Из просмотра исходных текстов Unit из библиотеки компонентов Delphi следует, что TRichEdit наследует метод FindText от своего ближайшего предка TCustomRichEdit.

Из файла RTL\WIN\richedit.pas описание типа

type
TCharRange = record
cpMin : Longint;
cpMax : LongInt;
end;

Этот тип нужен для передачи границ той части текста, внутри которой будет производиться поиск. Для Windows границы текста указывается так, cpMin - первый символ, с которого начинается поиск, а cpMax - последний символ, до которого производится поиск.

TFindText = TFindTextA;
TFindTextA = record
chrg: TCharRange;
lpstrText: PAnsiChar;

end;

Поле lpstrText это указатель на null-terminated строку (PAnsiChar обычная PChar).

Из файла VCL\comctrls.pas

TSearchType = (stWholeWord, stMatchCase);
TSearchTypes = set of TSearchType;

Данный тип определяет, каким образом должен происходить поиск. Если установлен stMatchCase, то при поиске необходимо учитывать регистр букв (большие или маленькие). Если установлен stWholeWord, то необходимо, чтобы найденная последовательность была ограничена разделителями (например, пробелами). В OSR2rus это называется "только слово целиком", хотя слов может быть несколько.

TCustomRichEdit = class(TCustomMemo)
 
function FindText(const SearchStr:string; StartPos, Length:Integer; Options:TSearchTypes): Integer;

Это описательная часть класса и далее в разделе реализации:

function TCustomRichEdit.FindText(const SearchStr:string; StartPos, Length:Integer; Options:TSearchTypes): Integer;
var
Find: TFindText;
Flags: Integer;
begin
with Find.chrg do
begin
cpMin := StartPos;
cpMax := cpMin + Length;

end;
Flags := 0;
if stWholeWord in Options then Flags := Flags or FT_WHOLEWORD;
if stMatchCase in Options then Flags := Flags or FT_MATCHCASE;
Find.lpstrText := PChar(SearchStr);
Result := SendMessage(Handle, EM_FINDTEXT, Flags, LongInt(@Find));

end;

Сначала устанавливаются границы диапазона поиска, причем вместо длины используется номер последнего символа для поиска (cpMax := cpMin + Length). Затем устанавливается флаг поиска. Далее устанавливается ссылка на Pchar строку-образец. И, наконец, посылается сообщение EM_FINDTEXT самому себе.


Lines.Add

Добавляет строку в конец текста

Класс TRichEdit наследует метод Add от класса TStrings, т.к. именно такой тип имеет свойство Lines. Из файла Source\VCL\classes.pas:

function TStrings.Add(const S:string) :Integer;
begin
Result := GetCount;
Insert(Result, S);

end;

Запрашиваем количество строк в тексте и вызываем метод Insert для вставки строки в конец текста. Обе используемые функции описаны там же, как:

TStrings = class(TPersistent)
protected
function GetCount: Integer; virtual; abstract;
public
procedure Insert(Index:Integer; const S:string); virtual; abstract;

То есть, абстрактны и виртуальны, а значит, в обязательном порядке должны быть переопределены в потомках. Таким потомком является класс TRichEditStrings, конструктор которого и вызывается для создания свойства Lines в конструкторе TCustomRichEdit, ближайшем родителе TRichEdit

Из файла Source\VCL\comctrls.pas

TRichEditStrings = class(TStrings)
protected
function GetCount: Integer; override;
public
procedure Insert(Index: Integer; const S: string); override;
function TRichEditStrings.GetCount: Integer;
begin
Result := SendMessage(RichEdit.Handle, EM_GETLINECOUNT, 0, 0);
if SendMessage(RichEdit.Handle, EM_LINELENGTH, SendMessage(RichEdit.Handle, EM_LINEINDEX, Result - 1, 0), 0) = 0 then Dec(Result);

end;

Запрашиваем количество строк в тексте и сохраняем его в служебной переменной Result. Если длина последней строки равна нулю (т.е. пуста) вычитаем ее из результата (Result).

procedure TRichEditStrings.Insert(Index:Integer; const S:string);
var
L: Integer;
Selection: TCharRange;
Fmt: PChar;
Str: string;
begin
if Index >= 0 then
begin
Selection.cpMin := SendMessage(RichEdit.Handle, EM_LINEINDEX, Index, 0);
if Selection.cpMin >= 0 then Fmt := '%s'#13#10
else begin
Selection.cpMin :=SendMessage(RichEdit.Handle, EM_LINEINDEX, Index - 1, 0);
if Selection.cpMin < 0 then Exit;
L := SendMessage(RichEdit.Handle, EM_LINELENGTH, Selection.cpMin, 0);
if L = 0 then Exit;
Inc(Selection.cpMin, L);
Fmt := #13#10'%s';

end;
Selection.cpMax := Selection.cpMin;
SendMessage(RichEdit.Handle, EM_EXSETSEL, 0, Longint(@Selection));
Str := Format(Fmt, [S]);
SendMessage(RichEdit.Handle, EM_REPLACESEL, 0, LongInt(PChar(Str)));

if RichEdit.SelStart <> (Selection.cpMax + Length(Str)) then
raise EOutOfResources.Create(sRichEditInsertError);

end;

end;

Используя номер строки, перед которой мы собираемся произвести вставку, определяем номер символа, с которого начинается строка, и сохраняем его в переменной Selection.cpMin. Если номер символа не отрицательный, то это значит, что строка с таким номером существует и в конец добавляемой строки необходимо включить признак конца строки. Если номер символа отрицательный, значит, такой строки нет. Но возможно предпринимается попытка вставить после последней строки, так что запросим номер символа для предыдущей строки. Если и теперь номер отрицательный или длина этой строки нулевая, прекращаем дальнейшее выполнение. При благоприятном исходе увеличиваем Selection.cpMin на длину последней строки, таким образом, теперь она указывает на последний символ в тексте (сюда и будем вставлять). Наличие не нулевой длины у последней строки говорит о том, что строка не завершена. Чтобы завершить ее необходимо передать в текст сначала, признак конца строки, а потом саму добавляемую строку, по этому признак конца строки включается в ее начало. Далее устанавливаем конец выделения на начало выделения (переменная Selection.cpMax) и перемещаем туда курсор посылкой сообщения EM_EXSETSEL. Форматируем строку и отправляем сообщение EM_REPLACESEL для размещения вставляемой строки. После удачной вставки новое положение курсора должно ровняться положению перед вставкой плюс длина вставляемой строки (Selection.cpMax + Length(Str)). Если нет, то вызывается исключительная ситуация (raise EOutOfResources.Create(sRichEditInsertError)).

Ошибка возникает потому, что сообщение EM_LINELENGTH использует только младшие 16 разрядов параметра. Этот параметр должен содержать номер любого символа из строки длину, которой надо определить. Если передается номер более 65 535, ну, например 65 536, то сообщение возвращает длину строки содержащую символ с номером 0. Таким образом, в методе Add при запросе функции GetCount может быть получено значение на единицу большее, если попадется строка не нулевой длины. И тогда процедуре Insert будет передано значение на единицу большее, чем номер последней строки. Это первая ошибка.

К стати свойство Count, которое обращается к функции GetCount, может давать на одну строку больше при превышении размера текста в 65 535 байт. Подчеркну, что ошибка возникает только если строка, содержащая усеченный номер символа, не пуста, то есть ошибка будет иметь плавающий характер.

Возвращаясь к методу Add, замечу, что эта ошибка не влияет на процедуру Insert. Потому что процедура Insert пробует не только указанную строку, но и, в случае не успеха, предыдущую, а это и будет как раз последней строкой в тексте. Но тут-то и кроется настоящая ошибка. Попав в эту ветвь if (т.е. после else) процедура Insert запрашивает длину строки с помощью сообщения EM_LINELENGTH и опять получает ошибочное значение. Обстоятельства нарастают как снежный ком, неотвратимо. Теперь неверная длина строки используется для определения номера последнего символа в тексте. А ведь длина настоящей последней строки возможно нулевая и это значит, что мы уже имеем номер последнего символа в тексте после предыдущего сообщения EM_LINEINDEX. После добавления к нему чужой длины, мы получаем номер символа заведомо выходящий за пределы текста. Из-за этого и произойдет вызов исключительной ситуации, но позже. Посылка сообщения EM_EXSETSEL с параметрами выходящими за границы текста проходит без катастрофических последствий, значение игнорируется, а курсор перемещается в конец текста. Сообщением EM_REPLACESEL добавляемая строка вставляется в конец текста. И вот, наконец, срабатывает цепь ошибок.

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

По-моему пример неплохо демонстрирует компенсационный характер Delphi и Windows, не глядя на массу ошибок таящихся в них (о чем это я, интересно?).

Избежать этой неприятной ситуации можно, если переопределить GetCount, Insert и Delete в классе TRichEditStrings, или исправить эти функции в исходных текстах, или написать свои.



Опубликовал admin
18 Сен, Четверг 2003г.



Программирование для чайников.