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

查看:39
本文介绍了如何通过拖动扩展窗口框架使 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]: 这个答案现在被接受了,因为它更多完全的;为了他的帮助,我给了杰弗里一个不错的大赏.

为了后人的缘故,这就是我是如何做到的(在相关的地方引用杰弗里的回答):

<块引用>

获取鼠标点击的位置(可能来自 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. 如果您的网格或其他顶级控件在扩展窗口框架上有背景纹理或渐变,整个网格区域都会响应点击,即使在任何完全透明的区域背景(请参阅视觉层中的命中测试).在这种情况下,您需要忽略对网格本身的点击,而只关注其中的控件.

因此:

//在主窗口中private bool 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){//一个控件被击中 - 如果它有背景,它可能是网格//扩展窗口框架上的纹理或渐变返回 result.VisualHit == windowGrid;}//没有被击中 - 假设该区域无论如何都被帧扩展覆盖返回真;}

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

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

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

//在我的托管 DWM API 包装类中,DwmApiInteroppublic static bool IsOnClientArea(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam){if (uMsg == WM_NCHITTEST){if (DefWindowProc(hWnd, uMsg, wParam, lParam).ToInt32() == HTCLIENT){返回真;}}返回假;}//在本地方法中[DllImport("user32.dll")]私有静态外部 IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);

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

//在主窗口中private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool处理){开关(味精){案例 DwmApiInterop.WM_NCHITTEST:if (DwmApiInterop.IsOnClientArea(hwnd, msg, 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天全站免登陆