Direct2D — это загадочное и малоизученное мной существо семейства Microsoft, обитающее в недрах DirectX. Давно хотел познакомиться с ним поближе.
Плюсом Direct2D является, что он шустрый и запросто взаимодействует с GDI, GDI+. Минусом, то что он аппаратно-зависимый. Последнее утверждение — мое субъективное мнение. Аппаратное ускорение — это супер, но ведь не на всех аппаратах и не все аппараты адекватны. Делая коммерческий софт на Direct2D, надо сразу обзавестись службой психически устойчивой поддержки.
Для нормальной работы требуется DirectX 11 и выше, Windows 7 и выше. Есть конечно DirectX 10 и Vista, именно там зародился Direct2D, но ведь и то, и другое, врагу не пожелаешь.
Цели
Вообще, все это дико интересно. Появилось время, решил покопаться в этой теме. Для начала, хотелось разобраться со следующим:
- Оценить скорость Direct2D в условиях прикладной задачи.
- Попробовать всякие интересности, типа эффектов и «новый» контекст.
- Понять, как рисовать на битмапе, когда он постоянно пересоздается.
- Понять, как работать с текстом сверх тех возможностей, что дает Delphi. Для начала — вывести контур текста, потому что это красиво и стильно.
- Интересует работа с градиентом. Понятно, что тут, как и везде есть градиентные и радиальные кисти, а также текстурные. Пощупать вживую было б очень интересно.
- Разобраться как работать с изображением. Отображение, масштаб, гауссово размытие, аффинные преобразования.
- Попробовать эффект тени.
Что получилось, видно на заставке к статье.
Подготовка
Direct2D в Delphi представлен классом TDirect2DCanvas (Vcl.Direct2D) и заголовочным модулем Winapi.D2D1. Имеющийся в Delphi набор — это базовый интерфейс версии 1.0, которая ныне даже не упоминается в википедии.
По большому счету, стандартный набор методов TDirect2DCanvas, наследника TCustomCanvas, не особо интересен. Он давно знаком, делает то, что ожидаешь, и не делает того, что нужно мне. Интересны плюшки Direct2D от версии 1.1. С этим возникает проблема, т.к. заголовочных файлов для этой версии в Delphi нет.
Гугл поломался и таки позволил мне найти две альтернативы:
DirectX 12: Набор заголовочных файлов FreePascal/Delphi
SVGIconImageList. Работа с SVG и прочие прелести. Классная библиотека.
Первый неудобно использовать одновременно со стандартным Direct2D Delphi, т.к. типы имеют ожидаемо одинаковое название, и надо постоянно указывать — какой тип имеется ввиду. Зато это наиболее полная коллекция заголовочных файлов. Поэтому буду брать оттуда нужные мне описания.
У второго взял заголовочный файл Winapi.D2DMissing, который является замечательным расширением стандартного набора Delphi и отлично с ним ладит.
Контур текста
IDWriteTextLayout — ключевой интерфейс при отрисовке текста. Помимо текста, он содержит массу другой необходимой информации, включая форматирование, пользовательские эффекты, одним словом много всего. В обычном режиме, IDWriteTextLayout используется как параметр функции ID2D1RenderTarget.DrawTextLayout.
IDWriteTextRenderer — интерфейс, предоставляющий набор обратных вызовов, которые выполняют отрисовку текста, встроенных объектов и украшений, таких как подчеркивание и зачеркивание. IDWriteTextRenderer является параметром метода Draw интерфейса IDWriteTextLayout.
Чтобы осуществить собственную отрисовку текста, необходимо сделать наследника IDWriteTextRenderer, который будет рисовать текст как вам угодно, допустим, с контуром. Затем вызывать IDWriteTextLayout.Draw, указав его в качестве параметра.
IDWriteTextRenderer
Имеет на борту методы:
DrawGlyphRun IDWriteTextLayout.Draw вызывает эту функцию, чтобы нарисовать серию глифов. |
DrawInlineObject IDWriteTextLayout.Draw вызывает эту функцию, когда ему нужно нарисовать встроенный объект. |
DrawStrikethrough IDWriteTextLayout.Draw вызывает эту функцию, когда надо нарисовать зачеркивание. |
DrawUnderline IDWriteTextLayout.Draw вызывает эту функцию, чтобы нарисовать подчеркивание. |
Глиф «по жизни» — это векторная форма символа/части символа. Символ может состоять из нескольких глифов. По глифам и не только — статья «Практический взгляд на базовые термины и анатомию шрифтов».
Здесь же под глифом понимается 16-разрядный беззнаковый индекс в CMAP таблице. Чтобы его (их) получить для набора символов, надо использовать функцию IDWriteFontFace.GetGlyphIndices. В этой статье функция задействована не будет, но информация не лишняя.
Чтобы получить геометрию контура глифа(ов) надо использовать IDWriteFontFace.GetGlyphRunOutline. Вот этим сейчас и займемся.
Берем за основу статью MSDN. Это готовая реализация на С++ того, что нужно. А нужно нам нарисовать контур текста. Переписываем все на Delphi.
Для начала описание нового интерфейсного типа. Наследуем от IDWriteTextRenderer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
TOutlineTextRenderer = class(TInterfacedObject, IDWriteTextRenderer) private FFillDraw: Boolean; FOutlineWidth: Single; FRenderTarget: ID2D1RenderTarget; FOutlineBrush: ID2D1Brush; FFillBrush: ID2D1Brush; { IDWritePixelSnapping } function IsPixelSnappingDisabled(clientDrawingContext: Pointer; var isDisabled: BOOL): HResult; stdcall; function GetCurrentTransform(clientDrawingContext: Pointer; var transform: TDwriteMatrix): HResult; stdcall; function GetPixelsPerDip(clientDrawingContext: Pointer; var pixelsPerDip: Single): HResult; stdcall; { IDWriteTextRenderer } function DrawGlyphRun(clientDrawingContext: Pointer; baselineOriginX: Single; baselineOriginY: Single; measuringMode: TDWriteMeasuringMode; var glyphRun: TDwriteGlyphRun; var glyphRunDescription: TDwriteGlyphRunDescription; const clientDrawingEffect: IUnknown): HResult; stdcall; function DrawUnderline(clientDrawingContext: Pointer; baselineOriginX: Single; baselineOriginY: Single; var underline: TDwriteUnderline; const clientDrawingEffect: IUnknown): HResult; stdcall; function DrawStrikethrough(clientDrawingContext: Pointer; baselineOriginX: Single; baselineOriginY: Single; var strikethrough: TDwriteStrikethrough; const clientDrawingEffect: IUnknown): HResult; stdcall; function DrawInlineObject(clientDrawingContext: Pointer; originX: Single; originY: Single; var inlineObject: IDWriteInlineObject; isSideways: BOOL; isRightToLeft: BOOL; const clientDrawingEffect: IUnknown): HResult; stdcall; public constructor Create(ARenderTarget: ID2D1RenderTarget; AOutlineBrush: ID2D1Brush; AFillBrush: ID2D1Brush; AOutlineWidth: Single); end; |
Нам сейчас нужен только DrawGlyphRun. Именно в нем магия. Остальные методы можно дописывать по мере необходимости.
IDWriteTextRenderer.DrawGlyphRun
Метод DrawGlyphRun вызывается из IDWriteTextLayout.Draw для группы подряд идущих символов с одинаковыми свойствами, находящихся на одной базовой линии. Параметры baselineOriginX, baselineOriginY: Single указывают на начало такой группы по X и позицию по Y. Параметр clientDrawingContext : Pointer может содержать указатель на контекст, на котором требуется рисовать. Он может быть задан первым параметром функции IDWriteTextLayout.Draw. Параметр glyphRun: TDwriteGlyphRun — искомая группа глифов.
Алгоритм работы DrawGlyphRun таков:
- Создать геометрию ID2D1PathGeometry, открыть ее описатель ID2D1GeometrySink.
- Извлечь и поместить геометрию контура серии глифов glyphRun в описатель с помощью вышеупомянутой GetGlyphRunOutline.
- Закрыть описатель.
- Полученную геометрию необходимо перенести в baselineOriginX, baselineOriginY. Для этого инициализируется матрица переноса.
- Создать с помощью метода ID2D1Factory.CreateTransformedGeometry новую геометрию на основе ранее полученных геометрии и матрицы переноса .
- Нарисовать контур и залить новую геометрию.
Собственно, реализация
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
function TOutlineTextRenderer.DrawGlyphRun( clientDrawingContext: Pointer; baselineOriginX, baselineOriginY: Single; measuringMode: TDWriteMeasuringMode; var glyphRun: TDwriteGlyphRun; var glyphRunDescription: TDwriteGlyphRunDescription; const clientDrawingEffect: IInterface): HResult; var rt: ID2D1RenderTarget; PathGeometry: ID2D1PathGeometry; Sink: ID2D1GeometrySink; matrix: TD2DMatrix3x2F; TransformedGeometry: ID2D1TransformedGeometry; begin if Assigned(clientDrawingContext) then rt := ID2D1RenderTarget(clientDrawingContext) else rt := FRenderTarget; // 1. Создать и открыть геометрию Result := D2DFactory.CreatePathGeometry(PathGeometry); if SUCCEEDED(Result) then Result := PathGeometry.Open(Sink); if not SUCCEEDED(Result) then Exit; // 2. Извлечь геометрию контура группы глифов и // поместить в описатель геометрии Result := glyphRun.FontFace.GetGlyphRunOutline( glyphRun.fontEmSize, glyphRun.glyphIndices, glyphRun.glyphAdvances, glyphRun.glyphOffsets, glyphRun.glyphCount, glyphRun.isSideways, false, Sink ); // 3. Закрыть описатель Sink.Close(); if not SUCCEEDED(Result) then Exit; // 4. Инициализация матрицы переноса группы глифов // в требуемое место matrix := TD2DMatrix3x2F.Translation( baselineOriginX, baselineOriginY); // 5. Создать новую геометрию для нужного места Result := D2DFactory.CreateTransformedGeometry( PathGeometry, matrix, TransformedGeometry); if not SUCCEEDED(Result) then Exit; // 6. Нарисовать контур и залить группу глифов rt.DrawGeometry( TransformedGeometry, FOutlineBrush, FOutlineWidth); rt.FillGeometry(TransformedGeometry, FFillBrush); Result := S_OK; end; |
В реальности чуть по другому, чуть больше. Но, чтобы не растекаться мыслью по древу — здесь так, близко к тексту MSDN. Этот шаблон всегда можно модифицировать и дополнить. Допустим, в реальности, во-первых, можно оставить только контур, без заливки. Во-вторых, ну градиент, конечно. Все в исходниках по ссылке ниже.
За скобками
Группа методов от IDWritePixelSnapping стандартна, подвержена безболезненному копипасту. В той же статье от MSDN говориться, что лучше писать так, и все.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function TOutlineTextRenderer.IsPixelSnappingDisabled(clientDrawingContext: Pointer; var isDisabled: BOOL): HResult; begin isDisabled := False; Result := S_OK; end; function TOutlineTextRenderer.GetCurrentTransform(clientDrawingContext: Pointer; var transform: TDwriteMatrix): HResult; var matrix: PD2D1Matrix3x2F; begin matrix := @transform; FRenderTarget.GetTransform(matrix^); Result := S_OK; end; function TOutlineTextRenderer.GetPixelsPerDip(clientDrawingContext: Pointer; var pixelsPerDip: Single): HResult; var x, Unused: Single; begin FRenderTarget.GetDpi(x, Unused); pixelsPerDip := x/96; Result := S_OK; end; |
Остальные методы имеют только эту фразу: Result := E_NOTIMPL;
Это не означает, что так и надо. Просто они здесь не реализованы, о чем и сообщают.
Конструктор почти такой же, как в статье MSDN. Добавил только толщину контура.
1 2 3 4 5 6 7 8 9 10 11 12 |
constructor TOutlineTextRenderer.Create(ARenderTarget: ID2D1RenderTarget; AOutlineBrush: ID2D1Brush; AFillBrush: ID2D1Brush; AOutlineWidth: Single); begin inherited Create(); FRenderTarget := ARenderTarget; FOutlineBrush := AOutlineBrush; FOutlineWidth := AOutlineWidth; FFillBrush := AFillBrush; FFillDraw := Assigned(FFillBrush); end; |
Внутри класса ничего не создается. Все передается в конструктор. Если что-то поменялось, надо пересоздать. Это безболезненно, надежно и, главное, быстро.
Например, поменялась у меня ширина контура в главной форме. Будет вызван следующий метод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TFmMain.UpdateTextRenderer; var OutlineColor: TColor; begin OutlineColor := cbxOutlineColor.Selected; RecreateGradients; FOutlineBrush := nil; FCanvas.RenderTarget.CreateSolidColorBrush( D2d1ColorF(OutlineColor), nil, FOutlineBrush); FTextRenderer := nil; FTextRenderer := TOutlineTextRenderer.Create(FCanvas.RenderTarget, FOutlineBrush, FFillBrush, speOutlineWidth.Value); end; |
Т.е., из-за какой-то ширины будут пересозданы кисти контура и заливки. Часто ли происходит смена какого-либо из параметров, чтобы кардинально повлиять на скорость? Зато можно передавать в конструктор любые кисти, текстурные, градиентные, как того требует задача. Класс остается неизменным, он спокойно работает с тем, что дают.
Эффекты
В версии 1.1. Direct2D обзаводится новым контекстом для рисования ID2D1DeviceContext, который на самом деле наследник ID2D1RenderTarget. Появляется новое понятие — эффект. Представлен интерфейсом ID2D1Effect.
Эффект — это алгоритм преобразования одного и более входных изображений в одно. В частности, нам понадобятся размытие, тень и аффинное преобразование. Эффект может быть нарисован/воспринят, как изображение.
Описаний этих интерфейсов в Delphi нет, но они есть в Winapi.D2DMissing, про который говорилось вначале статьи.
Для создания эффекта требуется экземпляр ID2D1DeviceContext. На самом деле он у нас уже есть, это RenderTarget канвы. Просто контекст надо получить таким образом:
1 |
FCanvas.RenderTarget.QueryInterface(ID2D1DeviceContext, context) |
или
1 |
context := FCanvas.RenderTarget as ID2D1DeviceContext; |
Далее, вызываем метод ID2D1DeviceContext.CreateEffect. В методе указываем, какой эффект хотим получить, и получаем его. После этого инициализируем. Даем на вход изображение. Задаем свойства.
Хватит теорий. Рассмотрим, как «заблюрить» изображение.
GaussianBlur: Размыть изображение
Нам потребуется изображение FBitmap типа ID2D1Bitmap. Получить его из обычного TBitmap очень просто. За обычный битмап отвечает FSource . Должен быть предварительно созданный FWICImage типа TWICImage.
1 2 3 4 5 |
FWICImage: TWICImage; ... FWICImage.Assign(FSource); FCanvas.RenderTarget.CreateBitmapFromWicBitmap( FWICImage.Handle, nil, FBitmap); |
Займемся блюром. Степень размытия находится в переменной valBlur: single. За контекст ID2D1DeviceContext отвечает переменная context.
1 2 3 4 5 6 7 8 9 10 11 12 |
var blurEffect: ID2D1Effect; begin if Succeeded(context.CreateEffect( CLSID_D2D1GaussianBlur, blurEffect)) then begin blurEffect.SetInput(0, FBitmap as ID2D1Image); blurEffect.SetValue( Cardinal(D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION), D2D1_PROPERTY_TYPE_FLOAT, @valBlur, SizeOf(valBlur)); end; end; |
Что тут происходит. При удачном создании эффекта, даем на вход изображение FBitmap. Затем устанавливаем свойство размытия valBlur.
Вывод изображения:
1 |
context.DrawImage(blurEffect as ID2D1Image); |
Обратите внимание, сейчас эффект выступает как изображение.
Отдельно эффект Гауссова размытия можно скачать из телеги. Там только блюр и больше ничего:
Shadow: Эффект тени
Это интересный эффект. Он не делает тень «под», он превращает в тень, все, что ему будет скормлено. Классный эффект, одним словом. Вот во что он превращает надпись
Очевидно, что ему надо скормить надпись, нарисовать, и потом поверх еще раз нарисовать эту надпись.
Но, об особенностях применения поговорим позже. Сейчас сам эффект.
1 2 3 4 5 6 7 8 9 10 |
if Succeeded(context.CreateEffect( CLSID_D2D1Shadow, shadowEffect)) then begin shadowEffect.SetInput(0, bitmapTarget as ID2D1Image); shadowEffect.SetValue( Cardinal(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION), D2D1_PROPERTY_TYPE_FLOAT, @valBlur, SizeOf(valBlur)); shadowEffect.SetValue(Cardinal(D2D1_SHADOW_PROP_COLOR), D2D1_PROPERTY_TYPE_VECTOR4, @clrShadow, SizeOf(clrShadow)); end; |
Что тут происходит. На вход эффекту скармливаем изображение bitmapTarget типа ID2D1Bitmap, в котором находится картинка надписи. Тень строится по всему, что не альфа(0), т.е., что непрозрачно. Далее задается свойство размытия значением valBlur. И, наконец, свойство цвета тени clrShadow, тип TD2D1_VECTOR_4F.
AffineTransform: Аффинные преобразования на плоскости
Куда ж без них. Этот эффект нужен здесь, чтобы сместить тень.
1 2 3 4 5 6 7 8 9 10 |
if Succeeded(context.CreateEffect( CLSID_D2D12DAffineTransform, affineTransformEffect)) then begin matrix := TD2DMatrix3X2F.Translation(speX.Value, speY.Value); affineTransformEffect.SetValue( Cardinal(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX), D2D1_PROPERTY_TYPE_MATRIX_3X2, PByte(@matrix), SizeOf(D2D_MATRIX_3X2_F)); SetInputEffect(affineTransformEffect, 0, shadowEffect, True); end; |
Вначале инициализируем матрицу трансформации. Задавая тем самым любую из возможных трансформаций или композицию их. Затем устанавливаем матрицу в эффект. И потом на вход подаем уже созданный эффект тени. На самом деле подается результат работы эффекта, но все равно, все очень просто и со вкусом. Мощно и красиво.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
procedure SetInputEffect(effect: ID2D1Effect; index: UINT32; inputEffect: ID2D1Effect; invalidate: longbool=TRUE); var output: ID2D1Image; begin output := nil; if Assigned(inputEffect) then inputEffect.GetOutput(output); if Assigned(output) then effect.SetInput(index, output, invalidate); output := nil; end; |
Компоновка
Теперь надо все аккуратно увязать друг с другом.
Рисовать будем в обработчике OnPaint компонента pb: TPaintBox. Считаем, что у нас уже есть FCanvas типа TDirect2DCanvas, который пересоздавать мы не собираемся, т.к. это занимает приличное время. Шаблон таков.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var bmp: TBitmap; rct: TRect; begin rct := pb.ClientRect; bmp := CreateBmpRect(rct, pf32bit); // 1. инициализация тут FCanvas.BeginDraw; try // 2. вся отрисовка тут finally FCanvas.EndDraw; pb.Canvas.Draw(0,0,bmp); FreeAndNil(bmp); end; end; |
Здесь каждый раз создается новый битмап. Надо заставить холст безболезненно рисовать на каждом таком битмапе.
Почему так. Рисовать ведь можно и без создания промежуточных битмап.
Вообще, когда технология умеет рисовать на любой поверхности, это весьма расширяет горизонты. Если для рисования не нужно hwnd-окно или визуальный TControl, когда достаточно только DC:HDC, это позволит выводить рисунок куда угодно. Лично мне нужно даже не столько скорость интерфейса, сколько графические возможности доступные сразу из коробки.
Есть столько интересных задач, связанных с обработкой фотографий, например.
Далее будет небольшой практический пример применения, правда, не с фото связанный.
Создавать холст предлагается такой процедурой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TFmMain.InitCanvas(const ACanvas: TCanvas; const ARect: TRect); begin if Assigned(FCanvas) then Exit; FCanvas := TDirect2DCanvas.Create(ACanvas, ARect); FCanvas.Font.Assign(pb.Font); if Assigned(FTextFormat) then Exit; FTextFormat := FCanvas.Font.Handle; FTextFormat.SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); FTextFormat.SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); end; |
Для рисования текста нужен экземпляр IDWriteTextFormat. Менять значения шрифта и выравниваний не предполагается, поэтому он также в глобальном единственном экземпляре. Дело в том, что при всяком обращении к свойству Font.Handle, будет создаваться новый объект, и хорошо если бы вместе с выравниванием, но ведь нет.
Одним словом, имеет смысл получить его один раз и все.
Инициализация
Проблема, как увязать единственный экземпляр FCanvas со всякий раз новым объектом bmp решается так.
При создании холста передаем любую канву.
1 |
InitCanvas(pb.Canvas, pb.ClientRect); |
А в обработчике в секции инициализации (1) уже назначаем вновь созданный битмап.
1 2 |
(FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( bmp.Canvas.Handle, bmp.Canvas.ClipRect); |
Теперь все рисунки пойдут в битмап bmp. И поэтому до момента отрисовки нам глубоко фиолетово, куда сейчас смотрит FCanvas.
Для рисования текста нужен IDWriteTextLayout. Смысла выносить в глобальные нет, это локальная переменная TextLayout обработчика. Создается так:
1 2 3 |
DWriteFactory.CreateTextLayout(PWideChar(FIPText), Length(FIPText), FTextFormat, bmp.Width, bmp.Height, TextLayout); |
Видим, задействован ранее созданный глобальный FTextFormat.
Получаем контекст и начинаем творить.
1 |
FCanvas.RenderTarget.QueryInterface(ID2D1DeviceContext, context); |
Все еще находясь в секции инициализации(1) обработчика вначале блюрим изображение с требуемыми параметрами. Это будет фон. Теперь надо сформировать тень.
Код указан выше. Есть нюанс. Перед тем, как что-то скормить тени, надо это что-то создать. В обычном варианте нужно было бы создать какой-то промежуточный битмап, в нем нарисовать текст и т.д. Для этих целей в Direct2D служит интерфейс ID2D1BitmapRenderTarget. Он необходим для промежуточного закадрового рисования. Его можно получить через ID2D1RenderTarget.CreateCompatibleRenderTarget. Далее просто рисуем текст в этом контексте, получаем на руки битмап с рисунком текста, освобождаем контекст.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TFmMain.CreateBitmapTarget(ATextLayout: IDWriteTextLayout; out bitmapTarget: ID2D1Bitmap); var bitmapRenderTarget: ID2D1BitmapRenderTarget; begin FCanvas.RenderTarget.CreateCompatibleRenderTarget(nil, nil, nil, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, bitmapRenderTarget); bitmapRenderTarget.BeginDraw; try ATextLayout.Draw(Pointer(bitmapRenderTarget), FTextRenderer, 0, 0); finally bitmapRenderTarget.EndDraw; bitmapRenderTarget.GetBitmap(bitmapTarget); end; end; |
Это тот самый битмап, которым кормим тень.
При рисовании текста используется наш класс TOutlineTextRenderer. Это глобальная переменная (поле формы) FTextRenderer: IDWriteTextRenderer. Создает и пересоздает экземпляр ранее описанный метод UpdateTextRenderer.
Рисование
Таким образом все готово. Компоновка такова:
- Пересоздать, если требуется, FTextRenderer через UpdateTextRenderer.
- Связать FCanvas с новой битмап bmp.
- Создать TextLayout.
- Получить context: ID2D1DeviceContext.
- Заблюрить фон.
- Получить изображение тени текста.
- Сместить тень эффектом аффинного преобразования.
- Рисуем фон.
- Рисуем тень.
- Рисуем текст.
- Завершение работы с ресурсами и освобождение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
procedure TFmMain.pbPaint(Sender: TObject); var bmp: TBitmap; rct: TRect; valBlur: single; clrShadow: TD2D1_VECTOR_4F; context: ID2D1DeviceContext; matrix: TD2DMatrix3X2F; bitmapTarget: ID2D1Bitmap; TextLayout: IDWriteTextLayout; blurEffect: ID2D1Effect; shadowEffect: ID2D1Effect; affineTransformEffect: ID2D1Effect; begin clrShadow := GetShadowColor; rct := pb.ClientRect; // 0. Создать битмап bmp := CreateBmpRect(rct, pf32bit); // 1. Пересоздать рендерер if (Sender <> pb) or (not Assigned(FTextRenderer)) then UpdateTextRenderer; // 2. Связать холст с битмапом (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( bmp.Canvas.Handle, bmp.Canvas.ClipRect); // 3. Создать текстовый слой DWriteFactory.CreateTextLayout(PWideChar(FIPText), Length(FIPText), FTextFormat, rct.Width, rct.Height, TextLayout); // 4. Получить контекст FCanvas.RenderTarget.QueryInterface(ID2D1DeviceContext, context); // 5. Заблюрить фон valBlur := speBlur.Value/10; if Succeeded(context.CreateEffect( CLSID_D2D1GaussianBlur, blurEffect)) then begin InitBitmap; blurEffect.SetInput(0, FBitmap as ID2D1Image); blurEffect.SetValue( Cardinal(D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION), D2D1_PROPERTY_TYPE_FLOAT, @valBlur, SizeOf(valBlur)); end; // 6. Получить изображение тени valBlur := speShadowBlur.Value/10; if Succeeded(context.CreateEffect( CLSID_D2D1Shadow, shadowEffect)) then begin CreateBitmapTarget(TextLayout, bitmapTarget); shadowEffect.SetInput(0, bitmapTarget as ID2D1Image); shadowEffect.SetValue( Cardinal(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION), D2D1_PROPERTY_TYPE_FLOAT, @valBlur, SizeOf(valBlur)); shadowEffect.SetValue(Cardinal(D2D1_SHADOW_PROP_COLOR), D2D1_PROPERTY_TYPE_VECTOR4, @clrShadow, SizeOf(clrShadow)); end; // 7. Сместить тень эффектом аффинного преобразования. if Succeeded(context.CreateEffect( CLSID_D2D12DAffineTransform, affineTransformEffect)) then begin matrix := TD2DMatrix3X2F.Translation(speX.Value, speY.Value); affineTransformEffect.SetValue( Cardinal(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX), D2D1_PROPERTY_TYPE_MATRIX_3X2, PByte(@matrix), SizeOf(D2D_MATRIX_3X2_F)); SetInputEffect(affineTransformEffect, 0, shadowEffect, True); end; FCanvas.BeginDraw; try // 8. Рисовать фон, масштаб по ширине if chbDrawPic.Checked and Assigned(blurEffect) then begin matrix := TD2DMatrix3X2F.Scale(rct.Width / FWICImage.Width, rct.Width / FWICImage.Width, D2D1PointF(0,0)); context.SetTransform(matrix); context.DrawImage(blurEffect as ID2D1Image); matrix := TD2DMatrix3X2F.Identity; context.SetTransform(matrix); end; // 9. Рисовать тень context.DrawImage(affineTransformEffect as ID2D1Image); // 10. Рисовать текст TextLayout.Draw(Pointer(context), FTextRenderer, 0,0); finally FCanvas.EndDraw; // 11. Завершение работы с ресурсами pb.Canvas.Draw(0,0,bmp); FreeAndNil(bmp); end; end; |
Полностью текст обработчика лучше копировать из исходников. Представленный листинг сильно сокращен. Если скопировать этот листинг, на Window 7 будет жесткий AV, т.к. экземпляр context не будет создан. Поэтому лучше скачать рабочий исходник.
Без промежуточных битмапов
Давайте посмотрим, как будет работать без промежуточных битмапов. Для этого у нас есть пункт контекстного меню в «рабочей» области pmiWithoutBitmap, который определяет режим «рисования». Если он с галочкой, значит рисуем прямо на канву pb.
Небольшие дополнения в код обработчика. Во-первых, битмап создается не всегда
1 2 3 4 |
// 0. Создать битмап if not pmiWithoutBitmap.Checked then bmp := CreateBmpRect(rct, pf32bit); |
Во-вторых, перенаправить холст либо на pb, либо на битмап
1 2 3 4 5 6 7 8 |
// 2. Связать холст с битмапом if Assigned(bmp) then (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( bmp.Canvas.Handle, bmp.Canvas.ClipRect) else (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( pb.Canvas.Handle, pb.Canvas.ClipRect); |
В-третьих, чуть подправить завершение
1 2 3 4 5 6 |
// 11. Завершение работы с ресурсами if Assigned(bmp) then begin pb.Canvas.Draw(0,0,bmp); FreeAndNil(bmp); end; |
Скорость ожидаемо возросла. Нет создания битмапов, нет дополнительной отрисовки средствами GDI на канву pb.
Также, скорость практически не меняется от размера окна. Даже растянутый на два монитора, Direct2D отлично держит скорость где-то около 10 миллисекунд (понятно, что на другой машине будут другие значения).
Копировать в буфер
Теперь обещанный пример. Зачастую надо скопировать изображение в буфер обмена для какой-то последующей обработки. Предлагается очень простой способ.
Есть пункт контекстного меню pmiCopy. Его обработчик:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure TFmMain.pmiCopyClick(Sender: TObject); begin FreeAndNil(FBuffer); FBuffer := CreateBmpRect(pb.ClientRect, pf32bit); try pbPaint(Sender); Clipboard.Assign(FBuffer); finally FreeAndNil(FBuffer); pbPaint(pb); end; end; |
И еще раз меняем перенаправление холста.
1 2 3 4 5 6 7 8 9 10 11 12 |
// 2. Связать холст с битмапом if Sender = pmiCopy then (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( FBuffer.Canvas.Handle, FBuffer.Canvas.ClipRect) else if Assigned(bmp) then (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( bmp.Canvas.Handle, bmp.Canvas.ClipRect) else (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( pb.Canvas.Handle, pb.Canvas.ClipRect); |
Теперь при выборе пункта меню Copy рендер рисует в битмап, который будет потом скопирован в буфер.
Таким образом, если создавая ID2D1HwndRenderTarget рендер, мы можем работать только с этим окном, и только в интерфейсе, то ID2D1DCRenderTarget позволяет рисовать не только на элементах интерфейса, но и на закадровых битмап.
Вылечить мерцание
На некоторых машинах наблюдается мерцание при отрисовке. Вот на такие случаи и рисуем на битмапе. У нас есть FBuffer: TBitmap, который используем для копирования содержимого. Используем его для еще одной цели.
В секции private формы пропишем следующее:
1 2 |
FOldWindowProc: TWndMethod; procedure NewWindowProc(var Message: TMessage); |
В событии OnCreate формы добавим такие строки:
1 2 |
FOldWindowProc := pnlArea.WindowProc; pnlArea.WindowProc := NewWindowProc; |
И реализация метода NewWindowProc:
1 2 3 4 5 6 7 8 9 10 11 |
type TMyPanel = class (TPanel); procedure TFmMain.NewWindowProc(var Message: TMessage); begin if (Message.Msg = WM_ERASEBKGND) and Assigned(FBuffer) then TMyPanel(pnlArea).Canvas.Draw(pnlArea.ClientRect.Left, pnlArea.ClientRect.Top, FBuffer) else FOldWindowProc(Message) end; |
Если FBuffer присутствует, то при очистке фона панели, на которой происходит рисование, на ее поверхность копируется ранее сохраненный битмап.
Сохранение буфера происходит в секции finally обработчика OnPaint:
1 2 3 4 5 6 7 8 9 10 11 |
// 11. Завершение работы с ресурсами if Assigned(bmp) then begin if (not Assigned(FBuffer)) then FBuffer := TBitmap.Create; if Sender <> pmiCopy then FBuffer.Assign(bmp); pb.Canvas.Draw(0,0,bmp); FreeAndNil(bmp); end else if Sender <> pmiCopy then FreeAndNil(FBuffer); |
Результат
Получилось такое небольшое приложение.
В заголовке время отрисовки в миллисекундах. Видно, что текущая отрисовка заняла 10.6 миллисекунд. Что неплохо при наличии изображения + размытия + масштаб + градиент + размытие на тень + контур.
Двойной клик по тексту откроет слева панель настроек, можно поменять значения свойств, посмотреть, как меняется время отрисовки. Чтобы вызвать отрисовку можно поиграть размерами окна, оценить время.
Пара слов, о моей субъективности насчет аппаратной зависимости.
На рисунке вид приложения, запущенного на тестовом ноуте 2014 года, при включенном Intel HD Graphics. Драйвера установлены самые последние. Это именно то, что назвал «аппаратной неадекватностью» в начале статьи. Понятно, что тут может быть масса причин, к железке отношения мало имеющих.
Все волшебным образом меняется, если на том же ноуте переключить на NVIDIA 710M.
Пользователю наплевать на причины, он купил софт и хочет видеть чудо, а не оправдания.
К слову сказать, больше ни один из трех мне доступных Intel HD Graphics’ов так себя не ведет. Но вот один такой нашелся. Правда, он самый древний из них.
Полезные ссылки
Render Using a Custom Text Renderer Та самая статья MSDN, на которую постоянно ссылаюсь в тексте. Посвящена созданию пользовательского рендерера. |
DirectWrite: CustomTextRenderer, Hit-Test Шикарная статья на русском. Рассказано не только о пользовательской отрисовке методом DrawGlyphRun, но и как сделать подчеркивание и hit-test’ы. |
Outline Text With DirectWrite Другой взгляд на отрисовку контура текста. Рендерера нет вообще, все в одной процедуре. |
Shadow effect Статья MSDN об эффекте тени. |
DirectX 12 Упоминается в начале статьи. Большая библиотека заголовочных файлов FreePascal/Delphi |
SVGIconImageList. Упоминается в начале статьи. Классная библиотека для работы с SVG. |
Общие сведения о геометрических контурах Статья MSDN об использовании объектов Direct2D для создания сложных рисунков |
Рисование с Direct2D Большая статья MSDN про рисование с Direct2D скажем так, «вообще». Линии, цвета, градиенты, фигуры и т.д. |
Геометрические элементы Direct2D и манипуляции над ними Очень классная статья на русском о геометрических путях и связанных с ними темами. |
Встроенные эффекты Справка MSDN. Список встроенных эффектов с описанием и ссылками. |
Про эффекты толковое смог найти только на MSDN. Вообще, информации по Direct2D в инете оказалось не так много, как хотелось бы.
В итоге штудировал MSDN.
Друзья, спасибо за внимание!
Надеюсь, на этом Direct2D для меня не заканчивается. Как только время позволит, обязательно покопаюсь еще. 66 эффектов, шутка ли! SVG опять же.
Да и Microsoft рекомендует работать с 2D графикой через Direct2D. Советует забывать про GDI и GDI+. Говорит, устарели…
Что узнаю интересного, обязательно расскажу.
Не пропустите, подписывайтесь на телегу. )))
Скачать
Исходники (Delphi XE 7-10) 200 Кб
Исполняемый файл 1.13 Мб
Если при запуске будет такой вид, без теней и блюра, это означает, что у Вас Windows 7 и DirectX 10. Следовательно, нет ID2D1DeviceContext, нет D2D1Effect. О чем свидетельствует надпись в левом нижнем углу окна программы. То есть, из доступного, только контур текста и градиент.
Необходимо накатить обновления из Windows Update. Есть инструкция для установки DirectX 11 под Windows 7. Должен быть предварительно установлен ServicePack 1.
Доброго дня. Не могу понять почему при заливки геометрии(FillGeometry), не работает кисть прозрачности(opacityBrush). Нет ли у вас примера работающего приложения применяющего этот метод?
Здравствуйте. Работающего примера под рукой нет. Это ведь про слои разговор (D2D1_LAYER_PARAMETERS)? Если нужна прозрачность кисти, можно просто установить это свойство кисти.
не совсем. в MSDN есть статья https://learn.microsoft.com/en-us/windows/win32/direct2d/opacity-masks-overview
суть в том что задаются две кисти одна с рисунком другая с маской прозрачности. Вот почему-то маска прозрачности не применяться.
В интернете есть пример но он тоже не работает почему-то.
Может дело в Win7 или Delphi10.3 rio
Действительно, не работает. Интересно будет потом поразбираться. Эта строка вместо FillGeometry — работает:
Спасибо тебе дорогой за науку