SpinEdit. Самозванец с плавающей запятой

SpinEdit Imposter

SpinEdit с плавающей запятой нужен, порой, как воздух. Но стандартный SpinEdit оперирует только целочисленными значениями. Поэтому для вещественных значений, либо пишется имитатор в связке Edit + UpDown, либо используется JvSpinEdit, либо cxSpinEdit, либо аналогичное. Хотя, стандартный SpinEdit отлично подходит для этих целей.

Предисловие

Delphi — удивительно продуманная среда. Механизм компонентов — просто шедевр инженерной мысли. Создание, установка и использование компонент — поразительно прост. Из-за этого компонент развелось немыслимое количество. Качество в 90 % случаев оставляет желать лучшего. Монстры на этом рынке стали безразмерно дороги и бесконечно объемны.

Мне периодически нужны разные «усовершенствования» стандартных компонент. Например, SpinEdit с плавающей запятой. Ради этого ставить Developer Express? Пугать заказчика ценником? Хотя, там дел на копейку.

Писать свои компоненты, дело приятное, но бессмысленное. Компонент, засоряющий собой палитру, имеет ряд неприятных свойств:

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

Поэтому компонент, который создан в рамках одного или группы проектов, всегда лучше компонента, красующегося в палитре. Потому что его легко модифицировать — список требований меньше. И от него легко отказаться, если что-то пошло не так.

В любом случае, это сильно экономит силы, время и финансы. Тем более на последующих этапах развития и сопровождения проекта.

Когда-то описал на habr’е метод «воровства личности» у стандартного Memo с целью сделать у него вертикальное выравнивание и TextHint. Если вкратце, то суть в том, что копирую свойства у стандартного, созданного в дизайне Memo, уничтожаю и заменяю его своим компонентом. В коде все обращения к этому Мemo остаются без изменения, но это уже не TMemo. Это — самозванец, прикинувшийся TMemo.

В последствии, метод стал частью «философии такеров». Классы, которые отбирают свойства у компонентов, созданных в дизайне, и могут даже заменить их, назвал «такерами». Это — не обязательно компонент, но сейчас речь пойдет именно о компоненте, который не регистрируется в палитре, не придерживается правил хорошего тона. Он реализует ряд отсутствующих у оригинала свойств и необходимый для проекта функционал. Это ни в коем случае не helper, это полноценный класс, со своими внутренними полями и методами.

Конечная цель

В итоге хочется получить возможность манипулировать вещественными числами в стандартном SpinEdit. Также, неплохо было бы иметь горизонтальное и вертикальное выравнивание. И текстовую подсказку TextHint.

Что не так с выравниванием. Вы замечали, что численные значения принято выравнивать по правому краю? Стандартный SpinEdit так не умеет, выравнивает по левому. Если задать высоту побольше, значение будет прилеплено к верхнему краю, а это выглядит некрасиво. Хочется выравнивать как минимум по центру.

Зачем нужна текстовая подсказка. Если удалить значение из SpinEdit, то поле ввода останется пустым. Тут надо показывать либо актуальное значение, либо текстовую подсказку. Для такого случая нужно предусмотреть в свойствах значение по умолчанию. Как раз на случай пустой строки.

Для вещественных значений не помешает строка формата. Потому что результат функции FloatToStr может выдать такой хоровод цифр после запятой, что значимая часть числа просто не вместится в поле ввода. А также, формат необходим, чтобы избежать дерганий при правостороннем выравнивании. Число может быть сейчас 1.5, в следующий раз 1.501. Так вот, чтобы значение выводилось ровно, и запятая всегда была на одном месте, имеет смысл указать формат ###,##0.000. В этом случае всегда имеем три знака после запятой и комфортный вывод.

Итак, надо сделать свойства MinValue, MaxValue, Value и Increment вещественными, типа Extended. Добавить в список свойств значение по умолчанию, строку формата, горизонтальное и вертикальное выравнивание.

Было бы очень хорошо, если подобной операции превращения в «вещественный» SpinEdit можно было подвергнуть Edit и другие компоненты. Потому что Edit часто используется как альтернатива SpinEdit’у для ввода чисел с плавающей запятой.

