Или окончательное решение цифирьного вопроса.
Преамбула
Ранее мы показывали простые способы, как обеспечить, чтобы в 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/(
Comments |
Comment on this)