Как разрешить ввод в текстовое поле только цифр
Часто возникает задача запрета ввода нецифровых значений. Как и подавляющее
большинство возникающих задач, решить проблему можно несколькими способами. Чаще
всего решают задачу через написание обработчиков событий компонента. Мы пойдем по
другому пути: повесим задачу слежения за вводом данных на операционную систему, хотя бы
частично.
Пусть доступным только для ввода цифр мы желаем сделать текстовое поле Edit1. Для
этого достаточно в обработчик создания формы поместить такой вызов:
SetWindowLong(Edit1.Handle, GWL_STYLE,
GetWindowLong(Edit1.Handle, GWL_STYLE) or ES_NUMBER);
|
Теперь ничего, кроме цифр, ввести будет невозможно. Но осталась возможность ввода
данных в контроль обходным путем - через буфер обмена. Для пресечения этой возможности
доработаем код следующим образом:
uses
Clipbrd;
function HandlePasteClipboardContentsToTextEdit(
// Дескриптор окна текстового контроля.
wnd: HWND;
// Сообщение операционной системы.
uMsg: UINT;
// Старший параметр.
wParam: WPARAM;
// Младший параметр.
lParam: LPARAM): Integer; stdcall; forward;
function HandlePasteClipboardContentsToTextEdit(wnd: HWND; uMsg: UINT;
wParam: WPARAM; lParam: LPARAM): Integer; stdcall;
var
ClipboardText: String;
I: Integer;
begin
// Обработка вставки текста из буфера обмена.
if (uMsg = WM_PASTE) and Clipboard.HasFormat(CF_TEXT) then
begin
ClipboardText := Clipboard.AsText;
for I := 1 to Length(ClipboardText) do
// Если в тексте буфера обмена есть символы отличные от цифр, отменяем вставку.
if not (ClipboardText[I] in ['0'..'9']) then
begin
uMsg := 0;
Break;
end;
end;
{$WARN UNSAFE_TYPE OFF}
Result := CallWindowProc(Pointer(GetWindowLong(wnd, GWL_USERDATA)),
wnd, uMsg, wParam, lParam);
{$WARN UNSAFE_TYPE ON}
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SetWindowLong(Edit1.Handle, GWL_STYLE,
GetWindowLong(Edit1.Handle, GWL_STYLE) or ES_NUMBER);
{$WARN UNSAFE_CODE OFF}
SetWindowLong(Edit1.Handle, GWL_USERDATA,
SetWindowLong(Edit1.Handle, GWL_WNDPROC,
LPARAM(@HandlePasteClipboardContentsToTextEdit)));
{$WARN UNSAFE_CODE ON}
end;
|
Получили рабочий код, не позволяющий ввод в текстовый контроль ничего, кроме цифр. Но
будьте внимательны, под запрет попали и знаки разделения целых и десятичных цифр и
символы задания знака числа. Если вам требуются эти символы, предложенное здесь
решение нужно доработать.
Давайте на основе предложенного выше кода напишем более приближенный к реальным
условиям вариант. К примеру, передо мной недавно встала задача создания контроля для
ввода TCP/IP-порта. Не вдаваясь в лишние подробности укажу, что диапазон возможных
значений лежит в границах [0..65535]. Потому кроме запрета ввода нецифровых символов,
возникло желание ограничения ввода больших значений.
Для начала переделал участок, отвечающий за отсеивание некорректного содержимого
буфера обмена. Я решил, что совсем пресекать вставку содержимого буфера слишком
жестоко и переделал код так:
function HandlePasteClipboardContentsToTextEdit(wnd: HWND; uMsg: UINT;
wParam: WPARAM; lParam: LPARAM): Integer; stdcall;
var
ClipboardText: String;
I: Integer;
begin
// Обработка вставки текста из буфера обмена.
if (uMsg = WM_PASTE) and Clipboard.HasFormat(CF_TEXT) then
begin
ClipboardText := Clipboard.AsText;
TempStr := EmptyStr;
for I := 1 to Length(ClipboardText) do
// Оставляем только цифровые символы.
if ClipboardText[I] in ['0'..'9'] then
TempStr := TempStr + ClipboardText[I];
Clipboard.AsText := TempStr;
end;
{$WARN UNSAFE_TYPE OFF}
Result := CallWindowProc(Pointer(GetWindowLong(wnd, GWL_USERDATA)),
wnd, uMsg, wParam, lParam);
{$WARN UNSAFE_TYPE ON}
end;
|
Код получился рабочий, но очень неоптимальный. Попробуйте представить, сколько будет
работать этот код, если пользователь попытается через буфер обмена впихнуть, например,
миллион цифр :) Потому переделаем код опять:
function HandlePasteClipboardContentsToTextEdit(wnd: HWND; uMsg: UINT;
wParam: WPARAM; lParam: LPARAM): Integer; stdcall;
var
ClipboardText, TempStr: String;
I, J, ClipboardLength: Integer;
begin
// Обработка вставки текста из буфера обмена.
if (uMsg = WM_PASTE) and Clipboard.HasFormat(CF_TEXT) then
begin
ClipboardText := Clipboard.AsText;
ClipboardLength := Length(ClipboardText);
if ClipboardLength > 0 then
begin
// Выделяем память под строку достаточной длины.
SetLength(TempStr, ClipboardLength);
// Заполняем выделенный участок памяти нулями.
{$WARN UNSAFE_CODE OFF}
FillChar(TempStr[1], ClipboardLength * SizeOf(TempStr[1]), 0);
{$WARN UNSAFE_CODE ON}
J := 0;
for I := 1 to Length(ClipboardText) do
// Оставляем только цифровые символы.
if ClipboardText[I] in ['0'..'9'] then
begin
Inc(J);
{$WARN UNSAFE_CODE OFF}
TempStr[J] := ClipboardText[I];
{$WARN UNSAFE_CODE ON}
end;
// Строку TempStr не очищаем от ведомых #0, так как в буфер
// передается PChar.
Clipboard.AsText := TempStr;
end;
end;
{$WARN UNSAFE_TYPE OFF}
Result := CallWindowProc(Pointer(GetWindowLong(wnd, GWL_USERDATA)),
wnd, uMsg, wParam, lParam);
{$WARN UNSAFE_TYPE ON}
end;
|
Теперь полученный код будет работать значительно быстрее. Но поставленная задача -
запрет ввода чисел, больших 65535 - еще не решена. Доработаем код, запретив вставку больших
чисел через буфер обмена. Кроме этого перестанем портить содержимое буфера обмена.
function HandlePasteClipboardContentsToTextEdit(wnd: HWND; uMsg: UINT;
wParam: WPARAM; lParam: LPARAM): Integer; stdcall;
var
EditTextLeft, EditTextMiddle, EditTextEnd: String;
SelStart, SelEnd, TempValue: Integer;
ClipboardText, Numerals, EditText, TempStr: String;
I, EditTextLength: Integer;
begin
if ((uMsg = WM_CHAR) and (wParam in [Ord('0')..Ord('9')])) or
((uMsg = WM_PASTE) and Clipboard.HasFormat(CF_TEXT)) then
begin
// Разобьем содержимое текстового поля на три части: левую, выделенную и правую.
EditText := EmptyStr;
EditTextLeft := EmptyStr;
EditTextMiddle := EmptyStr;
EditTextEnd := EmptyStr;
EditTextLength := SendMessage(wnd, WM_GETTEXTLENGTH, 0, 0);
if EditTextLength > 0 then
begin
SetLength(EditText, EditTextLength);
{$WARN UNSAFE_CODE OFF}
{$WARN UNSAFE_TYPE OFF}
SendMessage(wnd, WM_GETTEXT, EditTextLength + 1, Longint(PChar(EditText)));
{$WARN UNSAFE_TYPE ON}
SendMessage(wnd, EM_GETSEL, Longint(@SelStart), Longint(@SelEnd));
{$WARN UNSAFE_CODE ON}
EditTextLeft := Copy(EditText, 1, SelStart);
EditTextMiddle := Copy(EditText, SelStart + 1, SelEnd - SelStart);
EditTextEnd := Copy(EditText, SelEnd + 1, MaxInt);
end;
if not ((EditTextLeft = EmptyStr) and (EditTextEnd = EmptyStr)) and
not TryStrToInt(Format('%s%s', [EditTextLeft, EditTextEnd]), TempValue) then
raise Exception.Create('Значение не является числом');
case uMsg of
WM_CHAR:
begin
TempStr := Format('%s%s%s', [EditTextLeft, Char(wParam), EditTextEnd]);
// Если получаемое число больше максимально возможного, прерываем действие.
if (Length(TempStr) > 5) and (StrToInt(TempStr) > 65535) then
begin
lParam := 0;
wParam := 0;
MessageBeep(0);
end;
end;
WM_PASTE:
begin
ClipboardText := Clipboard.AsText;
Numerals := EmptyStr;
for I := 1 to Length(ClipboardText) do
if ClipboardText[I] in ['0'..'9'] then
begin
TempStr := Format('%s%s%s%s', [EditTextLeft, Numerals,
ClipboardText[I], EditTextEnd]);
if (Length(TempStr) <= 5) and
(StrToInt(TempStr) <= 65535) then
Numerals := Format('%s%s', [Numerals, ClipboardText[I]])
else
Break;
end;
if Numerals = EmptyStr then
MessageBeep(0)
else
{$WARN UNSAFE_TYPE OFF}
SendMessage(wnd, EM_REPLACESEL, 1, LongInt(PChar(Numerals)));
{$WARN UNSAFE_TYPE ON}
uMsg := WM_NULL;
end;
end;
end;
{$WARN UNSAFE_TYPE OFF}
Result := CallWindowProc(Pointer(GetWindowLong(wnd, GWL_USERDATA)),
wnd, uMsg, wParam, lParam)
{$WARN UNSAFE_TYPE ON}
end;
|
Функция получилась страшной, но это только на первый взгляд. На самом деле всё
достаточно прозаично. |