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

DirectWrite: CustomTextRenderer, Hit-Test

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

Вторая статья по изучанию DirectWrite API. В первой части мы рассмотрели как выводить текст с простым и мульти-форматированием. В этой части рассмотрим, как реализовать собственную отрисовку символов (например, добавить обводку),  и добавить проверку клика пользователя в область символа.

 Использование CustomTextRenderer

Что бы добавить к тексту такие эффекты, как изменение размера или шрифта, его стиля и т.п. в прошлой статье мы использовали интерфейс IDWriteTextLayout. Таким образом мы могли изменять настройки текста, которые можно выполнить с помощью самого шрифта. Если же мы хотим изменить сам процесс рисования, тогда нам потребуется использовать интерфейс IDWriteTextRenderer. Под изменением процесса рисования я имею в виду, например, добавить обводку для символов, использовать градиентную кисть для текста, изменить цвет линии подчеркивания, использовать различные цвета для символов и т.п.

Реализацию  класса, поддерживающего этот интерфейс, мы должна выполнить сами, где и определить все наши действия по рисованию. Реализация эта веьсма проста. Интерфейс созданного объекта передается в TextLayout при рисовании. Т.е. мы реализуем интерфейс обратного вызова. При выводе текста после рассчета всех позиций символов DirectWrite  будет использовать наш объект для рисования текста. Рисование связано с термином глиф (glyph) - фактическое представление буквы шрифта. В TextLayout глифы с одинаковым форматированием объединяются в группы Glyph Run. И вот уже эти группы передаются в TextRenderer для рисования. Сам по себе интерфейс IDWriteTextRenderer содержит 4 функции:

  • DrawGlyphRun - вызывается для рисования группы символов, имеющих одинаковое форматирование в TextLayout
  • DrawUnderline - вызывается для отрисовки линии подчеркивания
  • DrawStrikethruogh - вызывается для рисования линии зачеркивани
  • DrawInlineObject - вызывается для встроенного объекта (изображения добавленного в текст)

Рисование происходит с помощью геометрий Direct2D. Для полученной группы глифоф мы запрашивем их контур в виде геометрии, и затем уже можем делать с ними что угодно, например, обводку и заливку градиентом. DirectWrite при этом указывает нам позицию для рисования символов. Здесь же мы можем применть различные трансформации, например, повернуть символы на какой-то угол.
Создание самого класса TTextRenderer я описывать не буду. Здесь ничего сложного нет: необходимо добавить все методы интерфейса IDWriteTextRenderer (4 шт.) и его родительского интерфейса IDWritePixelSnapping (3 шт.). Реализация последнего интерфейса выглядит  в принципе всегда одинаково, так что можно вынести ее в базовый класс:

function TTextRenderer.GetCurrentTransform(clientDrawingContext: Pointer; var transform: TDwriteMatrix): HResult;
var mtr : PD2D1Matrix3x2F;
begin
    mtr := @transform;
    FRt.GetTransform(mtr^);
    result := S_OK;
end;

function TTextRenderer.GetPixelsPerDip(clientDrawingContext: Pointer; var pixelsPerDip: Single): HResult;
var x,y : single;
begin
    FRt.GetDpi(x,y);
    pixelsPerDip := x/96;
    result := S_OK;
end;


function TTextRenderer.IsPixelSnappingDisabled(clientDrawingContext: Pointer; var isDisabled: BOOL): HResult;
begin
    isDisabled := false;
    result := S_OK;
end;

 Конструктор моего класса TTextRenderer получает параметр - поверхность рисования ID2D1RenderTarget от канвы формы. Также класс содержит такие члены как кисти обводки, заливки, ссылка на фабрику Direct2D. Создание кистей проводится в методе InitResources().
Теперь вернемся к методам IDWriteTextRenderer. В случае, если какой то метод мы не поддерживаем, необходимо просто вернуть E_NOTIMPL. Мой TextRenderer не поддерживает рисование линии зачеркивания и встроенного объекта. При работе это приведет к тому, что если в TextLayout у меня будут зачеркнутые символы, то линия зачеркивания не будет наприсована. А вот оставшиеся два метода - рисование группы глифоф  и линии подчеркивания реализованы (если их тоже не реализовать, то наш текст попросту не будет нарисован). Рассмотрим метод DrawGlyphRun для рисования группы глифоф:

function TTextRenderer.DrawGlyphRun(clientDrawingContext: Pointer; baselineOriginX, baselineOriginY: Single;
          measuringMode: TDWriteMeasuringMode; var glyphRun: TDwriteGlyphRun;
          var glyphRunDescription: TDwriteGlyphRunDescription; const clientDrawingEffect: IInterface): HResult;
var pg : ID2D1PathGeometry;
    gs : ID2D1GeometrySink;
    mtr : TD2DMatrix3x2F;
    tg : ID2D1TransformedGeometry;
    size : integer;
begin
    FFactory.CreatePathGeometry(pg);
    pg.Open(gs);

    with glyphRun do begin
        FontFace.GetGlyphRunOutline(
            fontEmSize,
            glyphIndices,
            glyphAdvances,
            glyphOffsets,
            glyphCount,
            isSideways,
            false,
            gs
        );
    end;
    gs.Close();

    mtr := TD2DMatrix3x2F.Translation(baselineOriginX, baselineOriginY);
    FFactory.CreateTransformedGeometry(pg, mtr, tg);

    FOutlineBrush.SetColor(D2D1ColorF($b45f14));
    FRt.DrawGeometry(tg, FOutlineBrush,2);

    size := Round(glyphRun.fontEmSize*72/120);
    FFillBrush.SetStartPoint(D2D1PointF(0, baselineOriginY - size));
    FFillBrush.SetEndPoint(D2D1PointF(0,   baselineOriginY));
    FRt.FillGeometry(tg, FFillBrush);

    result := S_OK;
