如何通过拖动扩展窗口框架来移动WPF窗口?

在Windows资源管理器和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;
    }
}

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

我想要的只是为了

  • 标题栏和
  • 客户区的地区......
  • ......没有被我的控制占用
  • 可拖动。 也就是说,我只希望这些红色区域可拖动(客户区+标题栏):

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

    EDIT [4/24]:关于它的一个简单的方法是有一个无形的控制,甚至是窗口本身,回应MouseLeftButtonDown通过调用DragMove()的窗口(见罗斯的答案)。 问题是,由于某些原因,如果窗口最大化, DragMove()不起作用,所以对Windows 7 Aero Snap来说它不会很好。 由于我正在进行Windows 7集成,所以对我来说这不是一个可以接受的解决方案。


    示例代码

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

    完整的应用程序完全是MIT许可的,但您可能会将其分开,并将其代码放在您自己的位置,而不是完全使用应用程序代码 - 而不是许可证阻止您这样做。 另外,虽然我知道应用程序主窗口的设计与上面的线框没有任何近似之处,但这个想法与问题中提出的相同。

    希望这可以帮助别人!

    一步一步的解决方案

    我终于解决了它。 感谢Jeffrey L Whitledge为我指出了正确的方向! 他的回答被接受了,因为如果不是这样的话,我就不会设法找出解决方案。 编辑[9/8]:现在接受这个答案,因为它更完整; 我给杰弗里一个很好的大奖,而不是他的帮助。

    为了后代的缘故,我是这么做的(引用杰弗里的回答,在我有所涉及的地方):

    获取鼠标点击的位置(从wParam,lParam也许?),并用它来创建一个Point (可能与某种坐标转换?)。

    这些信息可以从WM_NCHITTEST消息的lParam中获得。 正如MSDN所描述的那样,光标的x坐标是其低位字,光标的y坐标是其高位字。

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

    然后调用静态方法VisualTreeHelper.HitTest(Visual,Point)传递给它thisPoint ,你只是做。 返回值将指示具有最高Z顺序的控件。

    我不得不传递顶层Grid控件,而不是this视觉来测试这个点。 同样,我必须检查结果是否为空,而不是检查它是否为窗口。 如果它为空,则光标不会触及网格的任何子控件 - 换句话说,它会触及未占用的窗口框架区域。 无论如何,关键是使用VisualTreeHelper.HitTest()方法。

    如上所述,如果你遵循我的步骤,有两点可能适用于你:

  • 如果不覆盖整个窗口,而只是部分扩展窗口框架,则必须将未被窗口框填充的矩形作为客户区域填充进行控制。

    就我而言,我的选项卡控件的内容区域恰好适合该矩形区域,如图所示。 在你的应用程序中,你可能需要放置一个Rectangle形状或一个Panel控件,并为它绘制适当的颜色。 这样控制就会被击中。

    关于客户区域填充的这个问题导致下一个:

  • 如果您的网格或其他顶层控件在扩展窗口框架上具有背景纹理或渐变,则即使在背景的任何完全透明区域(请参阅可视层中的命中测试),整个网格区域都会响应命中。 在这种情况下,你会忽略对网格本身的命中,只关注其中的控件。

  • 因此:

    // 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;
        }
    }
    

    以下是你可以尝试的东西:

    获取鼠标点击的位置(从wParam,lParam也许?),并用它来创建一个Point (可能与某种坐标转换?)。

    然后调用静态方法VisualTreeHelper.HitTest(Visual,Point)传递给它thisPoint ,你只是做。 返回值将指示具有最高Z顺序的控件。 如果这是你的窗口,那么做你的HTCAPTION巫术。 如果是其他控制,那么......不要。

    祝你好运!


    希望做同样的事情(让我的扩展Aero玻璃在我的WPF应用程序中可拖动),我刚刚通过谷歌浏览了这篇文章。 我通读了你的答案,但决定继续搜索,看看有没有更简单的东西。

    我发现了一个更少的代码密集型解决方案。

    只需在控件背后创建一个透明的项目,并为其添加一个鼠标左键按下事件处理函数,该函数调用窗口的DragMove()方法。

    以下是我的扩展Aero玻璃上显示的XAML部分:

    <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();
    }
    

    就是这样! 对于您的解决方案,您将不得不添加其中一个以实现非矩形可拖动区域。

    链接地址: http://www.djcxy.com/p/25911.html

    上一篇: How do I make a WPF window movable by dragging the extended window frame?

    下一篇: WPF custom drawing and transparency