Can I overlay a WPF window on top of another?

2019-01-07 09:13发布

问题:

I have a WPF window, which contains a WindowsFormsHost element. I need to draw things on top of this element, but the nature of WindowsFormsHost means that it's always on the top of the drawing pile. As I cannot draw in the same WPF window on top of the WindowsFormsHost component, can I overlay another window on top of it?

I've tried this in a rudimentary way, but I have a few problems:

1) I can't stop windows from other apps going in between the main window, and the overlay window.

2) When I Alt-Tab, the overlay window appears in the window list, which is pretty ugly.

Basically I need the concept of a "child window", and window which to all intents and purposes appears as part of another window. UserControls won't work for me, as the WindowsFormsHost will always draw on top of it.

Any Ideas?


Update [May 23 '11 at 10:13 ]

Thank you both for answers.

I've tried the ChildWindow approach, and the WindowsFormsHost element still draws on top. As I understand it, only a true window can draw on top of a WindowsFormsHost, anything in the same window will go under the WindowsFormsHost.

An element with the WindowsFormsHost will still draw under a WinForms component, they are always drawn on top, and that seems non-negotiable...

I guess what I'm looking for is a way to dock an external window to act as part of the main window. On the Mac, there is the concept of a true "child window", I'm looking for something like that.

回答1:

I've worked around this problem by using a Popup rather than a transparent Window

Update

I ended up with a subclassed Popup which I call AirspacePopup.

What AirspacePopup does

  • Follow its PlacementTarget.
  • Is not always-on-top, but placed relative to the Window in which it is being placed. This solution comes from Chris Cavanagh's Blog.
  • Is allowed to be moved "outside of the screen". This is achieved by clipping the Popup and setting negative Margin on its child once it moves off-screen. This solution comes from this StackOverflow post by Rick Sladkey

Here is an example where AirspacePopup is used to draw an Ellipse on top of a WebBrowser Control (which is in fact a WinForms Control) but it will work just as well with any WindowsFormsHost.

<Grid>
    <local:AirspacePopup PlacementTarget="{Binding ElementName=webBrowser}"
                         FollowPlacementTarget="True"
                         AllowOutsideScreenPlacement="True"
                         ParentWindow="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                         IsOpen="True"
                         AllowsTransparency="True"
                         Placement="Center"
                         Width="{Binding ElementName=googleBrowser, Path=ActualWidth}"
                         Height="{Binding ElementName=googleBrowser, Path=ActualHeight}">
        <Grid>
            <Ellipse Width="100" Height="100" Fill="Green" Margin="100"/>
        </Grid>
    </local:AirspacePopup>
    <WebBrowser Name="webBrowser" Loaded="WebBrowser_Loaded"/>
</Grid>

Simple Code behind to navigate..

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void WebBrowser_Loaded(object sender, RoutedEventArgs e)
    {
        WebBrowser webbrowser = sender as WebBrowser;
        webbrowser.Navigate("http://www.stackoverflow.com");
    }
}

AirspacePopup

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;

public class AirspacePopup : Popup
{
    public static readonly DependencyProperty IsTopmostProperty =
        DependencyProperty.Register("IsTopmost",
                                    typeof(bool),
                                    typeof(AirspacePopup),
                                    new FrameworkPropertyMetadata(false, OnIsTopmostChanged));

    public static readonly DependencyProperty FollowPlacementTargetProperty =
        DependencyProperty.RegisterAttached("FollowPlacementTarget",
                                            typeof(bool),
                                            typeof(AirspacePopup),
                                            new UIPropertyMetadata(false));

    public static readonly DependencyProperty AllowOutsideScreenPlacementProperty =
        DependencyProperty.RegisterAttached("AllowOutsideScreenPlacement",
                                            typeof(bool),
                                            typeof(AirspacePopup),
                                            new UIPropertyMetadata(false));

    public static readonly DependencyProperty ParentWindowProperty =
        DependencyProperty.RegisterAttached("ParentWindow",
                                            typeof(Window),
                                            typeof(AirspacePopup),
                                            new UIPropertyMetadata(null, ParentWindowPropertyChanged));

    private static void OnIsTopmostChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        AirspacePopup airspacePopup = source as AirspacePopup;
        airspacePopup.SetTopmostState(airspacePopup.IsTopmost);
    }

    private static void ParentWindowPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        AirspacePopup airspacePopup = source as AirspacePopup;
        airspacePopup.ParentWindowChanged();
    }

    private bool? m_appliedTopMost;
    private bool m_alreadyLoaded;
    private Window m_parentWindow;

    public AirspacePopup()
    {
        Loaded += OnPopupLoaded;
        Unloaded += OnPopupUnloaded;

        DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(PlacementTargetProperty, typeof(AirspacePopup));
        descriptor.AddValueChanged(this, PlacementTargetChanged);
    }

    public bool IsTopmost
    {
        get { return (bool)GetValue(IsTopmostProperty); }
        set { SetValue(IsTopmostProperty, value); }
    }
    public bool FollowPlacementTarget
    {
        get { return (bool)GetValue(FollowPlacementTargetProperty); }
        set { SetValue(FollowPlacementTargetProperty, value); }
    }
    public bool AllowOutsideScreenPlacement
    {
        get { return (bool)GetValue(AllowOutsideScreenPlacementProperty); }
        set { SetValue(AllowOutsideScreenPlacementProperty, value); }
    }
    public Window ParentWindow
    {
        get { return (Window)GetValue(ParentWindowProperty); }
        set { SetValue(ParentWindowProperty, value); }
    }

    private void ParentWindowChanged()
    {
        if (ParentWindow != null)
        {
            ParentWindow.LocationChanged += (sender, e2) =>
            {
                UpdatePopupPosition();
            };
            ParentWindow.SizeChanged += (sender, e2) =>
            {
                UpdatePopupPosition();
            };
        }
    }
    private void PlacementTargetChanged(object sender, EventArgs e)
    {
        FrameworkElement placementTarget = this.PlacementTarget as FrameworkElement;
        if (placementTarget != null)
        {
            placementTarget.SizeChanged += (sender2, e2) =>
            {
                UpdatePopupPosition();
            };
        }
    }

    private void UpdatePopupPosition()
    {
        FrameworkElement placementTarget = this.PlacementTarget as FrameworkElement;
        FrameworkElement child = this.Child as FrameworkElement;

        if (PresentationSource.FromVisual(placementTarget) != null &&
            AllowOutsideScreenPlacement == true)
        {
            double leftOffset = CutLeft(placementTarget);
            double topOffset = CutTop(placementTarget);
            double rightOffset = CutRight(placementTarget);
            double bottomOffset = CutBottom(placementTarget);
            Debug.WriteLine(bottomOffset);
            this.Width = Math.Max(0, Math.Min(leftOffset, rightOffset) + placementTarget.ActualWidth);
            this.Height = Math.Max(0, Math.Min(topOffset, bottomOffset) + placementTarget.ActualHeight);

            if (child != null)
            {
                child.Margin = new Thickness(leftOffset, topOffset, rightOffset, bottomOffset);
            }
        }
        if (FollowPlacementTarget == true)
        {
            this.HorizontalOffset += 0.01;
            this.HorizontalOffset -= 0.01;
        }
    }
    private double CutLeft(FrameworkElement placementTarget)
    {
        Point point = placementTarget.PointToScreen(new Point(0, placementTarget.ActualWidth));
        return Math.Min(0, point.X);
    }
    private double CutTop(FrameworkElement placementTarget)
    {
        Point point = placementTarget.PointToScreen(new Point(placementTarget.ActualHeight, 0));
        return Math.Min(0, point.Y);
    }
    private double CutRight(FrameworkElement placementTarget)
    {
        Point point = placementTarget.PointToScreen(new Point(0, placementTarget.ActualWidth));
        point.X += placementTarget.ActualWidth;
        return Math.Min(0, SystemParameters.VirtualScreenWidth - (Math.Max(SystemParameters.VirtualScreenWidth, point.X)));
    }
    private double CutBottom(FrameworkElement placementTarget)
    {
        Point point = placementTarget.PointToScreen(new Point(placementTarget.ActualHeight, 0));
        point.Y += placementTarget.ActualHeight;
        return Math.Min(0, SystemParameters.VirtualScreenHeight - (Math.Max(SystemParameters.VirtualScreenHeight, point.Y)));
    }

    private void OnPopupLoaded(object sender, RoutedEventArgs e)
    {
        if (m_alreadyLoaded) 
            return;

        m_alreadyLoaded = true;

        if (Child != null)
        {
            Child.AddHandler(PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(OnChildPreviewMouseLeftButtonDown), true);
        }

        m_parentWindow = Window.GetWindow(this);

        if (m_parentWindow == null) 
            return;

        m_parentWindow.Activated += OnParentWindowActivated;
        m_parentWindow.Deactivated += OnParentWindowDeactivated;
    }

    private void OnPopupUnloaded(object sender, RoutedEventArgs e)
    {
        if (m_parentWindow == null)
            return;
        m_parentWindow.Activated -= OnParentWindowActivated;
        m_parentWindow.Deactivated -= OnParentWindowDeactivated;
    }

    private void OnParentWindowActivated(object sender, EventArgs e)
    {
        SetTopmostState(true);
    }

    private void OnParentWindowDeactivated(object sender, EventArgs e)
    {
        if (IsTopmost == false)
        {
            SetTopmostState(IsTopmost);
        }
    }

    private void OnChildPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        SetTopmostState(true);
        if (!m_parentWindow.IsActive && IsTopmost == false)
        {
            m_parentWindow.Activate();
        }
    }

    protected override void OnOpened(EventArgs e)
    {
        SetTopmostState(IsTopmost);
        base.OnOpened(e);
    }

    private void SetTopmostState(bool isTop)
    {
        // Don’t apply state if it’s the same as incoming state
        if (m_appliedTopMost.HasValue && m_appliedTopMost == isTop)
        {
            return;
        }

        if (Child == null) 
            return;

        var hwndSource = (PresentationSource.FromVisual(Child)) as HwndSource;

        if (hwndSource == null) 
            return;
        var hwnd = hwndSource.Handle;

        RECT rect;

        if (!GetWindowRect(hwnd, out rect)) 
            return;

        Debug.WriteLine("setting z-order " + isTop);

        if (isTop)
        {
            SetWindowPos(hwnd, HWND_TOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
        }
        else
        {
            // Z-Order would only get refreshed/reflected if clicking the
            // the titlebar (as opposed to other parts of the external
            // window) unless I first set the popup to HWND_BOTTOM
            // then HWND_TOP before HWND_NOTOPMOST
            SetWindowPos(hwnd, HWND_BOTTOM, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
            SetWindowPos(hwnd, HWND_TOP, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
            SetWindowPos(hwnd, HWND_NOTOPMOST, rect.Left, rect.Top, (int)Width, (int)Height, TOPMOST_FLAGS);
        }

        m_appliedTopMost = isTop;
    }

    #region P/Invoke imports & definitions
    #pragma warning disable 1591 //Xml-doc
    #pragma warning disable 169 //Never used-warning
    // ReSharper disable InconsistentNaming
    // Imports etc. with their naming rules

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT

    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X,
    int Y, int cx, int cy, uint uFlags);

    static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
    static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
    static readonly IntPtr HWND_TOP = new IntPtr(0);
    static readonly IntPtr HWND_BOTTOM = new IntPtr(1);

    private const UInt32 SWP_NOSIZE = 0x0001;
    const UInt32 SWP_NOMOVE = 0x0002;
    const UInt32 SWP_NOZORDER = 0x0004;
    const UInt32 SWP_NOREDRAW = 0x0008;
    const UInt32 SWP_NOACTIVATE = 0x0010;

    const UInt32 SWP_FRAMECHANGED = 0x0020; /* The frame changed: send WM_NCCALCSIZE */
    const UInt32 SWP_SHOWWINDOW = 0x0040;
    const UInt32 SWP_HIDEWINDOW = 0x0080;
    const UInt32 SWP_NOCOPYBITS = 0x0100;
    const UInt32 SWP_NOOWNERZORDER = 0x0200; /* Don’t do owner Z ordering */
    const UInt32 SWP_NOSENDCHANGING = 0x0400; /* Don’t send WM_WINDOWPOSCHANGING */

    const UInt32 TOPMOST_FLAGS = 
        SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING;

    // ReSharper restore InconsistentNaming
    #pragma warning restore 1591
    #pragma warning restore 169
    #endregion
}


回答2:

After alot of testing the different solutions:

  1. Microsoft.DwayneNeed (https://microsoftdwayneneed.codeplex.com/)

    Pros:
    • Works well as far as i can tell (Have not tested it that much but it looked like it)
    • The performance-hit seems to be pritty low.

    Cons:
    • Relativly big library.

  2. AirspaceFixer (https://github.com/chris84948/AirspaceFixer)

    Pros:
    • Works well (Have not tested it that much but it looked like it)

    Cons:
    • There is a small performance hit. (Bigger than with Microsoft.DwayneNeed as far as i can tell)

  3. AirspacePopup from Fredrik Hedblad (https://stackoverflow.com/a/6452940/4870255)

    Pros:
    • Simple Code. Mostly :D

    Cons:
    • On fullscreen a part of the bottom is missing.
    • Multimonitor-problems: When moving from one Window to another the overlay isnt completly over the winformshostwindow.

For me the best solution was to use Microsoft.DwayneNeed. (I used it to get CefSharp-Winforms into a WPF-application)

Because geting it to work isnt straight forward her is a small tutorial:

  1. Go to https://microsoftdwayneneed.codeplex.com/SourceControl/latest
  2. Click download.
  3. Open Microsoft.DwayneNeed.sln
  4. Compile it.
  5. Reference Microsoft.DwayneNeed.dll in the created Debug or Releas folder.
  6. Add
    xmlns:interop="clr-namespace:Microsoft.DwayneNeed.Interop;assembly=Microsoft.DwayneNeed
    to your Window.
  7. Add
    <interop:AirspaceDecorator AirspaceMode="Redirect" Background="White" IsInputRedirectionEnabled="True" IsOutputRedirectionEnabled="True">
    <WindowsFormsHost x:Name="windowsFormsHost1" Visibility="Visible" />
    </interop:AirspaceDecorator>
    to your Grid.

Example:

<Window x:Class="Toll.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
		xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
		xmlns:interop="clr-namespace:Microsoft.DwayneNeed.Interop;assembly=Microsoft.DwayneNeed"
		xmlns:local="clr-namespace:Toll"
		mc:Ignorable="d"
		Title="MainWindow" Width="1500" Height="800" Closing="Window_Closing">
	<Grid Name="root">
		<interop:AirspaceDecorator AirspaceMode="Redirect"
						   Background="White"
						   IsInputRedirectionEnabled="True"
						   IsOutputRedirectionEnabled="True">
			<WindowsFormsHost x:Name="windowsFormsHost1" Visibility="Visible" />
		</interop:AirspaceDecorator>
		
		<Ellipse Width="100" Height="100" Fill="Green" Margin="100"/>
	</Grid>
</Window>



回答3:

In a typical MVP application using the CAL or Event aggregator, you might have to create another window and name it popup or child (ChildWindow.XAML) and have a get method in (ChildWindow.XAML.CS) like

public static ChildWindow Get()
        {
            ChildWindow dialogBox = new ChildWindow();
            return dialogBox;
        } 

Have a property in the maniwindow which can return Childwindow type when in need. like

public bool ShowChildWindow
{
    get
    {
        return PopUpDialog.Get().ShowDialog().GetValueOrDefault();
    }
}

Hope this helps,



回答4:

You could do the "overlaying" part in the WindowsFormsHost. So there you could have a child element which should be on top of other content in the hosted element.



回答5:

I'd suggest using the MahApps library.

I've been struggling with this issue for some time and found the MahApps library fixed the airspace problem without any configuration on my part. For more information see their FAQ on this:

https://github.com/MahApps/MahApps.Metro/wiki/FAQ#1-why-is-so-and-so-winforms-control-invisible-or-not-rendering-why-is-the-webbrowser-or-other-control-covering-my-flyout-or-another-control-airspace