Search code examples
c#wpfwinforms-interopwpf-interop

WPF Interop Control renders black when .Net Framework is changed from 3.5 to 4.5


I have an interop question about embedding WinForms controls into a WPF application and differences in the .Net framework versions.

The following should show a transparent wpf control (red box) over a WebBrowser control and works as expected when using .Net 3.5:

enter image description here

... but after compilation with .Net 4.5 the following occurs.

enter image description here

Everything works again when switching back to .Net 3.5

The following Code works for WPF with Win32 using .Net 3.0 up to .Net 3.5 but not with .Net 4.0/4.5:

Win32HostRenderer.xaml:

<UserControl x:Class="WPFInterop.Interop.Win32HostRenderer"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
      <Image Name="_interopRenderer" Stretch="None"/>    
    </Grid>
</UserControl>

Win32HostRenderer.xaml.cs:

using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

using Winforms = System.Windows.Forms;
using GDI = System.Drawing;

using System.Runtime;
using System.Runtime.InteropServices;


namespace WPFInterop.Interop
{
    public partial class Win32HostRenderer : System.Windows.Controls.UserControl
    {
        [DllImport("user32.dll")]
        private static extern bool PrintWindow(IntPtr hwnd, IntPtr hdcBlt, uint nFlags);


    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);

    [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")]
    private static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length);

    private DispatcherTimer     _rendererTimer;
    private int                 _rendererInterval = 33;

    private InteropForm         _interopForm;
    private Winforms.Control    _winformControl;

    private BitmapSource        _bitmapSource;
    private BitmapBuffer        _bitmapSourceBuffer;

    private GDI.Bitmap          _gdiBitmap;
    private GDI.Graphics        _gdiBitmapGraphics;
    private IntPtr              _hGDIBitmap;


    public Win32HostRenderer()
    {
        InitializeComponent();
    }

    private void InitializeInteropForm()
    {
        if (_winformControl == null) return;

        if (_interopForm != null)
        {
            TearDownInteropForm();
        }

        _interopForm = new InteropForm();
        _interopForm.Opacity = 0.01;
        _interopForm.Controls.Add(_winformControl);
        _interopForm.Width = _winformControl.Width;
        _interopForm.Height = _winformControl.Height;
    }

    private void TearDownInteropForm()
    {
        if (_interopForm == null) return;
        _interopForm.Hide();
        _interopForm.Close();
        _interopForm.Dispose();
        _interopForm = null;
    }

    private void InitializeRendererTimer()
    {
        TearDownRenderTimer();

        _rendererTimer = new DispatcherTimer();
        _rendererTimer.Interval = new TimeSpan(0, 0, 0, 0, _rendererInterval);
        _rendererTimer.Tick += new EventHandler(_rendererTimer_Tick);
        _rendererTimer.Start();
    }

    void _rendererTimer_Tick(object sender, EventArgs e)
    {
        RenderWinformControl();
    }

    private void TearDownRenderTimer()
    {
        if (_rendererTimer == null) return;

        _rendererTimer.IsEnabled = false;

    }
    private void RegisterEventHandlers()
    {
        Window currentWindow = Window.GetWindow(this);
        currentWindow.LocationChanged += new EventHandler(delegate(object sender, EventArgs e)
        {
            PositionInteropFormOverRender();
        });


        currentWindow.SizeChanged += new SizeChangedEventHandler(delegate(object sender, SizeChangedEventArgs e)
        {
            PositionInteropFormOverRender();
        });

        currentWindow.Deactivated += new EventHandler(delegate(object sender, EventArgs e)
        {
            //_interopForm.Opacity = 0;
        });

        currentWindow.Activated += new EventHandler(delegate(object sender, EventArgs e)
        {
           // _interopForm.Opacity = 0.01;
        });

        currentWindow.StateChanged += new EventHandler(delegate(object sender, EventArgs e)
        {
            PositionInteropFormOverRender();
        });


        _interopRenderer.SizeChanged += new SizeChangedEventHandler(delegate(object sender, SizeChangedEventArgs e)
        {
            PositionInteropFormOverRender();
        });
    }



    private void PositionInteropFormOverRender()
    {
        if (_interopForm == null) return;
        Window currentWindow = Window.GetWindow(this);

        Point interopRenderScreenPoint = _interopRenderer.PointToScreen(new Point());

        _interopForm.Left = (int)interopRenderScreenPoint.X;
        _interopForm.Top = (int)interopRenderScreenPoint.Y;

        int width = 0;

        if ((int)_interopRenderer.ActualWidth > (int)currentWindow.Width)
        {
            width = (int)currentWindow.Width;
        }
        else
        {
            width = (int)_interopRenderer.ActualWidth;
        }

        if ((int)currentWindow.Width < width) 
                        width = (int)currentWindow.Width;

        _interopForm.Width = width;
    }

    private void InitializeBitmap()
    {
        if (_bitmapSource == null)
        {
            TearDownBitmap();
        }

        int interopRenderWidth = _winformControl.Width;
        int interopRenderHeight = _winformControl.Height;
        int bytesPerPixel = 4;

        int totalPixels = interopRenderWidth * interopRenderHeight * bytesPerPixel;

        byte[] dummyPixels = new byte[totalPixels];

        _bitmapSource = BitmapSource.Create(interopRenderWidth, 
                                    interopRenderHeight, 
                                    96, 
                                    96, 
                                    PixelFormats.Bgr32, 
                                    null,
                                    dummyPixels,
                                    interopRenderWidth * bytesPerPixel);

        _interopRenderer.Source = _bitmapSource;

        _bitmapSourceBuffer = new BitmapBuffer(_bitmapSource);

        _gdiBitmap = new GDI.Bitmap(_winformControl.Width,
                                    _winformControl.Height, 
                                    GDI.Imaging.PixelFormat.Format32bppRgb);

        _hGDIBitmap = _gdiBitmap.GetHbitmap();

        _gdiBitmapGraphics = GDI.Graphics.FromImage(_gdiBitmap);
    }

    private void TearDownBitmap()
    {
        _bitmapSource = null;
        _bitmapSourceBuffer = null;

        if (_gdiBitmap != null)
        {
            _gdiBitmap.Dispose();
        }

        if (_gdiBitmapGraphics != null)
        {
            _gdiBitmapGraphics.Dispose();
        }

        if (_hGDIBitmap != IntPtr.Zero)
        {
            DeleteObject(_hGDIBitmap);
        }
    }

    private void InitializeWinformControl()
    {
        InitializeInteropForm();
        InitializeBitmap();
        RegisterEventHandlers();

        PositionInteropFormOverRender();
        _interopForm.StartPosition = System.Windows.Forms.FormStartPosition.Manual;


        _interopForm.Show();
        InitializeRendererTimer();

    }

    public Winforms.Control Child
    {
        get { return _winformControl; }
        set
        {
            _winformControl = value;
            InitializeWinformControl();
        }
    }

    private void RenderWinformControl()
    {
        PaintWinformControl(_gdiBitmapGraphics, _winformControl);

        GDI.Rectangle lockRectangle = new GDI.Rectangle(0, 
                                                        0, 
                                                        _gdiBitmap.Width, 
                                                        _gdiBitmap.Height);

        GDI.Imaging.BitmapData bmpData = _gdiBitmap.LockBits(lockRectangle, 
                                                        GDI.Imaging.ImageLockMode.ReadOnly, 
                                                        GDI.Imaging.PixelFormat.Format32bppRgb);
        System.IntPtr bmpScan0 = bmpData.Scan0;

        CopyMemory(_bitmapSourceBuffer.BufferPointer, 
                   bmpScan0,
                   (int)_bitmapSourceBuffer.BufferSize);

        _gdiBitmap.UnlockBits(bmpData);

        _interopRenderer.InvalidateVisual(); 
    }

    private void PaintWinformControl(GDI.Graphics graphics, Winforms.Control control)
    { 
        IntPtr hWnd = control.Handle;
        IntPtr hDC = graphics.GetHdc();

        PrintWindow(hWnd, hDC, 0);

        graphics.ReleaseHdc(hDC);
        }
    }
}

BitmapBuffer.cs:

using System;
using System.Collections.Generic;
using System.Text;

using System.Windows;

using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime;
using System.Runtime.InteropServices.ComTypes;

using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WPFInterop
{
    class BitmapBuffer
    {
        private BitmapSource _bitmapImage = null;

        private object _wicImageHandle = null;

        private object _wicImageLock = null;

        private uint _bufferSize = 0;

        private IntPtr _bufferPointer = IntPtr.Zero;

        private uint _stride = 0;

        private int _width;

        private int _height;

        public BitmapBuffer(BitmapSource Image)
        {
            //Keep reference to our bitmap image
            _bitmapImage = Image;

            //Get around the STA deal
            _bitmapImage.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new System.Windows.Threading.DispatcherOperationCallback(delegate
            {
                //Cache our width and height
                _width = _bitmapImage.PixelWidth;
                _height = _bitmapImage.PixelHeight;
                return null;
            }), null);

            //Retrieve and store our WIC handle to the bitmap
            SetWICHandle();

            //Set the buffer pointer
            SetBufferInfo();
        }

        /// <summary>
        /// The pointer to the BitmapImage's native buffer
        /// </summary>
        public IntPtr BufferPointer
        {
            get
            {
                //Set the buffer pointer
                SetBufferInfo();
                return _bufferPointer;
            }
        }

        /// <summary>
        /// The size of BitmapImage's native buffer
        /// </summary>
        public uint BufferSize
        {
            get { return _bufferSize; }
        }

        /// <summary>
        /// The stride of BitmapImage's native buffer
        /// </summary>
        public uint Stride
        {
            get { return _stride; }
        }

        private void SetBufferInfo()
        {
            int hr = 0;

            //Get the internal nested class that holds some of the native functions for WIC
            Type wicBitmapNativeMethodsClass = Type.GetType("MS.Win32.PresentationCore.UnsafeNativeMethods+WICBitmap, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

            //Get the methods of all the static methods in the class
            MethodInfo[] info = wicBitmapNativeMethodsClass.GetMethods(BindingFlags.Static | BindingFlags.NonPublic);

            //This method looks good
            MethodInfo lockmethod = info[0];

            //The rectangle of the buffer we are
            //going to request
            Int32Rect rect = new Int32Rect();

            rect.Width = _width;
            rect.Height = _height;

            //Populate the arguments to pass to the function
            object[] args = new object[] { _wicImageHandle, rect, 2, _wicImageHandle };

            //Execute our static Lock() method
            hr = (int)lockmethod.Invoke(null, args);

            //argument[3] is our "out" pointer to the lock handle
            //it is set by our last method invoke call
            _wicImageLock = args[3];

            //Get the internal nested class that holds some of the
            //other native functions for WIC
            Type wicLockMethodsClass = Type.GetType("MS.Win32.PresentationCore.UnsafeNativeMethods+WICBitmapLock, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

            //Get all the native methods into our array
            MethodInfo[] lockMethods = wicLockMethodsClass.GetMethods(BindingFlags.Static | BindingFlags.NonPublic);

            //Our method to get the stride value of the image
            MethodInfo getStrideMethod = lockMethods[0];

            //Fill in our arguments
            args = new object[] { _wicImageLock, _stride };

            //Execute the stride method
            getStrideMethod.Invoke(null, args);

            //Grab out or byref value for the stride
            _stride = (uint)args[1];

            //This one looks perty...
            //This function will return to us 
            //the buffer pointer and size
            MethodInfo getBufferMethod = lockMethods[1];

            //Fill in our arguments
            args = new object[] { _wicImageLock, _bufferSize, _bufferPointer };

            //Run our method
            hr = (int)getBufferMethod.Invoke(null, args);

            _bufferSize = (uint)args[1];
            _bufferPointer = (IntPtr)args[2];

            DisposeLockHandle();
        }

        private void DisposeLockHandle()
        {
            MethodInfo close = _wicImageLock.GetType().GetMethod("Close");
            MethodInfo dispose = _wicImageLock.GetType().GetMethod("Dispose");

            close.Invoke(_wicImageLock, null);
            dispose.Invoke(_wicImageLock, null);
        }

        private void SetWICHandle()
        {
            //Get the type of bitmap image
            Type bmpType = typeof(BitmapSource);

            //Use reflection to get the private property WicSourceHandle
            FieldInfo fInfo = bmpType.GetField("_wicSource",
                                               BindingFlags.NonPublic | BindingFlags.Instance);

            //Retrieve the WIC handle from our BitmapImage instance
            _wicImageHandle = fInfo.GetValue(_bitmapImage);
        }

    }
}

The InteropForm is just a derived System.Windows.Forms.Form and has no special magic.

Integration into WPF-Page is simple:

  <interop:Win32HostRenderer x:Name="_host" Grid.Row="0">
  </interop:Win32HostRenderer>

And after window Loaded event:

    System.Windows.Forms.WebBrowser browser = new System.Windows.Forms.WebBrowser();
    browser.Navigate("http://www.youtube.com");
    browser.Width = 700;
    browser.Height = 500;

    _host.Child = browser;

(code parts from Jeremiah Morrill, see this blog for more info)


Solution

  • Ok, your first problem is with this bit of code:

        //Get the methods of all the static methods in the class
        MethodInfo[] info = wicBitmapNativeMethodsClass.GetMethods(BindingFlags.Static | BindingFlags.NonPublic);
    
        //This method looks good
        MethodInfo lockmethod = info[0];
    

    In .Net 4, the "Lock" method is at index 1 (probably because a new "static" method has been added)...so your reflection code is using the wrong one.

    enter image description here

    Instead you can just use this to get it by name (suggest altering your other GetMethods to GetMethod):

    MethodInfo lockmethod = wicBitmapNativeMethodsClass.GetMethod("Lock", BindingFlags.Static | BindingFlags.NonPublic);
    

    Here it is working (in .NET 4)...I made it fill the client space...as I didn't have your full source code...alter as necessary to do your visual reflection, etc:

    enter image description here

    The changes I made were:

    MethodInfo lockmethod = wicBitmapNativeMethodsClass.GetMethod("Lock", BindingFlags.Static |   BindingFlags.NonPublic);
    
    private void PositionInteropFormOverRender()
    {
        if (_interopForm == null) return;
        Window currentWindow = Window.GetWindow(this);
    
        FrameworkElement firstchild = this.Content as FrameworkElement;
        if (firstchild != null)
        {
            Point interopRenderScreenPoint = currentWindow.PointToScreen(new Point());
    
            _interopForm.Left = (int)interopRenderScreenPoint.X;
            _interopForm.Top = (int)interopRenderScreenPoint.Y;
    
            _interopForm.Width = (int)firstchild.RenderSize.Width;
            _interopForm.Height = (int)firstchild.RenderSize.Height;
        }
    }
    
    private void _host_Loaded(object sender, RoutedEventArgs e)
    {
        System.Windows.Forms.WebBrowser browser = new System.Windows.Forms.WebBrowser();
        browser.Navigate("http://www.youtube.com");
        browser.Width = 700;
        browser.Height = 500;
        browser.Dock = System.Windows.Forms.DockStyle.Fill;
    
        _host.Child = browser;
    }
    
    private void InitializeInteropForm()
    {
        if (_winformControl == null) return;
    
        if (_interopForm != null)
        {
            TearDownInteropForm();
        }
    
        _interopForm = new InteropForm();
        _interopForm.Opacity = 0.5;
        _interopForm.Controls.Add(_winformControl);
        _interopForm.Width = _winformControl.Width;
        _interopForm.Height = _winformControl.Height;
    }
    

    There are other things you need to fix/do/be aware of i.e.:

    • when the screen DPI is not 96dpi, you will have to convert the RenderSize.Width/Height values accordingly (see: How do I convert a WPF size to physical pixels?)
    • your "browser" window is hidden in the z-order when you drag the main Window...you just need to bring it back to the front.