Фактор DirectX

Форматирование и прокрутка текста с помощью DirectWrite

Чарльз Петцольд

Исходный код можно скачать по ссылке.

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

Понимая особую природу текста в графике, проектировщики DirectX разделили работу с текстом между двумя основными подсистемами: Direct2D и DirectWrite. В интерфейсе ID2D1RenderTarget объявляются методы для вывода текста наряду с другими двухмерными графическими элементами, а интерфейсы, имена которых начинаются с «IDWrite», помогают подготавливать текст к выводу.

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

Простейший вывод текста

Фундаментальным интерфейсом для отображения текста является IDWriteTextFormat, который комбинирует семейство шрифта (Times New Roman или Arial, например) со стилем (курсив или наклонные знаки), плотность (полужирный или тонкий), длину символов (узкие или растянутые) и размер шрифта. Вероятно, вы определите указатель с учетом ссылок для объекта IDWriteTextFormat как закрытый член в заголовочном файле:

Microsoft::WRL::ComPtr<IDWriteTextFormat> m_textFormat;

Этот объект создается методом, определенным IDWriteFactory:

dwriteFactory->CreateTextFormat(
  L"Century Schoolbook", nullptr,
  DWRITE_FONT_WEIGHT_NORMAL,
  DWRITE_FONT_STYLE_ITALIC,
  DWRITE_FONT_STRETCH_NORMAL,
  24.0f, L"en-US", &m_textFormat);

В общем случае ваше приложение, по-видимому, будет создавать несколько объектов IDWriteTextFormat для разных семейств шрифтов, кеглей и стилей. Существуют аппаратно-независимые ресурсы, поэтому вы можете создавать их в любой момент после вызова DWriteCreateFactory для получения объекта IDWriteFactory и хранить в течение всего времени выполнения приложения.

Если вы неправильно напишете название семейства или если шрифт с таким названием отсутствует в вашей системе, вы получите шрифт по умолчанию. Второй аргумент указывает набор шрифтов, в котором следует искать такой шрифт. Подстановка nullptr указывает на системный набор шрифтов. Кроме того, у вас могут быть свои наборы шрифтов.

Размер в аппаратно-независимых единицах основан на разрешении в 96 единиц на дюйм, поэтому размер 24 эквивалентен шрифту 18 пт. Индикатор языка задает язык названия семейства шрифта — вместо него можно оставить пустую строку.

После создания объекта IDWriteTextFormat вся указанная вами информация становится неизменяемой. Если вам нужно сменить семейство шрифта, стиль или размер, вы должны заново создать этот объект.

Что еще нужно для рендеринга текста, помимо объекта IDWriteTextFormat? Очевидно, сам текст, место на экране, куда он будет выводиться, и цвет. Эти элементы указываются при рендеринге текста: место вывода текста задается не точкой, а прямоугольником типа D2D1_RECT_F. Цвет текста определяется кистью, которая может быть любого типа, например градиентной или кистью, содержащей некое изображение.

Вот типичный вызов DrawText:

deviceContext->DrawText(
  L"This is text to be displayed",
  28,    // Characters
  m_textFormat.Get(),
  layoutRect,
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE,
  DWRITE_MEASURING_MODE_NATURAL);

По умолчанию текст разбивается на строки и переносится по ширине прямоугольника (или его высоте для языков, в которых текст читается сверху вниз). Если текст слишком длинный для отображения в данном прямоугольнике, он будет продолжен за его нижней границей. В предпоследнем аргументе можно указывать необязательный флаг для обрезания текста, выходящего за границы прямоугольника, или отмены выравнивания символов по границам пикселей (что полезно, если вы анимируете текст), или (в Windows 8.1) для того, чтобы разрешить многоцветные символы шрифта.

В сопутствующий этой статье исходный код включена программа для Windows 8.1, которая использует IDWriteTextFormat и DrawText для отображения главы 7 из книги Льюиса Кэрролла «Алиса в стране чудес». (Я получил этот текст с веб-сайта Project Gutenberg, но немного модифицировал его так, чтобы он больше согласовался с типографией оригинального издания.) Программа называется PlainTextAlice, и я создал ее, используя шаблон DirectX App (XAML) в Visual Studio Express 2013 Preview for Windows 8.1. Этот шаблон проекта генерирует XAML-файл, содержащий SwapChainPanel и все необходимое для отображения DirectX-графики на нем.

Файл с текстом является частью контента проекта. Каждый абзац — это отдельная строка, и каждая из них отделяется пустой строкой. Класс DirectXPage загружает текст в обработчике события Loaded и перемещает его в класс PlainTextAliceMain (создаваемый как часть проекта), который потом передает его в класс PlainTextAliceRenderer (это мой класс).

Поскольку эта программа отображает довольно статичную графику, я отключил цикл рендеринга в DirectXPage, не присоединив к нему обработчик событий CompositionTarget::Rendering. Вместо этого PlainTextAliceMain определяет, когда следует перерисовать графику; это происходит, только когда загружается текст или когда изменяются размеры и/или ориентация окна приложения. В такие моменты PlainTextAliceMain вызывает метод Render в PlainTextAliceRenderer и метод Present в DeviceResources.

Часть класса PlainTextAliceRenderer, написанная на C++, показана на рис. 1. Для большей ясности я удалил проверки HRESULT.

Рис. 1. ФайлPlainTextAliceRenderer.cpp

#include "pch.h"
#include "PlainTextAliceRenderer.h"
using namespace PlainTextAlice;
using namespace D2D1;
using namespace Platform;
PlainTextAliceRenderer::PlainTextAliceRenderer(
  const std::shared_ptr<DeviceResources>& deviceResources) :
  m_text(L""),
  m_deviceResources(deviceResources)
{
  m_deviceResources->GetDWriteFactory()->
    CreateTextFormat(L"Century Schoolbook",
                     nullptr,
                     DWRITE_FONT_WEIGHT_NORMAL,
                     DWRITE_FONT_STYLE_NORMAL,
                     DWRITE_FONT_STRETCH_NORMAL,
                     24.0f,
                     L"en-US",
                     &m_textFormat);
  CreateDeviceDependentResources();
}
void PlainTextAliceRenderer::CreateDeviceDependentResources()
{
  m_deviceResources->GetD2DDeviceContext()->
    CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black),
                          &m_blackBrush);
  m_deviceResources->GetD2DFactory()->
    CreateDrawingStateBlock(&m_stateBlock);
}
void PlainTextAliceRenderer::CreateWindowSizeDependentResources()
{
  Windows::Foundation::Size windowBounds =
    m_deviceResources->GetOutputBounds();
  m_layoutRect = RectF(50, 0, windowBounds.Width - 50,
      windowBounds.Height);
}
void PlainTextAliceRenderer::ReleaseDeviceDependentResources()
{
  m_blackBrush.Reset();
  m_stateBlock.Reset();
}
void PlainTextAliceRenderer::SetAliceText(std::wstring text)
{
  m_text = text;
}
void PlainTextAliceRenderer::Render()
{
  ID2D1DeviceContext* context = 
    m_deviceResources->GetD2DDeviceContext();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::White));
  context->SetTransform(m_deviceResources->GetOrientationTransform2D());
  context->DrawText(m_text.c_str(),
                    m_text.length(),
                    m_textFormat.Get(),
                    m_layoutRect,
                    m_blackBrush.Get(),
                    D2D1_DRAW_TEXT_OPTIONS_NONE,
                    DWRITE_MEASURING_MODE_NATURAL);
  HRESULT hr = context->EndDraw();
  context->RestoreDrawingState(m_stateBlock.Get());
}

Заметьте, что член m_layoutRect вычисляется на основе размера окна приложения на экране, но с отступом на 50 пикселей слева и справа. Результат представлен на рис. 2.

Программа PlainTextAlice
Рис. 2. Программа PlainTextAlice

Прежде всего я хотел бы сказать пару добрых слов об этой программе: совершенно очевидно, что текст с минимальными издержками должным образом переносится и делится на четко разделенные абзацы.

Несовершенства проекта PlainTextAlice тоже очевидны: интервал между абзацами существует только потому, что в файле исходного кода есть пустые строки, заранее вставленные мной для этой цели. Если бы мне понадобилось изменить интервал между абзацами (уменьшить или увеличить его), это было бы невозможно. Более того, нет никакого способа выделять слова в тексте курсивом или полужирным.

Но самый крупный недостаток в том, что вы видите лишь начало главы. Можно было бы реализовать логику прокрутки, но как вы смогли бы определить, насколько далеко нужно прокрутить текст? По-настоящему серьезная проблема с IDWriteTextFormat и DrawText заключается в том, что высота отформатированного визуализируемого текста просто недоступна.

И последнее: использовать DrawText имеет смысл, только когда текст имеет одинаковое форматирование и вам известно, что в заданный прямоугольник уместится весь текст, или же вам безразлично, если какая-то часть текста слегка выйдет за его пределы. Для более изощренных целей требуется подход получше.

Прежде чем продолжить, обратите внимание на то, что информация, указываемая в методе CreateTextFormat, является неизменяемой в объекте IDWriteTextFormat, но в этом интерфейсе объявлено несколько методов, которые позволяют изменять то, как отображается текст. Например, SetParagraphAlignment меняет вертикальное размещение текста внутри прямоугольника, заданного в DrawText, а SetTextAlignment дает возможность модифицировать выравнивание строк абзаца — по левому или правому краю, по центру или с равномерным распределением между вертикальными границами прямоугольника. В аргументах этих методов используются такие слова, как «near», «far», «leading» и «trailing», чтобы сделать принципы выравнивания универсальными для текста, который читается как справа налево, так и сверху вниз. Вы также можете контролировать интервал между строками, перенос текста и шаг табуляции.

Подход с разметкой текста

Теперь сделаем большой шаг вперед от IDWriteTextFormat и рассмотрим другой подход, идеальный для большинства требований к выводу почти всего стандартного текста, включая проверку на попадание (hit-testing). При этом подходе используется объект типа IDWriteTextLayout, который не только наследует от IDWriteTextFormat, но и включает этот объект, когда создается его экземпляр.

Вот указатель (с учетом ссылок) на объект IDWriteTextLayout, обычно объявляемый в каком-то заголовочном файле:

Microsoft::WRL::ComPtr<IDWriteTextLayout> m_textLayout;

Создайте объект примерно так:

dwriteFactory->CreateTextLayout(
  pText, wcslen(pText),
  m_textFormat.Get(),
  maxWidth, maxHeight,
  &m_textLayout);

IDWriteTextLayout в отличие от интерфейса IDWriteTextFormat включает сам текст и нужные размеры прямоугольника (высоту и ширину) для форматирования текста. Методу DrawTextLayout, который отображает объект IDWriteTextLayout, нужно сообщить лишь двухмерные координаты точки верхнего левого угла, где должен появиться форматируемый текст:

deviceContext->DrawTextLayout(
  point,
  m_textLayout.Get(),
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE);

Поскольку объект IDWriteTextLayout имеет всю информацию, необходимую для вычисления разрывов строк до рендеринга текста, ему также известно, насколько большим будет этот текст. В IDWriteTextLayout имеется несколько методов — GetMetrics, GetOverhangMetrics, GetLineMetrics и GetClusterMetrics, — которые предоставляют море информации, помогающей эффективно работать с этим текстом. Например, GetMetrics сообщает общую ширину и высоту форматированного текста, а также количество строк и другие сведения.

Хотя метод CreateTextLayout включает аргументы с максимальной шириной и высотой, впоследствии им можно присваивать другие значения. (Но сам текст является неизменяемым.) Если изменяется область вывода (например, планшет поворачивается из альбомной в книжную ориентацию), вам не надо заново создавать объект IDWriteTextLayout. Просто вызовите методы SetMaxWidth и SetMaxHeight, объявленные в этом интерфейсе. На самом деле, впервые создавая объект IDWriteTextLayout, можно задать нулевые значения для аргументов максимальной ширины и высоты.

Проект ParagraphFormattedAlice использует IDWriteTextLayout и DrawTextLayout, и результаты показаны на рис. 3. Прокрутки текста по-прежнему нет, но некоторые строки выровнены по обеим границам, а в абзацах имеются отступы в первых строках. В заголовках используется более крупный шрифт, чем в остальном тексте, и некоторые слова выделены курсивом.

Программа ParagraphFormattedAlice
Рис. 3. Программа ParagraphFormattedAlice

Текстовый файл в этом проекте немного отличается от того, который был в первом проекте: каждый абзац по-прежнему является отдельной строкой, но эти абзацы больше не отделяются пустыми строками. Вместо использования одного объекта IDWriteTextFormat для всего текста, каждый абзац в ParagraphFormattedAlice находится в отдельном объекте IDWriteTextLayout, рендеринг которого осуществляется своим вызовом DrawTextLayout. Благодаря этому интервал между абзацами может быть установлен любым.

Для работы с текстом я определил структуру Paragraph:

struct Paragraph
{
  std::wstring Text;
  ComPtr<IDWriteTextLayout> TextLayout;
  float TextHeight;
  float SpaceAfter;
};

Вспомогательный класс AliceParagraphGenerator генерирует набор объектов Paragraph на основе строк в тексте.

В IDWriteTextLayout есть ряд методов, позволяющих задавать форматирование индивидуальных слов или других блоков текста. Например, вот как выделяются курсивом первые пять символов, начиная со смещения 23 в тексте:

DWRITE_TEXT_RANGE range = { 23, 5 };
textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, range);

Аналогичные методы доступны для семейства шрифта, набора, размера, толщины, сжатия/растяжения, подчеркивания и зачеркивания. В реальных приложениях форматирование текста обычно определяется разметкой (например, HTML), но для простоты класс AliceParagraphGenerator работает с чисто текстовым файлом и содержит «зашитые» в код позиции для слов, выделяемых курсивом.

В AliceParagraphGenerator также есть метод SetWidth для установки новой ширины области вывода (рис. 4). (Для ясности из этого листинга были удалены проверки HRESULT.) Ширина области вывода меняется при изменении размеров окна или изменении ориентации планшета. SetWidth перебирает в цикле все объекты Paragraph, вызывает SetMaxWidth из TextLayout, а затем получает новую высоту форматированного абзаца, сохраняемую в TextHeight. Ранее полю SpaceAfter просто присваивалось значение в 12 пикселей для большинства абзацев, 36 пикселей для заголовков и 0 для пары стихотворных строк. Это упрощает получение высоту каждого абзаца и общей высоты всего текста в главе.

Рис. 4. Метод SetWidth в AliceParagraphGenerator

float AliceParagraphGenerator::SetWidth(float width)
{
  if (width <= 0)
    return 0;
  float totalHeight = 0;
  for (Paragraph& paragraph : m_paragraphs)
  {
    HRESULT hr = paragraph.TextLayout->SetMaxWidth(width);
    hr = paragraph.TextLayout->SetMaxHeight(FloatMax());
    DWRITE_TEXT_METRICS textMetrics;
    hr = paragraph.TextLayout->GetMetrics(&textMetrics);
    paragraph.TextHeight = textMetrics.height;
    totalHeight += paragraph.TextHeight + paragraph.SpaceAfter;
  }
  return totalHeight;
}

Метод Render в ParagraphFormattedAliceRenderer также перебирает в цикле все объекты Paragraph и вызывает DrawTextLayout с другим началом координат на основе суммированной высоты текста.

Проблема отступов в первых строках абзацев

Как видно на рис. 3, программа ParagraphFormattedAlice создает отступы в первой строке каждого абзаца. Вероятно, самый простой способ делать такие отступы — вставлять какое-то пустое пространство в начало текстовой строки. В стандарте Unicode определены коды для широкого пробела (em space) (его ширина равна кеглю шрифта), нормального (en space) (половины широкого), четверти широкого пробела и для меньших пробелов, поэтому вы можете комбинировать их, чтобы получить нужный интервал. Преимущество этого способа в том, что отступ пропорционален кеглю шрифта.

Однако этот подход не годится для отрицательных значений отступа первой строки (такие отступы называют висячими строками, и в этом случае первая строка начинается левее остального абзаца). Более того, отступы первой строки обычно указываются в единицах, независимых от кегля (например, полдюйма).

По этим причинам я решил реализовать отступы первой строки с применением метода SetInlineObject интерфейса IDWriteTextLayout. Этот метод позволяет помещать любой графический объект в строку текста, благодаря чему этот объект становится почти что отдельным словом, размер которого учитывается при переносе строки.

Метод SetInlineObject часто используется для вставки в текст небольших битовых карт. Чтобы задействовать его для этой или другой цели, нужно написать класс, реализующий интерфейс IDWriteInlineObject, который, помимо трех стандартных методов IUnknown, объявляет методы GetMetrics, GetOverhangMetrics, GetBreakConditions и Draw. В основном этот класс вызывается, пока с текстом выполняются вычисления или рендеринг. Для своего класса FirstLineIndent я определил конструктор с аргументом, указывающим нужный отступ в пикселях, и это значение обычно возвращается вызовом GetMetrics, чтобы сообщить размер встроенного объекта. Реализация Draw в этом классе ничего не делает. Для висячих строк отлично работают отрицательные значения.

