[Из песочницы] Как в TMemo сделать вертикальное выравнивание, отступы и TextHint
Пролог
Итак, из-за чего все собственно и стряслось. Я пишу плагины для LedearTask. Также, в 2017 году, написал векторный редактор для турецкой фирмы станков по производству печатей MATUYA. Что LeaderTask, что MATUYA, выставили интересные требования — вертикальное выравнивание в многострочном редакторе, отступы и TextHint. TextHint имеется ввиду — такое серое приглашение ввести хоть что-нибудь, когда элемент ввода пуст.
LeaderTask: плагин «Лестница Целей» (ввод и хинт по центру в многострочном редакторе)
Matuya Stamp Systems (ввод текста выровнен по правому и нижнему краю)
Как известно, вертикального выравнивания, в отличии от горизонтального, в стандартном Windows API для контролов не существует.
Также, приглашения для ввода какой-то информации в многострочных контролах Windows не поддерживаются.
Таким образом, возникает дилемма –, а как? Хочется использовать стандартные компоненты, но при этом иметь расширенные возможности ввода текстовой информации.
Небольшое отступление
С некоторых, достаточно отдаленных, пор, я не пишу компоненты и не использую сторонние. В условиях вечного дедлайна намного выгодней, быстрее и экономичней все писать самому. То, что будет создано в real-time. Т.е. использовать богатые возможности дизайнера Delphi, но подменять классы «на лету» в real-time. Как это все выглядит, надеюсь описать подробнее чуть позже. Что это значит в текущем конкретном случае, опишу сейчас.
Что требуется
По сути, от меня требуется TMemo, в котором есть вертикальное выравнивание, скажем, свойство Layout, свойство TextHint — «приглашение» ввести какой-нибудь текст. Также, требуется сделать отступы сверху-снизу-слева-справа для ввода и отображения текста, чтоб он не «прилипал» к граням контрола.
Теория
Необходим TMemo с дополнительными возможностями. Очевидно, придется писать наследника от TMemo. Не от TCustomMemo, и уж тем более не от TCustomEdit. Нам нужен именно TMemo, потому что мы не собираемся писать библиотеку компонентов, а хотим сделать проект быстро и в срок.
Вертикальное выравнивание нам недоступно. Ну что ж, зато мы можем установить для контролов прямоугольник ввода текста. Это осуществляется следующим образом.
В нашем случае, это будет выглядеть так:
Perform(EM_SETRECT, 0, LPARAM(@ARect))
Где ARect –искомый прямоугольник для ввода текста. Очевидно, его верхняя грань должна зависеть от значения свойства Layout. Также, этот прямоугольник может определить и границы отступа от краев для ввода текста.
Далее, немного поковыряв исходники, находим виртуальный метод:
procedure PaintWindow(DC: HDC); virtual;
Он вызывается в любом случае, что для DoubleBuffered, что без него. Его вызов будет осуществлен при наличии csCustomPaint в ControlState компонента, после всех значимых отрисовок компонента. Его мы будем использовать для отрисовки TextHint.
Практика
Не буду останавливаться на некоторых, думаю, не особо интересных деталях реализации.
Для начала, у нас появляются следующие дополнительные свойства:
//-- выравнивание по вертикали ----------------------------------------
property Layout : TTextLayout read FLayout write SetLayout default tlTop;
//-- отступы от краев контрола, описание в исходниках ниже----------
property Margin : TxMargin read FMargin write SetMargin;
//-- серое приглашение ввести что-нибудь в пустой элемент ----------
property TextHint : string read FTextHint write SetTextHint;
И основные моменты.
1. Объявление класса безусловно таково:
type
TxIPMemo = class (TMemo)
2. В конструкторе пишем строку:
Constructor TxIPMemo.Create (AOwner : TComponent);
begin
inherited Create (AOwner);
//-- для того, чтобы метод PaintWindow был вызван ---------------
ControlState := ControlState + [csCustomPaint];
…
end;
3. Назначение прямоугольник ввода:
function TxIPMemo.SetRect (ARect : TRect) : boolean;
begin
result := Perform(EM_SETRECT, 0, LPARAM(@ARect))>0;
end;
4. Расчет прямоугольника ввода:
function TxIPMemo.CalcRect : TRect;
var s : string;
h : Integer;
rct : TRect;
begin
rct := Rect(0,0,ClientWidth,ClientHeight);
//-- вначале определяем смещения от краев контрола ----------------
rct.Top := rct.Top + FMargin.Top;
rct.Left := rct.Left + FMargin.Left;
rct.Right := rct.Right - FMargin.Right;
rct.Bottom := rct.Bottom - FMargin.Bottom;
//-- если выравнивание по верху - ничего не высчитваем, выходим ---
result := rct;
if Layout = tlTop then exit;
//-- битмап создается в конструкторе ------------------------------
FBitmap.Canvas.Font.Assign(Font);
s := Lines.Text;
//-- если строка пуста и нет фокуса берем значение хинта для расчетов
if (s = '') and (not Focused) then s := TextHint;
//-- вычисляем выосту текста --------------------------------------
h := CalcHeight(FBitmap.Canvas.Handle, WidthRect(rct)-2, s,
TrueWordWrap);
//-- находим смещение сверху для прямоугольника ввода -------------
case FLayout of
tlCenter : H := rct.Top + (HeightRect(rct) - H) div 2;
tlBottom : H := rct.Bottom - H;
end;
//-- небольша проверка на валидность ------------------------------
if (H > rct.Top) then
rct.Top := H;
result := rct;
end;
5. Что такое TrueWordWrap в вызове функции CalcHeight выше. Это метод, которые возвращает истинное значение переноса строк для TMemo, которое зависит на самом деле от ряда параметров:
function TxIPMemo.TrueWordWrap : boolean;
begin
result := not ((Alignment = taLeftJustify) and
(ScrollBars in [ssHorizontal, ssBoth]))
and (WordWrap or (Alignment <> taLeftJustify));
end;
6. Где происходит вызов перерасчета и назначения прямоугольника ввода:
procedure DoEnter; override;
procedure DoExit; override;
procedure Change; override;
procedure WMSetFont(var Message: TWMSetFont); message WM_SETFONT;
procedure CMRecreateWnd(var Message: TMessage); message CM_RECREATEWND;
7. Назначение свойства TextHint чрезвычайно просто, но с учетом того, если кому-то вдруг захочется регистрировать компонент:
procedure TxIPMemo.SetTextHint (Value : string);
begin
if FTextHint = Value then exit;
FTextHint := Value;
if not (csLoading in ComponentState) then
Change;
end;
8. Рисуем TextHint:
procedure TxIPMemo.PaintWindow(DC: HDC);
var rct : TRect;
str : string;
cnv : TCanvas;
begin
inherited PaintWindow(DC);
if Focused then exit;
str := Lines.Text;
if str <> '' then exit;
str := FTextHint;
if str = '' then exit;
rct := CalcRect;
InflateRect (rct, -2,-2);
cnv := TCanvas.Create;
cnv.Handle := DC;
cnv.Font.Assign(Font);
cnv.Font.Color := clBtnShadow;
DrawTextEx(cnv,rct,str,tlTop,Alignment,TrueWordWrap,false);
cnv.Free;
end;
9. Чтобы облегчить «подмену» стандартного TMemo на наш, есть следующий метод. Он забирает все основные события и свойства у переданного указателя на TCustomMemo. При выставленном флаге AWithFree, уничтожает экземпляр старого и становится на его место. Т.е., если в коде все завязано на «старый» TCustomMemo, ничего страшного не произойдет. Все будет работать с новым экземпляром, как со старым, за исключением тех моментов, когда будут использованы свойства, про которые вы и так в курсе, и обращаться к ним все равно придется как к TxIPMemo:
function TxIPMemo.SetMemo (AMemo : PMemoControl; AWithFree : boolean = true) : boolean;
var s : string;
begin
result := AMemo <> nil;
if not result then exit;
s := Amemo^.Name;
BoundsRect := AMemo^.BoundsRect;
Parent := AMemo^.Parent;
Align := Amemo^.Align;
Alignment := TxIPMemo(Amemo^).Alignment;
{$IFDEF VER_XE}
OnMouseEnter := TxIPMemo(Amemo^).OnMouseEnter;
OnMouseLeave := TxIPMemo(Amemo^).OnMouseLeave;
{$ENDIF}
BorderStyle := TxIPMemo(Amemo^).BorderStyle;
Font.Assign(TxIPMemo(Amemo^).Font);
Color := TxIPMemo(Amemo^).Color;
Visible := Amemo^.Visible;
TabOrder := TxIPMemo(Amemo^).TabOrder;
ScrollBars := TxIPMemo(Amemo^).ScrollBars;
WantTabs := TxIPMemo(Amemo^).WantTabs;
WantReturns := TxIPMemo(Amemo^).WantReturns;
WordWrap := TxIPMemo(Amemo^).WordWrap;
OnKeyPress := TxIPMemo(Amemo^).OnKeyPress;
OnKeyDown := TxIPMemo(Amemo^).OnKeyDown;
OnKeyUp := TxIPMemo(Amemo^).OnKeyUp;
OnChange := TxIPMemo(Amemo^).OnChange;
OnClick := TxIPMemo(Amemo^).OnClick;
OnMouseDown := TxIPMemo(Amemo^).OnMouseDown;
OnMouseMove := TxIPMemo(Amemo^).OnMouseMove;
OnMouseUp := TxIPMemo(Amemo^).OnMouseUp;
OnEnter := TxIPMemo(Amemo^).OnEnter;
OnExit := TxIPMemo(Amemo^).OnExit;
if AWithFree then begin
Amemo^.Free;
Name := s;
AMemo^ := self;
end;
end;
Как использовать
Предположим, что уже есть компонент TMemo на форме, которое Вы либо используете в динамике, подобно как выше-вкратце-описанные редакторы, либо статический компонент, созданный в дизайне. Это все не важно.
1. Объявляем где-то следующим образом:
FMemo : TxIPMemo;
2. В событии OnCreate формы, либо в её конструкторе, пишем следующее:
FMemo := TxIPMemo.Create (self);
//-- назначение дополнительных свойств ------------------
FMemo.TextHint := 'Введите сюда что-нибудь ...';
FMemo.Layout := tlCenter;
За уничтожение экземпляра класса не волнуемся — он будет гарантированно уничтожен в деструкторе.
3. Возможно, в том же обработчике FormCreate пишем:
FMemo.SetMemo (@Memo1);
Тем самым забирая свойства и события у Memo1, и уничтожая его, становясь на его место.
4. Собственно, и все. Назначаете необходимые свойства нашему компоненту. И пользуем TMemo с возможностями вертикального выравнивания, отступов (которые могут быть и отрицательными), приглашения для пользователя при пустом Text.
Окно демо-приложения
Эпилог
В исходниках, ссылка на которые ниже, все используемые в тексте вспомогательные функции представлены.
Версия компилятора определяется {$I} подключаемым файлом pro_param.inc.
Т.к., в силу специфики работы, придерживаюсь концепции один модуль — разные версии, представленные исходники компилируются в Delphi 7, Delphi XE 7 и Delphi 10.1 Berlin. По той простой причине, что Delphi 7 у меня куплен, а Delphi 10.1 Berlin — бесплатная лицензия для коммерческих продуктов. А заказчик в последнее время хочет быть абсолютно «белым». Поэтому не использую никаких платных библиотек.
Надеюсь, материал поможет в ситуациях разной степени тяжести.
Скачать:
xIPMemo
Demo D7, XE 7