Наследник SpinEdit

Чтобы в run-time компонент стал другим, существует всем известный прием. Назвать новый компонент также, как существующий. Тогда в дизайне оперируем со стандартным, назначаем свойства, работаем с географией расположения, обрабатываем свойства. А в run-time создается уже наш компонент.

Если «наш» компонент написан аккуратно, то способ вполне рабочий. Единственная неприятность — не забыть поставить в предложении uses модуль с реализацией «нашего» класса ПОСЛЕ модуля с оригинальным компонентом. Компилятор возьмет последнее описание и создаст уже «наш» компонент вместо стандартного.

Так вот, мы так делать не будем. Мы пишем честного самозванца. Поэтому класс будет называться TIPSpinEdit. И порожден он будет от стандартного TSpinEdit. Потому что если пишем новый класс, то и называться он будет как новый класс. В этом случае становится глубоко фиолетово на каком месте он стоит в предложении uses. Например, блок interface модуля главной формы из примеров выглядит так:

Использовать или не использовать хак — становится осознанным решением. Чтобы все заработало автоматически, надо прописать фразу TSpinEdit = class(TIPSpinEdit) до объявления класса формы. Поэтому никаких сюрпризов для разработчика возникнуть не должно, он ведь сам прописал эту фразу, понимая зачем это делает.

Хак действует только в пределах этого модуля. Правда, если другие модули не ссылаются на него в секции interface uses. Тогда надо будет лечить порядком моделей. Но скорее всего, модуль формы будет запрятан в implementation uses. В этом случае SpinEdit останется стандартным, со своими целочисленными свойствами

Почему порожден от TSpinEdit, а не от, скажем, TCustomEdit. Потому что фразы типа if Component is TSpinEdit должны работать всегда, не зависимо от того, подключен мой модуль или нет.

01. Вещественный SpinEdit

Сделать «вещественность» SpinEdit’а — самое простое из того, что предстоит. Если нужна только плавающая запятая, на этом разделе можно и закончить.

Реализация вещественного SpinEdit

[свернуть]

Этого уже достаточно, чтобы получить SpinEdit с плавающей запятой. Напишем небольшой тест. Бросим на форму три SpinEdit’а. Не забываем перед объявлением формы указать, кто теперь у нас TSpinEdit.

В обработчике OnChange для всех SpinEdit’ов пишем FloatToStr(TSpinEdit(Sender).Value), подразумевая, что значение Value уже вещественное. В событии OnCreate формы инициализируем свойство Increment значением 0.1 у всех найденных SpinEdit’ов. С помощью кнопок в компоненте и клавиш на клавиатуре значение меняется при каждом нажатии на 0.1, но можно ввести любое вещественное значение.

Рис.1. SpinEdit уже вещественный

Про экономию времени и сил. На этот код у меня ушло 20 минут. На статью — три дня.

Написание статьи можно сравнить с написанием полноценного компонента. Нужно предусмотрительно закрыть все возможные вопросы. Неплохо бы дать компоненту достойное описание. Снабдить код внятными комментариями и предусмотреть что-то вроде {$IFDEF CompilerVersion >= 1x}. По итогу, трудозатраты вырастают от недели до месяца. Дедлайн, сроки, нервы, инфаркт, некрасивая медсестра.


Скачать вещественный SpinEdit

01. Исходники (Delphi XE 7-10) 62.1 Кб

01. Исполняемый файл 872 Кб


02. Вертикальное выравнивание SpinEdit

Вот тут могли бы начаться танцы с бубном, если бы давным-давно не разобрался с этой проблемой (все та же статья на habr’е). Для вертикального выравнивания используем EM_SETRECT. С помощью этого сообщения устанавливаем прямоугольник для ввода текста.

Дописываем в класс TIPSpinEdit метод SetRect.

Добавим новые свойства в наш вещественный SpinEdit.

Margin — это класс, описанный в IP76.Takers.Routines, задает отступы, чтобы текст не «прилипал» к краям компонента. Alignment существовал в скрытом protected состоянии. Layout — новое свойство вертикального выравнивания.

Для расчета прямоугольника ввода пишем метод CalcRect.

Как видим, в зависимости от значения Layout считается верхняя граница прямоугольника. Мы не выравниваем текст, мы выравниваем прямоугольник текста.

Пишем дополнительные методы для вызова SetRect. Вначале метод, непосредственно вызывающий переустановку прямоугольника.

И ряд методов, которые вызывают UpdateRect.

В секции private объявляем:

В секции protected объявляем:

Реализация этих методов. Большинство из них просто события, в которых надо вычислить и установить прямоугольник ввода.

Метод WMPaste выбивается из общей картины мира. Он зачем-то удаляет из текста переносы строк. Связано вот с чем. Чтобы установить прямоугольник ввода и клиентской области, учитывая наличие кнопок справа, SpinEdit вынужден в CreateParams добавлять флаг ES_MULTILINE. Если переопределить метод, и убрать этот флаг, окончание прямоугольника ввода «спрячется» за кнопками. Следовательно, при выравнивании по правому краю, мы увидим только часть значения и не увидим курсора.

Наличие ES_MULTILINE разрешает вставлять текст с переносами строк. Обработчик WMPaste преобразует текст с переносами в одну строку. Что интересно, родная для SpinEdit функция IsValidChar позволяет ввод вещественного числа. Возможно, изначально планировался все таки вещественный SpinEdit.

Конечно, не забываем добавить в конструктор создание класса отступов, а в деструкторе освободить его.

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

В тестовом приложении добавил ряд настроек, чтобы экспериментировать и проверять свойства.

Рис.2. Выравнивания и отступы SpinEdit

Надо перевести фокус на нужный SpinEdit слева, установить свойства и нажать кнопку «Set …». В какой SpinEdit уйдут изменения написано на кнопках.

Сейчас верхний SpinEdit (1) выровнен по левому верхнему краю, средний(2) — по среднему, что по вертикали, что по горизонтали. Нижний(3) выровнен по правому нижнему краю. У всех SpinEdit’ов установлены отступы, равные 5 пикселям со всех краев.

Тестовый пример

[свернуть]

Скачать SpinEdit + выравнивания

02. Исходники (Delphi XE 7-10) 64.1 Кб

02. Исполняемый файл 880 Кб


03. TextHint в SpinEdit

Не смотря на то, что свойство TextHint в SpinEdit’е есть, наследие TCustomEdit спрятано в protected, вывод его в публичную область ничего не даст. Связано это все с тем же флагом ES_MULTILINE в CreateParams, который запрещает иметь подсказку многострочным элементам.

You cannot set a cue banner on a multiline edit control or on a rich edit control.

MSDN

Поэтому будем рисовать руками. Для этого переопределим метод PaintWindow.

Метод переопределяется исключительно для отрисовки TextHint. Поэтому, если элемент под фокусом, или Text не пуст, то сразу выходим. Если строка TextHint пуста, то есть рисовать нечего, также выходим.

У нас уже есть функция для вычисления текущего прямоугольника ввода при различных отступах/выравниваниях — CalcRect. Осталось только нарисовать текст с выравниванием по верхнему краю полученного прямоугольника. За это отвечает функция DrawTextEx из модуля IP76.Takers.Routines.

Напомню про метод, срабатывающий при потере фокуса элементом. Ранее его назначение было не раскрыто.

Что происходит. При утере фокуса, если текстовая подсказка пуста, происходит вывод актуального значения. Для пустой строки это будет значение по умолчанию. Иначе, будет нарисован TextHint.

В тестовом приложении чуть дополню обработчик OnCreate формы. Для каждого найденного SpinEdit’а буду случайным образом генерировать значение по умолчанию и выводить его в текстовой подсказке. Исходник в следующем разделе.

Теперь удалю текст в SpinEdit’ах.

Рис.3. TextHint в SpinEdit

Как видно на рис.3, текстовая подсказка появилась почти у всех SpinEdit. TextHint выводится с тем же выравниванием и отступами, что и основной текст. Как будет продемонстрировано в коде ниже, для каждого третьего SpinEdit’а текстовая подсказка остается пустой. Если текстовая подсказка пуста, будет показано значение по умолчанию.

Поддержка стилей

Теперь надо поработать со стилями. Добавлю три темы в опциях проекта. Внизу формы сделаю панель с RadioButton’ами. Это будут переключатели тем.

В конструктор добавлю инициализацию RadioButton’ов заголовками тем. И обработаю клик на них.

Рис.4. Тема Carbon

Как видим, на темах все работает, различимо. Но есть один маленький, но неприятный нюанс. При смене темы, вертикальное выравнивание сбрасывается. На рис.4. это видно по нижнему левому SpinEdit’у.

За событие смены стиля отвечает сообщение CM_STYLECHANGED. Добавим в секцию private объявление перехватчика.

И реализуем его.

Тестируем.

Рис.5. Тема Iceberg Classico

Все работает. Разговор про выравнивания можно считать закрытым.


Скачать SpinEdit + TextHint + Styles

03. Исходники (Delphi XE 7-10) 270 Кб

03. Исполняемый файл 1.21 Мб


04. SpinEdit Imposter. Самозванец

Допустим, у меня есть стандартный SpinEdit. Чтобы получить вещественное значение в интервале -1..1 я устанавливаю минимум и максимум в -100 и 100, а текущее значение делю на 100. Но в интерфейсе у меня отображаются вместо 0.55 целочисленный 55. А мне бы хотелось, чтобы отображалось 0.55. Чтобы при нажатии стрелки вверх или вниз значение менялось на 0.01, а не на единицу.

Еще у меня есть Edit+UpDown. Обрабатываю события, сам вношу значения, ради всего лишь отображения вещественного значения. Хотелось бы превратить эту связку в вещественный SpinEdit.

Предположим, у меня есть рабочий компонент, но мне не нравится концепция компонентного «рабства». Необходимость ради какой-то ерунды что-то ставить. Потом, спустя год, чтобы открыть проект, искать более свежую версию. Тем более мне не нравится перспектива писать свой компонент, тем более что сделать все надо сегодня.

На этапе дизайна мне по большому счету наплевать, какие значения в свойствах SpinEdit. Но в run-time хочу видеть нормальные значения, с плавающей запятой. Это означает, что мне точно известно, какие компоненты должны стать вещественными SpinEdit’ами.

То есть, я согласен что-то дописать руками. Таким образом начинается тема, как «отобрать личность» у компонента и заменить другой «личностью».

Этим делом занимается метод SetEdit. И ряд вспомогательных, которые вызывают этот метод Отличаются только способом вызова и набором параметров.

Объявление в секции public.

Директивы пусть пока не смущают. Про них разговор позже. Это для любителей классики хаков жанра.

Самый главный тут первый метод. Именно он делает всю работу.

Вспомогательные методы вызова SetEdit

[свернуть]

Функции CopyControl и CopyProperty находятся в модуле IP76.Takers.Routines.

CopyControl

Функция CopyControl осуществляет копирование минимального набора свойств из компонента ASource в компонент ADest.

Подразумевается, что всегда можно дописать тело SetEdit необходимыми свойствами, аналогично свойствам MaxValue, MinValue, Layout и т.д. Для конкретного проекта набор нужных свойств может быть разным.

Функции CopyControl

[свернуть]

CopyProperty

Функция CopyProperty предпринимает попытку скопировать значение свойства с именем ASourceName компонента ASource в свойство с именем APropName компонента ADest. При пустом ASourceName берется значение APropName.

Если предпринимается попытка присвоить целочисленному свойству значение с плавающей запятой, будет произведено округление. Объектные свойства и события также копируются.

Если указанных свойств не обнаружено, или их типы не совпадают, функция вернет FALSE.

Функции CopyProperty

[свернуть]

Практика #1. Edit+UpDown

Предположим, у меня есть три Edit в связке с UpDown. Чтобы выводить вещественные значения, ловлю OnClick c UpDown’ов, делю на 100 и вывожу в Edit вещественное значение. В Edit’ах ловлю OnChange, получаю текущий текст и присваиваю соответствующему SpinEdit’у. Все бы ничего, но если попытаюсь в Edit понажимать «вверх-вниз» значение будет браться из Position связанного UpDown, то есть текущая строка в Edit никак не учитывается. Поэтому надо выставить в Position нужное целочисленное значение при изменении содержимого.

