:: MVP ::
|
|
:: RSS ::
|
|
|
Периодически в интернете на сайтах встречаются формы, в которых текстовые поля имеют фон, что визуально делает форму более привлекательной.
Или другой момент – форма аутентификации. Можно разместить текстовую надпись, поясняющую назначение поля, рядом с полем ввода текста, а можно
внутри него. Второй вариант выгладит более симпатично, и к тому же еще экономит место. Неплохо бы научиться делать так и в Delphi.
В своих приложениях я использую этот прием в ситуации, когда на форме есть обязательные для заполнения поля, в этом случае я вывожу на фоне
компонента соответствующую надпись. Далее я расскажу о том, как я это делаю, и о “подводных камнях”, встретившихся мне на пути. Пример к статье
я буду писать, используя технологию подмены класса, так что если кто с ней еще не знаком, читаем статью
"Технология подмены класса".
Для начала решим основную задачу – вывод изображения на фоне компонента TEdit.
unit BackgroundEdit;
interface
uses
Windows, Messages, Classes, StdCtrls, Graphics, Controls;
type
TEdit = class( StdCtrls.TEdit )
private
Bitmap: TBitmap;
hbs: Cardinal;
protected
procedure WndProc( var Message: TMessage ); override;
public
constructor Create( AOwner: TComponent ); override;
destructor Free;
end;
implementation
{ TEdit }
constructor TEdit.Create(AOwner: TComponent);
begin
inherited;
Bitmap := TBitmap.Create;
Bitmap.LoadFromFile( 'Background.bmp' );
hbs := CreatePatternBrush( Bitmap.Handle );
end;
destructor TEdit.Free;
begin
DeleteObject( hbs );
Bitmap.Free;
end;
procedure TEdit.WndProc(var Message: TMessage);
begin
inherited;
if Message.Msg = CN_CTLCOLOREDIT then
begin
SetBkMode( Message.WParam, TRANSPARENT );
Message.Result := hbs;
end;
end;
end.
|
Все гениальное просто, красивое и элегантное решение! На основе битмапа создаем кисть, которую применяем при получении сообщения
CN_CTLCOLOREDIT. Протестировав код в Windows 2000/XP убеждаемся, что все работает как надо, причем как в обычном приложении, так и в MDI.
А вот при тестировании кода в Windows Vista и выше начинаются сюрпризы. Для начала разберемся с обычным приложением. Потаскаем форму по
рабочему столу, причем сделаем это так, чтобы форма с расположенными на ней Edit’ами выходила за край монитора. Мы увидим, что фон Edit’а
стал смазанным, он почему-то не перерисовывается. Но вот если на Edit навести курсор мыши, все становится нормально. Вывод напрашивается
сам собой – нужно писать код, зависящий от платформы. Но это чуть позже, а пока надо решить проблему, и решение оказывается достаточно простым.
procedure TEdit.WndProc(var Message: TMessage);
begin
inherited;
if Message.Msg = CN_CTLCOLOREDIT then
begin
SetBkMode( Message.WParam, TRANSPARENT );
Message.Result := hbs;
end;
if Message.Msg = WM_ERASEBKGND then
InvalidateRect( Handle, nil, false );
end;
|
Все что нужно сделать – перерисовать компонент при получении им сообщения WM_ERASEBKGND. Ну а как же обстоят дела с MDI приложениями.
Проверяем и видим, что сюрпризы для нас еще не закончены (Microsoft любит удивлять!). Чтобы увидеть сюрприз, возьмем дочернюю форму и
переместим ее так, чтобы она, вместе с расположенными на ней Edit’ами, вышла за левый край формы. Пока все нормально, начинаем
перетаскивать форму обратно. То, что начинает происходить, лучше один раз увидеть! Компонент начинает перерисовываться, и фон рисуется
не от левого края компонента (который еще скрыт за левой границей родительской формы), а от начала видимой части компонента (той,
которая не скрыта за левой границей формы). По мере “выезжания” формы фон смещается к началу компонента, создается эффект анимации –
фоновое изображение скользит по канве компонента. Такого же эффекта можно добиться, если перекрыть форму с Edit'ами другой формой,
“заехав” на текстовые поля с левой стороны.
Первые впечатления повергают в глубокий ступор, приходящее в последствии осознание абсурда происходящего усиливает это состояние.
Меня до сих пор мучает вопрос – зачем нужно было столь кардинально менять поведение компонента?.. Становится очевидно, в момент
получения компонентом события CN_CTLCOLOREDIT нужно произвести ряд дополнительных вычислений перед отрисовкой фона: посмотреть,
не перекрыта ли левая часть компонента какой нибудь формой, а если нет, то не зашел ли компонент за левую границу главной формы проекта.
С учетом всего вышесказанного получается следующий модуль.
unit BackgroundEdit;
interface
uses
Windows, Messages, SysUtils, Classes, StdCtrls, CommCtrl, Graphics, Forms,
Dialogs, Controls;
type
TEdit = class( StdCtrls.TEdit )
private
IncShamanism, isMDI: boolean;
Bitmap, Bkg: TBitmap;
hbs: Cardinal;
ShowBkg: boolean;
EdgeX: integer;
function CalcOffsetX: integer;
function CalcOffsetY: integer;
procedure WMSetFokus( var Msg: TMessage ); message WM_SETFOCUS;
procedure WMKillFokus( var Msg: TMessage ); message WM_KILLFOCUS;
procedure WMSize( var Msg: TMessage ); message WM_SIZE;
protected
procedure WndProc( var Message: TMessage ); override;
procedure Change; override;
public
constructor Create( AOwner: TComponent ); override;
destructor Free;
end;
implementation
{ TEdit }
function EnableShamanism: boolean;
var
OsVer: OSVERSIONINFO;
begin
OsVer.dwOSVersionInfoSize := SizeOf( OSVERSIONINFO );
GetVersionEx( OsVer );
Result := OsVer.dwMajorVersion >= 6;
end;
procedure ClearBitmap( var Bmp: TBitmap );
begin
Bmp.Canvas.Brush.Color := clWhite;
Bmp.Canvas.FillRect( Rect( 0, 0, Bmp.Width, Bmp.Height ) );
end;
constructor TEdit.Create(AOwner: TComponent);
begin
inherited;
IncShamanism := EnableShamanism;
if IncShamanism then
isMDI := ( AOwner as TForm ).FormStyle = fsMDIChild;
Bitmap := TBitmap.Create;
Bitmap.LoadFromFile( 'Background.bmp' );
Bkg := TBitmap.Create;
Bkg.Width := Width;
Bkg.Height := Height;
Bkg.Canvas.Draw( 0, 0, Bitmap );
hbs := CreatePatternBrush( Bkg.Handle );
EdgeX := GetSystemMetrics( SM_CXEDGE );
ShowBkg := Text = '';
InvalidateRect( Handle, nil, false );
end;
destructor TEdit.Free;
begin
DeleteObject( hbs );
Bkg.Free;
Bitmap.Free;
end;
procedure TEdit.WndProc(var Message: TMessage);
var
LeftPos: integer;
offX, offY: integer;
begin
inherited;
if ( Message.Msg = CN_CTLCOLOREDIT ) and ShowBkg then
begin
SetBkMode( Message.WParam, TRANSPARENT );
if IncShamanism then
begin
if isMDI and Visible then
begin
LeftPos := 0;
offX := CalcOffsetX;
offY := CalcOffsetY;
DeleteObject( hbs );
ClearBitmap( Bkg );
if not ( Owner as TForm ).Active then
begin
with Application.MainForm do
begin
if Assigned( ActiveMDIChild ) then
if PtInRect( ActiveMDIChild.BoundsRect, Point( offX, offY ) ) and
PtInRect( ActiveMDIChild.BoundsRect, Point( offX, offY + Self.Height ) ) and
( offX >= ActiveMDIChild.BoundsRect.Left ) then
LeftPos := -( ActiveMDIChild.BoundsRect.Right - offX - EdgeX )
else
if PtInRect( ActiveMDIChild.BoundsRect, Point( offX, offY ) ) and
( offY + Self.Height - ActiveMDIChild.BoundsRect.Bottom <= 2 ) then
LeftPos := -( ActiveMDIChild.BoundsRect.Right - offX - EdgeX )
else
if PtInRect( ActiveMDIChild.BoundsRect, Point( offX, offY + Self.Height ) ) and
( ActiveMDIChild.BoundsRect.Top - offY <= 2 ) then
LeftPos := -( ActiveMDIChild.BoundsRect.Right - offX - EdgeX )
else
LeftPos := 0;
end;
end;
if offX < 0 then
LeftPos := offX + EdgeX;
Bkg.Canvas.Draw( LeftPos, 0, Bitmap );
hbs := CreatePatternBrush( Bkg.Handle );
end;
end;
Message.Result := hbs;
end;
if IncShamanism then
if Message.Msg = WM_ERASEBKGND then
InvalidateRect( Handle, nil, false );
end;
procedure TEdit.Change;
begin
inherited;
ShowBkg := ( Text = '' ) and ( not Focused );
end;
function TEdit.CalcOffsetX: integer;
var
CX_MAIN_FRAME: Cardinal;
begin
if ( Owner as TForm ).BorderStyle in [bsSingle, bsToolWindow] then
CX_MAIN_FRAME := GetSystemMetrics( SM_CXFIXEDFRAME )
else
CX_MAIN_FRAME := GetSystemMetrics( SM_CXFRAME );
Result := ( Owner as TForm ).BoundsRect.Left + CX_MAIN_FRAME +
Self.ClientToParent( Self.ClientRect.TopLeft, ( Owner as TForm ) ).X;
end;
function TEdit.CalcOffsetY: integer;
var
CX_MAIN_FRAME,
CY_CAPTION: Cardinal;
begin
if ( Owner as TForm ).BorderStyle in [bsSingle, bsToolWindow] then
CX_MAIN_FRAME := GetSystemMetrics( SM_CXFIXEDFRAME )
else
CX_MAIN_FRAME := GetSystemMetrics( SM_CXFRAME );
CY_CAPTION := GetSystemMetrics( SM_CYCAPTION );
Result := ( Owner as TForm ).BoundsRect.Top + CX_MAIN_FRAME + CY_CAPTION +
Self.ClientToParent( Self.ClientRect.TopLeft, ( Owner as TForm ) ).Y;
end;
procedure TEdit.WMSetFokus(var Msg: TMessage);
begin
inherited;
ShowBkg := false;
if not IncShamanism then
begin
Perform( WM_NCPAINT, 0, 0 );
InvalidateRect( Handle, nil, false );
end;
end;
procedure TEdit.WMKillFokus(var Msg: TMessage);
begin
ShowBkg := Text = '';
if not IncShamanism then
begin
Perform( WM_NCPAINT, 0, 0 );
InvalidateRect( Handle, nil, false );
end;
inherited;
end;
procedure TEdit.WMSize(var Msg: TMessage);
begin
inherited;
if Visible then
begin
Bkg.Width := Self.Width;
DeleteObject( hbs );
Bkg.Canvas.Draw( 0, 0, Bitmap );
hbs := CreatePatternBrush( Bkg.Handle );
end;
end;
end.
|
Краткое пояснение происходящего. Первым делом смотрим, не перекрыт ли компонент какой либо формой, а это может быть только в случае,
когда родительская форма Edit'а не активна (if not ( Owner as TForm ).Active). Если перекрыт, смотрим, компонент перекрыт "по всей
высоте", или нет. Если да, рисовать нужно с того места, где заканчивается правый край перекрывающей формы, если нет, то от начала
компонента. И вновь не обошлось без подвохов! Окна имеют скругленные края, а следовательно, их ширина не всегда одинакова (если
можно так сказать), это нужно учитывать в расчетах, но, к сожалению, я не знаю как это сделать. В итоге фон немного дергается, в случае,
когда верхняя граница перекрывающей формы находится чуть ниже верхней границы перекрытого Edit'а. Я специально не оптимизировал код,
чтобы нагляднее продемонстрировать эту ситуацию (ее обработка происходит во 2-ом и 3-ем условии). Зачем разработчикам понадобилось
так усложнять работу Edit'а остается загадкой.
Затем проверим, не зашел ли Edit за левую сторону главной формы MDI приложения, за это отвечает функция CalcOffsetX.
В этой функции предполагается, что владельцем Edit’а является форма. Это так, если Edit брошен на форму в дизайнере, если же он
создается в runtime, нужно самостоятельно указать в качестве владельца форму. Если функция возвращает отрицательное значение,
значит часть компонента (или он весь) скрыта за левой границей главной формы приложения, и надо учитывать это значение при
отрисовке фона.
В момент получения Edit’ом фокуса фон скрывается, и показывается в момент потери фокуса при условии, что в компоненте отсутствует
текст. Обработка события Change нужна для того, чтобы скрыть фон компонента, в котором присутствует текст, в момент создания формы.
Так как в Windows XP (возможно и в более ранних, я не тестировал) не происходит перерисовка компонента при получении и потере фокуса,
приходится позаботиться об этом самостоятельно. При изменении размеров компонента модифицируем кисть, но тут есть одни нюанс.
Приведенный выше код нужно применять тогда, когда в качестве фона используется рисунок (вертикальный градиент или повторяющийся
орнамент), и нам нужно залить им всю канву компонента. Если в качестве фона используется надпись, например “Обязательное поле”,
нам нужно чтобы надпись появлялась один раз в начале компонента, и тогда процедуру надо слегка изменить.
procedure ClearBitmap( var Bmp: TBitmap );
begin
Bmp.Canvas.Brush.Color := clWhite;
Bmp.Canvas.FillRect( Rect( 0, 0, Bmp.Width, Bmp.Height ) );
end;
{...}
procedure TEdit.WndProc(var Message: TMessage);
begin
inherited;
if ( Message.Msg = CN_CTLCOLOREDIT ) and ShowBkg then
begin
SetBkMode( Message.WParam, TRANSPARENT );
if IncShamanism then
begin
if isMDI and Visible then
begin
DeleteObject( hbs );
ClearBitmap( Bkg );
{...}
end;
end;
Message.Result := hbs;
end;
if IncShamanism then
if Message.Msg = WM_ERASEBKGND then
InvalidateRect( Handle, nil, false );
end;
{...}
procedure TEdit.WMSize(var Msg: TMessage);
begin
inherited;
if Visible then
begin
Bkg.Width := Self.Width;
DeleteObject( hbs );
Bkg.Canvas.Draw( 0, 0, Bitmap );
hbs := CreatePatternBrush( Bkg.Handle );
end;
end;
|
В листинге приведены только изменения, полный код модуля находится в примерах к статье.
Следует иметь ввиду, что этот способ не подходит для отрисовыки фона на компонентах, свойство Enabled которых равно false,
так как такие компоненты не получают сообщение CN_CTLCOLOREDIT.
Но не только поля типа TEdit могут быть обязательными для заполнения, это может касаться и других текстовых полей, например
TMemo. Принцип реализации примерно такой же, как в случае с TEdit, по этому приводить код в статье я не буду (смотрите пример
к статье), а вот о нюансах расскажу. Их меньше, и таких “подводных камней” как в пред идущем случае нас не ожидает.
Дополнительной обработки в Windiws XP, по тем же причинам, что и в случае с TEdit, требуют события WM_SETFOKUS и WM_KILLFOKUS.
В случае отрисовки на канве сплошного фона не потребуется дополнительного битмапа и обработки сообщения WM_SIZE, а вот при выводе
текста действовать придется по аналогии с TEdit.
.: Пример к данной статье :.
|
При использовании материала - ссылка на сайт обязательна
|
|