Толик Панков
hex_laden
............ .................. ................
October 2025
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

C#, ввод только цифр (чисел) в текстовое поле (TextBox).

Или окончательное решение цифирьного вопроса.

Преамбула


Ранее мы показывали простые способы, как обеспечить, чтобы в TextBox можно было ввести только цифры, т.е. целое число (копия), а потом расширили пример до ввода в TextBox отрицательных (копия) и дробных чисел (копия)

К сожалению, во всех этих примерах есть фатальный недостаток, текст в них все-таки вставить можно, если воспользоваться стандартным контекстным меню или комбинацией клавиш CTRL+V. На уровне простого взаимодействия с формой и контролами это перехватить невозможно, придется несколько извернуться, т.к. для перехвата события "вставить", придется перехватить сообщение Windows WM_PASTE, которое отправляется окну, или элементу управления окна при выполнении операции вставки. Для Windows, тащемта, однохуйственно, кому отправлять сообщение, форме (окну) или, например, текстовому полю. Т.к. для Windows, и текстовое поле на самом деле окно, просто дочернее, т.е. размещенное в другом окне (форме, в нашем случае). Но это я залез глубоко в бок. Нам нужно добраться до сообщений. А это можно сделать только изнутри самого контрола, но не из событий стандартных контролов, так что будем писать свой!

Постановка задачи: необходимо создать свой контрол на основе текстового поля, который позволяет вводить только числа определенного типа - целые, целые отрицательные, отрицательные и положительные/отрицательные с дробной частью.

Начало


Подключим необходимые пространства имен:

using System.Windows.Forms;
using System.ComponentModel;
using System.Globalization;


Начнем делать свой контрол, наследуемый от TextBox. Т.е. создадим новый класс:

public class InputDigitControl:TextBox
{
	//тут будет код :)
}


Для начала необходимо определить сообщения, которые будем перехватывать. Заводим в классе константы, определяющие коды нужных сообщений:

const int WM_PASTE = 0x0302; //Сообщение "Вставка" (через к.м. и комбинацию клавиш)
const int WM_CHAR = 0x0102; //Сообщение - нажатие алфавитно-цифровой клавиши

WM_CHAR будет отправлено форме системой только тогда, когда будет нажата алфавитно-цифровая клавиша, его будем перехватывать для отслеживания цифр (и прочего). Правда есть важный нюанс - WM_CHAR посылается и при нажатии комбинаций клавиш, например Ctrl+C и т.д., а также некоторых клавиш, которые не совсем подходят под понятие "алфавитно-цифровая", например BACKSPACE. Это надо будет учесть.

Анализ ввода с клавиатуры будем производить в функции PreProcessMessage(), которую переопределим. PreProcessMessage() вызывается для предварительной обработки входящих сообщений, и нужно будет вернуть true, если это сообщение было обработано.

Т.е. алгоритм действий таков - мы проверяем входящий символ на соответствие, и, либо пропускаем его дальше (возвращаем base.PreProcessMessage(ref msg) или false), либо что-то делаем с содержимым текстового поля, если символ нужен (это понадобится при вводе отрицательных и дробей) и возвращаем true. Или ничего не делаем, если символ нежелательный, и просто вызываем true. В последнем случае, символ просто не попадет в поле ввода, т.к. контрол будет думать, что он уже обработан.

Вставку, как я уже говорил выше, тоже нужно будет обработать, но, естественно, несколько иначе, это будем делать в переопределенной функции WndProc()


Общее по перехвату ввода


В переопределенную функцию PreProcessMessage()добавляем следующий код:

if (msg.Msg == WM_CHAR) //перехватываем сообщение WM_CHAR
{

{


в if добавляем:

//была нажата комбинация клавиш
if ((ModifierKeys != Keys.None)&&
(ModifierKeys != Keys.Shift)) return false;


Control.ModifierKeys - внутреннее свойство класса Control, от которого наследуются элементы управления, оно позволяет определить, была ли нажата клавиша-модификатор (Ctrl, Alt или Shift). Для нашего случая (ввод цифр в TextBox), нужно пропустить стандартные комбинации клавиш для TextBox (CTRL+A, CTRL+C, CTRL+V, CTRL+X), чтобы не поломать работу TextBox'а. С комбинаций с SHIFT для TextBox нет никаких комбинаций, кроме заглавных букв и знаков препинания.

В WParam сообщения Windows WM_CHAR содержится UTF код символа, т.е. 32-битное число, преобразуем его в char:

char chr = (char)msg.WParam.ToInt32();

И да, есть несколько служебных клавиш, воспринимаемых системой из-за древнего legacy (тянущегося еще с тех времен, когда мониторов не было, а вывод происходил на принтер) алфавитно-цифровыми, нам надо чтоб работала одна из них - BACKSPACE, добавляем:

if (chr == '\b') return false; //backspace

Клавиши управления курсором, HOME, END и DELETE алфавитно-цифровыми не считаются, так что будут работать и так, т.к. сообщение WM_CHAR не будет посылаться контролу.

Ввод только цифр


С вышеописанным это несложно сделать, в if (msg.Msg == WM_CHAR) добавляем следующий код ниже:

//это цифры (ура, товарищи)
if (chr >= '0' && chr <= '9')
{
    return false;
}
else
{
	return true;
}


Т.е теперь у нас работают системные клавиатурные комбинации для TextBox, BACKSPACE и ввод только цифр. Продолжаем.

Отрицательные числа


Добавим в класс свойство, позволяющее включить или отключить ввод отрицательных чисел.

[Description("Enable or disable negative number input"),
Category("Behavior"), DefaultValue(false)]
public bool Negative { get; set; } //включает/отключает ввод отрицательных чисел


С помощью классов из пространства имен System.ComponentModel можно добавить описание свойства, категорию, в которую будет помещено свойство, когда включен вид по категориям в Properties Window в редакторе, а также значение свойства по умолчанию.

После пересборки проекта, свойство появится в Properties Window



Ввод минуса, алгоритм:

1. Сохранить позицию курсора в текстовом поле.
2. Проверить, есть ли в начале строки знак '-'
3.1. Если есть, его надо убрать, т.е. присвоить свойству this.Text значение this.Text.Substring(1), это все символы кроме первого.
3.2. Надо вернуть курсор на прежнее место, т.е. на 1 символ меньше, т.к. был удален 1 символ: this.SelectionStart = pos - 1
4.1. Если нет - надо добавить: this.Text = "-" + this.Text
4.2. И переставить курсор на 1 позицию вперед: this.SelectionStart = pos + 1.

Код:

//это цифры (ура, товарищи)
if (chr >= '0' && chr <= '9')
{
    return false;
}
else
{
    //получаем текущую позицию курсора для вставки точки/минуса
    int pos = this.SelectionStart;

    //нажали минус, ввод отрицательных разрешен свойством Negative
    if (chr == '-' && Negative)
    {                        
        if (this.Text.StartsWith("-")) //минус уже есть
        {
            this.Text = this.Text.Substring(1);//убираем
            //ставим курсор на прежнюю позицию. 
            //Т.е. на -1 от текущей, т.к. удалили 1 символ
            this.SelectionStart = pos - 1;
        }
        else //минуса нет
        {
            this.Text = "-" + this.Text; //добавили
            //переставили курсор
            this.SelectionStart = pos + 1;
        }
        
    } //конец ввод отрицательных            
    return true;
}


Действительные (дробные) числа.


Т.е. надо вставлять разделитель целой и дробной части.

Добавим свойство, включающее и выключающее этот режим, как было в случае отрицательных:

[Description("Enable or disable fractional number input"),
Category("Behavior"), DefaultValue(false)]
public bool Fractional { get; set; } //включает/отключает ввод дробных чисел


Также, я решил добавить свойство, позволяющее задать разделитель (точку или запятую), а остальные запретить, ибо нефиг. Чтоб так сделать, можно генерировать исключение прямо в свойстве:
private char separator = '.';
[Description("Decimal separator, may be '.' or ','"),
Category("Format"), DefaultValue('.')] //разделитель дробной и целой части числа
public char Separator 
{
    get { return separator; }
    set
    {
        if ((value != '.') && (value != ','))
        {
            throw new ArgumentOutOfRangeException("Separator",
                "Value must be '.' or ','");
        }
        else
        {
            separator = value;
        }
    }
}


Пересобираем проект, работает. Если попробовать ввести что-нибудь кроме точки или запятой в IDE - получим вот такое окно:



Переходим к вводу, алгоритм:

1. Поле ввода будет реагировать как на ввод точки, так и на ввод запятой в качестве разделителя (ибо заебало переключаться/вспоминать, что разделитель другой в зависимости от языка). Отображать поле будет разделитель, указанный в свойстве Separator.
2. Проверяем, нет ли в тексте разделителя, если есть, отменяем ввод, вернув true.
3. Если поле пустое, а введен разделитель, то добавим лидирующий 0 перед разделителем, а курсор переместим в конец строки.
3.1. Если нет, то в WParam запишем код разделителя из свойства контрола, вне зависимости от того, что было нажато, точка или запятая.
3.2. Проверим, не поставили ли разделитель в начале строки.
3.2.1 Если поставили, надо проверить, начинается ли текст с минуса и если да, отменить ввод - разделителя перед знаком "-" не бывает.
3.2.2 Если минуса в начале строки нет, значит добавляем в начало текста 0, разделитель, и перемещаем курсор на 2 символа от начала строки. Возвращаем true.
3.3. Проверим, не начинается ли строка с символа "-" и не введен ли разделитель, когда курсор стоит сразу после минуса.
3.3.1. Если да, вставляем в начало текста -0, разделитель, и изначальный текст кроме первого символа (первым символом был минус), устанавливаем курсор после разделителя, возвращаем true.

Код (после } //конец ввод отрицательных и перед return true;):

//ввод разделителя дробной части
//поле реагирует и на . и на ,
if ((chr == '.' || chr == ',') && Fractional)
{
    //проверяем, чтоб в строке не было двух разделителей
    if (this.Text.Contains(separator.ToString()))
    {
        return true;
    }

    //если поле пустое, добавляем 0 перед разделителем
    if (this.Text == string.Empty)
    {
        this.Text = "0" + separator.ToString();
        //ставим курсор в конец текста
        this.SelectionStart = this.Text.Length;
    }
    else
    {
        //меняем WParam на код разделителя
        msg.WParam = (IntPtr)separator;
        
        //проверяем, не поставили ли разделитель
        //в начале текста
        if (this.SelectionStart == 0)
        {
            //если поставили и текст начинается с -
            //игнорируем нажатие, перед "-" 
            //разделителя не бывает
            if (this.Text.StartsWith("-")) return true;
            
            //добавляем лидирующий 0
            this.Text = "0" + separator.ToString() 
                + this.Text;
            this.SelectionStart = 2;
            return true;
        }
        
        //если курсор стоит после "-"
        if ((this.SelectionStart == 1) &&
            this.Text.StartsWith("-"))
        {
            //добавляем "-0," или "-0." к началу текста

            this.Text = "-0" + separator.ToString() +
                this.Text.Substring(1);
            this.SelectionStart = 3;

            return true;
        }
        
        return false;
    }
}


Вставка


Для начала надо переопределить WndProc, общую функцию для любого контрола, куда попадает большинство оконных сообщений:

protected override void WndProc(ref Message m)
{
   //тут будет код
    base.WndProc(ref m);
}


В WndProc перехватываем сообщение WM_PASTE для которого мы выше заготовили константу.

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_PASTE) //перехватываем сообщение "вставка"
    {
        //тут будет код
    }

    base.WndProc(ref m);
}


Далее, необходимо перехватить данные из буфера обмена, а потом проверять их в зависимости от того, какие данные может принимать наш контрол.

//получаем строку из буфера обмена
IDataObject obj = Clipboard.GetDataObject();
string input = (string)obj.GetData(typeof(string));
//надо будет в дальнейшем
ulong tmpulong = 0;
long tmplong = 0;


Целые числа без знака


Тут все просто, попытаемся сконвертировать содержимое буфера обмена в максимально возможный беззнаковый тип (UInt64, он же ulong) - получилось, разрешаем вставку, не получилось, пишем в Result сообщения (IntPtr)0, тем самым отменяя вставку, и выходим из функции.

if ((!Fractional) && (!Negative)) //только цифры
{
    //пытаемся конвертировать в беззнаковый long
    if (!ulong.TryParse(input,out tmpulong))
    {
        //не получилось
        m.Result = (IntPtr)0; //отменяем вставку
        return;
    }
}


Целые числа со знаком


Тоже просто, действуем по вышеуказанному алгоритму, только конвертируем не в беззнаковый, а знаковый тип (Int64, он же long):

//отрицательные и положительные целые
if ((!Fractional) && (Negative))
{
    //пытаемся конвертировать в знаковый long
    if (!long.TryParse(input,out tmplong))
    {
        //не получилось
        m.Result = (IntPtr)0; //отменяем вставку
        return;
    }
}


