如何通过拖动扩展的窗口框架使 WPF 窗口可移动? [英] How do I make a WPF window movable by dragging the extended window frame?

查看:16
本文介绍了如何通过拖动扩展的窗口框架使 WPF 窗口可移动?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

对于 WinForms 应用程序,表单和控件尽可能接近原生 Win32 API;可以简单地以他们的形式覆盖 WndProc() 处理程序,处理 ).问题是如果窗口最大化,由于某种原因 DragMove() 不起作用,因此它不能很好地与 Windows 7 Aero Snap 配合使用.由于我打算进行 Windows 7 集成,因此在我的情况下这不是一个可接受的解决方案.

解决方案

示例代码

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

整个完整的应用程序是 MIT 许可的,但您可能会将其拆开,并将其代码的一部分放在您自己的周围,而不是完整地使用应用程序代码 - 并不是说​​许可会阻止您这样做任何一个.此外,虽然我知道应用程序主窗口的设计与上面的线框并不相似,但想法与问题中提出的相同.

希望这对某人有所帮助!

分步解决方案

我终于解决了.感谢 Jeffrey L Whitledge 为我指明了正确的方向!他的回答被接受了,因为如果不是这样,我就无法找到解决方案. 编辑 [9/8]: 这个答案现在被接受了,因为它更完全的;我要给 Jeffrey 一笔不错的大笔赏金,以换取他的帮助.

为了子孙后代,这就是我的做法(在相关的地方引用 Jeffrey 的回答):

<块引用>

获取鼠标点击的位置(可能来自 wParam,lParam?),并使用它来创建 Point(可能通过某种坐标变换?).

此信息可以从 WM_NCHITTEST 消息的 lParam 获得.光标的 x 坐标是它的低位词,光标的 y 坐标是它的高位词,如 MSDN 描述.

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

<块引用>

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

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

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

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

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

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

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

因此:

//在主窗口中私有布尔 IsOnExtendedFrame(int lParam){int x = lParam <<16>>16,y = lParam>>16;var point = PointFromScreen(new Point(x, y));//在 XAML 中:<Grid x:Name="windowGrid">...</Grid>var 结果 = VisualTreeHelper.HitTest(windowGrid, point);如果(结果!= null){//一个控件被击中 - 如果它有背景,它可能是网格//扩展窗口框架上的纹理或渐变返回结果.VisualHit == windowGrid;}//没有命中 - 假设这个区域被框架扩展覆盖返回真;}

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

但这还不是全部.回想一下第一个插图,包含窗口边框的非客户区也受到 HTCAPTION 的影响,因此窗口不再可调整大小.

为了解决这个问题,我必须检查光标是击中客户区还是非客户区.为了检查这一点,我需要使用 DefWindowProc() 函数并查看它是否返回 HTCLIENT:

//在我的托管 DWM API 包装类中,DwmApiInterop公共静态布尔 IsOnClientArea(IntPtr hWnd,int uMsg,IntPtr wParam,IntPtr lParam){如果(uMsg == WM_NCHITTEST){if (DefWindowProc(hWnd, uMsg, wParam, lParam).ToInt32() == HTCLIENT){返回真;}}返回假;}//在 NativeMethods 中[DllImport("user32.dll")]私有静态外部 IntPtr DefWindowProc(IntPtr hWnd,int uMsg,IntPtr wParam,IntPtr lParam);

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

//在主窗口中私人 IntPtr WndProc(IntPtr hwnd,int msg,IntPtr wParam,IntPtr lParam,ref bool 处理){开关(味精){案例 DwmApiInterop.WM_NCHITTEST:如果(DwmApiInterop.IsOnClientArea(hwnd,味精,wParam,lParam)&&IsOnExtendedFrame(lParam.ToInt32())){处理=真;返回新的 IntPtr(DwmApiInterop.HTCAPTION);}返回 IntPtr.Zero;默认:返回 IntPtr.Zero;}}

In applications like Windows Explorer and Internet Explorer, one can grab the extended frame areas beneath the title bar and drag windows around.

For WinForms applications, forms and controls are as close to native Win32 APIs as they can get; one would simply override the WndProc() handler in their form, process the WM_NCHITTEST window message and trick the system into thinking a click on the frame area was really a click on the title bar by returning HTCAPTION. I've done that in my own WinForms apps to delightful effect.

