- PVSM.RU - https://www.pvsm.ru -
Эта статья продолжает серию наших рассказов, в которых мы делимся своим опытом разработки визуальных WinRT контролов в стиле Windows 8 UI.
В прошлый раз [1] мы приводили базовые шаги, необходимые для создания своего WinRT контрола и SDK для него, а сейчас речь пойдёт о применении технологии Direct2D для создания визуальных эффектов в вашем WinRT компоненте.
В данной статье мы рассмотрим процесс создания кругового индикатора aka гейдж (gauge control), у которого стрелка будет размываться при движении.
Примечание: полный код этого проекта вы можете скачать по следующей ссылке: go.devexpress.com/Habr_WinRTSample.aspx [2]
Технология Direct2D [3] — это ускоренный аппаратным обеспечением API для двухмерной графики, который обеспечивает высокую производительность и высококачественное отображение двухмерной геометрии, растровых изображений и текста.
Direct2D API разработан компанией Microsoft для создания приложений под управлением операционной системы Windows и для взаимодействия с существующим кодом, который использует GDI, GDI+, или Direct3D.
Когда бывает необходимо использовать Direct2D в своих приложениях?
Типичным примером является оптимизация производительности приложений, в которых происходит отрисовка большого количества графических элементов (например, такое бывает нужно при создании графиков с большим объёмом данных, карт и всевозможных индикаторов-гейджей).
Более подробно узнать про Direct2D и особенности её применения можно в соответствующем разделе [4] MSDN.
Для разработки приложений в стиле Windows 8 UI (METRO) вам понадобятся Windows 8 и Visual Studio 2012. Более подробно об этом можно прочитать в нашей предыдущей статье [5].
Чтобы использовать Direct2D в вашем приложении, надо написать WinRT компонент на C++.
Для этого выполним следующие шаги:
Данная библиотека будет содержать реализацию объявление и реализацию нескольких интерфейсов, предназначенных для отрисовки с помощью технологии 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;
}
}
На следующем этапе необходимо добавить в наше решение ещё один проект – на этот раз 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;
}
Вот и всё. Наш компонент готов, и теперь мы можем использовать его в реальном приложении.
Для того, чтобы продемонстрировать работу нашего компонента, добавим ещё один проект в наше решение. Пусть это будет шаблон 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 [2]
P.S. Всех тех, кто хочет задать нам свои вопросы про разработку компонентов для WinRT, мы будем рады видеть 7-го сентября в Москве на Windows 8 Camp [6]! Если у вас нет возможности посетить это мероприятие, то следите за онлайн трансляцией!
Автор: AlexKravtsov
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/14341
Ссылки в тексте:
[1] прошлый раз: http://habrahabr.ru/company/devexpress/blog/148928/
[2] go.devexpress.com/Habr_WinRTSample.aspx: http://go.devexpress.com/Habr_WinRTSample.aspx
[3] Direct2D: http://ru.wikipedia.org/wiki/Direct2D
[4] соответствующем разделе: http://msdn.microsoft.com/en-us/library/windows/desktop/dd370987(v=vs.85).aspx
[5] предыдущей статье: http://habrahabr.ru/company/devexpress/blog/148928/?
[6] Windows 8 Camp: http://www.microsoft.com/ru-ru/events/windowscamp/
Нажмите здесь для печати.