Вы вызываете SetInlineObject из IDWriteFontLayout точно так же, как SetFontStyle и другие методы, основанные на диапазонах текста, но лишь когда я начал использовать SetInlineObject, я обнаружил, что диапазон не может иметь нулевую длину. Другими словами, вы просто не можете вставить строчный объект. Строчный объект (inline object) должен заменять хотя бы один символ текста. По этой причине, несмотря на определение объектов Paragraph, код вставляет в начало каждой строки символ пробела нулевой ширины (no-width space character) (\x200B), который визуально никак не проявляется, но может быть заменен при вызове SetInlineObject.

Прокрутка по принципу DIY

Программа ParagraphFormattedAlice не поддерживает прокрутку, но располагает всей информацией, важной для ее реализации, а именно: общей высотой визуализируемого текста. Проект ScrollableAlice демонстрирует один из подходов к прокрутке: он по-прежнему отправляет свой вывод в SwapChainPanel размером с окно программы, но смещает рендеринг на основе пользовательского ввода мышью или касанием. Я рассматриваю такой подход как прокрутку по принципу «сделай сам» (do it yourself, DIY).

Я создал проект ScrollableAlice в Visual Studio 2013 Preview с помощью того же шаблона DirectX App (XAML), который я использовал для предыдущих проектов, но задействовал преимущества другого интересного аспекта этого шаблона. Он содержит код в DirectXPage.cpp, создающий дополнительный поток для обработки событий Pointer от SwapChainPanel. Это позволяет избежать загромождения UI-потока обработкой этого ввода.

Конечно, введение дополнительного потока все усложняет. Я хотел получить некоторую инерцию при прокрутке, а значит, мне нужны были события Manipulation, а не Pointer. Для этого нужно было, чтобы DirectXPage создавал объект GestureRecognizer (тоже в дополнительном потоке), который генерирует события Manipulation на основе событий Pointer.

Предыдущая программа ParagraphFormattedAlice перерисовывала окно программы при загрузке текста и изменении размеров окна. ScrollableAlice тоже делает это, и эта перерисовка по-прежнему выполняется в UI-потоке. Кроме того, ScrollableAlice перерисовывает окно при генерации событий ManipulationUpdated, но это происходит в дополнительном потоке, созданном для обработки ввода мышью (pointer input).

Как быть, если тексту придается хорошее ускорение пальцем так, что он продолжает прокручиваться по инерции, и, пока он прокручивается, размеры окна изменяются? Есть большая вероятность того, что двум потокам одновременно будут выданы перекрывающиеся DirectX-вызовы, и это проблема.

Здесь нужна некая синхронизация потоков, и хорошее простое решение включает класс critical_section из пространства имен Concurrency. В заголовочном файле объявляется следующее:

Concurrency::critical_section m_criticalSection;

Класс critical_section содержит встраиваемый класс scoped_lock. Следующее выражение создает объект lock типа scoped_lock, вызывая конструктор с передачей объекта critical_section:

critical_section::scoped_lock lock(m_criticalSection);

Этот конструктор предполагает владение объектом m_criticalSection и блокирует выполнение, если этот объект захвачен другим потоком. Что приятно в классе scoped_lock, — деструктор освобождает владение m_criticalSection, когда объект lock выходит за область видимости, поэтому пользоваться им невероятно легко — достаточно раскидать объекты этого типа в различных методах, которые могут вызываться одновременно.

Я решил, что проще всего реализовать эту критическую секцию в DirectXPage, который содержит некоторые критически важные вызовы класса DeviceResources (например, UpdateForWindowSizeChanged), требующие блокировки других потоков на это время. Хотя блокировать UI-поток — не самая лучшая идея (что происходит при генерации событий Pointer), эти блокировки чрезвычайно короткие.

К тому моменту, когда информация о прокручивании попадает в класс ScrollableAliceRenderer, она принимает форму значения с плавающей точкой, которое хранится в переменной m_scrollOffset и варьируется от 0 до максимальной величины, равной разнице между высотой всей главы текста и высотой окна. Метод Render использует это значение, чтобы определить, как начинать и заканчивать отображение абзацев (рис. 5).

Рис. 5. Реализация прокрутки в ScrollableAlice

std::vector<Paragraph> paragraphs =
  m_aliceParagraphGenerator.GetParagraphs();
float outputHeight = m_deviceResources->GetOutputBounds().Height;
D2D1_POINT_2F origin = Point2F(50, -m_scrollOffset);
for (Paragraph paragraph : paragraphs)
{
  if (origin.y + paragraph.TextHeight + paragraph.SpaceAfter > 0)
    context->DrawTextLayout(origin, paragraph.TextLayout.Get(),
    m_blackBrush.Get());
  origin.y += paragraph.TextHeight + paragraph.SpaceAfter;
  if (origin.y > outputHeight)
    break;
}

Прокрутка с отскакиванием

Хотя программа ScrollableAlice реализует прокрутку с инерцией при сенсорном вводе, в ней отсутствует характерное для Windows 8 отскакивание при попытке прокрутки за пределы верхней или нижней границы текста. Это теперь уже привычное отскакивание включено в ScrollViewer, и, хотя попытка продублировать в своем коде отскакивание в стиле ScrollViewer может быть делом увлекательным, это упражнение не на сегодня.

Поскольку прокручиваемый текст может быть длинным, лучше всего не пытаться выполнять рендеринг всего текста на одной поверхности или битовой карте. DirectX накладывает ограничения на размеры этих поверхностей. Программа ScrollableAlice обходит эти ограничения, лимитируя свой вывод в SwapChainPanel размером с окно программы, и это нормально работает. Но чтобы работал ScrollViewer, контент должен иметь размер в разметке, отражающий полную высоту форматированного текста.

К счастью, Windows 8 поддерживает элемент, который делает как раз то, что нужно. Класс VirtualSurfaceImageSource наследует от SurfaceImageSource, который в свою очередь наследует от ImageSource, поэтому он может выступать в роли источника битовой карты для элемента Image в ScrollViewer. VirtualSurfaceImageSource может быть любого нужного размера (который можно изменять без создания данного объекта заново) и обходит ограничения DirectX на размер за счет виртуализации рабочей области и реализации рисования по запросу. (Однако SurfaceImageSource и VirtualSurfaceImageSource не являются оптимальным выбором для покадровых анимаций, требующих высокой производительности.)

VirtualSurfaceImageSource — это ссылочный (ref) класс Windows Runtime. Чтобы использовать его в сочетании с DirectX, его нужно привести к объекту типа IVirtualSurfaceImageSourceNative, который предоставляет методы, используемые в реализации рисования по запросу. Эти методы сообщают, что прямоугольную область нужно обновить, и позволяют уведомлять программу о новых прямоугольниках обновления, предоставляя класс, который реализует IVirtualSurfaceUpdatesCallbackNative.

Проект, демонстрирующий эту методику, — BounceScrollableAlice, и, поскольку он не требовал применения SwapChainPanel, я создал его в Visual Studio 2013 Preview на основе шаблона Blank App (XAML). Для требуемого класса, который реализует IVirtualSurfaceUpdatesCallbackNative, я создал класс с именем VirtualSurfaceImageSourceRenderer. Класс AliceVsisRenderer наследует от VirtualSurfaceImageSourceRenderer, чтобы обеспечить рисование, специфичное для Алисы.

Прямоугольники обновления, доступные от IVirtualSurfaceImageSourceNative, относительны полному размеру VirtualSurfaceImageSource, но координаты рисования относительны прямоугольникам обновления. Это означает, что вызовы DrawTextLayout в BounceScrollableAlice практически те же, что и на рис. 5, за исключением того, что начальная позиция задается как отрицательная вершина прямоугольника обновления, а не смещение прокрутки, и значение outputHeight является разницей между нижней и верхней границами этого прямоугольника.

Если в эту смесь ввести ScrollViewer, вывод текста станет по-настоящему эквивалентным принятому в Windows 8, и вы сможете использовать жест расширения/сужения (pinch gesture), чтобы делать текст крупнее или мельче.


Чарльз Петцольд (Charles Petzold) — давний «пишущий» редактор MSDN Magazine и автор книги «Programming Windows, 6th edition» (O’Reilly Media, 2012) о написании приложений для Windows 8. Его веб-сайт находится по адресу charlespetzold.com.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Джиму Галасину (Jim Galasyn) и Джастину Пеньяну (Justin Panian).