По описанию выше возникает ощущение геморрности мероприятия?

Рис.6. Исходные Edit+UpDown

И вот тут начинаются пляски с бубном, чего можно было бы избежать, будь у меня вещественный SpinEdit. Значения в Edit’ах не выровнены. Выглядит неаккуратно. Масса ненужного кода. Комментарии в коде оставлены для демонстрации начала плясок, даже до бубна.

Но у нас есть SpinEdit с плавающей запятой. Поэтому пишем метод TakeUpDowns и вызываем его по кнопке в интерфейсе «Make SpinEdit’s».

Что происходит. Бежим по всем UpDown’ам, и если с ними связан Edit, преобразуем этот Edit в «наш» вещественный SpinEdit. Параметры минимума, максимума, инкремента и т.д. для нового SpinEdit’а берутся из соответствующих полей UpDown’а. В качестве делителя берется значение свойства Tag. Затем этот UpDown уничтожается.

Обратите внимание, вначале формируется список из жертв. Только потом происходит пробег по этому списку. Если все делать сразу, надо понимать, что внутри тела цикла будут происходить по два удаления и одно создание за итерацию. Список компонент изменится. Адресация нарушится.

Рис.7. Преобразование Edit+UpDown в вещественный SpinEdit

В итоге видим ряд аккуратно выровненных SpinEdit’ов, адекватно реагирующих на стрелки и клавиши, никаких половецких плясок не нужно. Событие OnChange с Edit’ов (которых уже не существует) перекочевало на SpinEdit’ы. После удаления всех ненужных закомментаренных участков обработчик приобретает следующий аккуратный вид.

Специально использую прямую адресацию к полям формы, чтобы продемонстрировать, что новые SpinEdit’ы расположены в «теле» ликвидированных Edit. Обработчик UpDown1Click становится ненужным вообще.

Практика #2. SpinEdit с плавающей запятой без хака

По большому счету, хак с одинаковым названием типа и не нужен. Закомментарим строку объявления типа перед формой.

Конечно, компилятор в паре мест ругнется. Например, в обработчике OnCreate формы нужно проверять уже не is TSpinEdit, а is TIPSpinEdit.

Перепишем обработчик события OnChange для SpinEdit’ов. Надо помнить, что после отмены хака для формы эти поля уже имеют стандартный тип TSpinEdit, т.е. MinValue, MaxValue, Increment и Value — целочисленные. Да и потом, хочется что-то более наглядное, чем просто вывод значения в заголовок. Поэтому кидаем на форму три компонента, умеющих показывать диапазон: Gauge, ProgressBar и TrackBar.

Напишем метод TakeSpinEdits, который преобразует стандартные SpinEdit’ы в вещественные. Вызов по кнопке «Take SpinEdit’s».

Рис.8. Преобразование стандартного SpinEdit в вещественный

Вот и все, собственно. Теперь SpinEdit’ы вещественные, с выравниваниями, отступами, текстовой подсказкой и прочими возможными плюшками, которые имеет смысл дописать.

Практика #3. Вещественный SpinEdit не только для Edit

Если все равно планируем использовать в run-time «наш» вещественный SpinEdit, мутить что-то с UpDown смысла никакого. Можно сделать также, как в TakeSpinEdits. Мы ведь знаем, какие компоненты должны подвергнуться преобразованию.

Более того, преобразованию могут быть подвергнуты и другие компоненты, не обязательно наследники TCustomEdit. Ведь компонент нам нужен в первую очередь для визуального проектирования.

Поэтому добавлю на форму три Edit‘а, StaticText, Label и Panel.

Рис.9. Дополнительные компоненты для преобразования

Обратите внимание, кнопки назначения выравнивания и отступов неактивны. Связано с тем, что мы отказались от хука, и пока не было преобразования в «наш» SpinEdit, это все еще стандартные SpinEdit’ы, у которых нет соответствующих свойств.

