如何通过拖动扩展的窗口框架使 WPF 窗口可移动?

Posted

技术标签:

【中文标题】如何通过拖动扩展的窗口框架使 WPF 窗口可移动?【英文标题】:How do I make a WPF window movable by dragging the extended window frame? 【发布时间】:2011-07-26 11:24:28 【问题描述】:

在 Windows Explorer 和 Internet Explorer 等应用程序中,可以抓住标题栏下方的扩展框架区域并拖动窗口。

对于 WinForms 应用程序,表单和控件尽可能接近原生 Win32 API;可以简单地覆盖表单中的WndProc() 处理程序,处理WM_NCHITTEST 窗口消息并通过返回HTCAPTION 欺骗系统认为点击框架区域实际上是点击标题栏。我已经在我自己的 WinForms 应用程序中做到了这一点,效果非常好。

在 WPF 中,我还可以实现一个类似的WndProc() 方法,并将它挂接到我的 WPF 窗口句柄,同时将窗口框架扩展到客户区,如下所示:

// In MainWindow
// For use with window frame extensions
private IntPtr hwnd;
private HwndSource hsource;

private void Window_SourceInitialized(object sender, EventArgs e)

    try
    
        if ((hwnd = new WindowInteropHelper(this).Handle) == IntPtr.Zero)
        
            throw new InvalidOperationException("Could not get window handle for the main window.");
        

        hsource = HwndSource.FromHwnd(hwnd);
        hsource.AddHook(WndProc);

        AdjustWindowFrame();
    
    catch (InvalidOperationException)
    
        FallbackPaint();
    


private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)

    switch (msg)
    
        case DwmApiInterop.WM_NCHITTEST:
            handled = true;
            return new IntPtr(DwmApiInterop.HTCAPTION);

        default:
            return IntPtr.Zero;
    

问题在于,由于我盲目地设置handled = true 并返回HTCAPTION,单击任何地方,但窗口图标或控制按钮会导致窗口被拖动。也就是说,下面以红色突出显示的所有内容都会导致拖动。这甚至包括窗口侧面(非客户区)的调整大小手柄。我的 WPF 控件,即文本框和选项卡控件,也因此停止接收点击:

我想要的只是为了

    标题栏和 客户区的区域... ...没有被我的控件占用

可拖动。也就是说,我只希望这些红色区域是可拖动的(客户区+标题栏):

如何修改我的 WndProc() 方法和窗口的 XAML/代码隐藏的其余部分,以确定哪些区域应该返回 HTCAPTION,哪些不应该?我正在考虑使用Points 来检查我的控件位置的点击位置,但我不确定如何在 WPF 领域进行。

编辑 [4/24]: 一种简单的方法是让一个不可见的控件,甚至是窗口本身,通过在窗口上调用 DragMove() 来响应 MouseLeftButtonDown(参见Ross's answer)。问题是由于某种原因DragMove() 在窗口最大化时不起作用,因此它不能很好地与 Windows 7 Aero Snap 配合使用。由于我打算进行 Windows 7 集成,因此在我的情况下这不是一个可接受的解决方案。

【问题讨论】:

这个问题包括一个关于在 C# 中处理 windows 消息的简短教程,它有精心制作的插图,准确地指出了所要求的内容,并且没有明显的拼写错误。 +1,宝贝! @Jeffrey L Whitledge:哎呀,谢谢(+1 给你)!最后我必须编辑的一件事是问题标题......我发誓以前的标题不是我在发布之前写的。 【参考方案1】:

示例代码

感谢我今天早上收到的一封电子邮件,提示我制作一个工作示例应用程序来展示这个功能。我现在已经做到了;你可以在GitHub(或now-archived CodePlex)上找到它。只需克隆存储库或下载并解压缩存档,然后在 Visual Studio 中打开它,然后构建并运行它。

整个完整的应用程序已获得 MIT 许可,但您可能会将其拆开并将其代码的一部分放在您自己的周围,而不是完整地使用应用程序代码 - 并不是说​​许可会阻止您这样做任何一个。此外,虽然我知道应用程序的主窗口的设计与上面的线框并不相似,但想法与问题中提出的相同。

希望这对某人有所帮助!

分步解决方案

我终于解决了。感谢Jeffrey L Whitledge 为我指明了正确的方向! 他的回答被接受了,因为如果不是这样,我就无法找到解决方案。 编辑 [9/8]: 这个答案现在被接受了,因为它更完全的;我要给 Jeffrey 一大笔赏金,而不是为了他的帮助。

为了子孙后代,这就是我的做法(在相关的地方引用 Jeffrey 的回答):

获取鼠标点击的位置(可能来自 wParam,lParam?),并使用它来创建Point(可能进行某种坐标转换?)。

这个信息可以从WM_NCHITTEST消息的lParam获得。光标的x坐标是它的低位字,光标的y坐标是它的高位字,如@9​​87654324@。

由于坐标是相对于整个屏幕的,我需要在我的窗口上调用Visual.PointFromScreen() 将坐标转换为相对于窗口空间的坐标。

然后调用静态方法VisualTreeHelper.HitTest(Visual,Point) 传递它this 和你刚刚创建的Point。返回值将指示具有最高 Z-Order 的控件。

我必须传入*** Grid 控件而不是 this 作为视觉对象来测试这一点。同样,我必须检查结果是否为空,而不是检查它是否是窗口。如果它为空,则光标没有击中任何网格的子控件——换句话说,它击中了未被占用的窗口框架区域。无论如何,关键是使用VisualTreeHelper.HitTest() 方法。

现在,话虽如此,如果您按照我的步骤操作,有两个警告可能适用于您:

    如果您没有覆盖整个窗口,而只是部分扩展了窗口框架,则必须在没有被窗口框架填充的矩形上放置一个控件作为客户区填充物。

    在我的例子中,我的选项卡控件的内容区域正好适合该矩形区域,如图所示。在您的应用程序中,您可能需要放置一个Rectangle 形状或Panel 控件并将其涂上适当的颜色。这样控件就会被击中。

    这个关于客户区填充的问题引出了下一个:

    如果您的网格或其他***控件在扩展的窗口框架上具有背景纹理或渐变,整个网格区域都会响应点击,即使在任何完全透明的区域上也是如此背景(见Hit Testing in the Visual Layer)。在这种情况下,您需要忽略对网格本身的点击,而只关注其中的控件。

因此:

// In MainWindow
private bool IsOnExtendedFrame(int lParam)

    int x = lParam << 16 >> 16, y = lParam >> 16;
    var point = PointFromScreen(new Point(x, y));

    // In XAML: <Grid x:Name="windowGrid">...</Grid>
    var result = VisualTreeHelper.HitTest(windowGrid, point);

    if (result != null)
    
        // A control was hit - it may be the grid if it has a background
        // texture or gradient over the extended window frame
        return result.VisualHit == windowGrid;
    

    // Nothing was hit - assume that this area is covered by frame extensions anyway
    return true;

现在可以通过单击并拖动窗口的未占用区域来移动窗口。

但这还不是全部。回想一下在第一个插图中,包含窗口边框的非客户区也受到HTCAPTION 的影响,因此窗口不再可调整大小。

为了解决这个问题,我必须检查光标是击中客户区还是非客户区。为了检查这一点,我需要使用DefWindowProc() 函数并查看它是否返回HTCLIENT

// In my managed DWM API wrapper class, DwmApiInterop
public static bool IsOnClientArea(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam)

    if (uMsg == WM_NCHITTEST)
    
        if (DefWindowProc(hWnd, uMsg, wParam, lParam).ToInt32() == HTCLIENT)
        
            return true;
        
    

    return false;


// In NativeMethods
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);

最后,这是我的最终窗口过程方法:

// In MainWindow
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)

    switch (msg)
    
        case DwmApiInterop.WM_NCHITTEST:
            if (DwmApiInterop.IsOnClientArea(hwnd, msg, wParam, lParam)
                && IsOnExtendedFrame(lParam.ToInt32()))
            
                handled = true;
                return new IntPtr(DwmApiInterop.HTCAPTION);
            

            return IntPtr.Zero;

        default:
            return IntPtr.Zero;
    

【讨论】:

脑筋急转弯,核心倾倒。你想出了一个多么好的解决方案。我求主永远不要处理类似的要求。 +1。 @Markust:你用核心转储双关语让我度过了一个下午。【参考方案2】:

您可以尝试以下方法:

获取鼠标点击的位置(可能来自 wParam、lParam?),并使用它创建一个Point(可能进行某种坐标转换?)。

然后调用静态方法VisualTreeHelper.HitTest(Visual,Point) 传递它this 和你刚刚创建的Point。返回值将指示具有最高 Z-Order 的控件。如果那是你的窗口,那就做你的HTCAPTION voodoo。如果是其他控制,那么……不要。

祝你好运!

【讨论】:

这里是棘手的部分:WinForms 的Control 类有一个PointToClient() 方法来执行所需的转换。但是 WPF 的 Visual 类没有那个方法。大概是因为它是 WPF。 为了完整起见,我正在让我的答案被接受。作为一种感谢的方式,有一个赏金!【参考方案3】:

想做同样的事情(让我的扩展 Aero 玻璃在我的 WPF 应用程序中可拖动),我刚刚通过 Google 看到了这篇文章。我阅读了您的回答,但决定继续搜索,看看是否有更简单的方法。

我找到了一个代码密集程度低得多的解决方案。

只需在您的控件后面创建一个透明项目,并为其提供一个鼠标左键按下事件处理程序,该处理程序调用窗口的DragMove() 方法。

这是我的 XAML 部分,它出现在我的扩展 Aero 玻璃上:

<Grid DockPanel.Dock="Top">
    <Border MouseLeftButtonDown="Border_MouseLeftButtonDown" Background="Transparent" />
    <Grid><!-- My controls are in here --></Grid>
</Grid>

还有代码隐藏(这是在Window 类中,所以DragMove() 可以直接调用):

private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

    DragMove();

就是这样!对于您的解决方案,您必须添加多个这些以实现您的非矩形可拖动区域。

【讨论】:

我也考虑过DragMove(),但它的问题是它在窗口最大化时不起作用,所以它与 Aero Snap 并没有很好的配合。 你是对的。我刚刚用我的解决方案测试了 Aero Snap,虽然它在最大化和停靠到左/右方面没有问题,但它不适用于从最大化状态“取消捕捉”。 再说一次,Aero Snap 是 Windows 7 的一项功能,如果 Aero Snap 不起作用,DragMove() 没关系。 出于兴趣 - 您的解决方案是否适用于“双击最大化”? 是的,确实如此。由于我让我的窗口程序在单击玻璃区域时发送假的HTCAPTIONs,因此玻璃区域的行为就像标题/标题栏一样。【参考方案4】:

简单的方法是 为标题栏创建堆栈面板或您想要的所有内容 XAML

 <StackPanel Name="titleBar" Background="Gray" MouseLeftButtonDown="titleBar_MouseLeftButtonDown" Grid.ColumnSpan="2"></StackPanel>

代码

  private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
     
         DragMove();
     

【讨论】:

这与罗斯两年半前发布的答案有何不同(除了使用StackPanel 而不是Border)?

以上是关于如何通过拖动扩展的窗口框架使 WPF 窗口可移动?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用户控件像窗口一样在屏幕上可拖动

WPF 窗口中的可拖动和自动调整用户控件

没有边框的可拖动 WPF 窗口

如何使窗口可拖动(C# Winforms)?

WPF 窗口仅垂直拖动

在 qml 中拖动无框窗口“抖动”