Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4484.html
 

Мыльные пузыри в Delphi #1

Опубликовано 31.07.2011 г. 20:20

Наверное, многие видели заставку Мыльные пузыри в Windows (Vista/7). Если не видели, то можете просмотреть: Персонализация -> Заставка -> Мыльные пузыри. Я решил задаться вопросом, каким образом можно запрограммировать подобные пузыри с помощью Delphi. В наших руках есть хороший инструментарий - Direct2D. Это будет первая часть статьи, в которой будет рассматриваться реализация отрисовки таких пузырей. Движение же оставим на следующую часть.

Канву нашей формы мы заменим на Direct2D канву. Форму будем открывать в полный экран, при этом предварительно скопируем изображение экрана. Далее мы создадим два класса - TBubble и TBubbleManager. Класс TBubble будет описывать состояние объекта пузыря - его цвет, положение и т.п. Менеджер пузырей будет управлять отрисовкой и содержать все Direct2D ресурсы, такие как кисти и геометрии. Так что при запуске формы мы раскрываем ее на полный экран, копируем изображение экрана и создаем менеджер пузырей, в который передаем ссылку на канву, а вернее на RenderTarget:
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. Ниже представлен рисунок с двумя пузырями: слева выше - пузырь из заставки Мыльные пузыри, ниже справа - пузырь нарисованный в графическом редакторе:
 
Побольше оригинальных пузырей вы можете увидеть по этой ссылке. Из чего состоит изображение пузыря:
  1. На заднем фоне мы видим отбрасываемую тень. Она имеет размытые границы.
  2. В верхней левой части пузыря мы видим белый блик в виде эллипса. Блик непрозрачен (возможно это косяк ;) )
  3. Само тело пузыря - Во первых круг пузыря заливается линейным градиентом, сверху вниз. Внизу цвет светлее. (ну или примерно так, на самом деле оно кажется сложнее).
  4. В верхней части пузыря, есть эллипс, прозрачность которого выше чем у остальной части, оно и формирует четкую верхнюю границу и "зазубрины" в середине
  5. На пузырь накладывается маска прозрачности - Середина как бы вырезается.
Логично предположить что проще всего нарисовать блик и тень, поэтому с них я и начал. Посмотрим на описание класса TBubbleManager:
    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;
Градиент прозрачности имеет три позиции. С помощью этого мы как вы вырежем середину нашего круга, а края останутся какими были. Вы можете заметить, что в градиенте таки указываются цвета белый и черный. Но повторюсь цвет не важен, это сделано лишь с одной целью, чтобы маску можно было просто нарисовать на канве и посмотреть как она выглядит сама по себе. Ну и теперь непосредственно сама отрисовка пузыря:
  1. Используем трансформацию переноса в центр пузыря.
  2. Создаем кисть пузыря с новым цветом
  3. Создаем новый слой layer TD2D1Layer
  4. Заполняем свойства слоя - lp : TD2D1LayerParameters. Одним из членов данной структуры является кисть прозрачности, которую мы и указываем.
  5. Устанавливаем слой PushLayer(), непрозрачность кисти = 1. Отрисовываем группу геометрий пузыря. Внутренний эллипс получается незалитым. Т.е. имеем круг с дыркой.
  6. Уменьшаем непрозрачность кисти и закрашиваем внутренний эллипс.
  7. Добавляем изображение слоя на канву - 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();
На этом рисование мыльных пузырей можно считать завершенным. Отрисованный пузырь выглядит следующим образом:

В красной рамке - оригинальный пузырь из заставки. Остальные пузыри - скриншот во время выполнения программы. Для тех кому интересен полный исходный код, то вы можете скачать его по этой ссылке. Там же вложен и скомпилированный файл для тех, кому интересно посмотреть на пузыри не компилируя программу.
Метки:  Direct2D 

Комментарии

karoziya
01.08.2011 в 16:17
Красота, спасибо за статью
ter
01.08.2011 в 22:02
да не за что (:
ter
17.12.2011 в 12:42
действительно странно ( : попробуйте найти различия между своим кодом и скачанным (:
Denis
07.11.2012 в 17:44
Уважение! Устал искать материал. Большое спасибо. автору, а то уже сил нет микрософтовский мануал переводить.
teran
13.11.2012 в 20:27
да не за что (:
а информации по D2D в Delphi действительно не много.
Vadim
04.03.2014 в 14:55
Спасибо за статью! Нашел то, что искал!
Алексей Кряжев
03.01.2017 в 13:43
Друзья, что на изображении показывает? у меня не выводится.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно