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

Direct2D и слоистые окна. Дубль 2.

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

В начале 2011 года я пробовал решить проблему рисования прозрачных окон с использованием Direct2D, в результате была написана статья "Direct2D и слоистые окна", но решения я так и не нашел. Недавно у статьи появился комментарий, направивший меня в нужном направлении, и я решил перечитать материалы, которые читал тогда. В итоге решение оказалось достаточно простым, видимо в то время сказался недостаток опыта.

Первоисточником явлась статья Кенни Кера "Слоистые окна в Direct2D". В статье приведены несколько вариантов, я остановился на варианте Direct2D и WIC, как наиболее мне знакомом (DX я не знаю, GDI тоже, так что юзай, что осталось (: ). В общем суть задачи - использовать альфа канал при рисовании на форме (сама форма как бы является прозрачной). Запрограммировать это действительно весьма просто, надо лишь следовать инструкциям автора. Единственная причина, по которой сразу все не заработало: устанавливать стиль окна WS_EX_LAYERED следует руками в коде, а не надеяться на установку свойств AlphaBlend формы.

Для обновления содержимого слоистого окна могут быть использованы две функции - UpdateLayeredWindow и UpdateLayeredWindowIndirect, разница между ними в том, что вторая инкапуслирует все параметры в виде одной структуры. Автор использует вторую функцию, которая не описана в Delphi, поэтому необходимо описать ее сигнатуру и параметры:

type
    TUpdateLayeredWindowInfo = record
        cbSize : DWORD;
        hdcDst : HDC;
        pptDst : PPoint;
        psize  : PSize;
        hdcSrc : HDC;
        pptSrc : PPoint;
        crKey  : TColorRef;
        pblend : PBlendFunction;
        dwFlags : DWORD;
        prcDirty : PRect;
    end;
    PUpdateLayeredWindowInfo = ^TUpdateLayeredWindowInfo;

function UpdateLayeredWindowIndirect(Handle: THandle;
                    info : PUpdateLayeredWindowInfo): Boolean; stdcall;
                    external user32 name 'UpdateLayeredWindowIndirect';

При старте формы устанавливаем стиль окна WS_EX_LAYERED, получаем ссылки на две фабрики: D2D и WIC. С помощью последней создаем битовую карту IWICBitmap, на корой и будем в дальнейшем рисовать. Далее необходимо заполнить пару вспомогательных структур, которые будет использованы при формировании параметра к UpdateLayeredWindowIndirect (позиция и размер окна), а также структуру TBlendFunction, определяющую алгоритм применения прозрачности. С помощью кода это реализуется следующим образом:

procedure TMainForm.FormCreate(Sender: TObject);
var exStyle : DWORD;
begin

    exStyle := GetWindowLongA(Handle, GWL_EXSTYLE);
    if (exStyle and WS_EX_LAYERED = 0) then
        SetWindowLong(Handle, GWL_EXSTYLE, exStyle or WS_EX_LAYERED);

    FD2DFactory := D2DFactory();
    FWicFactory := CreateComObject(CLSID_WICImagingFactory) as IWICImagingFactory;
    FWicFactory.CreateBitmap(width, height, @GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnLoad, FWicBitmap);

    FSourcePosition := point(0,0);
    FWindowPosition := point(0,0);
    FSize.cx := Width;      
    FSize.cy := Height;     

    FBlend.BlendOp := AC_SRC_OVER;
    FBlend.BlendFlags := 0;
    FBlend.SourceConstantAlpha := 255;
    FBlend.AlphaFormat := AC_SRC_ALPHA;

    Render();
end;

 В конце метода вызывается процедура Render, которая непосредственно рисует содержимое окна. Слоистые окна не используют сообщения WM_PAINT для рисования. Перед рисованием необходимо создать все зависимые от поверхности (renderTarget) объекты, как и саму поверхность. В отличие от HWNDRenderTarget, все оставшиеся поверхности не могут изменять свой размер, поэтому при изменении размеров формы их следует пересоздавать, а в вместе с ними и все зависимые объекты (кисти и т.п., т.е все то, что было создано с помощью данного экземпляра RenderTarget). Впрочем кажлый раз пересоздавать все это не надо, а лишь в том случае, если получен соответствующий код возврата при вызове EndDraw(), либо при смене размеров (возможно, конечно, есть  и другие варианты).

В общем говоря, теперь нам следует создавать поверхность рисования связанную с WIC-битовой картой, получить от нее интрефейс совместимый с GDI+, а также создать необходимые кисти. Для демонстрации прозрачности я создам простую круговую градиентную кисть, где в качестве градиента будет выступть прозрачность. Градиент создан с 4-мя ступенями, прозрачность изменяется от позиции 0.2 до 0.8 (т.е в центре круга у нас будет прозрачная зона, пропускающая клики мыши):

procedure TMainForm.CreateDeviceResources();
var pf : TD2D1PixelFormat;
    rtp : TD2D1RenderTargetProperties;
    bp : TD2D1BrushProperties;

    gsc : ID2D1GradientStopCollection;
    rgbp : TD2D1RadialGradientBrushProperties;
    gs : array[0..4] of TD2D1GradientStop;
begin
    pf.format    := DXGI_FORMAT_B8G8R8A8_UNORM;
    pf.alphaMode := D2D1_ALPHA_MODE_PREMULTIPLIED;

    rtp := D2D1RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT, pf, 0,0, D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE);


    FD2DFactory.CreateWicBitmapRenderTarget(FWicBitmap, rtp, FRt);
    FInteropTarget := FRt as ID2D1GdiInteropRenderTarget;

    //create radial gradiant brush
    rgbp := D2D1RadialGradientBrushProperties(D2D1PointF(200,200), D2D1PointF(0,0), 200,200);

    bp.opacity := 1;
    bp.transform := TD2DMatrix3x2F.Identity;

    gs[0] := D2D1GradientStop(0,   D2D1ColorF($50c739, 0));
    gs[1] := D2D1GradientStop(0.2, D2D1ColorF($50c739, 0));
    gs[2] := D2D1GradientStop(0.8, D2D1ColorF($50c739, 1));
    gs[3] := D2D1GradientStop(1,   D2D1ColorF($50c739, 1));

    FRt.CreateGradientStopCollection(@gs[0], 4, D2D1_GAMMA_2_2, D2D1_EXTEND_MODE_CLAMP, gsc);

    FRt.CreateRadialGradientBrush(rgbp, @bp, gsc, FBrush);
end;

Теперь дело за рисованием и обновлением окна, а именно метод Render(). С помощью созданной кисти мы рисуем круг на форме. Затем с помощью совместимого с GDI  интeрфейса ID2D1GdiInteropRenderTarget получаем контекст устройства и обновлем наше слоистое окно, используя нарисованное изображение, используя метод UpdateWindow().

procedure TMainForm.Render();
var dc : HDC;
    rc : TRect;
    hr : HResult;
begin
    ZeroMemory(@rc, sizeof(rc));
    CreateDeviceResources();

    FRt.BeginDraw();
    try
        FRt.Clear(D2D1ColorF(clRed, 0));

        //draw something here with transparency
        FRt.FillEllipse(D2D1Ellipse(point(200,200), 150,150), FBrush);

        FInteropTarget.GetDC(D2D1_DC_INITIALIZE_MODE_COPY, dc);
        UpdateWindow(dc);
        FInteropTarget.ReleaseDC(rc);
    finally
        hr := Frt.EndDraw();
    end;
end;

 UpdateWindow() сводит все параметры структуры TUpdateLayeredWindowInfo и вызывает UpdateLayeredWindowIndirect:

procedure TMainForm.UpdateWindow(sourceDC : HDC);
var info : TUpdateLayeredWindowInfo;
    e : cardinal;
begin
    ZeroMemory(@info, sizeof(info));
    with info do begin
        cbSize := sizeof(TUpdateLayeredWindowInfo);
        pptSrc := @FSourcePosition;
        pptDst := @FWindowPosition;
        psize  := @FSize;
        pblend := @FBlend;
        dwFlags := ULW_ALPHA;
    end;

    info.hdcSrc := SourceDC;
    if not UpdateLayeredWindowIndirect(handle, @info) then begin
        RaiseLastOSError();
    end;
end;

 В общем-то на этом все, и мы получаем форму в виде круга с прозрачностью:

 Для интереса я попробовал видоизменить визуализацию олимпийских колец, также используя слоистое окно с прозрачным фоном, результат вышел такой (кликабельно):

 Автору комментария Alex93, сподвигшего меня на повторное рассмотрение статьи, отдельное спасибо (:

Метки:  Direct2D  |  WIC 

Комментарии

Vad
24.12.2012 в 11:21
Не нашел, как написать в личку тут :)
Помогите, пожалуйста, с моим вопросом по Direct2D, а потом можно его и удалить...
В Delphi XE есть сэмпл в "RAD Studio\8.0\Samples\Delphi\VCL\Direct2D". Скомпилировал его под Win7 SP1 Pro - всё ок. Заметил странную вещь: если загрузить в проект картинку размером сопоставимым с разрешением монитора и потаскать ее за "таскабельный" кружок, то скорость отрисовки _заметно_ ниже с включенным режимом Direct2D, чем в режиме GDI. Это нормально разве?
teran
24.12.2012 в 17:53
Я честно сказать с GDI по скорости никогда не сравнивал, но на данную тему весьма много в сети писалось. Возможно, корень проблемы в использовании сглаживания, при котором скорости д2д может действительно оказаться меньше чем GDI, где сглаживания нет (хотя врядли это сказывается на изображениях).
попробовал запустить демку на рабочей машине, здесь встроенная графика от intel, картинка 1280х1024, разницы не вижу при перетаскивании.

в общем то я и не припомню, чтобы где то в документации говорилось о том, что д2д быстрее чем GDI. Там упор всегда делался на то, что это аппаратно ускоренный интерфейс, позволяющий рисовать более качественное изображение, чем GDI.

