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 модуля главной формы из примеров выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls // , Vcl.Samples.Spin // it is my SpinEdit , IP76.Takers.Spin // it is standart SpinEdit , Vcl.Samples.Spin ; type TSpinEdit = class(TIPSpinEdit); TFmMain = class(TForm) Edit1: TEdit; Edit2: TEdit; Edit3: TEdit; ... |
Использовать или не использовать хак — становится осознанным решением. Чтобы все заработало автоматически, надо прописать фразу TSpinEdit = class(TIPSpinEdit) до объявления класса формы. Поэтому никаких сюрпризов для разработчика возникнуть не должно, он ведь сам прописал эту фразу, понимая зачем это делает.
Хак действует только в пределах этого модуля. Правда, если другие модули не ссылаются на него в секции interface uses. Тогда надо будет лечить порядком моделей. Но скорее всего, модуль формы будет запрятан в implementation uses. В этом случае SpinEdit останется стандартным, со своими целочисленными свойствами
Почему порожден от TSpinEdit, а не от, скажем, TCustomEdit. Потому что фразы типа if Component is TSpinEdit должны работать всегда, не зависимо от того, подключен мой модуль или нет.
01. Вещественный SpinEdit
Сделать «вещественность» SpinEdit’а — самое простое из того, что предстоит. Если нужна только плавающая запятая, на этом разделе можно и закончить.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
unit IP76.Takers.Spin; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.StdCtrls, Vcl.Samples.Spin, System.Types, // this project IP76.Takers.Routines ; type TIPSpinEdit = class(Vcl.Samples.Spin.TSpinEdit) private FMinValue: Extended; FMaxValue: Extended; FIncrement: Extended; FDefaultValue: Extended; FFormatString: String; function GetValue: Extended; function CheckValue (NewValue: Extended): Extended; procedure SetValue (NewValue: Extended); function GetDefaultValue: Extended; procedure CMExit(var Message: TCMExit); message CM_EXIT; protected procedure UpClick (Sender: TObject); override; procedure DownClick (Sender: TObject); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property DefaultValue: Extended read GetDefaultValue write FDefaultValue; property FormatString: String read FFormatString write FFormatString; property Increment: Extended read FIncrement write FIncrement; property MaxValue: Extended read FMaxValue write FMaxValue; property MinValue: Extended read FMinValue write FMinValue; property Value: Extended read GetValue write SetValue; end; implementation uses System.Math, Winapi.CommCtrl, System.StrUtils, Vcl.Themes; { TIPSpinEdit } constructor TIPSpinEdit.Create(AOwner: TComponent); begin inherited Create(AOwner); FFormatString := DEFAULT_FLOAT_FMTSTR; FIncrement := 1.0; FMaxValue := 0.0; FMinValue := 0.0; FDefaultValue := -MaxInt; end; destructor TIPSpinEdit.Destroy; begin inherited Destroy; end; function TIPSpinEdit.GetDefaultValue: Extended; begin Result := Max(FDefaultValue, FMinValue); end; procedure TIPSpinEdit.UpClick (Sender: TObject); begin if ReadOnly then MessageBeep(0) else Value := Value + FIncrement; end; procedure TIPSpinEdit.DownClick (Sender: TObject); begin if ReadOnly then MessageBeep(0) else Value := Value - FIncrement; end; function TIPSpinEdit.GetValue: Extended; begin Result := StrToFloatDef(Text, DefaultValue); Result := CheckValue(Result); end; procedure TIPSpinEdit.SetValue (NewValue: Extended); begin if FFormatString.IsEmpty then Text := FloatToStr(CheckValue(NewValue)) else Text := FormatFloat(FFormatString, CheckValue(NewValue)) end; function TIPSpinEdit.CheckValue (NewValue: Extended): Extended; begin Result := NewValue; if (FMaxValue <> FMinValue) then begin if NewValue < FMinValue then Result := FMinValue else if NewValue > FMaxValue then Result := FMaxValue; end; end; procedure TIPSpinEdit.CMExit(var Message: TCMExit); begin inherited; if CheckValue(Value) <> StrToFloatDef(Text, Value + Integer(TextHint.IsEmpty)) then SetValue(Value); end; end. |
Этого уже достаточно, чтобы получить SpinEdit с плавающей запятой. Напишем небольшой тест. Бросим на форму три SpinEdit’а. Не забываем перед объявлением формы указать, кто теперь у нас TSpinEdit.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
unit UnMain01; interface uses System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls , IP76.Takers.Spin , Vcl.Samples.Spin ; type TSpinEdit = class(TIPSpinEdit); TFmMain = class(TForm) SpinEdit1: TSpinEdit; SpinEdit2: TSpinEdit; SpinEdit3: TSpinEdit; procedure SpinEdit1Change(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } end; var FmMain: TFmMain; implementation {$R *.dfm} procedure TFmMain.FormCreate(Sender: TObject); var i: Integer; begin for i := 0 to ComponentCount-1 do begin if Components[i] is TSpinEdit then TSpinEdit(Components[i]).Increment := 0.1; end; end; procedure TFmMain.SpinEdit1Change(Sender: TObject); begin if Sender is TSpinEdit then Caption := FloatToStr(TSpinEdit(Sender).Value); end; |
В обработчике OnChange для всех SpinEdit’ов пишем FloatToStr(TSpinEdit(Sender).Value), подразумевая, что значение Value уже вещественное. В событии OnCreate формы инициализируем свойство Increment значением 0.1 у всех найденных SpinEdit’ов. С помощью кнопок в компоненте и клавиш на клавиатуре значение меняется при каждом нажатии на 0.1, но можно ввести любое вещественное значение.
Про экономию времени и сил. На этот код у меня ушло 20 минут. На статью — три дня.
Написание статьи можно сравнить с написанием полноценного компонента. Нужно предусмотрительно закрыть все возможные вопросы. Неплохо бы дать компоненту достойное описание. Снабдить код внятными комментариями и предусмотреть что-то вроде {$IFDEF CompilerVersion >= 1x}. По итогу, трудозатраты вырастают от недели до месяца. Дедлайн, сроки, нервы, инфаркт, некрасивая медсестра.
Скачать вещественный SpinEdit
01. Исходники (Delphi XE 7-10) 62.1 Кб
01. Исполняемый файл 872 Кб
02. Вертикальное выравнивание SpinEdit
Вот тут могли бы начаться танцы с бубном, если бы давным-давно не разобрался с этой проблемой (все та же статья на habr’е). Для вертикального выравнивания используем EM_SETRECT. С помощью этого сообщения устанавливаем прямоугольник для ввода текста.
Дописываем в класс TIPSpinEdit метод SetRect.
1 2 3 4 5 6 7 8 9 10 |
// Установить прямоугольник ввода для элемента function TIPSpinEdit.SetRect(ARect: TRect): Boolean; begin if (not (csLoading in ComponentState)) and (Parent <> nil) then Result := Perform(EM_SETRECT, 0, LPARAM(@ARect)) > 0 else Result := False; end; |
Добавим новые свойства в наш вещественный SpinEdit.
1 2 3 4 5 6 7 8 9 10 |
published // горизонтальное выравнивание property Alignment; // вертикальное выравнивание property Layout: TVerticalAlignment read FLayout write SetLayout default taAlignTop; // отступы с краев property Margin: TxMargin read FMargin write SetMargin; |
Margin — это класс, описанный в IP76.Takers.Routines, задает отступы, чтобы текст не «прилипал» к краям компонента. Alignment существовал в скрытом protected состоянии. Layout — новое свойство вертикального выравнивания.
Для расчета прямоугольника ввода пишем метод CalcRect.
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 32 33 34 35 |
function TIPSpinEdit.CalcRect(ARect: TRect): TRect; var s : string; h : Integer; rct : TRect; begin Result := Rect(0,0,0,0); if Parent = nil then Exit; ARect.Right := ARect.Right - Button.Width - 2; ARect.Height := Height; rct := FMargin.CalcRect(ARect); if Integer(BorderStyle) > 0 then InflateRect(rct,0,-1); Result := rct; if Layout = taAlignTop then Exit; s := Text; if (s = '') and (not Focused) then s := TextHint; h := GetMinHeight(Font.Handle,False); case Layout of taVerticalCenter : H := rct.Top + (rct.Height - H) div 2; taAlignBottom : H := rct.Bottom - H; end; if (H > rct.Top) then rct.Top := H; result := rct; end; |
Как видим, в зависимости от значения Layout считается верхняя граница прямоугольника. Мы не выравниваем текст, мы выравниваем прямоугольник текста.
Пишем дополнительные методы для вызова SetRect. Вначале метод, непосредственно вызывающий переустановку прямоугольника.
1 2 3 4 5 |
procedure TIPSpinEdit.UpdateRect; begin SetRect(CalcRect(ClientRect)); end; |
И ряд методов, которые вызывают UpdateRect.
В секции private объявляем:
1 2 3 4 |
procedure CMRecreateWnd(var Message: TMessage); message CM_RECREATEWND; procedure WMSize(var Message: TWMSize); message WM_SIZE; procedure WMPaste(var Message: TWMPaste); message WM_PASTE; |
В секции protected объявляем:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Что-то изменилось в компоненте procedure Change; override; // Изменились размеры компонента procedure Resize; override; // Изменились отступы procedure ObjectChange(Sender: TObject); // Установить прямоугольник ввода для элемента function SetRect(ARect: TRect): Boolean; // Посчитать новый прямоугольник ввода function CalcRect(ARect: TRect): TRect; // Посчитать и установить новый прямоугольник ввода procedure UpdateRect; |
Реализация этих методов. Большинство из них просто события, в которых надо вычислить и установить прямоугольник ввода.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 |
procedure TIPSpinEdit.CMRecreateWnd(var Message: TMessage); begin inherited; UpdateRect; end; procedure TIPSpinEdit.WMSize(var Message: TWMSize); begin inherited; UpdateRect; end; procedure TIPSpinEdit.WMPaste(var Message: TWMPaste); var s: string; i: Integer; begin inherited; s := ''; for i := 1 to Length(Text) do if IsValidChar(Text[i]) then s := s + Text[i]; Text := s; end; procedure TIPSpinEdit.Change; begin inherited; if Parent <> nil then UpdateRect; end; procedure TIPSpinEdit.ObjectChange(Sender: TObject); begin if Sender = FMargin then UpdateRect; end; procedure TIPSpinEdit.Resize; begin inherited Resize; UpdateRect; end; |
Метод WMPaste выбивается из общей картины мира. Он зачем-то удаляет из текста переносы строк. Связано вот с чем. Чтобы установить прямоугольник ввода и клиентской области, учитывая наличие кнопок справа, SpinEdit вынужден в CreateParams добавлять флаг ES_MULTILINE. Если переопределить метод, и убрать этот флаг, окончание прямоугольника ввода «спрячется» за кнопками. Следовательно, при выравнивании по правому краю, мы увидим только часть значения и не увидим курсора.
Наличие ES_MULTILINE разрешает вставлять текст с переносами строк. Обработчик WMPaste преобразует текст с переносами в одну строку. Что интересно, родная для SpinEdit функция IsValidChar позволяет ввод вещественного числа. Возможно, изначально планировался все таки вещественный SpinEdit.
Конечно, не забываем добавить в конструктор создание класса отступов, а в деструкторе освободить его.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
constructor TIPSpinEdit.Create(AOwner: TComponent); begin inherited Create(AOwner); FFormatString := DEFAULT_FLOAT_FMTSTR; FIncrement := 1.0; FMaxValue := 0.0; FMinValue := 0.0; FDefaultValue := -MaxInt; FMargin := TxMargin.Create; FMargin.OnChange := ObjectChange; FLayout := taAlignTop; end; destructor TIPSpinEdit.Destroy; begin FreeAndNil (FMargin); inherited Destroy; end; |
Тестовое приложение
В тестовом приложении добавил ряд настроек, чтобы экспериментировать и проверять свойства.
Надо перевести фокус на нужный SpinEdit слева, установить свойства и нажать кнопку «Set …». В какой SpinEdit уйдут изменения написано на кнопках.
Сейчас верхний SpinEdit (1) выровнен по левому верхнему краю, средний(2) — по среднему, что по вертикали, что по горизонтали. Нижний(3) выровнен по правому нижнему краю. У всех SpinEdit’ов установлены отступы, равные 5 пикселям со всех краев.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
procedure TFmMain.SpinEdit1Enter(Sender: TObject); begin if Sender is TSpinEdit then SetLastSpinEdit(TSpinEdit(Sender)); end; procedure TFmMain.SetLastSpinEdit(const ASpinEdit: TSpinEdit); begin FLastSpinEdit := ASpinEdit; Button1.Enabled := Assigned(FLastSpinEdit); Button2.Enabled := Button1.Enabled; Button3.Enabled := Button1.Enabled; if Assigned(FLastSpinEdit) then begin Button1.Caption := 'Set Alignment to ' + FLastSpinEdit.Name; Button2.Caption := 'Set Layout to ' + FLastSpinEdit.Name; Button3.Caption := 'Set Margin to ' + FLastSpinEdit.Name; end else begin Button1.Caption := 'SpinEdit not selected...'; Button2.Caption := Button1.Caption; Button3.Caption := Button1.Caption; end; end; procedure TFmMain.Button2Click(Sender: TObject); begin if Assigned(FLastSpinEdit) then begin if Sender = Button1 then begin if RadioButton5.Checked then FLastSpinEdit.Alignment := taLeftJustify; if RadioButton6.Checked then FLastSpinEdit.Alignment := taCenter; if RadioButton7.Checked then FLastSpinEdit.Alignment := taRightJustify; end; if Sender = Button2 then begin if RadioButton8.Checked then FLastSpinEdit.Layout := taAlignTop; if RadioButton9.Checked then FLastSpinEdit.Layout := taVerticalCenter; if RadioButton10.Checked then FLastSpinEdit.Layout := taAlignBottom; end; if Sender = Button3 then begin FLastSpinEdit.Margin.Top := Round(SpinEdit4.Value); FLastSpinEdit.Margin.Left := Round(SpinEdit5.Value); FLastSpinEdit.Margin.Right := Round(SpinEdit6.Value); FLastSpinEdit.Margin.Bottom := Round(SpinEdit7.Value); end; end; end; |
Скачать 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.
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 32 33 34 35 |
procedure TIPSpinEdit.PaintWindow(DC: HDC); var rct: TRect; str: string; cnv: TCanvas; LStyle: TCustomStyleServices; FontColor: TColor; begin inherited PaintWindow(DC); str := Trim(TextHint); if Focused or (Text <> '') or (str.IsEmpty) then Exit; rct := CalcRect(ClientRect); cnv := TCanvas.Create; try cnv.Handle := DC; cnv.Font.Assign(Font); LStyle := StyleServices; if StyleServices.Enabled and (seFont in StyleElements) then FontColor := LStyle.GetStyleFontColor(sfEditBoxTextDisabled) else FontColor := clBtnShadow; cnv.Font.Color := FontColor; DrawTextEx(cnv, rct, str, Alignment, taAlignTop, False, False, Alignment <> taRightJustify); finally cnv.Free; end; end; |
Метод переопределяется исключительно для отрисовки TextHint. Поэтому, если элемент под фокусом, или Text не пуст, то сразу выходим. Если строка TextHint пуста, то есть рисовать нечего, также выходим.
У нас уже есть функция для вычисления текущего прямоугольника ввода при различных отступах/выравниваниях — CalcRect. Осталось только нарисовать текст с выравниванием по верхнему краю полученного прямоугольника. За это отвечает функция DrawTextEx из модуля IP76.Takers.Routines.
Напомню про метод, срабатывающий при потере фокуса элементом. Ранее его назначение было не раскрыто.
1 2 3 4 5 6 7 8 9 |
procedure TIPSpinEdit.CMExit(var Message: TCMExit); begin inherited; if CheckValue(Value) <> StrToFloatDef(Text, Value + Integer(TextHint.IsEmpty)) then SetValue(Value); end; |
Что происходит. При утере фокуса, если текстовая подсказка пуста, происходит вывод актуального значения. Для пустой строки это будет значение по умолчанию. Иначе, будет нарисован TextHint.
В тестовом приложении чуть дополню обработчик OnCreate формы. Для каждого найденного SpinEdit’а буду случайным образом генерировать значение по умолчанию и выводить его в текстовой подсказке. Исходник в следующем разделе.
Теперь удалю текст в SpinEdit’ах.
Как видно на рис.3, текстовая подсказка появилась почти у всех SpinEdit. TextHint выводится с тем же выравниванием и отступами, что и основной текст. Как будет продемонстрировано в коде ниже, для каждого третьего SpinEdit’а текстовая подсказка остается пустой. Если текстовая подсказка пуста, будет показано значение по умолчанию.
Поддержка стилей
Теперь надо поработать со стилями. Добавлю три темы в опциях проекта. Внизу формы сделаю панель с RadioButton’ами. Это будут переключатели тем.
В конструктор добавлю инициализацию RadioButton’ов заголовками тем. И обработаю клик на них.
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 32 33 34 35 36 |
procedure TFmMain.FormCreate(Sender: TObject); var i: Integer; c: TComponent; begin // Работа со стилями for i := 1 to Length(TStyleManager.StyleNames) do begin c := FindComponent('RadioButton'+IntToStr(i)); if Assigned(c) and (c is TRadioButton) then TRadioButton(c).Caption := TStyleManager.StyleNames[i-1]; end; RadioButton1.Checked := True; // Поиск и инициализация SpinEdit'ов for i := 0 to ComponentCount-1 do begin if Components[i] is TSpinEdit then with TSpinEdit(Components[i]) do begin Increment := 0.1; DefaultValue := random(50)/10 + 1; if i mod 3 <> 0 then TextHint := 'Default: ' + FormatFloat(FormatString, DefaultValue); end; end; end; // Выбор и установка темы procedure TFmMain.RadioButton1Click(Sender: TObject); begin if (Sender is TRadioButton) and TRadioButton(Sender).Checked then TStyleManager.SetStyle(TRadioButton(Sender).Caption); end; |
Как видим, на темах все работает, различимо. Но есть один маленький, но неприятный нюанс. При смене темы, вертикальное выравнивание сбрасывается. На рис.4. это видно по нижнему левому SpinEdit’у.
За событие смены стиля отвечает сообщение CM_STYLECHANGED. Добавим в секцию private объявление перехватчика.
1 |
procedure CMStyleChanged(var Message: TMessage); message CM_STYLECHANGED; |
И реализуем его.
1 2 3 4 5 6 |
procedure TIPSpinEdit.CMStyleChanged(var Message: TMessage); begin inherited; UpdateRect; end; |
Тестируем.
Все работает. Разговор про выравнивания можно считать закрытым.
Скачать 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.
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 |
// Скопировать свойства AEdit и если AWithFree, // уничтожить его и встать на его место function SetEdit(AEdit: PControl; const AWithFree: boolean = True): Boolean; overload; // См. выше function SetEdit(AEdit: TCustomEdit; const AWithFree: boolean = True): TCustomEdit; overload; // Создать экземпляр TIPSpinEdit, скопировать свойства AEdit, // уничтожить AEdit, встать на его место. // Если AEdit имеет свойства MaxValue, MinValue, Increment, // Value, DefaultValue - поделить их на ADivider class function TakeEdit(AEdit: TCustomEdit; const ADivider: Integer = 1): {$IFDEF SPIN_REPLACE}TSpinEdit {$ELSE}TIPSpinEdit{$ENDIF}; overload; // Создать экземпляр TIPSpinEdit, скопировать свойства AEdit, // уничтожить AEdit, встать на его место. // Проинициализировать свойства указанными значениями class function TakeEdit(AEdit: TCustomEdit; const AMinValue, AMaxValue, AIncrement, AValue: Extended; const ADefault: Extended = -MaxInt; const AFormatStr: String = DEFAULT_FLOAT_FMTSTR): {$IFDEF SPIN_REPLACE}TSpinEdit {$ELSE} TIPSpinEdit{$ENDIF}; overload; |
Директивы пусть пока не смущают. Про них разговор позже. Это для любителей классики хаков жанра.
Самый главный тут первый метод. Именно он делает всю работу.
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 |
function TIPSpinEdit.SetEdit(AEdit: PControl; const AWithFree: Boolean = True): Boolean; var EditName: string; begin Result := CopyControl(AEdit^, Self); if not Result then Exit; EditName := AEdit.Name; CopyProperty(AEdit^, Self, 'MaxValue'); CopyProperty(AEdit^, Self, 'MinValue'); CopyProperty(AEdit^, Self, 'Increment'); CopyProperty(AEdit^, Self, 'DefaultValue'); CopyProperty(AEdit^, Self, 'FormatString'); CopyProperty(AEdit^, Self, 'Layout'); CopyProperty(AEdit^, Self, 'Value'); if AWithFree then begin AEdit^.Free; Name := EditName; AEdit^ := Self; end; end; |
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
function TIPSpinEdit.SetEdit(AEdit: TCustomEdit; const AWithFree: boolean = true): TCustomEdit; begin Result := AEdit; if AEdit = nil then exit; if not SetEdit(@Result, AWithFree) then Result := nil; end; class function TIPSpinEdit.TakeEdit(AEdit: TCustomEdit; const ADivider: Integer = 1): {$IFDEF SPIN_REPLACE} TSpinEdit {$ELSE} TIPSpinEdit {$ENDIF}; var val: Extended; begin Result := TIPSpinEdit.Create(AEdit.Owner); TIPSpinEdit(Result).SetEdit(AEdit); if ADivider > 1 then begin val := TIPSpinEdit(Result).Value; Result.FMaxValue := Result.MaxValue / ADivider; Result.FMinValue := Result.MinValue / ADivider; Result.FIncrement := Result.Increment / ADivider; Result.FDefaultValue := Result.DefaultValue / ADivider; Result.Value := val/ADivider; end; end; class function TIPSpinEdit.TakeEdit(AEdit : TCustomEdit; const AMinValue, AMaxValue, AIncrement, AValue: Extended; const ADefault: Extended = -MaxInt; const AFormatStr: String = DEFAULT_FLOAT_FMTSTR): {$IFDEF SPIN_REPLACE} TSpinEdit {$ELSE} TIPSpinEdit {$ENDIF}; begin Result := TIPSpinEdit.Create(AEdit.Owner); Result.SetEdit(AEdit); Result.FFormatString := AFormatStr; Result.FMaxValue := AMaxValue; Result.FMinValue := AMinValue; Result.FDefaultValue := ADefault; Result.FIncrement := AIncrement; Result.Value := AValue; end; |
Функции CopyControl и CopyProperty находятся в модуле IP76.Takers.Routines.
CopyControl
Функция CopyControl осуществляет копирование минимального набора свойств из компонента ASource в компонент ADest.
1 |
function CopyControl(ASource, ADest: TControl): Boolean; |
Подразумевается, что всегда можно дописать тело SetEdit необходимыми свойствами, аналогично свойствам MaxValue, MinValue, Layout и т.д. Для конкретного проекта набор нужных свойств может быть разным.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
uses Vcl.StdCtrls, System.TypInfo; type TMyControl = class(TControl); TMyWinControl = class(TWinControl); function CopyControl(ASource, ADest: TControl): Boolean; var s, d: TMyControl; begin Result := (Assigned(ASource) and Assigned(ADest)); if not Result then Exit; s := TMyControl(ASource); d := TMyControl(ADest); d.AutoSize := s.AutoSize; d.BoundsRect := s.BoundsRect; d.Parent := s.Parent; d.Align := s.Align; d.Anchors := s.Anchors; d.Font.Assign(s.Font); d.Color := s.Color; d.Visible := s.Visible; d.Enabled := s.Enabled; d.Margins := s.Margins; d.AlignWithMargins := s.AlignWithMargins; d.StyleElements := s.StyleElements; d.OnClick := s.OnClick; d.OnDblClick := s.OnDblClick; d.OnMouseDown := s.OnMouseDown; d.OnMouseMove := s.OnMouseMove; d.OnMouseUp := s.OnMouseUp; d.Caption := s.Caption; if (ASource is TWinControl) and (ADest is TWinControl) then begin TMyWinControl(d).TabOrder := TMyWinControl(s).TabOrder; TMyWinControl(d).OnKeyPress := TMyWinControl(s).OnKeyPress; TMyWinControl(d).OnKeyDown := TMyWinControl(s).OnKeyDown; TMyWinControl(d).OnKeyUp := TMyWinControl(s).OnKeyUp; TMyWinControl(d).OnEnter := TMyWinControl(s).OnEnter; TMyWinControl(d).OnExit := TMyWinControl(s).OnExit; end; if (ASource is TCustomEdit) and (ADest is TCustomEdit) then begin TCustomEdit(d).Text := TCustomEdit(s).Text; end; CopyProperty(ASource, ADest, 'Alignment'); CopyProperty(ASource, ADest, 'TextHint'); CopyProperty(ASource, ADest, 'BorderStyle'); CopyProperty(ASource, ADest, 'OnChange'); CopyProperty(ASource, ADest, 'OnMouseEnter'); CopyProperty(ASource, ADest, 'OnMouseLeave'); end; |
CopyProperty
Функция CopyProperty предпринимает попытку скопировать значение свойства с именем ASourceName компонента ASource в свойство с именем APropName компонента ADest. При пустом ASourceName берется значение APropName.
1 2 |
function CopyProperty(ASource, ADest: TControl; const APropName: String; const ASourceName: String = ''): Boolean; |
Если предпринимается попытка присвоить целочисленному свойству значение с плавающей запятой, будет произведено округление. Объектные свойства и события также копируются.
Если указанных свойств не обнаружено, или их типы не совпадают, функция вернет FALSE.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
function CopyProperty(ASource, ADest: TControl; const APropName: String; const ASourceName: String = ''): Boolean; var ps, pd: PPropInfo; sName: String; begin Result := (Assigned(ASource) and Assigned(ADest)); if not Result then Exit; sName := ASourceName; if sName.IsEmpty then sName := APropName; ps := GetPropInfo(ASource.ClassInfo, sName); pd := GetPropInfo(ADest.ClassInfo, APropName); Result := (Assigned(ps) and Assigned(pd) and ((ps^.PropType^.Kind = pd^.PropType^.Kind) or ((ps^.PropType^.Kind = tkInteger) and (pd^.PropType^.Kind = tkFloat)) or ((ps^.PropType^.Kind = tkFloat) and (pd^.PropType^.Kind = tkInteger)) )); if not Result then Exit; case ps^.PropType^.Kind of tkMethod: SetMethodProp(ADest, APropName, GetMethodProp(ASource, sName)); tkClass: SetOrdProp(ADest, APropName, GetOrdProp(ASource, sName)); tkInteger: if pd^.PropType^.Kind = tkFloat then SetFloatProp(ADest, APropName, GetOrdProp(ASource, sName)) else SetOrdProp(ADest, APropName, GetOrdProp(ASource, sName)); tkFloat: if pd^.PropType^.Kind = tkInteger then SetOrdProp(ADest, APropName, Round(GetFloatProp(ASource, sName))) else SetFloatProp(ADest, APropName, GetFloatProp(ASource, sName)) else SetPropValue(ADest, APropName, GetPropValue(ASource, sName, False)); end; end; |
Практика #1. Edit+UpDown
Предположим, у меня есть три Edit в связке с UpDown. Чтобы выводить вещественные значения, ловлю OnClick c UpDown’ов, делю на 100 и вывожу в Edit вещественное значение. В Edit’ах ловлю OnChange, получаю текущий текст и присваиваю соответствующему SpinEdit’у. Все бы ничего, но если попытаюсь в Edit понажимать «вверх-вниз» значение будет браться из Position связанного UpDown, то есть текущая строка в Edit никак не учитывается. Поэтому надо выставить в Position нужное целочисленное значение при изменении содержимого.
По описанию выше возникает ощущение геморрности мероприятия?
И вот тут начинаются пляски с бубном, чего можно было бы избежать, будь у меня вещественный SpinEdit. Значения в Edit’ах не выровнены. Выглядит неаккуратно. Масса ненужного кода. Комментарии в коде оставлены для демонстрации начала плясок, даже до бубна.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
procedure TFmMain.UpDown1Click(Sender: TObject; Button: TUDBtnType); begin if not ((Sender is TUpDown) and Assigned(TUpDown(Sender).Associate) // and (TUpDown(Sender).Associate.Tag = 0) ) then Exit; // TUpDown(Sender).Associate.Tag := 1; // try TEdit(TUpDown(Sender).Associate).Text := FloatToStr(TUpDown(Sender).Position / 100); // finally // TUpDown(Sender).Associate.Tag := 0; // end; end; procedure TFmMain.Edit1Change(Sender: TObject); begin // if (Sender is TComponent) // and // (TComponent(Sender).Tag <> 0) // then // Exit; // TComponent(Sender).Tag := 1; // try if Sender = Edit1 then begin SpinEdit1.Text := Edit1.Text; // if Assigned(UpDown1) then // UpDown1.Position := Round(StrToFloatDef(Edit1.Text,UpDown1.Position/100)*100); end; if Sender = Edit2 then begin SpinEdit2.Text := Edit2.Text; // if Assigned(UpDown2) then // UpDown2.Position := Round(StrToFloatDef(Edit2.Text,UpDown2.Position/100)*100); end; if Sender = Edit3 then begin SpinEdit3.Text := Edit3.Text; // if Assigned(UpDown3) then // UpDown3.Position := Round(StrToFloatDef(Edit3.Text,UpDown3.Position/100)*100); end; // finally // TComponent(Sender).Tag := 0; // end; end; |
Но у нас есть SpinEdit с плавающей запятой. Поэтому пишем метод TakeUpDowns и вызываем его по кнопке в интерфейсе «Make SpinEdit’s».
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 32 33 34 35 36 37 38 39 40 |
procedure TFmMain.TakeUpDowns; var UpDownList: TList; UpDown: TUpDown; Edit: TEdit; D: Integer; i: Integer; c: TComponent; begin UpDownList := TList.Create; try for i := 0 to ComponentCount - 1 do begin c := Components[i]; if (c is TUpDown) and (TUpDown(c).Associate is TEdit) then UpDownList.Add(c); end; for i := 0 to UpDownList.Count-1 do begin UpDown := TUpDown(UpDownList[i]); Edit := TEdit(UpDown.Associate); D := UpDown.Tag; if D < 1 then D := 1; with TIPSpinEdit.TakeEdit(Edit, UpDown.Min/D, UpDown.Max/D, UpDown.Increment/D, UpDown.Position/D) do begin Align := alClient; Layout := taVerticalCenter; end; FreeAndNil(UpDown) end; finally FreeAndNil(UpDownList); end; end; |
Что происходит. Бежим по всем UpDown’ам, и если с ними связан Edit, преобразуем этот Edit в «наш» вещественный SpinEdit. Параметры минимума, максимума, инкремента и т.д. для нового SpinEdit’а берутся из соответствующих полей UpDown’а. В качестве делителя берется значение свойства Tag. Затем этот UpDown уничтожается.
Обратите внимание, вначале формируется список из жертв. Только потом происходит пробег по этому списку. Если все делать сразу, надо понимать, что внутри тела цикла будут происходить по два удаления и одно создание за итерацию. Список компонент изменится. Адресация нарушится.
В итоге видим ряд аккуратно выровненных SpinEdit’ов, адекватно реагирующих на стрелки и клавиши, никаких половецких плясок не нужно. Событие OnChange с Edit’ов (которых уже не существует) перекочевало на SpinEdit’ы. После удаления всех ненужных закомментаренных участков обработчик приобретает следующий аккуратный вид.
1 2 3 4 5 6 7 8 9 |
procedure TFmMain.Edit1Change(Sender: TObject); begin if Sender = Edit1 then SpinEdit1.Text := Edit1.Text; if Sender = Edit2 then SpinEdit2.Text := Edit2.Text; if Sender = Edit3 then SpinEdit3.Text := Edit3.Text; end; |
Специально использую прямую адресацию к полям формы, чтобы продемонстрировать, что новые SpinEdit’ы расположены в «теле» ликвидированных Edit. Обработчик UpDown1Click становится ненужным вообще.
Практика #2. SpinEdit с плавающей запятой без хака
По большому счету, хак с одинаковым названием типа и не нужен. Закомментарим строку объявления типа перед формой.
1 2 3 4 5 |
type // TSpinEdit = class(TIPSpinEdit); TFmMain = class(TForm) |
Конечно, компилятор в паре мест ругнется. Например, в обработчике OnCreate формы нужно проверять уже не is TSpinEdit, а is TIPSpinEdit.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Поиск и инициализация SpinEdit'ов for i := 0 to ComponentCount-1 do begin if Components[i] is TIPSpinEdit then with TIPSpinEdit(Components[i]) do begin Increment := 0.1; DefaultValue := random(50)/10 + 1; if i mod 3 <> 0 then TextHint := 'Default: ' + FormatFloat(FormatString, DefaultValue); end; end; |
Перепишем обработчик события OnChange для SpinEdit’ов. Надо помнить, что после отмены хака для формы эти поля уже имеют стандартный тип TSpinEdit, т.е. MinValue, MaxValue, Increment и Value — целочисленные. Да и потом, хочется что-то более наглядное, чем просто вывод значения в заголовок. Поэтому кидаем на форму три компонента, умеющих показывать диапазон: Gauge, ProgressBar и TrackBar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TFmMain.SpinEdit1Change(Sender: TObject); var d: Extended; begin if Sender is TIPSpinEdit then with TIPSpinEdit(Sender) do begin d := (MaxValue - MinValue); if d < 0.001 then d := 100; if Sender = SpinEdit1 then Gauge1.Progress := Round(100 * (Value - MinValue) / d); if Sender = SpinEdit2 then ProgressBar1.Position := Round(ProgressBar1.Max *(Value - MinValue) / d); if Sender = SpinEdit3 then TrackBar1.Position := Round(TrackBar1.Max *(Value - MinValue) / d); end; end; |
Напишем метод TakeSpinEdits, который преобразует стандартные SpinEdit’ы в вещественные. Вызов по кнопке «Take SpinEdit’s».
1 2 3 4 5 6 7 |
procedure TFmMain.TakeSpinEdits; begin TIPSpinEdit.TakeEdit(SpinEdit1, -5, 5, 0.1, 0, 0, '##0.0'); TIPSpinEdit.TakeEdit(SpinEdit2, -5, 5, 0.1, 0, 0, '##0.0'); TIPSpinEdit.TakeEdit(SpinEdit3, -5, 5, 0.1, 0, 0, '##0.0'); end; |
Вот и все, собственно. Теперь SpinEdit’ы вещественные, с выравниваниями, отступами, текстовой подсказкой и прочими возможными плюшками, которые имеет смысл дописать.
Практика #3. Вещественный SpinEdit не только для Edit
Если все равно планируем использовать в run-time «наш» вещественный SpinEdit, мутить что-то с UpDown смысла никакого. Можно сделать также, как в TakeSpinEdits. Мы ведь знаем, какие компоненты должны подвергнуться преобразованию.
Более того, преобразованию могут быть подвергнуты и другие компоненты, не обязательно наследники TCustomEdit. Ведь компонент нам нужен в первую очередь для визуального проектирования.
Поэтому добавлю на форму три Edit‘а, StaticText, Label и Panel.
Обратите внимание, кнопки назначения выравнивания и отступов неактивны. Связано с тем, что мы отказались от хука, и пока не было преобразования в «наш» SpinEdit, это все еще стандартные SpinEdit’ы, у которых нет соответствующих свойств.
Для Edit’ов обработчик OnChange полностью аналогичен представленному выше.
1 2 3 4 5 6 7 8 9 10 |
procedure TFmMain.Edit4Change(Sender: TObject); begin if Sender = Edit4 then SpinEdit1.Text := Edit4.Text; if Sender = Edit5 then SpinEdit2.Text := Edit5.Text; if Sender = Edit6 then SpinEdit3.Text := Edit6.Text; end; |
Для остальных нет события OnChange. Зато есть OnClick.
1 2 3 4 5 6 7 8 9 10 |
procedure TFmMain.StaticText1Click(Sender: TObject); begin if Sender = StaticText1 then SpinEdit1.Text := StaticText1.Caption; if Sender = Label6 then SpinEdit2.Text := Label6.Caption; if Sender = Panel9 then SpinEdit3.Text := Panel9.Caption; end; |
Пишем метод TakeOtherSpinEdits, который вызывается по кнопке «Other SpinEdit’s».
1 2 3 4 5 6 7 8 9 10 11 |
procedure TFmMain.TakeOtherSpinEdits; begin TIPSpinEdit.TakeEdit(Edit4, -5, 5, 0.1, 0, 0, '##0.0'); TIPSpinEdit.TakeEdit(Edit5, -5, 5, 0.1, 0, 0, '##0.0'); TIPSpinEdit.TakeEdit(Edit6, -5, 5, 0.1, 0, 0, '##0.0'); TakeSpinEdit(StaticText1, -5, 5, 0.1, 0).OnChange := StaticText1Click; TakeSpinEdit(Label6, -5, 5, 0.1, 0).OnChange := StaticText1Click; TakeSpinEdit(Panel9, -5, 5, 0.1, 0).OnChange := StaticText1Click; end; |
Результат работы функции TakeSpinEdit — экземпляр TIPSpinEdit. Поэтому сразу привязываем его событие OnChange к обработчику StaticText1Click
Функцию TakeSpinEdit пишем тут же, в этом модуле, чуть выше.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function TakeSpinEdit(AControl: TControl; const AMinValue, AMaxValue, AIncrement, AValue: Extended): TIPSpinEdit; begin Result := TIPSpinEdit.Create(ACOntrol.Owner); Result.SetEdit(@AControl); with Result do begin FormatString := '##0.0'; MaxValue := AMaxValue; MinValue := AMinValue; DefaultValue := 0; Increment := AIncrement; Value := AValue; Layout := taVerticalCenter; end; end; |
Легальным методом TIPSpinEdit.TakeEdit воспользоваться не можем (можем, усложнять не хотелось), поэтому написали обертку для SetEdit.
Компоненты на нижней панели выровнены с использованием свойств Margins и AlignWithMargins. Выравнивание при преобразовании сохранено. Для вновь созданных SpinEdit’ов использовано то выравнивание Alignment, которое было настроено в компоненте. Если свойство есть, то CopyProperty его обработает.
Директивы
Метод придумал давно. И споров по его поводу возникало немало. Есть у меня такой условно упертый оппонент — собирательный персонаж, который горой за описанный выше хак. Лично мне этот хак не нравится. По той простой причине, что люблю контролировать процесс. Но для любителей острых ощущений и неприятных сюрпризов, ввел такую запись для типа.
1 2 3 4 5 6 7 |
type {$IFDEF SPIN_REPLACE} TSpinEdit = class(Vcl.Samples.Spin.TSpinEdit) {$ELSE} TIPSpinEdit = class(Vcl.Samples.Spin.TSpinEdit) {$ENDIF} |
Соответственно, где-то выше должна быть объявлена директива SPIN_REPLACE (у меня она закомментарена в коде). Или указать в опциях проекта.
1 2 3 4 5 6 7 |
//{$DEFINE SPIN_REPLACE} interface uses ... |
После объявления самого типа такой фрагмент
1 2 3 4 5 6 7 8 |
{$IFDEF SPIN_REPLACE} TIPSpinEdit = TSpinEdit; {$ELSE} // Лучше эту строку делать до объявления формы, в модуле формы // TSpinEdit = class(TIPSpinEdit); {$ENDIF} |
При таком подходе становится важен порядок следования модулей в предложении interface uses. Чтобы компилятор взял последнее описание для SpinEdit. Одним словом, не рекомендую, но ради порядка упомянул.
Краткая инструкция по использованию
IPSpinEdit — это не компонент, это род imposter, семейства taker’ов, маскирующийся под компонент. Требуется инициализация в run-time.
- Надо скопировать каталог IP76.Takers. Поместить его в каталог проекта или группы проектов.
- Прописать путь к нему в опциях проекта. Лучше, если путь будет относительным.
- В модуле формы, где требуются вещественные SpinEdit’ы, в предложении interface uses прописать IP76.Takers.Spin.
- Если хотите, чтобы все SpinEdit’ы на форме стали вещественными, перед описанием класса формы необходимо указать фразу
1 |
TSpinEdit = class(TIPSpinEdit); |
Далее, например в 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 Мб
Роман, спасибо за очередную статью! Интересный подход, который, думаю, в принципе, применять к самым разнообразным компонентам (и в принципе, к классам). Иногда действительно такое бывает нужно.
Спасибо, Алексей! Твои комментарии как всегда мотивируют не бросать «писательство».
Хочется показать, что безудержная страсть к компонентам и компоненто-строительсту — это беда проектов любой степени сложности.
Хочется предложить простую и быструю альтернативу стандартному трудозатратному подходу.
Не хватает, конечно, критики и обсуждения. Прикручу возможно обсуждения к телеграму.