Эта статья продолжает серию наших рассказов, в которых мы делимся своим опытом разработки визуальных 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
P.S. Всех тех, кто хочет задать нам свои вопросы про разработку компонентов для WinRT, мы будем рады видеть 7-го сентября в Москве на Windows 8 Camp! Если у вас нет возможности посетить это мероприятие, то следите за онлайн трансляцией!
Автор: AlexKravtsov