вдобавок еще вопрос в том, как это все в VCL реализовано. Насколько я помню там частенько при работе с д2д объекты кучами пересоздаются, по крайней мере не однокрано натыкался на места где было пересоздание объектов, когда можно было обойтись и без этого. вобщще поддержка D2D в Delphi сделана, как мне кажется, во многом "для галочки" и для качественного использования придется использовать нативные интерфейсы. впрочем, ситуация в GDI в целом такая же. Просто для Direct2D можно было больше DesingTime инструментариев сделать, а-дя редакторы градиентных кистей и так далее.
Vad
25.12.2012 в 08:55
Спасибо за ответ! Насколько я вижу в сэмпле не используется сглаживание, по-крайней мере линии такие же зубчатые, как и в GDI рисовании. Видеокарта достаточно мощная - GeForce GTX 560. Наверное, действительно причина в "при работе с д2д объекты кучами пересоздаются"...
Совет можно получить от Вас? Хотел использовать Direct2D для приложения типа Google Maps, т.е. картография + пользовательский слой с векторными объектами. Или лучше использовать GDI/GDI+ для этого, что посоветуете?
teran
25.12.2012 в 16:44
ну для картинок то сглаживане не используется я так понимаю само по себе. а что касается линий, то там на вкладочке одной есть use AntiAliasing, или что то подобное (демку уже удалил к сожалению), вот если галку эту поставить, то линии в д2д режиме будут красивые (лучше смотреть на кривых, конечно).
Что касается что лучше использовать, то честно сказать хз. я напрямую с GDI никогда и не работал.
сам по себе д2д удобен в работе, ибо манипулируешь объектами, и при этом имеешь достаточно широкий набор функций по рисованию + вектор (что вам и нужно). Ведь в d2d чтобы увеличить/переместить объект, нужно всего лишь применить соответствующую трансформацию (матрицу) к поверхности рисования (render target).
Но нужно быть готовым к тому, что встанет вопрос с производительностью (я когда рисовал мыльные пузыри, то натыкался на всякие камни) поэтому надо все таки весьма детально эту технологию изучить, чтобы добиться качественных результатов.
Vad
26.12.2012 в 09:03
Как бы то ни было, спасибо :)
Один нюанс еще, не подскажете, есть ли возможность загружать в д2д png/jpg из области памяти/стрима/блоба, что-то типа LoadFromMemory(mem: pointer; size: dword)? Так мало инфы по использованию д2д, что не нашел в сети ничего подобного :(
teran
26.12.2012 в 11:18
вопрос интересный. наверное надо копнуть в сторону WIC. пойду изучу вопрос ради интереса (:
teran
26.12.2012 в 15:34
вот собственно - http://teran.karelia.pro/articles/item_5782.html
Mortarez
16.05.2013 в 07:42
Реализация "дырявых окон" без слоистости, мб будет для кого-то полезно.

var
  prop: D2D1_RENDER_TARGET_PROPERTIES;
  Factory: ID2D1Factory;
  RenderTarget: ID2D1DCRenderTarget;
...
D2D1.D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       IID_ID2D1Factory, nil, Factory);
Prop := D2D1RenderTargetProperties();
prop.pixelFormat.format := DXGI_FORMAT_B8G8R8A8_UNORM;
Prop.pixelFormat.alphaMode := D2D1_ALPHA_MODE_PREMULTIPLIED;
prop.usage := D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE;
Factory.CreateDCRenderTarget(prop, RenderTarget);
Self.Repaint;
...
procedure TMain.FormPaint(Sender: TObject);
begin
RenderTarget.BindDC(self.Canvas.Handle, Self.ClientRect);
RenderTarget.BeginDraw;
RenderTarget.Clear(D2D1.D2D1ColorF(0, 0, 0, 1));
...
//Draw smthng
...
RenderTarget.EndDraw();
end;
...


В свойствах формы включить TransparentColor, и выбрать TransparentColorValue, в моем случае выбран черный.

За статьи по D2D лютое бешеное спасибо, они мне очень помогли.
Mortarez
18.05.2013 в 13:28
Добавлю, что, если требуется отрисовка картинки с переменной прозрачностью, то все-таки придется юзать слоистость. Если же одного уровня прозрачности хватает, то alphablend в свойствах формы с этим справится.
teran
19.05.2013 в 20:13
дак да, согласен с вами. если нужно заменить один цветы на "дырки" то можно использовать TransparentColor. но такое обычно дает не очень гладкий край.
Дмитрий
16.01.2014 в 05:49
С Direct2D дел не имел, но есть проблема, которая часто встречается. Суть проблемы - "вывод своего окна поверх всех окон в системе". Это не проблема, если только не требуется вывести свое окно поверх например игры. Полноэкранные приложения на DirectX не дают адекватно вывести такое окно. Собственно вопрос к автору: а возможно ли такое решение с помощью описанных методов? Возможно ли размещение на таком окне своих элементов управления (кнопок например)?
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно