我可以在另一个WPF窗口之上叠加一个窗口吗?

34
我有一个WPF窗口,包含一个WindowsFormsHost元素。我需要在此元素之上绘制东西,但是WindowsFormsHost的特性意味着它始终处于绘图堆的顶部。由于我无法在WindowsFormsHost组件之上在同一WPF窗口中进行绘制,因此我可以在其上叠加另一个窗口吗?
我已经采用了简单的方法尝试过,但遇到了几个问题:
1)我无法阻止其他应用程序的窗口出现在主窗口和覆盖窗口之间。
2)当我按下Alt-Tab键时,叠加窗口将出现在窗口列表中,这相当丑陋。
基本上,我需要一个“子窗口”的概念,即在所有方面都与另一个窗口相同的窗口。UserControls对我行不通,因为WindowsFormsHost将始终在其上方绘制。
有什么想法吗?
更新[ 2011年5月23日10:13 ]
谢谢两位的回答。
我已经尝试了ChildWindow方法,但WindowsFormsHost元素仍然在其上绘制。据我所知,只有真正的窗口才能在WindowsFormsHost之上绘制,同一窗口中的任何内容都将在WindowsFormsHost下面。
具有WindowsFormsHost的元素仍然会在WinForms组件下面绘制,它们始终在顶部绘制,这似乎是无法更改的......
我想我正在寻找的是一种将外部窗口停靠以充当主窗口一部分的方法。在Mac上,有真正的“子窗口”概念,我正在寻找类似于此的东西。

不使用WinAPI和第三方库 - Geograph
5个回答

44

我通过使用Popup而不是透明的Window来解决了这个问题。

更新

最终我得到了一个名为AirspacePopup的子类化Popup

AirspacePopup的功能

  • 跟随其PlacementTarget
  • 不总是置顶,而是相对于放置它的Window进行定位。这个解决方案来自于Chris Cavanagh's Blog
  • 允许被移动到“屏幕外”。这是通过剪辑Popup并在其移出屏幕后在其子元素上设置负Margin来实现的。这个解决方案来自于Rick Sladkey的此StackOverflow帖子

以下是一个示例,其中AirspacePopup用于在WebBrowser控件(实际上是WinForms控件)上方绘制一个Ellipse,但对于任何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>

用于导航的简单代码..

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
}

如何使HitTestVisible可见?我尝试将Grid的背景设置为“透明”,但没有效果。当我将背景设置为“白色”并将不透明度设置为0.01时,它是HitTestVisible!也许有人有想法? - Andreas
1
当窗口最大化时,底部存在一个小间隙,弹出窗口无法正确跟随。我该如何解决这个问题? - Andreas
1
当从最小化状态恢复窗口时,这与dwm动画不兼容。椭圆形立即出现,而主窗口仍在动画中。有解决方法吗? - Roald
截至2021年,仍然似乎是最可靠的解决方案。 - Michael

4

经过多次测试不同的解决方案后:

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

    优点:
    • 据我所知,它的工作效果很好(我没有进行太多测试,但看起来是这样的)
    • 性能损失似乎相对较低。

    缺点:
    • 相对较大的库。

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

    优点:
    • 工作效果很好(我没有进行太多测试,但看起来是这样的)

    缺点:
    • 存在一定的性能损失。(据我所知,比使用Microsoft.DwayneNeed时要大)

  3. Fredrik Hedblad的AirspacePopup (https://dev59.com/2m025IYBdhLWcg3wT0Hq#6452940)

    优点:
    • 简单的代码。大部分是:D

    缺点:
    • 在全屏模式下,底部的一部分会消失。
    • 多监视器问题:从一个窗口移动到另一个窗口时,覆盖层不完全覆盖winformshostwindow。

对我来说,最好的解决方案是使用Microsoft.DwayneNeed。(我将其用于将CefSharp-Winforms嵌入到WPF应用程序中)

因为它的使用方法并不直接,这里有一个小教程:

  1. 进入 https://microsoftdwayneneed.codeplex.com/SourceControl/latest
  2. 点击下载。
  3. 打开Microsoft.DwayneNeed.sln
  4. 编译它。
  5. 在创建的Debug或Releas文件夹中引用Microsoft.DwayneNeed.dll。
  6. 在您的Window中添加
    xmlns:interop="clr-namespace:Microsoft.DwayneNeed.Interop;assembly=Microsoft.DwayneNeed
  7. 在您的Grid中添加
    <interop:AirspaceDecorator AirspaceMode="Redirect" Background="White" IsInputRedirectionEnabled="True" IsOutputRedirectionEnabled="True">
    <WindowsFormsHost x:Name="windowsFormsHost1" Visibility="Visible" />
    </interop:AirspaceDecorator>

示例:

<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>


无法运行: System.NullReferenceException 对象引用未设置为对象的实例。 在 Microsoft.DwayneNeed.Interop.RedirectedHwndHost.UpdateOutputRedirection() 中。 在 Microsoft.DwayneNeed.Interop.RedirectedHwndHost.<.ctor>b__31_2(Object e, EventArgs a) 中。 在 System.Windows.Threading.DispatcherTimer.FireTick(Object unused) 中。 在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs) 中。 .... - folibis
所有的工作都可以通过Microsoft.DwayneNeed轻松完成。但是!!!在我的情况下,WinForm控件内部非常重要的功能是拖放内容,不幸的是,在使用AirspaceDecorator装饰WindowsFormsHost后,它停止工作了。我无法追踪和解决这个问题:( - uzrgm
使用Microsoft.DwayneNeed存在一个巨大的问题...当我在Winform上的TextBox上有焦点并按下Chrome(或任何其他程序),然后再次回到Winform上的TextBox时,当我开始输入时,它会在我之前点击的其他程序中输入,尽管焦点在TextBox上...:/ 有人遇到过这个问题吗?! - Camadas
@folibis 我遇到了同样的问题,但我必须稍微编辑库源代码。如果你想这么做,请导航到 Microsoft.DwayneNeed/Interop/RedirectedWindow.cs,然后转到第165行并将 _interopBitmap.Invalidate(); 编辑为 _interopBitmap?.Invalidate();。这似乎是一个毫无意义的hack,但这个方法有效,到目前为止我还没有遇到任何问题。 - Veerakran Sereerungruangkul

0
在使用CAL或事件聚合器的典型MVP应用程序中,您可能需要创建另一个窗口并将其命名为弹出窗口或子窗口(ChildWindow.XAML),并在其中拥有一个获取方法(ChildWindow.XAML.CS)。
public static ChildWindow Get()
        {
            ChildWindow dialogBox = new ChildWindow();
            return dialogBox;
        } 

在编程中,可以在主窗口中设置一个属性,以便在需要时返回子窗口类型。例如:
public bool ShowChildWindow
{
    get
    {
        return PopUpDialog.Get().ShowDialog().GetValueOrDefault();
    }
}

希望这能有所帮助,

0
你可以在 WindowsFormsHost 中完成“覆盖”部分。因此,在那里,你可以有一个子元素,它应该位于托管元素中其他内容的顶部。

WPF似乎不允许您将任何内容叠加在WindowsFormsHost之上 - 因此我认为这不会起作用。 - Destek

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接