65.9K
CodeProject 正在变化。 阅读更多。
Home

D3dHost - MDX 和 WPF 互操作

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2012年3月23日

CPOL

5分钟阅读

viewsIcon

36220

downloadIcon

2736

本文介绍了如何在 WPF 窗口中渲染可互操作的 MDX (Managed DirectX) 场景。

Sample Image

目录

简介

Windows Presentation Foundation (WPF) 提供了用于渲染 3D 图形的一组元素。这些元素非常适合设计 3D 控件和渲染一些简单的 3D 场景。但如果我们想渲染更复杂的场景,可能需要使用更底层的技术。

要使用非托管代码渲染 DirectX 场景,我们可以使用 D3DImage 类,正如《D3DImage 简介》一文中所解释的那样。但如果我们想完全使用托管代码编写整个应用程序,我们可能需要使用一个支持使用托管代码渲染 DirectX 场景的框架。

要使用托管代码渲染 DirectX 场景,我们有 Managed DirectX (MDX) 框架。使用 MDX,我们可以创建一个 Device 并使用其方法渲染我们的场景。要在 WPF 窗口中渲染 MDX 场景,我们可以 使用 WPF 窗口的句柄创建 MDX 设备,或者 使用托管在 WPF 窗口中的 Windows.Forms 控件创建 MDX 设备,该控件通过 WindowsFormsHost 进行托管。

这些技术可能适用于渲染 MDX 场景的独立区域。但当我们想与其他 WPF 元素交互时,会发现 WPF 的某些效果(例如,不透明度、事务等)无法按预期工作。

本文介绍了如何在 WPF 窗口中将 MDX 场景渲染为可互操作的 WPF 控件。

工作原理

渲染 MDX 场景 

创建用于容纳 MDX 设备的控件

为了支持 WPF 和 MDX 之间的互操作性,我们创建了一个 WPF 自定义控件来托管 MDX 场景。

public class D3dHost : Control
{
    static D3dHost()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(D3dHost), 
            new FrameworkPropertyMetadata(typeof(D3dHost)));
    }
}

在该控件中,我们添加了一个 Windows.Forms Panel

#region D3dHostingPanel
private System.Windows.Forms.Panel _d3dHostingPanel;
protected System.Windows.Forms.Panel D3dHostingPanel
{
    get
    {
        if (_d3dHostingPanel == null)
        {
            _d3dHostingPanel = new System.Windows.Forms.Panel();

            int surfaceWidth = (int)D3dSurfaceWidth;
            int surfaceHeight = (int)D3dSurfaceHeight;

            _d3dHostingPanel.Width = (surfaceWidth > 0) ? surfaceWidth : 1;
            _d3dHostingPanel.Height = (surfaceHeight > 0) ? surfaceHeight : 1;
        }

        return _d3dHostingPanel;
    }
}
#endregion

#region D3dSurfaceWidth
public double D3dSurfaceWidth
{
    get { return (double)GetValue(D3dSurfaceWidthProperty); }
    set { SetValue(D3dSurfaceWidthProperty, value); }
}

public static readonly DependencyProperty D3dSurfaceWidthProperty =
    DependencyProperty.Register("D3dSurfaceWidth", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(1000.0, OnD3dSurfaceWidthChanged));

private static void OnD3dSurfaceWidthChanged(DependencyObject sender,
    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    dh.UpdateDeviceWidth();
}

private void UpdateDeviceWidth()
{
    int surfaceWidth = (int)D3dSurfaceWidth;
    D3dHostingPanel.Width = (surfaceWidth > 0) ? surfaceWidth : 1;
}
#endregion

#region D3dSurfaceHeight
public double D3dSurfaceHeight
{
    get { return (double)GetValue(D3dSurfaceHeightProperty); }
    set { SetValue(D3dSurfaceHeightProperty, value); }
}

public static readonly DependencyProperty D3dSurfaceHeightProperty =
    DependencyProperty.Register("D3dSurfaceHeight", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(1000.0, OnD3dSurfaceHeightChanged));

private static void OnD3dSurfaceHeightChanged(DependencyObject sender,
    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    dh.UpdateDeviceHeight();
}

private void UpdateDeviceHeight()
{
    int surfaceHeight = (int)D3dSurfaceHeight;
    D3dHostingPanel.Height = (surfaceHeight > 0) ? surfaceHeight : 1;
}
#endregion

并使用该 Panel 创建一个 MDX Device

#region D3dDevice       
private Device _d3dDevice;
public Device D3dDevice
{
    get 
    {
        if (_d3dDevice == null)
        {
            InitDevice();
        }

        return _d3dDevice; 
    }
}

protected void InitDevice()
{
    ReleaseDevice();

    PresentParameters presentParams = new PresentParameters();
    presentParams.Windowed = true;
    presentParams.SwapEffect = D3dSwapEffect;
    presentParams.EnableAutoDepthStencil = D3dEnableAutoDepthStencil;
    presentParams.AutoDepthStencilFormat = D3dAutoDepthStencilFormat; 

    _d3dDevice = new Device(0, D3dDeviceType, D3dHostingPanel, D3dCreateFlags, presentParams);
}

protected void ReleaseDevice()
{
    if (_d3dDevice != null)
    {
        _d3dDevice.Dispose();
        _d3dDevice = null;
    }
}
#endregion

#region D3dDeviceType
public DeviceType D3dDeviceType
{
    get { return (DeviceType)GetValue(D3dDeviceTypeProperty); }
    set { SetValue(D3dDeviceTypeProperty, value); }
}

public static readonly DependencyProperty D3dDeviceTypeProperty =
       DependencyProperty.Register("D3dDeviceType", typeof(DeviceType), typeof(D3dHost), 
       new UIPropertyMetadata(DeviceType.Hardware, OnD3dDeviceTypeChanged));

private static void OnD3dDeviceTypeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

#region D3dCreateFlags
public CreateFlags D3dCreateFlags
{
    get { return (CreateFlags)GetValue(D3dCreateFlagsProperty); }
    set { SetValue(D3dCreateFlagsProperty, value); }
}

public static readonly DependencyProperty D3dCreateFlagsProperty =
    DependencyProperty.Register("D3dCreateFlags", typeof(CreateFlags), typeof(D3dHost), 
        new UIPropertyMetadata(CreateFlags.SoftwareVertexProcessing, OnD3dCreateFlagsChanged));

private static void OnD3dCreateFlagsChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}        
#endregion

