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

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

对于WinForms应用程序,表单和控件尽可能接近本机Win32 API。 可以简单地以其形式覆盖MouseLeftButtonDown处理程序,处理MouseLeftButtonDown窗口消息,然后通过返回DragMove()来欺骗系统,使系统认为单击框架区域实际上是单击标题栏。我已经在自己的WinForms应用中完成了此操作 达到令人愉快的效果。

在WPF中,我还可以实现类似的MouseLeftButtonDown方法,并将其扩展到工作区,同时将其钩到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;
    }
}

问题是,由于我盲目设置MouseLeftButtonDown并返回DragMove(),因此单击窗口图标或控制按钮之外的任何位置都会导致窗口被拖动。 也就是说,下面用红色突出显示的所有内容都会导致拖动。 这甚至包括窗口侧面(非工作区)的调整大小手柄。 我的WPF控件(即文本框和标签控件)也因此停止接收点击:

我想要的只是

  1. 标题栏,以及
  2. 客户区域的范围...
  3. ...未被我的控件占用

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

如何修改我的MouseLeftButtonDown方法以及窗口的其余XAML /代码隐藏,以确定哪些区域应返回DragMove(),哪些不应返回? 我正在考虑使用DragMove()s对照控件的位置检查单击位置的方法,但是我不确定如何在WPF领域中进行操作。

编辑[4/24]:一种简单的方法是拥有一个不可见的控件,甚至是窗口本身,通过在窗口上调用DragMove()来响应MouseLeftButtonDown(请参阅Ross的回答)。 问题是由于某种原因,如果窗口最大化,则DragMove()无法正常工作,因此在Windows 7 Aero Snap上无法很好地发挥作用。 由于我要进行Windows 7集成,因此对于我而言,这不是可接受的解决方案。

BoltClock asked 2020-01-13T22:58:56Z
4个解决方案
29 votes

样例代码

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

完整的应用程序整体上是MIT许可的,但您可能需要将其拆开,然后将其代码放在自己的周围,而不是完全使用应用程序代码-并不是该许可会阻止您执行此操作。 另外,虽然我知道应用程序主窗口的设计与上面的线框几乎没有相似之处,但想法与问题中所提出的相同。

希望这对某人有帮助!

分步解决方案

我终于解决了。 感谢Jeffrey L Whitledge为我指明了正确的方向!他的回答被接受,因为如果不这样做,我将无法设法解决。编辑[9/8]:由于更完整,现在可以接受此答案; 我要给Jeffrey很大的赏赐,而不是他的帮助。

为了后代的缘故,这是我的操作方式(在我感兴趣的地方引用Jeffrey的回答):

获取鼠标单击的位置(可能来自wParam,还是来自lParam?),然后使用它来创建HTCLIENT(可能进行某种坐标转换?)。

可以从Panel消息的HTCLIENT获得此信息。 游标的x坐标是其低阶单词,而游标的y坐标是其高阶单词,如MSDN所述。

由于坐标是相对于整个屏幕的,因此我需要在窗口上调用HTCLIENT,以将坐标转换为相对于窗口空间。

然后调用静态方法HTCLIENT,将其传递给您刚创建的PanelVisualTreeHelper.HitTest()。 返回值将指示具有最高Z顺序的控件。

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

话虽如此,如果您按照我的步骤进行操作,可能有两个注意事项:

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

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

    有关客户区填充程序的问题导致下一个问题:

  2. 如果您的网格或其他顶级控件在扩展的窗口框架上具有背景纹理或渐变,则即使在背景的任何完全透明的区域上,整个网格区域也将对命中做出响应(请参见视觉层中的命中测试)。 在这种情况下,您将要忽略对网格本身的影响,而只需注意其中的控件。

因此:

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

现在,仅通过单击并拖动窗口的未占用区域即可移动该窗口。

但这还不是全部。 回想一下第一个图示,包含窗口边界的非客户区域也受到了HTCLIENT的影响,因此窗口不再可调整大小。

为了解决这个问题,我必须检查光标是在客户区还是非客户区。 为了检查这一点,我需要使用HTCLIENT函数,看看它是否返回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;
    }
}
BoltClock answered 2020-01-13T23:00:53Z
15 votes

您可以尝试以下方法:

获取鼠标单击的位置(可能来自wParam,还是来自lParam?),然后使用它来创建VisualTreeHelper.HitTest(Visual,Point)(可能进行某种坐标转换?)。

然后调用静态方法VisualTreeHelper.HitTest(Visual,Point),将其传递给您刚创建的thisPoint。 返回值将指示具有最高Z顺序的控件。 如果那是您的窗户,请使用HTCAPTION伏都教徒。 如果是其他控件,则...不要。

祝好运!

Jeffrey L Whitledge answered 2020-01-13T23:01:26Z
6 votes

想要做同样的事情(在WPF应用程序中使我的扩展Aero玻璃可拖动),我只是通过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();
}

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

Ross answered 2020-01-13T23:02:09Z
1 votes

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

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

  private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
     {
         DragMove();
     }
Hady Mahmoodi answered 2020-01-13T23:02:34Z
translate from https://stackoverflow.com:/questions/5493149/how-do-i-make-a-wpf-window-movable-by-dragging-the-extended-window-frame