Для Edit’ов обработчик OnChange полностью аналогичен представленному выше.

Для остальных нет события OnChange. Зато есть OnClick.

Пишем метод TakeOtherSpinEdits, который вызывается по кнопке «Other SpinEdit’s».

Результат работы функции TakeSpinEdit — экземпляр TIPSpinEdit. Поэтому сразу привязываем его событие OnChange к обработчику StaticText1Click

Функцию TakeSpinEdit пишем тут же, в этом модуле, чуть выше.

Легальным методом TIPSpinEdit.TakeEdit воспользоваться не можем (можем, усложнять не хотелось), поэтому написали обертку для SetEdit.

Рис.10. Преобразование всего, что попадется, в вещественный SpinEdit

Компоненты на нижней панели выровнены с использованием свойств Margins и AlignWithMargins. Выравнивание при преобразовании сохранено. Для вновь созданных SpinEdit’ов использовано то выравнивание Alignment, которое было настроено в компоненте. Если свойство есть, то CopyProperty его обработает.

Директивы

Метод придумал давно. И споров по его поводу возникало немало. Есть у меня такой условно упертый оппонент — собирательный персонаж, который горой за описанный выше хак. Лично мне этот хак не нравится. По той простой причине, что люблю контролировать процесс. Но для любителей острых ощущений и неприятных сюрпризов, ввел такую запись для типа.

Соответственно, где-то выше должна быть объявлена директива SPIN_REPLACE (у меня она закомментарена в коде). Или указать в опциях проекта.

После объявления самого типа такой фрагмент

При таком подходе становится важен порядок следования модулей в предложении interface uses. Чтобы компилятор взял последнее описание для SpinEdit. Одним словом, не рекомендую, но ради порядка упомянул.

Краткая инструкция по использованию

IPSpinEdit — это не компонент, это род imposter, семейства taker’ов, маскирующийся под компонент. Требуется инициализация в run-time.

  1. Надо скопировать каталог IP76.Takers. Поместить его в каталог проекта или группы проектов.
  2. Прописать путь к нему в опциях проекта. Лучше, если путь будет относительным.
  3. В модуле формы, где требуются вещественные SpinEdit’ы, в предложении interface uses прописать IP76.Takers.Spin.
  4. Если хотите, чтобы все SpinEdit’ы на форме стали вещественными, перед описанием класса формы необходимо указать фразу

Далее, например в OnCreate формы произвести инициализацию SpinEdit. Если используется вещественный SpinEdit, то захочется как минимум Increment сделать вещественным.

В случае, когда п.4 выполнен, достаточно простого присваивания. В противном случае, когда необходимо превращать стандартные компоненты, необходимо использовать TIPSpinEdit.TakeEdit с набором необходимых параметров. Детально описано в разделах «Практика #1..3».

Недостатки метода

Ничто не совершенно, и данный метод не без недостатков.

Ситуация — ни директива SPIN_REPLACE, ни объявление TSpinEdit = class(TIPSpinEdit) не используется. Ряд компонентов сохраняется в список или просто запоминаются указатели на них. Затем производится преобразование в IPSpinEdit. Последующее обращение к элементам списка или ранее сохраненным указателям приведет к жесткому AV.

Лечится тем, что вначале сделать преобразование, а затем уж манипулировать указателями. Сохранять в списки, запоминать указатели и т.д.

Компоненты, как поля формы не страдают никак. То есть, если раньше был Edit1 типа TEdit, затем путем преобразования он стал типом TIPSpinEdit, обращение к нему будет все также по Edit1, все также корректно и без сюрпризов.


Друзья, спасибо за внимание!

Не пропустите новые такеры ))), подписывайтесь на телегу.

Если есть вопросы, с удовольствием отвечу.


Скачать

04. Исходники (Delphi XE 7-10) 270 Кб

04. Исполняемый файл 1.22 Мб


5 4 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Alexey
Alexey
1 месяц назад

Роман, спасибо за очередную статью! Интересный подход, который, думаю, в принципе, применять к самым разнообразным компонентам (и в принципе, к классам). Иногда действительно такое бывает нужно.

2
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x