Мыльные пузыри в Delphi #1
Опубликовано 31.07.2011 г. 20:20
Наверное, многие видели заставку Мыльные пузыри в Windows (Vista/7). Если не видели, то можете просмотреть: Персонализация -> Заставка -> Мыльные пузыри. Я решил задаться вопросом, каким образом можно запрограммировать подобные пузыри с помощью Delphi. В наших руках есть хороший инструментарий - Direct2D. Это будет первая часть статьи, в которой будет рассматриваться реализация отрисовки таких пузырей. Движение же оставим на следующую часть.
Канву нашей формы мы заменим на Direct2D канву. Форму будем открывать в полный экран, при этом предварительно скопируем изображение экрана. Далее мы создадим два класса - TBubble и TBubbleManager. Класс TBubble будет описывать состояние объекта пузыря - его цвет, положение и т.п. Менеджер пузырей будет управлять отрисовкой и содержать все Direct2D ресурсы, такие как кисти и геометрии. Так что при запуске формы мы раскрываем ее на полный экран, копируем изображение экрана и создаем менеджер пузырей, в который передаем ссылку на канву, а вернее на RenderTarget:
Побольше оригинальных пузырей вы можете увидеть по этой ссылке. Из чего состоит изображение пузыря:
С помощью этого мы закрасим круг, но эллипс будет "вырезан". Теперь вопрос чем мы будем закрашивать круг. Нам потребуется кисть с линейным градиентом - FBubbleBrush. Поскольку мы не можем менять цвет такой кисти, то создавать ее потребуется при рисовании каждого пузыря. Поэтому ее создание вынесено в отдельную процедуру CreateBubbleBrush:
В красной рамке - оригинальный пузырь из заставки. Остальные пузыри - скриншот во время выполнения программы. Для тех кому интересен полный исходный код, то вы можете скачать его по этой ссылке. Там же вложен и скомпилированный файл для тех, кому интересно посмотреть на пузыри не компилируя программу.
procedure TScreenForm.FormCreate(Sender: TObject); var dth : HWND; dtdc : HDC; bm : TBitmap; begin SetWindowLong(handle,GWL_STYLE, WS_POPUP and WS_VISIBLE); top := 0; left := 0; width := Screen.Width; height := Screen.Height; FCanvas := TDirect2dCanvas.Create( handle ); FCanvas.RenderTarget.SetDpi(96,96); bm := TBitmap.Create(); try dth := GetDesktopWindow(); dtdc := getDC(dth); bm.Height := height; bm.Width := width; try BitBlt(bm.Canvas.Handle, 0, 0, width, height, dtdc, 0, 0, SRCCOPY); FDesktopBitmap := canvas.CreateBitmap(bm); finally ReleaseDC(dth, dtdc); end; FBubbleManager := TBubbleManager.Create(FCanvas.RenderTarget as ID2D1HwndRenderTarget); finally bm.Free(); end; end;Процедура рисования на форме будет сводится к паре вызовов - отрисовка фонового изображения, и вызов метода Render для менеджера пузырей:
procedure TScreenForm.FormPaint(Sender: TObject); begin FCanvas.BeginDraw(); try if assigned(FDesktopBitmap) then FCanvas.RenderTarget.DrawBitmap(FDesktopBitmap); FBubbleManager.Render(); finally FCanvas.EndDraw(); end; end;Кстати говоря о рисовании, не все методы рисования возвращают код возврата. И о возникновении ошибки можно судить только по возвращаемому результату EndDraw. Однако для совместимости с TCustomCanvas этот метод является процедурой а не функцией, и не возвращает кода ошибки. Не очень удобно. Можно таки было сделать хотя бы какой нить хук в виде события. Но в принципе никто не заставляет нас пользоваться методом EndDraw канвы, а можно напрямую использовать RenderTarget. На текущий момент класс TBubble выглядит следующим образом:
TBubble = class(TObject) strict private FColor : TColor; FXPos, FYPos : integer; function getPoint() : TPoint; public constructor Create(); procedure Change(); property Color : TColor read FColor; property Point : TPoint read getPoint; end;Основным тут является свойства Color & Point для определения местоположения и цвета пузырей. Рассматривать данный класс сейчас смысла нет, поскольку все параметры генерируются случайным образом. Теперь надо определиться как выглядят пузыри, и попробовать для начала воссоздать их в графическом редакторе, чтобы лучше понять как их нарисовать с помощью Direct2D. Ниже представлен рисунок с двумя пузырями: слева выше - пузырь из заставки Мыльные пузыри, ниже справа - пузырь нарисованный в графическом редакторе:

