StandardSwitcher preview for WindowsFormsHost controls

Docking/MDI for WPF Forum

Posted 6 years ago by Timothy
Version: 13.2.0592
Avatar

StandardSwitcher does not show any content in its preview area if the selected docking window contains a WindowsFormsHost control. Based on http://www.actiprosoftware.com/community/thread/3528/image-on-standard-switcher-window I tried to override OnSelectedWindowChanged, extract a bitmap of the Forms control, convert it to a WPF bitmap, and assign it to the according StandardSwitcher element. This is my code so far:

public static class WinFormsWpfBitmapConversion
{
    public static BitmapSource ToWpfBitmap(this System.Drawing.Bitmap bitmap)
    {
        using (var stream = new MemoryStream())
        {
            bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            stream.Position = 0;
            var result = new BitmapImage();
            result.BeginInit();
            result.CacheOption = BitmapCacheOption.OnLoad;
            result.StreamSource = stream;
            result.EndInit();
            result.Freeze();
            return result;
        }
    }
}

internal class WindowsFormsSwitcher : StandardSwitcher
{
    public WindowsFormsSwitcher()
    {
        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        // creep through logical tree till preview border
        var outerBorder = GetTemplateChild("outerBorder") as Border;
        if (outerBorder == null)
            return;
        var dockPanel = outerBorder.Child as DockPanel;
        if (dockPanel == null)
            return;
        if (dockPanel.Children == null || dockPanel.Children.Count < 3)
            return;
        var border1 = dockPanel.Children[2] as Border;
        if (border1 == null)
            return;
        var grid = border1.Child as Grid;
        if (grid == null)
            return;
        if (grid.Children == null || grid.Children.Count < 5)
            return;
        var border2 = grid.Children[4] as Border;
        if (border2 == null)
            return;
        var shadow = border2.Child as DropShadowChrome;
        if (shadow == null)
            return;
        var viewBox = shadow.Child as Viewbox;
        if (viewBox == null)
            return;
        var border3 = viewBox.Child as Border;
        if (border3 == null)
            return;
        var preview = border3.Child as DockingWindowPreview;
        if (preview == null)
            return;
        _previewBorder = preview.Child as Border;
    }
    
    protected override void OnSelectedWindowChanged(DockingWindow oldValue, DockingWindow newValue)
    {
        if (SelectedWindow == null || _previewBorder == null)
        {
            base.OnSelectedWindowChanged(oldValue, newValue);
            return;
        }
        // if window does not host a WindowsFormsHost call base method
        var windowsFormsHost = SelectedWindow.Content as WindowsFormsHost;
        if (windowsFormsHost == null)
        {
            base.OnSelectedWindowChanged(oldValue, newValue);
            return;
        }
        // show image of WindowsFormsHost
        var bitmapSource = GetBitmapSource(windowsFormsHost.Child);
        var imageBrush = new ImageBrush { ImageSource = bitmapSource };
        _previewBorder.Background = imageBrush;
        // do not call base
    }

    BitmapSource GetBitmapSource(System.Windows.Forms.Control control)
    {
        var bitmap = new System.Drawing.Bitmap(control.Width, control.Height);
        control.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
        var bitmapSource = bitmap.ToWpfBitmap();
        return bitmapSource;
    }

    private Border _previewBorder;
}

 Unfortunately it does not work. Changes to _previewBorder have no effect on the view. May I ask you for your suggestions?

Comments (9)

Posted 6 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Timothy,

I would suggest that you first try to see if your imageBrush is actually returning an image or not.  For instance, add a WPF Border control on your window and set the Background to that imageBrush to see if you see any valid results or not.  It could be that some of the bitmap capturing doesn't work correctly due to interop.


Actipro Software Support

Posted 6 years ago by Timothy
Avatar

Hi,

thank you for your suggestion. I did as you told me, but imageBrush contains the correct content. Snoop shows an empty VisualBrush as background of the switcher's border as if my brush has been overwritten afterwards.

Posted 6 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Timothy,

The control in the StandardSwitcher that shows the preview is DockingWindowPreview.  It refreshes its display any time it is Loaded or when its DockingWindow property is changed.

The StandardSwitcher template does this:

<docking:DockingWindowPreview DockingWindow="{TemplateBinding SelectedWindow}" />

You effectively would have to update the Border.Background any time after that binding updates.  Perhaps you could override StandardSwitcher.OnSelectedWindowChanged and run your code there?


Actipro Software Support

Posted 5 years ago by Timothy
Avatar

Hi,

I beg your pardon, but isn’t that exactly what I am doing now? I override StandardSwitcher.OnSelectedWindowChanged and set there a new brush to the background of the border contained by the DockingWindowPreview. But afterwards the preview still shows no content and Snoop presents an empty VisualBrush as background of the border instead of my ImageBrush.

Answer - Posted 5 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Timothy,

I apologize, I had meant to elaborate more on that.  You are running your code in OnSelectedWindowChanged which should occur right as the property change happens, but likely before the TemplateBinding I mentioned takes effect.  You might try using the Dispatcher to Invoke your code instead, at a dispatcher priority lower than the DataBinding one, to see if that helps.

If it doesn't maybe take a step back and make a plain Red SolidColorBrush and see if that is working in either case (what your code is now and using the Invoked version).

Please let us know the results.


Actipro Software Support

Posted 5 years ago by Timothy
Avatar

Hi,

I followed your suggestion and invoked the exchange of the background with low priority. This is the code so far:

protected override void OnSelectedWindowChanged(DockingWindow oldValue, DockingWindow newValue)
{
    ...
    // show image of WindowsFormsHost
    if (Application.Current == null)
        return;
    var applicationDispatcher = Application.Current.Dispatcher;
    applicationDispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() => SetPreview(windowsFormsHost)));
    // do not call base
}

void SetPreview(WindowsFormsHost windowsFormsHost)
{
    //var bitmapSource = GetBitmapSource(windowsFormsHost.Child);
    //var imageBrush = new ImageBrush { ImageSource = bitmapSource };
    //_previewBorder.Background = imageBrush;
    _previewBorder.Background = new SolidColorBrush(Colors.DeepPink);
}

 But, alas, no success: the perview remains unchanged.

Posted 5 years ago by Timothy
Avatar

Hi,

the problem is solved. Apparently the border for the preview cannot be extracted in advance. If it is searched just in time everything works fine. This is the finally working code:

internal class WindowsFormsSwitcher : StandardSwitcher
{
    protected override void OnSelectedWindowChanged(DockingWindow oldValue, DockingWindow newValue)
    {
        if (SelectedWindow == null)
        {
            base.OnSelectedWindowChanged(oldValue, newValue);
            return;
        }
        // if window does not host a WindowsFormsHost call base method
        var windowsFormsHost = SelectedWindow.Content as WindowsFormsHost;
        if (windowsFormsHost == null)
        {
            base.OnSelectedWindowChanged(oldValue, newValue);
            return;
        }
        // show image of WindowsFormsHost
        if (Application.Current == null)
            return;
        var applicationDispatcher = Application.Current.Dispatcher;
        applicationDispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() => SetPreview(windowsFormsHost)));
        // do not call base
    }

    private void SetPreview(WindowsFormsHost windowsFormsHost)
    {
        // get border showing the preview
        var previewBorder = GetPreviewBorder();
        if (previewBorder == null)
            return;
        // set image of WindowsFormsHost as background
        var bitmapSource = GetBitmapSource(windowsFormsHost.Child);
        var imageBrush = new ImageBrush { ImageSource = bitmapSource };
        previewBorder.Background = imageBrush;
    }

    private BitmapSource GetBitmapSource(System.Windows.Forms.Control control)
    {
        var bitmap = new System.Drawing.Bitmap(control.Width, control.Height);
        control.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
        return bitmap.ToWpfBitmap();
    }

    private Border GetPreviewBorder()
    {
        // creep through logical tree till preview border
        var outerBorder = GetTemplateChild("outerBorder") as Border;
        if (outerBorder == null)
            return null;
        var dockPanel = outerBorder.Child as DockPanel;
        if (dockPanel == null)
            return null;
        if (dockPanel.Children == null || dockPanel.Children.Count < 3)
            return null;
        var border1 = dockPanel.Children[2] as Border;
        if (border1 == null)
            return null;
        var grid = border1.Child as Grid;
        if (grid == null)
            return null;
        if (grid.Children == null || grid.Children.Count < 5)
            return null;
        var border2 = grid.Children[4] as Border;
        if (border2 == null)
            return null;
        var shadow = border2.Child as DropShadowChrome;
        if (shadow == null)
            return null;
        var viewBox = shadow.Child as Viewbox;
        if (viewBox == null)
            return null;
        var border3 = viewBox.Child as Border;
        if (border3 == null)
            return null;
        var preview = border3.Child as DockingWindowPreview;
        if (preview == null)
            return null;
        var previewBorder = preview.Child as Border;
        return previewBorder;
    }
}

 Thank you very much for your support. You made my day ;-)

Posted 5 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Great, I'm glad you have it working.  My main concern is that GetPreviewBorder method though because if we ever alter the template then it won't work.  Perhaps you could do something like this instead to get to the DockingWindowPreview:

var preview = ActiproSoftware.Windows.Media.VisualTreeHelperExtended.GetFirstDescendant(this, 
  typeof(DockingWindowPreview)) as DockingWindowPreview.


Actipro Software Support

Posted 5 years ago by Timothy
Avatar

Yes, good idea. Although it seems that I cannot become totally immune to changes in your template it looks a lot better now. The preview in this version shows also only the upper left corner of the docking window’s content as is the default behavior in StandardSwitcher.

internal class WindowsFormsSwitcher : StandardSwitcher
{
    protected override void OnSelectedWindowChanged(DockingWindow oldValue, DockingWindow newValue)
    {
        // if window does not host a WindowsFormsHost call base method
        WindowsFormsHost windowsFormsHost = null;
        if (SelectedWindow != null)
        {
            windowsFormsHost = SelectedWindow.Content as WindowsFormsHost;
        }
        if (windowsFormsHost == null || Application.Current == null)
        {
            base.OnSelectedWindowChanged(oldValue, newValue);
            return;
        }
        // show image of WindowsFormsHost after template binding has been resolved
        var applicationDispatcher = Application.Current.Dispatcher;
        applicationDispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() => SetPreview(windowsFormsHost)));
        // do not call base
    }

    private void SetPreview(WindowsFormsHost windowsFormsHost)
    {
        // get border showing the preview
        double previewBorderVisibleWidth;
        double previewBorderVisibleHeight;
        int displayPixelWidth;
        int displayPixelHeight;
        var previewBorder = GetPreviewBorder(out previewBorderVisibleWidth, out previewBorderVisibleHeight, out displayPixelWidth, out displayPixelHeight);
        if (previewBorder == null)
            return; // no need to call base.OnSelectedWindowChanged, it won't show anything anyway
        // set image of WindowsFormsHost's upper left corner as background
        var bitmap = new System.Drawing.Bitmap(displayPixelWidth, displayPixelHeight); // WindowsForms knows only physical pixels
        windowsFormsHost.Child.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
        var bitmapSource = bitmap.ToWpfBitmap();
        var imageBrush = new ImageBrush { ImageSource = bitmapSource };
        previewBorder.Width = previewBorderVisibleWidth;
        previewBorder.Height = previewBorderVisibleHeight;
        previewBorder.Background = imageBrush;
    }

    private Border GetPreviewBorder(out double previewBorderVisibleWidth, out double previewBorderVisibleHeight, out int displayPixelWidth, out int displayPixelHeight)
    {
        previewBorderVisibleWidth = previewBorderVisibleHeight = 200; // default value that will show an image but maybe distorted
        displayPixelWidth = displayPixelHeight = 200;
        // creep through logical tree till preview border
        var outerBorder = GetTemplateChild("outerBorder") as Border;
        if (outerBorder == null)
            return null;
        var shadow = ActiproSoftware.Windows.Media.VisualTreeHelperExtended.GetFirstDescendant(outerBorder, typeof(DropShadowChrome)) as DropShadowChrome;
        if (shadow == null)
            return null;
        var preview = ActiproSoftware.Windows.Media.VisualTreeHelperExtended.GetFirstDescendant(outerBorder, typeof(DockingWindowPreview)) as DockingWindowPreview;
        if (preview == null)
            return null;
        // get visible size of preview area (without framing border)
        previewBorderVisibleWidth = shadow.ActualWidth - 2;
        previewBorderVisibleHeight = shadow.ActualHeight - 2;
        // get visible size in physical screen pixels
        var source = PresentationSource.FromVisual(shadow);
        if (source != null && source.CompositionTarget != null)
        {
            var transformToDevice = source.CompositionTarget.TransformToDevice;
            var pixelSize = (Size)transformToDevice.Transform(new Vector(previewBorderVisibleWidth, previewBorderVisibleHeight));
            displayPixelWidth = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Width));
            displayPixelHeight = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Height));
        }
        var previewBorder = preview.Child as Border;
        return previewBorder;
    }
}
The latest build of this product (v2019.1 build 0683) was released 1 month ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.