Дробные числа


Тут немного сложнее, для начала придется написать функцию для их конверсии, которая учитывает разделитель точку и разделитель запятую:

1. Меняем разделитель на какой-нибудь один (пусть тот, который задан в свойстве Separator контрола):

st = st.Replace('.', separator);
st = st.Replace(',', separator);


2. Создаем формат для функции конвертации, как это описано здесь (копия):

NumberFormatInfo format = new NumberFormatInfo();
format.NumberDecimalSeparator = separator.ToString();


3. Пытаемся сконвертировать, получилось - возвращаем true, нет - false:

try
{
    double d = Convert.ToDouble(st, format);
    return true;
}
catch
{
    return false;
}


Целиком

Теперь можно приступить к анализу буфера обмена:
1. Если дробные числа разрешены, пытаемся конвертировать в Double, не получилось - отменяем вставку:

//пытаемся конвертировать в double
if (!IsDouble(input))
{
//не получилось
m.Result = (IntPtr)0; //отменяем вставку
return;
}


2. Заменяем разделитель на тот, который установлен в свойстве Separator контрола:

//заменяем разделитель на установленный в контроле
input = input.Replace('.', separator);
input = input.Replace(',', separator);


3. Добавляем лидирующий 0 для положительных чисел, т.е. если строка начинается с разделителя, заменяем разделитель на 0 + разделитель (например, на 0,)

4. То же проделываем для отрицательных, т.е. заменяем - + разделитель (например -,) на -0:

//добавляем лидирующий 0 если надо
if (input.StartsWith(separator.ToString()))
{
    input = input.Replace(separator.ToString(),
        "0" + separator.ToString());
}
if (input.StartsWith("-" + separator.ToString()))
{
    input = input.Replace("-" + separator.ToString(),
        "-0" + separator.ToString());
}


Дробные не отрицательные.


Используя предыдущий алгоритм, просто добавляем проверку, чтоб строка в буфере не начиналась с минуса:

//дробные не отрицательные
if (!Negative)
{
    if (input.StartsWith("-"))
    {
        m.Result = (IntPtr)0; //отменяем вставку
        return;
    }
}


В конце вставки дробных чисел меняем содержимое буфера обмена:

//меняем содержимое буфера обмена
Clipboard.SetText(input);


Конец функции вставки


В конце функции вставки разрешаем вставлять числа только целиком, иначе это усложнит код (может допилю в будущих версиях), просто удаляя содержимое текстового поля, а вставку за нас система сделает:

//вставка чисел целиком
this.Text = string.Empty;


Вся функция целиком

Исходники


Контрол



Тестовое приложение

Репозиторий

Это репост с сайта http://tolik-punkoff.com
Оригинал: http://tolik-punkoff.com/2021/08/10/c-vvod-tolko-tsifr-chisel-v-tekstovoe-pole-textbox/

From:
(will be screened)
Identity URL: 
имя пользователя:    
Вы должны предварительно войти в LiveJournal.com
 
E-mail для ответов: 
Вы сможете оставлять комментарии, даже если не введете e-mail.
Но вы не сможете получать уведомления об ответах на ваши комментарии!
Внимание: на указанный адрес будет выслано подтверждение.
Username:
Password:
Subject:
No HTML allowed in subject
Message:



Notice! This user has turned on the option that logs IP addresses of anonymous posters.