Побольше оригинальных пузырей вы можете увидеть по этой ссылке. Из чего состоит изображение пузыря:
- На заднем фоне мы видим отбрасываемую тень. Она имеет размытые границы.
- В верхней левой части пузыря мы видим белый блик в виде эллипса. Блик непрозрачен (возможно это косяк ;) )
- Само тело пузыря - Во первых круг пузыря заливается линейным градиентом, сверху вниз. Внизу цвет светлее. (ну или примерно так, на самом деле оно кажется сложнее).
- В верхней части пузыря, есть эллипс, прозрачность которого выше чем у остальной части, оно и формирует четкую верхнюю границу и "зазубрины" в середине
- На пузырь накладывается маска прозрачности - Середина как бы вырезается.
TBubbleManager = class(TObject) strict private FBubbles : TObjectList<TBubble>; FRenderTarget : ID2D1HwndRenderTarget; FBrush : ID2D1SolidColorBrush; FShadowBrush : ID2D1RadialGradientBrush; FBubbleBrush : ID2D1LinearGradientBrush; FBubbleOpacityBrush : ID2D1RadialGradientBrush; FBubbleGeometries : array[0..1] of ID2D1EllipseGeometry; FBubble : ID2D1GeometryGroup; FWhiteEllipse : ID2D1EllipseGeometry; procedure InitD2DResources(); procedure CreateBubbleBrush(aColor : TColor); public constructor Create(rt : ID2D1HwndRenderTarget); destructor Destroy(); procedure Render(); procedure ReArrange(); end;По ходу рассказа я расскажу зачем используются все члены класса. Сначала нам необходимо нарисовать тени для всех пузырей, потому что они находятся как бы на нижнем слое. Как нарисовать тень - самый простой способ - просто ткнуть кистью с круговым градиентом. Вообще сама кисть черная, но в центре прозрачность равна нулю, а на краях - единице. Тень также смещается от центра пузыря вправо-вниз. Метод InitD2DResources() создает все ресурсы, которые мы можем постоянно использовать, в том числе и кисть для тени. В данном методе я добавил вложенные процедуры для создания различных ресурсов. Кисть тени мы сохраним в private члене FShadowBrush. Создание кисти весьма простое - круговой градиент, а чтобы кисть была не черная, а сероватая, то мы просто уменьшаем прозрачность самой кисти.
procedure CreateShadowBrush(); var gStops : array[0..2] of TD2D1GradientStop; gStopsCollection : ID2D1GradientStopCollection; begin gStops[0] := D2D1GradientStop(0, D2D1ColorF(clBlack, 0.2)); gStops[1] := D2D1GradientStop(0.75, D2D1ColorF(clBlack, 0.17)); gStops[2] := D2D1GradientStop(1, D2D1ColorF(clBlack, 0)); FRenderTarget.CreateGradientStopCollection(@gStops, 3, D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, gStopsCollection); FRenderTarget.CreateRadialGradientBrush( D2D1RadialGradientBrushProperties(point(0,0), point(0,0), 110,110), nil, gStopsCollection, FShadowBrush); FShadowBrush.SetOpacity(0.5); end;Теперь отрисуем все тени. Отрисовка проводится в методе Render.
for b in FBubbles do begin mt := TD2DMatrix3x2F.Translation( Point(b.Point.X + 30 , b.Point.Y + 30)); FRenderTarget.SetTransform(mt); FRenderTarget.FillRectangle(rect(-120,-120,120,120), FShadowBrush); end;Мы используем сдвиг на (30,30) пикселей и закрашиваем квадрат, получая нужную нам круговую тень. Для блика используем обычную сплошную кисть:
FRenderTarget.CreateSolidColorBrush(D2D1ColorF(clWhite), nil , FBrush);И чтобы нарисовать блик нам понадобится закрасить обычный эллипс этой белой кистью. Геометрию эллипса мы тоже будем хранить - в FWhiteEllipse.
procedure CreateWhiteEllipse(); begin with ellipse do begin point := D2D1PointF(0, 0); radiusX := 20; radiusY := 7; end; factory.CreateEllipseGeometry(ellipse, FWhiteEllipse); end;здесь ellipse - локальная переменная типа TD2D1Ellipse. Чтобы нарисовать блик нам надо использовать две трансформации - перенос и поворот. Так что определим две матрицы поворота mr и переноса mt. Искомая трансформация будет их произведением.
mr := TD2DMatrix3x2F.Rotation(-25, 0, 0); mt := TD2DMatrix3x2F.Translation(b.Point.x - 35, b.Point.y - 70); FRenderTarget.SetTransform(mr * mt); FRenderTarget.FillGeometry(FWhiteEllipse, FBrush);Как видим мы повернули эллипс на -25 градусов, и передвинули на (-35; -70) пикселей от центра пузыря. Перемножение матриц доступно благодаря перегрузке оператора умножения. Теперь займемся самим пузырем. Геометрически он будет состоять из двух фигур, это круг и внутри него эллипс. Прозрачность у эллипса будет выше. Поскольку данные фигуры будут часто использоваться, то сохраним их в FBubbleGeometries. Создание фигур выглядит так:
procedure CreateBubbleGeometry(); begin with ellipse do begin point := D2D1PointF(0,0); radiusX := 100; radiusY := 100; end; Factory.CreateEllipseGeometry(ellipse, FBubbleGeometries[0]); with ellipse do begin point := D2D1PointF(0,-33); radiusX := 84; radiusY := 65; end; Factory.CreateEllipseGeometry(ellipse, FBubbleGeometries[1]); Factory.CreateGeometryGroup(D2D1_FILL_MODE_ALTERNATE, @FBubbleGeometries, 2, FBubble); end;Круг имеет радиус 100 пикселей и находится в центре пузыря (он собственно и есть пузырь (: ). Центр эллипс сдвинут вверх от центра на 33 пикселя. После создания двух фигур-эллипсов создается группа геометрий. Обратите внимание что метод заполнения указан как D2D1_FILL_MODE_ALTERNATE. В одной из предыдущих статей про Direct2D я уже приводил ссылку на иллюстрацию режимов заливки:

С помощью этого мы закрасим круг, но эллипс будет "вырезан". Теперь вопрос чем мы будем закрашивать круг. Нам потребуется кисть с линейным градиентом - FBubbleBrush. Поскольку мы не можем менять цвет такой кисти, то создавать ее потребуется при рисовании каждого пузыря. Поэтому ее создание вынесено в отдельную процедуру CreateBubbleBrush:
procedure TBubbleManager.CreateBubbleBrush(aColor: TColor); var gStops : array[0..1] of TD2D1GradientStop; gsc : ID2D1GradientStopCollection; endColor : TColor; begin endColor := aColor; gStops[0] := D2D1GradientStop(0, D2D1ColorF(aColor, 0.3)); gStops[1] := D2D1GradientStop(1, D2D1ColorF(endColor , 0.15)); FRenderTarget.CreateGradientStopCollection(@gStops,2, D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, gsc); FRenderTarget.CreateLinearGradientBrush( D2D1LinearGradientBrushProperties(point(0,-50), point(0,50)), nil, gsc, FBubbleBrush); end;Для градиента мы используем один цвет, но прозрачность "нижней" части круга будет выше. Кисть расчитана на радиус круга в 50 пикселей. Мы предполагаем использовать метод FillGeometry для заливки пузыря нашей кистью. Но вспоминаем о том, что нам необходимо также наложить прозрачность. Метод FillGeometry как раз имеет третий необязательный параметр - кисть маски прозрачности. Но не стоит обольщаться. Эта дополнительная кисть прозрачности может использоваться только когда в роли первой - рисующей кисти используется растровая кисть. Так что для накладывания маски прозрачности потребуется использовать слои. О создании слоя немного позже, а пока что вернемся к маске прозрачности. Для создания маски нам опять таки потребуется кисть с круговым градиентом. Цвет градиента здесь не важен, важна только составляющая прозрачности (альфа-канал). Эту кисть мы будем также использовать повторно, так что сохраним ее в FBubbleOpacityBrush.
procedure CreateBubbleOpacityBrush(); var gStops : array[0..2] of TD2D1GradientStop; gStopsCollection : ID2D1GradientStopCollection; bp : TD2D1BrushProperties; begin gStops[0] := D2D1GradientStop(0, D2D1ColorF(clWhite, 0)); gStops[1] := D2D1GradientStop(0.7, D2D1ColorF(clBlack, 0.2)); gStops[2] := D2D1GradientStop(1, D2D1ColorF(clBlack, 1)); FRenderTarget.CreateGradientStopCollection(@gStops, 3, D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, gStopsCollection); FRenderTarget.CreateRadialGradientBrush( D2D1RadialGradientBrushProperties(point(0,0), point(0,0), 100, 100), nil, gStopsCollection, FBubbleOpacityBrush); end;Градиент прозрачности имеет три позиции. С помощью этого мы как вы вырежем середину нашего круга, а края останутся какими были. Вы можете заметить, что в градиенте таки указываются цвета белый и черный. Но повторюсь цвет не важен, это сделано лишь с одной целью, чтобы маску можно было просто нарисовать на канве и посмотреть как она выглядит сама по себе. Ну и теперь непосредственно сама отрисовка пузыря:
- Используем трансформацию переноса в центр пузыря.
- Создаем кисть пузыря с новым цветом
- Создаем новый слой layer TD2D1Layer
- Заполняем свойства слоя - lp : TD2D1LayerParameters. Одним из членов данной структуры является кисть прозрачности, которую мы и указываем.
- Устанавливаем слой PushLayer(), непрозрачность кисти = 1. Отрисовываем группу геометрий пузыря. Внутренний эллипс получается незалитым. Т.е. имеем круг с дыркой.
- Уменьшаем непрозрачность кисти и закрашиваем внутренний эллипс.
- Добавляем изображение слоя на канву - PopLayer
mt := TD2DMatrix3x2F.Translation(b.Point); FRenderTarget.SetTransform(mt); CreateBubbleBrush(b.Color); FRenderTarget.CreateLayer(nil, layer); zeroMemory(@lp, sizeof(lp)); lp.contentBounds := D2D1RectF(-MINSHORT,-MINSHORT, MINSHORT, MINSHORt); lp.opacity := 1; lp.opacityBrush := FBubbleOpacityBrush; FRenderTarget.PushLayer(lp,layer); FBubbleBrush.SetOpacity(1); FRenderTarget.FillGeometry(FBubble, FBubbleBrush); FbubbleBrush.SetOpacity(0.5);; FRenderTarget.FillGeometry(FBubbleGeometries[1], FBubbleBrush); FRenderTarget.PopLayer();На этом рисование мыльных пузырей можно считать завершенным. Отрисованный пузырь выглядит следующим образом:

В красной рамке - оригинальный пузырь из заставки. Остальные пузыри - скриншот во время выполнения программы. Для тех кому интересен полный исходный код, то вы можете скачать его по этой ссылке. Там же вложен и скомпилированный файл для тех, кому интересно посмотреть на пузыри не компилируя программу.
01.08.2011 в 16:17
01.08.2011 в 22:02
17.12.2011 в 12:42
07.11.2012 в 17:44
13.11.2012 в 20:27
а информации по D2D в Delphi действительно не много.
04.03.2014 в 14:55
03.01.2017 в 13:43