end;

 

 Сигнатура вызова выглядит немного громоздкой, но это только видимость. Первый параметр - контекст рисования. Это пользовательский контекст. Мы можем его передавать при начале рисования, используя TextLayout.Draw() метод. Далее два числовых параметра baselineOriginX и Y определяют позицию базовой линии на которой нарисован символ. measuringMode определяет режим расчетов позицией символов и их размера (приблизительное толкование). Далее параметры  glyphRun и glyphRunDescription содержат информацию о группе глифоф, которые мы рисуем. И последним параметром явлется ссылка на используемый эффект рисования. Эффекты эти реализуем мы сами.

С помощью фабрики мы создаем геометрию и открываем ее для заполнения. Используя информацию а группе глифоф (параметр glyphRun), мы получаем контур геометрии для нее. Далее мы создаем матрицу параллельного переноса в нужное место, где должен быть выведен символ, используя baseline-параметры, и формируем на ее основе трансформированную геометрию набора глифоф. Теперь все просто: рисуем контур фигуры, затем задаем параметры градиентной кисти и закрашиваем ее. Как итог мы получаем закрашенный градентом текст с обводкой, как на рисунке ниже (цвета те же что и у фона, только перевернутые):

 

 Как видите цвет линии подчеркивания у нас отличается от цвета шрифтов. Это определено в методе DrawUnderline. Логика работы данного метода схожа, здесь мы тоже получаем информацию о коордитанах линии. Создаем соответствующую прямоугольную геометрию, и закрашиваем ее, в данном случае красным цветом:

function TTextRenderer.DrawUnderline(clientDrawingContext: Pointer; baselineOriginX, baselineOriginY: Single;
      var underline: TDwriteUnderline; const clientDrawingEffect: IInterface): HResult;
var rc : TD2D1RectF;
    rg : ID2D1RectangleGeometry;
    mtr : TD2D1Matrix3x2F;
    tg : ID2D1TransformedGeometry;
begin
    with underline do
        rc := D2D1RectF(0, offset, width, offset + thickness);

    FFactory.CreateRectangleGeometry(rc, rg);

    mtr := TD2D1Matrix3x2F.Translation(baselineOriginX, baselineOriginY);
    FFactory.CreateTransformedGeometry(rg, mtr, tg);

    FoutlineBrush.SetColor(D2D1ColorF(clRed));
    FRt.DrawGeometry(tg,FoutlineBrush);
    FRt.FillGeometry(tg, FOutlineBrush);

    result := S_OK;
end;

 последний параметр обоих методов clientDrawingEffect - полностью реализуется пользоваетелем. Эффект мы можем применить к диапазону текста в TextLayout. API не накладывает никаких ограничений на этот интерфейс. Он просто сохраняет установленную для диапазона ссылку на эффект и передает его при выводе группы глифоф. Таким образом, самый простой пример: мы хотим раскрастить текст разным цветом. Для этого мы реализуем свой класс, поддерживающий интерфейс IInterface. Класс этот сохраняет значение цвета, интерфейс соответственно имеет метод getColor для его получения. При формировании TextLayout мы создаем экземпляр объекта цвета, получаем ссылку на его интерфейс, и применяем этот эффект к диапазону (метод SetDrawingEffect). Когда управление передается в TextRenderer, мы получаем этот самый эффект как параметр. Здесь мы извлекаем обратно значение цвета, и рисуем текст соответствующей кистью. Сейчас мы это рассматривать не будем, т.к. это будет затронуто в следующей статье в купе с эффектами Direct2D, которые являются нововведением в Windows 8.

Hit-тесты в DirectWrite

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

procedure TMainForm.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var isTrailing, isInside : LongBool;
    metrics : TDwriteHitTestMetrics;
    isUnderlined : LongBool;
    range : TDwriteTextRange;
begin
    FTextLayout.HitTestPoint(x,y, isTrailing, isInside, metrics);
    if not isInside then exit;

    FTextLayout.GetUnderline(metrics.textPosition, isUnderlined, range);

    range.startPosition := metrics.textPosition;
    range.length := 1;
    FTextLayout.SetUnderline(not isUnderlined, range);

    invalidate();
end;

Сначала проводим тестирование для нашей точки (х,у). Если клик попадает в строку, то значение параметра isInside устанавливается в true. Структура metrics содержит информацию о символе, в который мы попали. Узнаем статус символа, подчеркнут он или нет. Далее инвертируем полученное значение и применяем его к выбранному символу.
В дополнение следует отметить, что при изменении размера формы необходимо также изменять размер нашего TextLayout (в моем примере он растянут на всю форму). А вывод текста теперь реализуется не с помощью RenderTarget.DrawTextLayout(), а с помощью TextLayout.Draw(), где параметром является TextRenderer, который в свою очередь уже значет, на какой канве рисовать. Исходный код проекта прилагается.

Метки:  Direct2D  |  DirectWrite 

Комментарии

James
13.03.2018 в 10:07
I value the post.Thanks Again. Awesome.
James
13.03.2018 в 10:44
I value the post.Thanks Again. Awesome.
James
25.04.2018 в 23:40
Greetings! Very useful advice within this article! It is the little changes that make the most important changes. Many thanks for sharing!
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно