Использование технологии Direct2D для создания WinRT компонентов
AlexKravtsov
Эта статья продолжает серию наших рассказов, в которых мы делимся своим опытом разработки визуальных WinRT контролов в стиле Windows 8 UI.
В прошлый раз мы приводили базовые шаги, необходимые для создания своего WinRT контрола и SDK для него, а сейчас речь пойдёт о применении технологии Direct2D для создания визуальных эффектов в вашем WinRT компоненте.
В данной статье мы рассмотрим процесс создания кругового индикатора aka гейдж (gauge control), у которого стрелка будет размываться при движении.
Примечание: полный код этого проекта вы можете скачать по следующей ссылке: go.devexpress.com/Habr_WinRTSample.aspx
Что такое Direct2D ?Технология Direct2D - это ускоренный аппаратным обеспечением API для двухмерной графики, который обеспечивает высокую производительность и высококачественное отображение двухмерной геометрии, растровых изображений и текста.Direct2D API разработан компанией Microsoft для создания приложений под управлением операционной системы Windows и для взаимодействия с существующим кодом, который использует GDI, GDI+, или Direct3D.Когда бывает необходимо использовать Direct2D в своих приложениях?Типичным примером является оптимизация производительности приложений, в которых происходит отрисовка большого количества графических элементов (например, такое бывает нужно при создании графиков с большим объёмом данных, карт и всевозможных индикаторов-гейджей).Более подробно узнать про Direct2D и особенности её применения можно в соответствующем разделе MSDN.
Системные требованияДля разработки приложений в стиле Windows 8 UI (METRO) вам понадобятся Windows 8 и Visual Studio 2012. Более подробно об этом можно прочитать в нашей предыдущей статье .
Создание С++ библиотеки с компонентом, использующим Direct2DЧтобы использовать Direct2D в вашем приложении, надо написать WinRT компонент на C++.Для этого выполним следующие шаги:
- Запустим Visual Studio 2012 (если вы её вообще когда-либо закрываете :-) )
- Создадим новый Visual C++ проект типа Windows Runtime Component
Данная библиотека будет содержать реализацию объявление и реализацию нескольких интерфейсов, предназначенных для отрисовки с помощью технологии Direct2D.
- Затем необходимо в свойствах нашего C++ проекта добавить ссылки на библиотеки Direct2D: d2d1.lib и d3d11.lib.
- Далее необходимо написать реализацию отрисовки нашего компонента с использованием Direct2D.
Во-первых, создаем статический класс DrawingFactory с одним единственным методомCreateRenderer, который будет инициализировать библиотеку DirectX, создавать и возвращать экземпляр класса Renderer:
public ref class DrawingFactory sealed {
public:
static IRenderer^ CreateRenderer();
};
Во-вторых, описываем основной интерфейс IRenderer, который будет содержать следующие методы и свойства: - метод Reset, предназначенный для пересоздания поверхности для рисования, в нашем случае этоImageBrush с размерами, которые были переданы. В дальнейшем полученную ImageBrush мы будем присваивать какому-либо элементу в визуальном дереве нашего WinRT компонента; - свойство TargetBrush, которое будет возвращать созданную кисть в методе Reset и должен содержать результат отрисовки; - свойства EnableMotionBlur, MotionBlurAngle, MotionBlurDeviation, которые служат для включения и настройки эффекта размытия при рендеринге примитивов; - методы CreateGeometry, DrawGeometry, FillGeometry, предназначенные для создания и рисования геометрий; - Кроме того рассматриваемый интерфейс содержит еще несколько простых и понятных методов и свойств, таких как BeginDraw, EndDraw, Clear, TargetWidth и TargetHeight.
public interface class IRenderer
{
property int TargetWidth { int get(); }
property int TargetHeight { int get(); }
property ImageBrush^ TargetBrush { ImageBrush^ get(); }
property bool EnableMotionBlur { bool get(); void set(bool value); }
property float MotionBlurAngle { float get(); void set(float value); }
property float MotionBlurDeviation { float get(); void set(float value); }
void Reset(int width, int height);
void BeginDraw();
void EndDraw();
void Clear(Color color);
IShapeGeometry^ CreateGeometry();
void DrawGeometry(IShapeGeometry^ geometry, Color color, float width);
void FillGeometry(IShapeGeometry^ geometry, Color color);
};
А теперь расскажем о том, как мы будем делать эффект размытия. С Direct2D данная задача решается очень просто. В методе Reset нужно создать стандартный DirectionalBlur effect, и временный битмап (m_inputSource), в который будем рисовать и использовать его в качестве источника для ранее созданного эффекта:
void Renderer::Reset(int width, int height)
{
D3D_FEATURE_LEVEL featureLevels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1
};
ThrowIfFailed(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, 0,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, featureLevels, ARRAYSIZE(featureLevels),
D3D11_SDK_VERSION, &m_d3dDevice, NULL, &m_d3dContext));
ThrowIfFailed(m_d3dDevice.As(&m_dxgiDevice));
ThrowIfFailed(m_d2dFactory->CreateDevice(m_dxgiDevice.Get(), &m_d2dDevice));
ThrowIfFailed(m_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_d2dContext));
m_imageSource = ref new SurfaceImageSource(width, height, false);
IInspectable* inspectable = (IInspectable*) reinterpret_cast<IInspectable*>(m_imageSource);
ThrowIfFailed(inspectable->QueryInterface(__uuidof(ISurfaceImageSourceNative), (void**)&m_imageSourceNative));
ThrowIfFailed(m_imageSourceNative->SetDevice(m_dxgiDevice.Get()));
m_imageBrush = ref new ImageBrush();
m_imageBrush->ImageSource = m_imageSource;
m_targetWidth = width;
m_targetHeight = height;
ThrowIfFailed(m_d2dContext->CreateEffect(CLSID_D2D1DirectionalBlur, &m_blurEffect));
m_d2dContext->CreateBitmap(D2D1::SizeU(m_targetWidth, m_targetHeight), nullptr, 0,
D2D1::BitmapProperties1(D2D1_BITMAP_OPTIONS_TARGET,D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED)), &m_inputSource);
m_blurEffect->SetInput(0, m_inputSource.Get());
}
Затем в методе BeginDraw в качестве поверхности для рисования, используем промежуточный битмап m_inputSource, а при завершении рисования в методе EndDraw устанавливаем поверхностьTargetBrush и, в зависимости от значения свойства EnableMotionBlur, рисуем либо эффект, либо промежуточный битмап.
void Renderer::BeginDraw()
{
if (!m_drawingInProcess && (m_d2dContext.Get() != NULL))
{
m_d2dContext->BeginDraw();
m_d2dContext->SetTarget(m_inputSource.Get());
m_drawingInProcess = true;
}
}
void Renderer::EndDraw()
{
if (m_drawingInProcess)
{
m_d2dContext->EndDraw();
ComPtr<ID2D1Bitmap1> bitmap;
PrepareSurface(&bitmap);
m_d2dContext->SetTarget(bitmap.Get());
m_d2dContext->BeginDraw();
m_d2dContext->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
if (m_motionBlurEnabled)
{
m_blurEffect->SetValue(D2D1_DIRECTIONALBLUR_PROP_STANDARD_DEVIATION,
m_motionBlurDeviation);
m_blurEffect->SetValue(D2D1_DIRECTIONALBLUR_PROP_ANGLE, m_motionBlurAngle);
m_d2dContext->DrawImage(m_blurEffect.Get());
}
else
m_d2dContext->DrawImage(m_inputSource.Get());
m_d2dContext->EndDraw();
m_d2dContext->SetTarget(NULL);
m_imageSourceNative->EndDraw();
m_drawingInProcess = false;
}
}
Создание С# библиотеки, содержащей WinRT компонентНа следующем этапе необходимо добавить в наше решение ещё один проект - на этот раз C#. Этот проект представляет собой библиотеку, содержащую WinRT компонент, который будет оперировать с написанной ранее C++ библиотекой.Для этого проекта нужно добавить ссылки на созданную ранее C++ сборку:В этой библиотеке будет содержаться собственно сам WinRT Gauge. Чтобы включить его в нашу сборку, добавим новый Templated Control:Все визуальные элементы, кроме стрелки, будем отрисовывать стандартными средствами WinRT, а именно, зададим в темплейте:<Style TargetType="local:Gauge">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Gauge">
<Grid>
<Grid Margin="0,0,0,0">
<Rectangle Fill="#FF252525" RadiusX="30" RadiusY="30"/>
<Path Stretch="Uniform" VerticalAlignment="Top" HorizontalAlignment="Center"
Stroke="#FFDAA5F8" Fill="#FFD365F8" StrokeThickness="3"
StrokeStartLineCap="Square" StrokeEndLineCap="Square" Margin="150,0,150,0">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0">
<PathFigure.Segments>
<LineSegment Point="0.1,0"/>
<ArcSegment Point="10.1,0" Size="5,5"
RotationAngle="180" IsLargeArc="False"
SweepDirection="Counterclockwise"/>
<LineSegment Point="10.2,0"/>
<ArcSegment Point="0,0" Size="5.1,5.1"
RotationAngle="180" IsLargeArc="False"
SweepDirection="Clockwise"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<TextBlock Text="{Binding Value,
RelativeSource={RelativeSource Mode=TemplatedParent}}"
FontSize="100" Foreground="#FFD365F8"
VerticalAlignment="Top" HorizontalAlignment="Center"/>
<Border x:Name="RendererSurface"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
В рассматриваемом визуальном представлении компонента есть пустой Border с именем RendererSurface. Данный элемент получим в методе OnApplyTemplate и сохраним в переменную класса, чтобы затем в его свойство Background присвоить Brush из renderer. protected override void OnApplyTemplate() {
base.OnApplyTemplate();
rendererSurface = (Border)GetTemplateChild("RendererSurface");
}
В конструкторе нашего класса мы должны c помощью DrawingFactory создать IRenderer, а также подписаться на событие SizeChanged. На этом событии будем вызывать метод IRenderer.Reset, для того чтобы размер кисти, в которую будет производится отрисовка, соответствовал размеру компонента. Также подписываемся на статическое событие CompositionTarget.Rendering - на обработчике этого события мы будем рисовать нашу стрелку.public Gauge() {
renderer = DrawingFactory.CreateRenderer()
DataContext = this
this.DefaultStyleKey = typeof(Gauge)
arrowStrokeColor = ColorHelper.FromArgb(0xFF, 0xD3, 0xAA, 0xF8)
arrowFillColor = ColorHelper.FromArgb(0xFF, 0xD3, 0x65, 0xF8)
this.SizeChanged += Gauge_SizeChanged
CompositionTarget.Rendering += CompositionTarget_Rendering
}
void Gauge_SizeChanged(object sender, SizeChangedEventArgs e) {
renderer.Reset((int)e.NewSize.Width, (int)e.NewSize.Height)
rendererSurface.Background = renderer.TargetBrush
}
void CompositionTarget_Rendering(object sender, object e) {
Render()
}
Теперь нам остается только добавить одно свойство зависимости Value и отрисовать геометрию стрелки:static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value",
typeof(double), typeof(Gauge), new PropertyMetadata(0.0, ValuePropertyChanged))
public double Value {
get { return (double)GetValue(ValueProperty)
set { SetValue(ValueProperty, value)
}
public static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Gauge gauge = d as Gauge
if (gauge != null)
gauge.renderer.EnableMotionBlur = true
}
void Render() {
double angle = Math.PI * Value / 1000 - Math.PI / 2.0
IShapeGeometry geometry = CreateArrowGeometry(angle)
renderer.BeginDraw()
renderer.MotionBlurAngle = (float)(angle / Math.PI * 180)
renderer.MotionBlurDeviation = 5.0f
renderer.Clear(Colors.Transparent)
renderer.FillGeometry(arrow, ColorHelper.FromArgb(0xFF, 0xD3, 0x65, 0xF8))
renderer.DrawGeometry(arrow, ColorHelper.FromArgb(0xFF, 0xD3, 0xAA, 0xF8), 3.0f)
renderer.EndDraw()
if (renderer.EnableMotionBlur)
renderer.EnableMotionBlur = false
}
Вот и всё. Наш компонент готов, и теперь мы можем использовать его в реальном приложении.
Использование WinRT компонента GaugeДля того, чтобы продемонстрировать работу нашего компонента, добавим ещё один проект в наше решение. Пусть это будет шаблон Blank App (XAML). Назовём наш тестовый проект GaugeMatrix:Затем в этот проект добавим ссылки на проекты Gauge и DrawingLayer, созданные ранее:Далее добавим Grid в MainPage.xaml и создадим в нём две строки и два столбца.<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
После этого в каждую ячейку кладем наш компонент Gauge, а также Slider длядинамического изменения значения, отображаемого с помощью стрелки на Gauge контроле:<Grid Grid.Column="0" Grid.Row="0" Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Slider Grid.Row="0" Name="ValueSlider1" Minimum="0" Maximum="1000" Value="0"
Width="1000" Margin="50,0,50,0"/>
<g:Gauge Grid.Row="1" Value="{Binding ElementName=ValueSlider1, Path=Value}"/>
</Grid>
Делаем проект GaugeMatrix стартовым, запускаем приложение и… всё, у вас получилось использовать рисование с помощью Direct2D в вашем WinRT приложении! Поздравляем!Примечание: полный код этого проекта вы можете скачать по следующей ссылке: go.devexpress.com/Habr_WinRTSample.aspx
|