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

时间:2011-03-30 22:18:52

标签: c# .net wpf winapi windows-7

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

对于WinForms应用程序,表单和控件尽可能接近原生Win32 API;一个人只需覆盖其表单中的WndProc()处理程序,处理WM_NCHITTEST窗口消息并欺骗系统,认为点击框架区域实际上是通过返回{{1}点击标题栏}。我已经在我自己的WinForms应用程序中做到了令人愉快的效果。

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

WndProc()

问题是,因为我盲目地设置// 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,然后点击任何地方,但窗口图标或控制按钮会导致窗口被拖动。也就是说,以下红色突出显示的所有内容都会导致拖动这甚至包括窗口两侧的调整大小手柄(非客户区域)。我的WPF控件,即文本框和选项卡控件,也因此停止接收点击次数:

我想要的只是

  1. 标题栏,
  2. 客户区的区域......
  3. ...没有被我的控件占用
  4. 可以拖延。也就是说,我只希望这些红色区域可以拖动(客户区+标题栏):

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

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

4 个答案:

答案 0 :(得分:29)

示例代码

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

完整的应用程序完全是麻省理工学院许可的,但你可能会将它分开并将其部分代码放在你自己的代码而不是完全使用应用程序代码 - 而不是许可证阻止你这样做无论是。此外,虽然我知道应用程序主窗口的设计与上面的线框不太接近,但这个想法与问题中提出的想法相同。

希望这有助于某人!

逐步解决方案

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

为了后人的缘故,我就是这样做的(引用Jeffrey的回答,我跟你说的相关):

  

获取鼠标单击的位置(从wParam,lParam可能?),并用它来创建Point(可能有某种坐标转换?)。

可以从lParam消息的WM_NCHITTEST获取此信息。光标的x坐标是其低位字,光标的y坐标是其高位字,为MSDN describes

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

  

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

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

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

  1. 如果您没有覆盖整个窗口,而只是部分地扩展窗口框架,则必须对窗口框未填充的矩形进行控制,作为客户区填充。

    就我而言,我的标签控件的内容区域恰好适合矩形区域,如图所示。在您的应用程序中,您可能需要放置Rectangle形状或Panel控件并将其绘制为适当的颜色。这样控制就会被击中。

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

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

  3. 因此:

    // 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 :(得分:14)

这是你可以尝试的东西:

获取鼠标单击的位置(从wParam,lParam可能?),并用它来创建Point(可能有某种坐标转换?)。

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

祝你好运!

答案 2 :(得分:6)

想要做同样的事情(在我的WPF应用程序中使我的扩展Aero玻璃可拖动),我刚刚通过谷歌发现了这篇文章。我读完你​​的答案,但决定继续寻找是否有更简单的东西。

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

只需在控件后面创建一个透明项目,然后给它一个鼠标左键按下事件处理程序,调用窗口的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();
}

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

答案 3 :(得分:1)

简单的方法是 创建stackpanel或标题栏所需的每件事 XAML

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

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