Использование технологии Direct2D для создания WinRT компонентов

в 12:17, , рубрики: .net, c++, devexpress, metro ui, vs2012, Windows 8, WinRT, Блог компании DevExpress, разработка, метки: , , , , , ,

Использование технологии Direct2D для создания WinRT компонентов Эта статья продолжает серию наших рассказов, в которых мы делимся своим опытом разработки визуальных 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++.

Для этого выполним следующие шаги:

  1. Запустим Visual Studio 2012 (если вы её вообще когда-либо закрываете :-) )
  2. Создадим новый Visual C++ проект типа Windows Runtime Component

    Использование технологии Direct2D для создания WinRT компонентов

    Данная библиотека будет содержать реализацию объявление и реализацию нескольких интерфейсов, предназначенных для отрисовки с помощью технологии Direct2D.

  3. Затем необходимо в свойствах нашего C++ проекта добавить ссылки на библиотеки Direct2D: d2d1.lib и d3d11.lib.

    Использование технологии Direct2D для создания WinRT компонентов

  4. Далее необходимо написать реализацию отрисовки нашего компонента с использованием 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++ библиотекой.

Использование технологии Direct2D для создания WinRT компонентов

Для этого проекта нужно добавить ссылки на созданную ранее C++ сборку:

Использование технологии Direct2D для создания WinRT компонентов

В этой библиотеке будет содержаться собственно сам WinRT Gauge. Чтобы включить его в нашу сборку, добавим новый Templated Control:

Использование технологии Direct2D для создания WinRT компонентов

Все визуальные элементы, кроме стрелки, будем отрисовывать стандартными средствами 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:

Использование технологии Direct2D для создания WinRT компонентов

Затем в этот проект добавим ссылки на проекты Gauge и DrawingLayer, созданные ранее:

Использование технологии Direct2D для создания WinRT компонентов

Далее добавим 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 приложении! Поздравляем!

Использование технологии Direct2D для создания WinRT компонентов

Примечание: полный код этого проекта вы можете скачать по следующей ссылке: go.devexpress.com/Habr_WinRTSample.aspx

P.S. Всех тех, кто хочет задать нам свои вопросы про разработку компонентов для WinRT, мы будем рады видеть 7-го сентября в Москве на Windows 8 Camp! Если у вас нет возможности посетить это мероприятие, то следите за онлайн трансляцией!

Автор: AlexKravtsov

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js