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

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

对于WinForms应用程序,表单和控件尽可能接近本机Win32 API; 我们可以简单地重写WndProc()处理程序,处理WM_NCHITTEST窗口消息,并诱使系统认为点击框架区域是通过返回HTCAPTION实际上是标题栏上的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 ,单击任何地方,但窗口图标或控制button导致窗口被拖动。 也就是说,下面以红色突出显示的内容都会导致拖动。 这甚至包括窗口两侧的resize手柄(非客户区域)。 我的WPF控件,即文本框和选项卡控件,也停止接收点击结果:

我想要的只是为了

  1. 标题栏和
  2. 客户区的地区…
  3. …没有被我的控制权占用

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

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

编辑[4/24]:关于它的一个简单的方法是通过在窗口上调用DragMove()来获得一个不可见的控件,甚至是窗口本身,以响应MouseLeftButtonDown (参见Ross的答案 )。 问题是,由于某种原因DragMove()不能工作,如果窗口最大化,所以它不能很好地与Windows 7的Aero捕捉。 由于我要进行Windows 7集成,对我来说这不是一个可以接受的解决scheme。

示例代码

感谢我今天上午收到的一封电子邮件,我被提示制作一个工作示例应用程序来演示这个function。 我现在已经这样做了; 你可以在http://wpfdraggableframe.codeplex.comfind它。; 只需从“ 源代码”选项卡下载最新版本,在Visual Studio中打开它,然后构build并运行它。

整个完整的应用程序是MIT许可的,但是你可能会把它分开,把代码放在你自己的位置,而不是完全使用应用程序代码,而不是许可证阻止你这样做。 另外,虽然我知道应用程序的主窗口的devise与上面的线框没有太接近的地方,但是这个想法与问题中提出的相同。

希望这有助于某人!

一步一步的解决scheme

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

为了后代的缘故,我是这样做的(引用杰弗里的回答,在我相关的时候):

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

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

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

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

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

现在,说了这么多,如果你遵循我的步骤,有两点可能适用于你:

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

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

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

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

因此:

 // 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影响,所以窗口不再可resize。

要解决这个问题,我必须检查光标是否在客户区或非客户区。 为了检查这个我需要使用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也许?),并用它来创build一个Point (可能与某种坐标转换?)。

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

祝你好运!

想要做同样的事情(使我的扩展Aero玻璃拖动在我的WPF应用程序),我只是通过谷歌碰到这个职位。 我读了你的答案,但决定继续search,看看有没有更简单的东西。

我发现了一个更less的代码密集型解决scheme。

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

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

简单的方法是创buildstackpanel或者你想要的标题栏XAML的每一件事情

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

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