Счетчик HotLog

На главнуюЧто я делаю...Программы автора и не только...Творчество
Статьи автораКнига отзывов и предложенийОбо мне, любимомФотоальбом

 
 

Как разрешить ввод в текстовое поле только цифр

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

Пусть доступным только для ввода цифр мы желаем сделать текстовое поле 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;

Функция получилась страшной, но это только на первый взгляд. На самом деле всё достаточно прозаично.

 
 

18.03.2007

 
     
Hosted by uCoz