#region D3dSwapEffect
public SwapEffect D3dSwapEffect
{
    get { return (SwapEffect)GetValue(D3dSwapEffectProperty); }
    set { SetValue(D3dSwapEffectProperty, value); }
}

public static readonly DependencyProperty D3dSwapEffectProperty =
    DependencyProperty.Register("D3dSwapEffect", typeof(SwapEffect), typeof(D3dHost), 
        new UIPropertyMetadata(SwapEffect.Discard, OnD3dSwapEffectChanged));

private static void OnD3dSwapEffectChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}              
#endregion

#region D3dEnableAutoDepthStencil
public bool D3dEnableAutoDepthStencil
{
    get { return (bool)GetValue(D3dEnableAutoDepthStencilProperty); }
    set { SetValue(D3dEnableAutoDepthStencilProperty, value); }
}

public static readonly DependencyProperty D3dEnableAutoDepthStencilProperty =
    DependencyProperty.Register("D3dEnableAutoDepthStencil", typeof(bool), typeof(D3dHost),
        new UIPropertyMetadata(true, OnD3dEnableAutoDepthStencilChanged));

private static void OnD3dEnableAutoDepthStencilChanged(DependencyObject sender, 
                    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

#region D3dAutoDepthStencilFormat
public DepthFormat D3dAutoDepthStencilFormat
{
    get { return (DepthFormat)GetValue(D3dAutoDepthStencilFormatProperty); }
    set { SetValue(D3dAutoDepthStencilFormatProperty, value); }
}

public static readonly DependencyProperty D3dAutoDepthStencilFormatProperty =
    DependencyProperty.Register("D3dAutoDepthStencilFormat", typeof(DepthFormat), typeof(D3dHost),
    new UIPropertyMetadata(DepthFormat.D16, OnD3dAutoDepthStencilFormatChanged));

private static void OnD3dAutoDepthStencilFormatChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

创建用于渲染 MDX 场景的区域

要在 WPF 控件上渲染 MDX 场景,我们为渲染 MDX 场景的区域添加了一个 TemplatePart

[TemplatePart(Name = "PART_D3dRegion", Type = typeof(Rectangle))]
public class D3dHost : Control
{
}

创建一个包含 Rectangle 的默认样式,该 RectangleTemplatePart 的名称命名。

<Style TargetType="{x:Type local:D3dHost}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:D3dHost}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Rectangle Name="PART_D3dRegion" 
                                Stroke="Transparent" 
                                StrokeThickness="0" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

并根据名称查找 Rectangle

private Rectangle _d3dRegion;

public override void OnApplyTemplate()
{
    _d3dRegion = GetTemplateChild("PART_D3dRegion") as Rectangle;

    base.OnApplyTemplate();
}

渲染 MDX 设备的表面

要渲染 MDX 场景,我们获取 MDX 表面的副本,并将其设置为 RectangleFill。我们可以通过以下几种方式实现:

  • 使用 D3DImage

    #region SetD3dRegionFillUsingD3DImage
    
    // First approach for setting the Fill of the 3D region, can be using a D3DImage.
    // For presenting a DirectX scene using a D3DImage,
    // we can set the D3DImage's back-buffer with the surface's pointer. 
    // But, If we set the D3DImage (that holds the surface's pointer) as the Fill of the 3D region,
    // every change to the surface, can be reflected directly to the Fill of the 3D region.
    // Therefore, in order to update the Fill of the 3D region,
    // only when the scene has been completely rendered,
    // we set the Fill of the 3D region with a copy of the D3Dimage's back-buffer.
    // Since the D3DImage.CopyBackBuffer method is protected,
    // we create another class for getting the copy of the back-buffer (D3DImageEx).
    
    protected class D3DImageEx : D3DImage
    {
        public D3DImageEx()
        {
        }
    
        public D3DImageEx(double width, double height)
            : base(width, height)
        {
        }
    
        public BitmapSource GetBackBufferCopy()
        {
            return CopyBackBuffer();
        }
    }
    
    private void SetD3dRegionFillUsingD3DImage(Surface s)
    {
        bool isD3dRegionUpdateNeeded = true;
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.Invoke(
            new ThreadStart(() =>
            {
                try
                {
                    // Create a D3DImage that holds the surface's pointer.
                    D3dHost.D3DImageEx di = new D3dHost.D3DImageEx(D3dSurfaceWidth, D3dSurfaceHeight);
                    SetD3dImageBackBuffer(di, s);
    
                    // Set the Fill of the 3D region, to the D3DImage's back-buffer's copy.
                    BitmapSource bs = di.GetBackBufferCopy();
                    _d3dRegion.Fill = new ImageBrush(bs);
    
                    isD3dRegionUpdateNeeded = false;
    
                    _isSetD3dRegionFillUsingD3DImageSupported = true;
                }
                catch (Exception ex)
                {
                    if (_isSetD3dRegionFillUsingD3DImageSupported != true)
                    {
                        // The update using D3DImage isn't supported...
                        _isSetD3dRegionFillUsingD3DImageSupported = false;
                    }
                }
            }), TimeSpan.FromMilliseconds(MillisecondsForDispatcherInvokeTimeout));
    
        if (isD3dRegionUpdateNeeded && _continueUpdateD3dRegionThread)
        {
            InvalidateD3dRegion();
        }
    }
    
    private void SetD3dImageBackBuffer(D3DImage di, Surface s)
    {
        if (di == null || s == null)
        {
            return;
        }
    
        IntPtr backBuffer;
        unsafe
        {
            backBuffer = new IntPtr(s.UnmanagedComPointer);
        }
    
        di.Lock();
        di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, backBuffer);
        di.AddDirtyRect(new Int32Rect(0, 0, di.PixelWidth,
            di.PixelHeight));
        di.Unlock();
    }
    
    #endregion
  • 使用内存中的缓冲区

    #region SetD3dRegionFillUsingMemory
    
    // A second approach for setting the Fill of the 3D region, can be using a buffer in the memory.
    
    private GraphicsStream _d3dGraphicsStream;
    
    private void SetD3dRegionFillUsingMemory(Surface s)
    {
        GraphicsStream oldGraphicsStream = _d3dGraphicsStream;
    
        // Store the back-buffer as an image in the memory.
        GraphicsStream newGraphicsStream = SurfaceLoader.SaveToStream(ImageFileFormat.Bmp, s);
        newGraphicsStream.Seek(0, System.IO.SeekOrigin.Begin);
    
        lock (_d3dRegion)
        {
            _d3dGraphicsStream = newGraphicsStream;
        }
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_d3dRegion)
                {
                    if (_continueUpdateD3dRegionThread)
                    {
                        try
                        {
                            // Create an ImageSource that contains the image of the back-buffer.
                            BitmapImage bi = new BitmapImage();
                            bi.BeginInit();
                            bi.StreamSource = _d3dGraphicsStream;
                            bi.EndInit();
    
                            // Set the Fill of the 3D region to the image of the back-buffer.
                            _d3dRegion.Fill = new ImageBrush(bi);
    
                            // The operation succeeded. So, it is supported.
                            _isSetD3dRegionFillUsingMemorySupported = true;
                        }
                        catch
                        {
                            if (_isSetD3dRegionFillUsingMemorySupported == true)
                            {
                                // There is a failure in the operation. But, it's supported.
                                // Maybe we have to free the memory of the unneeded GraphicsStream objects.
                                _isMemoryFreeNeeded = true;
                            }
                            else
                            {
                                // The update using the memory isn't supported...
                                _isSetD3dRegionFillUsingMemorySupported = false;
                            }
    
                            // This operation has failed. Give it another chance.
                            _updateD3dRegionEvent.Set();
                        }
                    }
                }
            }));
    
        ReleaseD3dGraphicsStream(oldGraphicsStream);
    }
    
    private void ReleaseD3dGraphicsStream(GraphicsStream d3dGraphicsStream)
    {
        if (d3dGraphicsStream != null)
        {
            d3dGraphicsStream.Close();
    
            // Extra Close ( http://www.eggheadcafe.com/microsoft/ 
            //    Win32-DirectX-Managed/31961917/
            //    surfaceloadersavetostream-major-memory-leak.aspx )
            d3dGraphicsStream.Close();
        }
    }
    
    protected void ReleaseD3dRegionMemory()
    {
        lock (_d3dRegion)
        {
            if (_d3dGraphicsStream != null)
            {
                ReleaseD3dGraphicsStream(_d3dGraphicsStream);
                _d3dGraphicsStream = null;
            }
        }
    }
    #endregion
  • 使用磁盘上的文件

    #region SetD3dRegionFillUsingFile
    
    // A third approach for setting the Fill of the 3D region, can be using an image file.
    
    private string _currentD3dRegionFillFileName;
    
    #region UsedFileNames
    private List<string> _usedFileNames;
    public List<string> UsedFileNames
    {
        get { return _usedFileNames ?? (_usedFileNames = new List<string>()); }
    }
    #endregion
    
    private void SetD3dRegionFillUsingFile(Surface s)
    {
        // Get available file name, for the storing the back-buffer.
        string currD3dRegionFillFileName = GetAvailableFileName();
               
        // Save the back-buffer as a file.
        SurfaceLoader.Save(currD3dRegionFillFileName, ImageFileFormat.Jpg, s);
    
        lock (_d3dRegion)
        {
            _currentD3dRegionFillFileName = currD3dRegionFillFileName;
    
            // Store the back-buffer file's name, in order to delete it later.
            UsedFileNames.Add(currD3dRegionFillFileName);
        }
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_d3dRegion)
                {
                    if (_continueUpdateD3dRegionThread)
                    {
                        try
                        {
                            // Set the Fill of the 3D region to the saved back-buffer's file.
                            _d3dRegion.Fill =
                                new ImageBrush(new BitmapImage(
                                    new Uri(_currentD3dRegionFillFileName, UriKind.Relative)));
                        }
                        catch
                        {
                        }
                    }
                }
            }));
    
        // Delete the used files except the last one (we use it as the current Fill...).
        DeleteD3dRegionFiles(false);
    }
    
    private string GetAvailableFileName()
    {
        string fileNameBegin = "MdxWpf";
        string fileNameEnd = ".jpg";
    
        int fileNameCounter = 1;
    
        string currFileName = string.Format("{0}{1}{2}", 
            fileNameBegin, fileNameCounter.ToString(), fileNameEnd);
    
        while (File.Exists(currFileName) && fileNameCounter > 0)
        {
            fileNameCounter++;
    
            currFileName = string.Format("{0}{1}{2}",
                fileNameBegin, fileNameCounter.ToString(), fileNameEnd);
        }
    
        return currFileName;
    }
    
    protected void DeleteD3dRegionFiles(bool deleteLastFile)
    {
        string[] usedFileNamesCopy = null;
    
        lock (_d3dRegion)
        {
            usedFileNamesCopy = UsedFileNames.ToArray();
        }
    
        int filesCount = usedFileNamesCopy.Length;
    
        if (filesCount < 1)
        {
            return;
        }
    
        if (!deleteLastFile)
        {
            filesCount--;
        }
    
        for (int fileInx = 0; fileInx < filesCount; fileInx++)
        {
            string currFileName = usedFileNamesCopy[fileInx];
    
            try
            {
                if (File.Exists(currFileName))
                {
                    File.Delete(currFileName);
                }
    
                lock (_d3dRegion)
                {
                    UsedFileNames.Remove(currFileName);
                }
            }
            catch
            {
            }
        }
    }
    #endregion

由于我们不希望在场景繁重时阻塞 UI,因此我们在另一个线程中更新 3D 区域。

#region TryUseD3DImageBeforeUsingMemory
public bool TryUseD3DImageBeforeUsingMemory { get; set; }
#endregion

#region TryUseMemoryBeforeUsingFilesSystem
public bool TryUseMemoryBeforeUsingFilesSystem { get; set; }
#endregion

#region FreeMemoryBeforeUpdateD3dRegion
public bool FreeMemoryBeforeUpdateD3dRegion { get; set; }
#endregion

#region MillisecondsForDispatcherInvokeTimeout
public double MillisecondsForDispatcherInvokeTimeout { get; set; }
#endregion

public void InvalidateD3dRegion()
{
    if (_d3dRegion == null)
    {
        return;
    }

    // Start the thread that updates the Fill of the 3D region, if it is needed.
    if (_updateD3dRegionThread == null)
    {
        StartUpdateD3dRegionThread();
    }

    // Indicate that the Fill of the 3D region is invalid.
    _updateD3dRegionEvent.Set();
}

#region UpdateD3dRegion

private Thread _updateD3dRegionThread = null;
private bool _continueUpdateD3dRegionThread;
private AutoResetEvent _updateD3dRegionEvent = new AutoResetEvent(false);
private bool _isMemoryFreeNeeded = false;
private bool? _isSetD3dRegionFillUsingD3DImageSupported = null;
private bool? _isSetD3dRegionFillUsingMemorySupported = null;

private void UpdateD3dRegion()
{
    if (_d3dRegion == null)
    {
        return;
    }

    //Lock the D3dHostingPanel, for waiting to the scene to be fully rendered.
    Monitor.Enter(D3dHostingPanel);

    if (FreeMemoryBeforeUpdateD3dRegion || _isMemoryFreeNeeded)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    try
    {
        // Get the device's back-buffer.
        Surface s = D3dDevice.GetBackBuffer(0, 0, BackBufferType.Mono);

        if (TryUseD3DImageBeforeUsingMemory && _isSetD3dRegionFillUsingD3DImageSupported != false)
        {
            SetD3dRegionFillUsingD3DImage(s);
        }
        else if (TryUseMemoryBeforeUsingFilesSystem && _isSetD3dRegionFillUsingMemorySupported != false)
        {
            SetD3dRegionFillUsingMemory(s);
        }
        else
        {
            SetD3dRegionFillUsingFile(s);
        }
    }
    catch
    {
    }
    finally
    {
        // Unlock the D3dHostingPanel, for letting the scene to be rendered.
        Monitor.Exit(D3dHostingPanel);
    }
}

private void StartUpdateD3dRegionThread()
{
    if (_updateD3dRegionThread == null)
    {
        _continueUpdateD3dRegionThread = true;

        _updateD3dRegionThread = new Thread(new ThreadStart(() =>
            {
                while (_continueUpdateD3dRegionThread)
                {
                    _updateD3dRegionEvent.WaitOne();

                    if (_continueUpdateD3dRegionThread)
                    {
                        UpdateD3dRegion();
                    }
                }
            }));

        _updateD3dRegionThread.Start();
    }
}

private void StopUpdateD3dRegionThread()
{
    if (_updateD3dRegionThread != null)
    {
        _continueUpdateD3dRegionThread = false;
        _updateD3dRegionEvent.Set();
        _updateD3dRegionThread.Join();
        _updateD3dRegionThread = null;
    }
}

#endregion

protected void ReleaseD3dRegion()
{
    StopUpdateD3dRegionThread();

    ReleaseD3dRegionMemory();

    DeleteD3dRegionFiles(true);
}

为确保场景在呈现之前完全渲染,我们添加了指示绘图开始和结束的方法。

public void BeginDrawing()
{
    // Lock the D3dHostingPanel, for ensuring that the scene is fully rendered.
    Monitor.Enter(D3dHostingPanel);
}

public void EndDrawing()
{
    // Unlock the D3dHostingPanel, for letting the scene to be presented.
    Monitor.Exit(D3dHostingPanel);
            
    // Present the scene.
    InvalidateD3dRegion();
}

通知 MDX 区域大小的变化

为了通知托管 MDX 场景的区域的大小变化,我们添加了该区域的实际宽度和高度的属性。

#region D3dRegionActualWidth
public double D3dRegionActualWidth
{
    get { return (double)GetValue(D3dRegionActualWidthProperty); }
    private set { SetValue(D3dRegionActualWidthProperty, value); }
}

public static readonly DependencyProperty D3dRegionActualWidthProperty =
    DependencyProperty.Register("D3dRegionActualWidth", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(0.0));
#endregion

#region D3dRegionActualHeight
public double D3dRegionActualHeight
{
    get { return (double)GetValue(D3dRegionActualHeightProperty); }
    private set { SetValue(D3dRegionActualHeightProperty, value); }
}

public static readonly DependencyProperty D3dRegionActualHeightProperty =
    DependencyProperty.Register("D3dRegionActualHeight", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(0.0));
#endregion

并添加了一个 RoutedEvent,每次 MDX 场景区域的大小发生变化时都会引发该事件。

#region D3dRegionSizeChanged
public static readonly RoutedEvent D3dRegionSizeChangedEvent = EventManager.RegisterRoutedEvent(
    "D3dRegionSizeChanged", RoutingStrategy.Bubble, typeof(SizeChangedEventHandler), typeof(D3dHost));

public event SizeChangedEventHandler D3dRegionSizeChanged
{
    add { AddHandler(D3dRegionSizeChangedEvent, value); }
    remove { RemoveHandler(D3dRegionSizeChangedEvent, value); }
}
#endregion

public override void OnApplyTemplate()
{
    _d3dRegion = GetTemplateChild("PART_D3dRegion") as Rectangle;

    if (_d3dRegion != null)
    {
        D3dRegionActualWidth = _d3dRegion.ActualWidth;
        D3dRegionActualHeight = _d3dRegion.ActualHeight;

        _d3dRegion.SizeChanged += (s, e) =>
            {
                D3dRegionActualWidth = _d3dRegion.ActualWidth;
                D3dRegionActualHeight = _d3dRegion.ActualHeight;

                // Raise the D3dRegionSizeChanged on the D3D region's size is changed.
                e.RoutedEvent = D3dHost.D3dRegionSizeChangedEvent;
                RaiseEvent(e);
            };
    }

    base.OnApplyTemplate();
}

通知鼠标事件

为了通知在 MDX 场景上发生的鼠标事件,我们为每个鼠标事件创建一个 RoutedEventGotMouseCaptureLostMouseCaptureMouseEnterMouseLeaveMouseMoveMouseDownMouseLeftButtonDownMouseLeftButtonUpMouseRightButtonDownMouseRightButtonUpMouseUpMouseWheelPreviewMouseDownPreviewMouseLeftButtonDownPreviewMouseMovePreviewMouseRightButtonDownPreviewMouseRightButtonUpPreviewMouseUpPreviewMouseWheel)。例如,这是 MouseMove 事件的 RoutedEvent

public static readonly RoutedEvent D3dSurfaceMouseMoveEvent = EventManager.RegisterRoutedEvent(
    "D3dSurfaceMouseMove", RoutingStrategy.Bubble, 
    typeof(D3dSurfaceMouseEventHandler), typeof(D3dHost));

public event D3dSurfaceMouseEventHandler D3dSurfaceMouseMove
{
    add { AddHandler(D3dSurfaceMouseMoveEvent, value); }
    remove { RemoveHandler(D3dSurfaceMouseMoveEvent, value); }
}

为了使用 MDX 表面上的鼠标位置引发适当的 RoutedEvent,我们获取表面的鼠标位置。

private Point GetD3dSurfaceMousePosition(MouseEventArgs mouseArgs)
{
    // Get the mouse position on the 3D region.
    Point d3dRegionMousePosition = mouseArgs.GetPosition(_d3dRegion);

    // Calculate the mouse position on the MDX surface.
    Point d3dSurfaceMousePosition =
        new Point(d3dRegionMousePosition.X * D3dSurfaceWidth / D3dRegionActualWidth,
            d3dRegionMousePosition.Y * D3dSurfaceHeight / D3dRegionActualHeight);

    return d3dSurfaceMousePosition;
}

获取适当的鼠标事件。

private RoutedEvent GetD3dSurfaceMouseEvent(MouseEventArgs mouseArgs)
{
    string d3dRegionEventName = mouseArgs.RoutedEvent.Name;

    string d3dSurfaceEventName;
    if (d3dRegionEventName.StartsWith("Preview"))
    {
        d3dSurfaceEventName = "PreviewD3dSurface" + d3dRegionEventName.Substring(7);
    }
    else
    {
        d3dSurfaceEventName = "D3dSurface" + d3dRegionEventName;
    }

    RoutedEvent d3dSurfaceMouseEvent =
        EventManager.GetRoutedEvents().FirstOrDefault(
            re => re.OwnerType == typeof(D3dHost) && re.Name == d3dSurfaceEventName);

    return d3dSurfaceMouseEvent;
}

并在原始鼠标事件的事件处理程序中引发适当的鼠标事件。

private void RegisterD3dRegionMouseEvents()
{
    ...
    _d3dRegion.MouseMove += OnD3dRegionMouseEvent;
    ...
}

private void OnD3dRegionMouseEvent(object sender, MouseEventArgs e)
{
    RoutedEvent d3dSurfaceMouseEvent = GetD3dSurfaceMouseEvent(e);

    if (d3dSurfaceMouseEvent != null)
    {
        D3dSurfaceMouseEventArgs d3dSurfaceEventArgs = 
            new D3dSurfaceMouseEventArgs(d3dSurfaceMouseEvent)
        {
            MouseEventArgs = e,
            D3dSurfaceMousePosition = GetD3dSurfaceMousePosition(e)
        };

        RaiseEvent(d3dSurfaceEventArgs);
    }
}

如何使用

环境设置

防止出现“LoaderLock 已检测到”弹出窗口

在某些情况下,调试代码时可能会出现“LoaderLock 已检测到”的弹出窗口。为了停止它,我们可以选择“Debug”菜单下的“Exceptions”选项,然后在“Managed Debugging Assistants”组下取消选中“LoaderLock”项。

设置配置以支持“混合模式程序集”

为了支持在 .NET 4 上使用混合模式程序集,我们可以将应用程序配置的 startup 元素的 useLegacyV2RuntimeActivationPolicy 属性设置为 true,如下所示:

<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

渲染场景

为了演示 D3dHost 控件在渲染场景中的使用,我们创建了一个显示一些旋转锥体的窗口。

为了比较 MDX 3D 框架和 WPF 3D 框架,我们使用 MDX 和 WPF 渲染相同的场景。

要渲染场景,我们添加一个 Grid,其中包含一个用于容纳场景的 ContentControl 和一个用于确定显示的锥体数量的 Slider

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Name="txtHeader"
                FontSize="36"
                HorizontalAlignment="Center" />
    <Viewbox Grid.Row="1">
        <ContentControl Name="content3d"
                        Width="1000"
                        Height="1000"/>
    </Viewbox>
    <DockPanel Grid.Row="2" Margin="5,0,5,5">
        <TextBlock Text="Cones quantity: " DockPanel.Dock="Left" />
        <TextBlock Text=")" DockPanel.Dock="Right" />
        <TextBlock Name="txtConesQuantity" DockPanel.Dock="Right" />
        <TextBlock Text=" (" DockPanel.Dock="Right" />
        <Slider Name="conesSlider"
            Minimum="1" Maximum="200" Value="5" 
            ValueChanged="conesSlider_ValueChanged" />
    </DockPanel>
</Grid>

创建一个用于保存场景数据的类。

public class SceneData
{
    public SceneData()
    {
        CameraPosition = new Point3D(0, 0, 1000);
        FarPlaneDistance = 10000;
    }

    public Point3D CameraPosition { get; set; }

    public double FarPlaneDistance { get; set; }

    #region Cones
    private List<ConeData> _cones;
    public List<ConeData> Cones
    {
        get { return _cones ?? (_cones = new List<ConeData>()); }
    }
    #endregion
}

public class ConeData
{
    public double Height { get; set; }
    public double BaseRadius { get; set; }
    public Color MaterialColor { get; set; }
    public Point3D CenterPosition { get; set; }
    public double RotationX { get; set; }
    public double RotationZ { get; set; }
}

根据 Slider 的值初始化场景数据,使其包含一些锥体。

private SceneData _scene;

private void InitScene()
{
    lock (_scene)
    {
        _scene.Cones.Clear();

        Color[] colors =
            new Color[] { Colors.Red, Colors.Green, Colors.Blue,
                        Colors.Purple, Colors.Orange, Colors.DarkCyan };

        int numOfRows = (int)conesSlider.Value;
        int numOfColumns = (int)conesSlider.Value;

        double coneHeight = 300;
        double coneBaseRadius = 150;
        double conesDistance = 400;

        double cameraPositionZ =
            (Math.Cos(Math.PI / 8) / Math.Sin(Math.PI / 8)) *
                (conesDistance * ((double)numOfRows + 1) / 2);
        _scene.CameraPosition = new System.Windows.Media.Media3D.Point3D(0, 0, cameraPositionZ);
        _scene.FarPlaneDistance = cameraPositionZ + conesDistance;

        int colorIndexCounter = 0;
        for (int rowInx = 0; rowInx < numOfRows; rowInx++)
        {
            for (int colInx = 0; colInx < numOfColumns; colInx++)
            {
                double coneX = ((double)(numOfColumns - 1) / -2 + colInx) * conesDistance;
                double coneY = ((double)(numOfRows - 1) / -2 + rowInx) * conesDistance;

                _scene.Cones.Add(new ConeData
                {
                    Height = coneHeight,
                    BaseRadius = coneBaseRadius,
                    CenterPosition = new System.Windows.Media.Media3D.Point3D(coneX, coneY, 0),
                    MaterialColor = colors[colorIndexCounter % colors.Length]
                });

                colorIndexCounter++;
            }
        }

        txtConesQuantity.Text = _scene.Cones.Count.ToString();
    }
}

根据 RenderType 初始化窗口(此值通过窗口构造函数的参数设置)。

public enum RenderType
{
    MDX,
    WPF
}

private RenderType _renderType;

private D3dHost _mdxHost;
private Viewport3D _viewport3d;

private void InitWindow()
{
    if (_renderType == RenderType.WPF)
    {
        _viewport3d = new Viewport3D();
        content3d.Content = _viewport3d;
        txtHeader.Text = "WPF Scene";
    }
    else
    {
        _mdxHost = new D3dHost();
        content3d.Content = _mdxHost;
        txtHeader.Text = "MDX Scene";
    }
}

并创建用于更新场景和渲染场景的线程。

private Thread _updateThread;
private Thread _renderThread;
private bool _continueUpdateThread;
private bool _continueRenderThread;

private void StartThreads()
{
    _continueUpdateThread = true;
    _updateThread = new Thread(new ThreadStart(() =>
        {
            while (_continueUpdateThread)
            {
                UpdateScene();

                Thread.Sleep(10);
            }
        }));
    _updateThread.Start();

    _continueRenderThread = true;
    _renderThread = new Thread(new ThreadStart(() =>
        {
            while (_continueRenderThread)
            {
                RenderScene();

                Thread.Sleep(100);
            }
        }));
    _renderThread.Start();
}

private void UpdateScene()
{
    Random rand = new Random(DateTime.Now.Millisecond);

    lock (_scene)
    {
        foreach (ConeData cd in _scene.Cones)
        {
            int currRotationAxis = rand.Next(2);
            if (currRotationAxis == 1)
            {
                cd.RotationZ += 1;
            }
            else
            {
                cd.RotationX += 1;
            }
        }
    }
}

private void RenderScene()
{
    if (_renderType == RenderType.WPF)
    {
        // Since we add elements to a Viewport3D (and it is a part of the UI),
        // we have to render the scene in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_scene)
                {
                    WpfSceneRenderer.WpfRenderScene(_scene, _viewport3d);
                }
            }));
    }
    else
    {
        lock (_scene)
        {
            MdxSceneRenderer.MdxRenderScene(_scene, _mdxHost);
        }
    }
}

RenderScene 方法根据 RenderType 调用 MdxRenderScene 方法或 WpfRenderScene 方法。以下是这些方法的实现:

MDX 场景 


WPF 场景

public static void MdxRenderScene(SceneData scene, 
       D3dHost mdxHost)
{
    if (scene == null || mdxHost == null)
    {
        return;
    }

    mdxHost.BeginDrawing();

    Device device = mdxHost.D3dDevice;

    device.RenderState.ZBufferEnable = true;
    device.RenderState.Lighting = true;

    device.Clear(
        ClearFlags.Target |
            ClearFlags.ZBuffer,
            Color.White, 1.0f, 0);

    // Init camera matrices
    device.Transform.View =
        Matrix.LookAtLH(new Vector3(
                (float)scene.CameraPosition.X,
                (float)scene.CameraPosition.Y,
                (float)scene.CameraPosition.Z),
            new Vector3(0.0f, 0.0f, 0.0f),
            new Vector3(0.0f, 1.0f, 0.0f));
    device.Transform.Projection =
        Matrix.PerspectiveFovLH(
            (float)Math.PI / 4.0f, 1.0f, 1.0f,
            (float)scene.FarPlaneDistance);

    // Add directional light
    device.Lights[0].Type =
        LightType.Directional;
    device.Lights[0].Diffuse = Color.White;
    device.Lights[0].Direction =
        Vector3.Normalize(
            new Vector3(-1, -1, -1));
    device.Lights[0].Enabled = true;

    device.BeginScene();

    foreach (ConeData cone in scene.Cones)
    {
        MdxRenderCone(cone, device);
    }

    device.EndScene();

    mdxHost.EndDrawing();
}
public static void WpfRenderScene(SceneData scene, 
       Viewport3D viewport3d)
{
    if (scene == null || viewport3d == null)
    {
        return;
    }

    viewport3d.Children.Clear();

    // Init camera matrices
    viewport3d.Camera = new PerspectiveCamera
    {
        Position = scene.CameraPosition,
        UpDirection = new Vector3D(0, 1, 0),
        FarPlaneDistance = scene.FarPlaneDistance
    };

    // Add directional light
    Vector3D lightDirection = 
        new Vector3D(-1, -1, -1);
    lightDirection.Normalize();
    ModelVisual3D dirlight = new ModelVisual3D
    {
        Content = new DirectionalLight(
            Colors.White, lightDirection)
    };
    viewport3d.Children.Add(dirlight);

    foreach (ConeData cone in scene.Cones)
    {
        WpfRenderCone(cone, viewport3d);
    }
}
private static void MdxRenderCone(ConeData cone,
        Device device)
{
    float coneHeight = (float)cone.Height;
    float coneBaseRadius = 
        (float)cone.BaseRadius;

    // Create the cone's material
    Color col = Color.FromArgb(
        ColorToInt(cone.MaterialColor));
    Material mtrl = new Material();
    mtrl.Diffuse = col;
    device.Material = mtrl;

    // Create the cone's geometry

    int numOfPoints = (int)cone.BaseRadius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    double partAngle = Math.PI * 2 / numOfPoints;

    // Create the vertices' collections.
    CustomVertex.PositionNormal[] bodyVertices =
        new CustomVertex.PositionNormal[
            numOfPoints + 2];
    CustomVertex.PositionNormal[] baseVertices =
        new CustomVertex.PositionNormal[
            numOfPoints + 2];

    // Set the top vertex.
    bodyVertices[0].Position =
        new Vector3(0, coneHeight / 2, 0);
    bodyVertices[0].Normal =
        new Vector3(0, 1, 0);

    // Set the base center vertex.
    baseVertices[0].Position =
        new Vector3(0, coneHeight / -2, 0);
    baseVertices[0].Normal =
        new Vector3(0, -1, 0);

    float bodyNormalY =
        (float)(Math.Sin(Math.PI -
            Math.Atan(coneHeight / 
                coneBaseRadius) * 2) *
        Math.Sqrt(coneHeight * coneHeight +
            coneBaseRadius * coneBaseRadius));

    for (int vertexInx = 0; 
            vertexInx <= numOfPoints; 
            vertexInx++)
    {
        double currAngle = 
            vertexInx * partAngle;
        float currX =
            (float)(coneBaseRadius * 
                Math.Cos(currAngle));
        float currZ =
            (float)(coneBaseRadius * 
                Math.Sin(currAngle));

        // Set current body vertex.
        bodyVertices[numOfPoints + 1 - vertexInx].Position =
            new Vector3(
                currX, coneHeight / -2, currZ);
        bodyVertices[numOfPoints + 1 - vertexInx].Normal =
            Vector3.Normalize(new Vector3(
                currX, bodyNormalY, currZ));

        // Set current base vertex.
        baseVertices[vertexInx + 1].Position =
            new Vector3(
                currX, coneHeight / -2, currZ);
        baseVertices[vertexInx + 1].Normal =
            new Vector3(0, -1, 0);
    }

    // Set the world matrix
    float rotateXRadians =
        (float)(cone.RotationX / 180 * Math.PI);
    float rotateZRadians =
        (float)(cone.RotationZ / 180 * Math.PI);
    device.Transform.World =
        Matrix.RotationX(rotateXRadians) *
        Matrix.RotationZ(rotateZRadians) *
        Matrix.Translation(new Vector3(
            (float)cone.CenterPosition.X,
            (float)cone.CenterPosition.Y,
            (float)cone.CenterPosition.Z));

    // Render the cone
    device.VertexFormat =
        CustomVertex.PositionNormal.Format;
    device.DrawUserPrimitives(
        PrimitiveType.TriangleFan,
        numOfPoints, bodyVertices);
    device.DrawUserPrimitives(
        PrimitiveType.TriangleFan,
        numOfPoints, baseVertices);
}

private static int ColorToInt(
    System.Windows.Media.Color color)
{
    return (int)color.A << 24 |
        (int)color.R << 16 |
        (int)color.G << 8 |
        (int)color.B;
}
private static void WpfRenderCone(ConeData cone,
        Viewport3D viewport3d)
{
    // Create the cone's material
    DiffuseMaterial coneMaterial =
        new DiffuseMaterial(
            new SolidColorBrush(
                cone.MaterialColor));

    // Create the cone's geometry

    int numOfPoints = (int)cone.BaseRadius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    double partAngle = Math.PI * 2 / numOfPoints;

    // Create the vertices' collections.
    MeshGeometry3D coneMesh = new MeshGeometry3D();
    coneMesh.Positions = new Point3DCollection();
    coneMesh.Normals = new Vector3DCollection();
    coneMesh.TriangleIndices = new Int32Collection();

    // Set the top vertex.
    coneMesh.Positions.Add(new Point3D(
        0, cone.Height / 2, 0));
    coneMesh.Normals.Add(new Vector3D(0, 1, 0));

    // Set the base center vertex.
    coneMesh.Positions.Add(new Point3D(
        0, cone.Height / -2, 0));
    coneMesh.Normals.Add(new Vector3D(0, -1, 0));

    double bodyNormalY =
        Math.Sin(Math.PI -
            Math.Atan(cone.Height / 
                cone.BaseRadius) * 2) *
        Math.Sqrt(cone.Height * cone.Height +
            cone.BaseRadius * cone.BaseRadius);

    for (int vertexInx = 0;
         vertexInx <= numOfPoints;
         vertexInx++)
    {
        double currAngle = vertexInx * partAngle;
        double currX =
            cone.BaseRadius * Math.Cos(currAngle);
        double currZ =
            cone.BaseRadius * Math.Sin(currAngle);

        // Set current body vertex.
        coneMesh.Positions.Add(new Point3D(
            currX, cone.Height / -2, currZ));
        Vector3D bodyNormal = new Vector3D(
            currX, bodyNormalY, currZ);
        bodyNormal.Normalize();
        coneMesh.Normals.Add(bodyNormal);

        // Set current base vertex.
        coneMesh.Positions.Add(new Point3D(
            currX, cone.Height / -2, currZ));
        coneMesh.Normals.Add(
            new Vector3D(0, -1, 0));

        // Set current body and base indices.
        if (vertexInx > 0)
        {
            // Set current body index.
            coneMesh.TriangleIndices.Add(0); // Top                    
            coneMesh.TriangleIndices.Add(
                (vertexInx + 1) * 2);
            coneMesh.TriangleIndices.Add(
                vertexInx * 2);

            // Set current base index.
            coneMesh.TriangleIndices.Add(1); // Base center                    
            coneMesh.TriangleIndices.Add(
                vertexInx * 2 + 1);
            coneMesh.TriangleIndices.Add(
                (vertexInx + 1) * 2 + 1);
        }
    }

    GeometryModel3D coneGeometry =
        new GeometryModel3D(
            coneMesh, coneMaterial);

    // Set the world matrix
    Transform3DGroup transGroup =
        new Transform3DGroup();
    transGroup.Children.Add(
        new RotateTransform3D(
            new AxisAngleRotation3D(
                new Vector3D(1, 0, 0),
                cone.RotationX)));
    transGroup.Children.Add(
        new RotateTransform3D(
            new AxisAngleRotation3D(
                new Vector3D(0, 0, 1),
                cone.RotationZ)));
    transGroup.Children.Add(
        new TranslateTransform3D(
            cone.CenterPosition.X,
            cone.CenterPosition.Y,
            cone.CenterPosition.Z));

    // Render the cone
    ModelVisual3D coneModel =
        new ModelVisual3D
        {
            Content = coneGeometry,
            Transform = transGroup
        };
    viewport3d.Children.Add(coneModel);
}

结果可以如下所示:

MDX scene WPF scene

与 WPF 元素交互

为了演示使用 D3dHost 控件在 MDX 和 WPF 之间的互操作性,我们创建了一个显示可互操作 MDX 场景的窗口。

在该窗口中,我们添加了一个 D3dHost 控件来显示场景。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>
       
    <Grid Grid.Row="1"
            Opacity="{Binding Value, ElementName=opacitySlider}">
        <Grid.LayoutTransform>
            <RotateTransform Angle="{Binding Value, ElementName=rotationSlider}" />
        </Grid.LayoutTransform>
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible"
            VerticalScrollBarVisibility="Visible">
            <MdxWpfInteroperability:D3dHost x:Name="mdxHost"
                                            D3dSurfaceMouseLeave="mdxHost_D3dSurfaceMouseLeave"
                                            D3dSurfaceMouseMove="mdxHost_D3dSurfaceMouseMove"/>
        </ScrollViewer>           
    </Grid>

</Grid>

添加一个 Border 以启用某些效果。

<ToggleButton Name="optionsToggle"
                Content="Options"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" />

<Border Grid.Row="1"
        Visibility="{Binding IsChecked, ElementName=optionsToggle, 
                     Converter={StaticResource BooleanToVisibilityConverter}}"
        BorderBrush="DarkCyan"
        BorderThickness="2"
        Background="DarkBlue"
        TextElement.Foreground="Cyan"
        CornerRadius="5"
        Opacity="0.7"
        HorizontalAlignment="Left"
        VerticalAlignment="Top">
    <StackPanel Margin="5">
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Opacity: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=opacitySlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="opacitySlider" Minimum="0" Maximum="1" Value="0.8"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Rotation: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text=" degrees" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=rotationSlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="rotationSlider" Minimum="0" Maximum="360" Value="10"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Zoom: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=zoomSlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="zoomSlider" Minimum="0.05" Maximum="1" Value="0.5"
                    ValueChanged="zoomSlider_ValueChanged"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
    </StackPanel>
</Border>

添加一个 Border 来显示表面的鼠标位置。

<Border HorizontalAlignment="Right"
        BorderBrush="Green"
        BorderThickness="2"
        Background="DarkGreen"
        TextElement.Foreground="LightGreen"
        CornerRadius="5"
        Padding="5">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Surface mouse position: " />
        <TextBlock Name="surfaceMousePosition"
            Text="Out of surface" />
    </StackPanel>
</Border>

并添加背景以显示不透明度效果。

<Grid.Resources>
    <Border x:Key="backgroundVisual"
        Background="LightGreen"
        Opacity="0.2"
        Padding="10">
        <TextBlock Text="MDX & WPF Interoperability" 
                Foreground="DarkGreen"
                FontSize="32" />
    </Border>
</Grid.Resources>

<Grid.Background>
    <VisualBrush Visual="{StaticResource backgroundVisual}"
                    Viewport="0,0,0.33,0.2"
                    TileMode="Tile"/>
</Grid.Background>

为了演示鼠标互操作性,我们绘制了一个圆圈和一个显示圆圈中心位置的文本。

private void RenderScene()
{
    float circleCenterX = 200;
    float circleCenterY = 300;
    float circleRadius = 50;

    // Clear the surface.
    mdxHost.D3dDevice.Clear(Microsoft.DirectX.Direct3D.ClearFlags.Target, 
            ColorToInt(Colors.DarkGray), 1.0f, 0);

    // Draw a circle.
    Render2dCircle(circleCenterX, circleCenterY, circleRadius, 
                   Colors.Red, mdxHost.D3dDevice); // Stroke
    Render2dCircle(circleCenterX, circleCenterY, circleRadius - 3, 
                   Colors.DarkRed, mdxHost.D3dDevice); // Fill
    Render2dCircle(circleCenterX, circleCenterY, 5, Colors.Salmon, mdxHost.D3dDevice); // Center indication

    // Draw a text that presents the circle's center position.
    Render2dText(string.Format("Circle center: ({0},{1})", circleCenterX, circleCenterY),
        (int)(circleCenterX - circleRadius), (int)(circleCenterY + circleRadius + 10),
        36f, Colors.White, mdxHost.D3dDevice);

    // Present the scene on the D3dHost control.
    mdxHost.InvalidateD3dRegion();
}        

public void Render2dCircle(float centerX, float centerY, float radius, Color color,
    Microsoft.DirectX.Direct3D.Device device)
{
    int convertedColor = ColorToInt(color);

    int numOfPoints = (int)radius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored[] vertices = 
        new Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored[numOfPoints + 2];
    vertices[0].Position = new Microsoft.DirectX.Vector4(centerX, centerY, 0, 1.0f);
    vertices[0].Color = convertedColor;

    double partAngle = Math.PI * 2 / numOfPoints;

    for (int vertexInx = 0; vertexInx <= numOfPoints; vertexInx++)
    {
        double currAngle = vertexInx * partAngle;
        float currX = (float)(centerX + radius * Math.Cos(currAngle));
        float currY = (float)(centerY + radius * Math.Sin(currAngle));

        vertices[vertexInx + 1].Position = 
            new Microsoft.DirectX.Vector4(currX, currY, 0, 1.0f);
        vertices[vertexInx + 1].Color = convertedColor;
    }

    device.BeginScene();
    device.VertexFormat = Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored.Format;
    device.DrawUserPrimitives(Microsoft.DirectX.Direct3D.PrimitiveType.TriangleFan, numOfPoints, vertices);
    device.EndScene();
}

public void Render2dText(string text, int x, int y, float fontSize, 
       Color color, Microsoft.DirectX.Direct3D.Device device)
{
    System.Drawing.Font systemfont = 
       new System.Drawing.Font("Arial", fontSize, System.Drawing.FontStyle.Regular);
    Microsoft.DirectX.Direct3D.Font d3dFont = 
       new Microsoft.DirectX.Direct3D.Font(mdxHost.D3dDevice, systemfont);

    device.BeginScene();

    d3dFont.DrawText(null, text, new System.Drawing.Point(x, y),
        System.Drawing.Color.FromArgb(ColorToInt(color)));

    device.EndScene();

    d3dFont.Dispose();
}

处理 D3dSurfaceMouseMove 事件,在鼠标光标位于表面上时显示表面的鼠标位置。

private void mdxHost_D3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
{
    surfaceMousePosition.Text = string.Format("({0},{1})",
        e.D3dSurfaceMousePosition.X, e.D3dSurfaceMousePosition.Y);
}

并处理 D3dSurfaceMouseLeave 事件,在鼠标光标不在表面上时显示“Out of surface”。

private void mdxHost_D3dSurfaceMouseLeave(object sender, D3dSurfaceMouseEventArgs e)
{
    surfaceMousePosition.Text = "Out of surface";
}

结果可以如下所示:

Interoperability example

历史记录

  • 2012 年 3 月 23 日 - 初始版本。
  • 当前 - 添加了一个选项(默认选项),使用 D3DImage 来渲染 MDX 场景。
© . All rights reserved.