In WPF, I can also implement a similar WndProc() method and hook it to my WPF window's handle while extending the window frame into the client area, like this:

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

The problem is that, since I'm blindly setting handled = true and returning HTCAPTION, clicking anywhere but the window icon or the control buttons causes the window to be dragged. That is, everything highlighted in red below causes dragging. This even includes the resize handles at the sides of the window (the non-client area). My WPF controls, namely the text boxes and the tab control, also stop receiving clicks as a result:

What I want is for only

  1. the title bar, and
  2. the regions of the client area...
  3. ... that aren't occupied by my controls

to be draggable. That is, I only want these red regions to be draggable (client area + title bar):

How do I modify my WndProc() method and the rest of my window's XAML/code-behind, to determine which areas should return HTCAPTION and which shouldn't? I'm thinking something along the lines of using Points to check the location of the click against the locations of my controls, but I'm not sure how to go about it in WPF land.

EDIT [4/24]: one simple way about it is to have an invisible control, or even the window itself, respond to MouseLeftButtonDown by invoking DragMove() on the window (see Ross's answer). The problem is that for some reason DragMove() doesn't work if the window is maximized, so it doesn't play nice with Windows 7 Aero Snap. Since I'm going for Windows 7 integration, it's not an acceptable solution in my case.

解决方案

Sample code

Thanks to an email I got this morning, I was prompted to make a working sample app demonstrating this very functionality. I've done that now; you can find it on GitHub (or in the now-archived CodePlex). Just clone the repository or download and extract an archive, then open it in Visual Studio, and build and run it.

The complete application in its entirety is MIT-licensed, but you'll probably be taking it apart and putting bits of its code around your own rather than using the app code in full — not that the license stops you from doing that either. Also, while I know the design of the application's main window isn't anywhere near similar to the wireframes above, the idea is the same as posed in the question.

Hope this helps somebody!

Step-by-step solution

I finally solved it. Thanks to Jeffrey L Whitledge for pointing me in the right direction! His answer was accepted because if not for it I wouldn't have managed to work out a solution. EDIT [9/8]: this answer is now accepted as it's more complete; I'm giving Jeffrey a nice big bounty instead for his help.

For posterity's sake, here's how I did it (quoting Jeffrey's answer where relevant as I go):

Get the location of the mouse click (from the wParam, lParam maybe?), and use it to create a Point (possibly with some kind of coordinate transformation?).

This information can be obtained from the lParam of the WM_NCHITTEST message. The x-coordinate of the cursor is its low-order word and the y-coordinate of the cursor is its high-order word, as MSDN describes.

Since the coordinates are relative to the entire screen, I need to call Visual.PointFromScreen() on my window to convert the coordinates to be relative to the window space.

Then call the static method VisualTreeHelper.HitTest(Visual,Point) passing it this and the Point that you just made. The return value will indicate the control with the highest Z-Order.

I had to pass in the top-level Grid control instead of this as the visual to test against the point. Likewise I had to check whether the result was null instead of checking if it was the window. If it's null, the cursor didn't hit any of the grid's child controls — in other words, it hit the unoccupied window frame region. Anyway, the key was to use the VisualTreeHelper.HitTest() method.

Now, having said that, there are two caveats which may apply to you if you're following my steps:

  1. If you don't cover the entire window, and instead only partially extend the window frame, you have to place a control over the rectangle that's not filled by window frame as a client area filler.

    In my case, the content area of my tab control fits that rectangular area just fine, as shown in the diagrams. In your application, you may need to place a Rectangle shape or a Panel control and paint it the appropriate color. This way the control will be hit.

    This issue about client area fillers leads to the next:

  2. If your grid or other top-level control has a background texture or gradient over the extended window frame, the entire grid area will respond to the hit, even on any fully transparent regions of the background (see Hit Testing in the Visual Layer). In that case, you'll want to ignore hits against the grid itself, and only pay attention to the controls within it.

Hence:

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

The window is now movable by clicking and dragging only the unoccupied areas of the window.

But that's not all. Recall in the first illustration that the non-client area comprising the borders of the window was also affected by HTCAPTION so the window was no longer resizable.

To fix this I had to check whether the cursor was hitting the client area or the non-client area. In order to check this I needed to use the DefWindowProc() function and see if it returned 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);

Finally, here's my final window procedure method:

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

这篇关于如何通过拖动扩展的窗口框架使 WPF 窗口可移动?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