У контролов типа TEdit, TMemo и других наследников от TCustomEdit есть свойство – MaxLength. Свойство ограничивает ввод пользователя до указанного количества символов. Это свойство особенно полезно при работе с базами данных – как правило, размер текстовых полей имеет ограничение.
Ну например, при работе с Oracle, если попытаться сохранить строку, которая не помещается в поле в БД, получим ошибку вида:
ORA-12899: value too large for column "OWNER"."TABLE"."COLUMN" (actual: 20, maximum: 15)
Чтобы этого избежать, достаточно у полей ввода выставить MaxLength равным размеру поля в БД, и тогда пользователь просто не сможет ввести строку большего размера.
Однако есть такой нюанс: размер поля в БД может задаваться не в символах, а в байтах. При этом, в зависимости от кодировки со стороны БД, размер символа в байтах может занимать N-е число байт.
Давайте посмотрим, что это может означать.
Предположим, что у нас кодировка – UTF16. В этом случае можно предполагать, что размер символа – два байта. Это будет справедливо для латиницы, кириллицы и большого числа спец.символов. В этом случае, определяя MaxLength для контролов, достаточно делить размер поля на два. (Есть конечно понятие – суррогатная пара, но будем считать, что мы не китайцы.)
А теперь, предположим, что у нас кодировка – UTF8. Вот тут для латиницы – один символ = один байт, для кириллицы – один символ = два байта, а есть ещё символы, которые занимают по три байта (например символ “№”). И просто так ограничить размер поля количеством символов нельзя – нужно ограничивать в байтах.
Для решения проблемы необходимо перехватывать пользовательский ввод (а именно сообщения WM_CHAR и WM_PASTE), вычислять размер строки в байтах в нужной нам кодировке и лишние символы отбрасывать. При этом “дёргать” функцию MessageBeep, сигнализируя пользователю, что что-то не так.
И снова я предлагаю не делать свои наследники от стандартных контролов, а изменять поведение прямо в существующих компонентах. Для этого достаточно в RunTime подменить процедуру WindowProc. Но при этом нам надо где-то хранить ссылку на предыдущую процедуру, поэтому я предлагаю такой класс.
type TCustomEditLengthLimiter = class(TComponent) private type TFriendlyCustomEdit = class(TCustomEdit); private FCustomEdit: TFriendlyCustomEdit; FOldWndProc: TWndMethod; procedure NewWndProc(var Message: TMessage); protected class function CustomLength(const Text: string): Integer; virtual; abstract; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; class procedure Install(ACustomEdit: TCustomEdit); class procedure Uninstall(ACustomEdit: TCustomEdit); end;
Класс TCustomEditLengthLimiter содержит новую процедуру NewWndProc и сохраняет ссылку на предыдущую процедуру в FOldWndProc. В конструкторе в качестве Owner’а указывается модифицируемый контрол. Я решил этот класс наследовать именно от TComponent для того, чтобы наша обёртка над оконной процедурой контрола автоматически удалялась при уничтожении контрола.
Реализация класса у меня получилась такой:
constructor TCustomEditLengthLimiter.Create(AOwner: TComponent); begin FCustomEdit := TFriendlyCustomEdit(AOwner as TCustomEdit); inherited Create(AOwner); FOldWndProc := FCustomEdit.WindowProc; FCustomEdit.WindowProc := NewWndProc; end; destructor TCustomEditLengthLimiter.Destroy; begin FCustomEdit.WindowProc := FOldWndProc; inherited Destroy; end; class procedure TCustomEditLengthLimiter.Install(ACustomEdit: TCustomEdit); begin Create(ACustomEdit); end; class procedure TCustomEditLengthLimiter.Uninstall(ACustomEdit: TCustomEdit); var I: Integer; begin for I := ACustomEdit.ComponentCount - 1 downto 0 do if ACustomEdit.Components[I] is TCustomEditLengthLimiter then begin ACustomEdit.Components[I].Free; Exit; end; end; procedure TCustomEditLengthLimiter.NewWndProc(var Message: TMessage); function AllowInputChar(const Ch: Char): Boolean; var CheckStr: string; CursorPos: Integer; begin Result := True; if (Ch > #0) and (Ch <> #8) then begin CheckStr := FCustomEdit.Text; CursorPos := FCustomEdit.SelStart + 1; Delete(CheckStr, CursorPos, FCustomEdit.SelLength); Insert(Ch, CheckStr, CursorPos); if CustomLength(CheckStr) > FCustomEdit.MaxLength then begin Result := False; Beep; end; end; end; procedure DoPasteText(const Text: string); var CheckStr: string; CursorPos: Integer; CharsCount: Integer; begin CheckStr := FCustomEdit.Text; CursorPos := FCustomEdit.SelStart + 1; Delete(CheckStr, CursorPos, FCustomEdit.SelLength); Insert(Text, CheckStr, CursorPos); Dec(CursorPos); CharsCount := Length(Text); while (CharsCount > 0) and (CustomLength(CheckStr) > FCustomEdit.MaxLength) do begin Delete(CheckStr, CursorPos + CharsCount, 1); Dec(CharsCount); end; FCustomEdit.SelText := Copy(Text, 1, CharsCount); if (CharsCount = 0) and (Text <> '') then Beep; end; begin case Message.Msg of WM_CHAR: if FCustomEdit.MaxLength > 0 then if not AllowInputChar(Char(TWMChar(Message).CharCode)) then Exit; WM_PASTE: if FCustomEdit.MaxLength > 0 then begin DoPasteText(Clipboard.AsText); Exit; end; end; FOldWndProc(Message); end;
Тут всё достаточно просто. При создании обёртки назначается новая процедура, при удалении – возвращается старая. Самое интересное – в самой процедуре, тут обрабатываются интересующие нас сообщения, в которых:
- формируется строка (которую ожидает увидеть пользователь);
- проверяется размер строки методом CustomLength;
- если размер больше, чем установленный MaxLength, то ограничиваем или отменяем пользовательский ввод.
Обратите внимание на метод CustomLength – он абстрактный. Для ограничения в нужной нам кодировке достаточно создать наследник с реализацией этого метода. Вот так выглядит код для кодировки UTF8:
TUTF8EditLengthLimiter = class(TCustomEditLengthLimiter) protected class function CustomLength(const S: string): Integer; override; end; class function TUTF8EditLengthLimiter.CustomLength(const S: string): Integer; begin Result := Length(UTF8Encode(S)); end;
Использовать в коде это можно так:
TUTF8EditLengthLimiter.Install(Edit1); TUTF8EditLengthLimiter.Install(Memo1);
А теперь сюрприз. Или задачка. В VCL есть компонент – TCombobox. Если у него установлен стиль csDropDown или csSimple, то комбобокс ведёт себя как обычный Edit. Однако он наследуется от TCustomCombo, а не от TCustomEdit, поэтому предложенную обёртку так просто использовать не получится.
Предложите свой вариант, как можно реализовать класс (или классы), чтобы избежать дублирования кода и можно было использовать один метод для включения обёртки и над TEdit и над TCombobox.
UPD: в следующей заметке предложено решение. Там же можно скачать исходник целиком.
0 коммент.:
Отправить комментарий