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-vv