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

Bol*_*ock 52 .net c# wpf winapi windows-7

在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我想要的只是

  1. 标题栏,和
  2. 客户区的地区......
  3. ......没有被我的控件占据

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

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

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

Bol*_*ock 29

示例代码

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

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

希望这有助于某人!

分步解决方案

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

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

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

可以从获得该信息lParam的的WM_NCHITTEST消息.正如MSDN所描述的那样,光标的x坐标是它的低位字,光标的y坐标是它的高位字.

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

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

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

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

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

    就我而言,我的选项卡控件的内容区域恰好适合矩形区域,如图所示.在您的应用程序中,您可能需要放置一个Rectangle形状或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;
}
Run Code Online (Sandbox Code Playgroud)

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

但那还不是全部.回想一下,第一个插图中包含窗口边框的非客户区也受到影响,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);
Run Code Online (Sandbox Code Playgroud)

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

// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 大脑炒,核心倾倒.你提出了一个很棒的解决方案.我恳求主永远不要处理类似的要求.+1. (3认同)

Jef*_*dge 15

这是你可以尝试的东西:

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

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

祝好运!

  • 为了完整,我正在回答我接受的答案.作为一种感谢,有一个赏金! (2认同)

Ros*_*oss 6

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

我找到了一个代码密集程度更低的解决方案.

只需控件后面创建一个透明项,然后给它一个鼠标左键按下事件处理程序,它调用窗口的DragMove()方法.

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

<Grid DockPanel.Dock="Top">
    <Border MouseLeftButtonDown="Border_MouseLeftButtonDown" Background="Transparent" />
    <Grid><!-- My controls are in here --></Grid>
</Grid>
Run Code Online (Sandbox Code Playgroud)

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

private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    DragMove();
}
Run Code Online (Sandbox Code Playgroud)

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