You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

限制WPF子窗口在其父窗口边界内

解决WPF子窗口超出父窗口边界的闪烁问题

我之前也踩过这个坑——用LocationChanged事件修正位置确实会有闪烁问题,因为窗口已经跑到父窗口外面了才触发调整,视觉上就会跳一下。这里有两个更优雅的解决方案,能彻底避免闪烁:

方案一:通过Win32钩子拦截窗口移动消息(兼容系统标题栏拖动)

这个方法是在窗口移动过程中就限制位置,而不是移动完成后再修正,所以完全不会有闪烁。核心是拦截WM_MOVING消息,直接修改窗口的拟移动位置。

首先需要定义Win32相关的结构体和API:

using System.Runtime.InteropServices;

public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

public static class Win32Helper
{
    public const int WM_MOVING = 0x0216;

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}

然后在子窗口中添加消息钩子:

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    if (PresentationSource.FromVisual(this) is HwndSource hwndSource)
    {
        hwndSource.AddHook(WndProc);
    }
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    if (msg == Win32Helper.WM_MOVING)
    {
        // 获取父窗口的屏幕边界
        Win32Helper.GetWindowRect(Owner.Handle, out var parentRect);
        // 获取子窗口即将移动到的位置
        var childRect = Marshal.PtrToStructure<RECT>(lParam);

        int childWidth = childRect.Right - childRect.Left;
        int childHeight = childRect.Bottom - childRect.Top;

        // 限制左边界不超出父窗口左边缘
        if (childRect.Left < parentRect.Left)
            childRect.Left = parentRect.Left;
        // 限制右边界不超出父窗口右边缘(父窗口右边界 - 子窗口宽度)
        else if (childRect.Right > parentRect.Right)
            childRect.Left = parentRect.Right - childWidth;

        // 限制上边界不超出父窗口上边缘
        if (childRect.Top < parentRect.Top)
            childRect.Top = parentRect.Top;
        // 限制下边界不超出父窗口下边缘(父窗口下边界 - 子窗口高度)
        else if (childRect.Bottom > parentRect.Bottom)
            childRect.Top = parentRect.Bottom - childHeight;

        // 更新子窗口的拟移动位置
        Marshal.StructureToPtr(childRect, lParam, true);
        handled = true;
    }
    return IntPtr.Zero;
}

方案二:自定义窗口拖动逻辑(无需Win32 API)

如果你的子窗口是自定义样式(比如隐藏了系统标题栏),可以自己处理鼠标拖动事件,全程控制窗口位置,从根源上避免超出父窗口。

子窗口的拖动逻辑实现:

private Point _dragStartMousePos;
private bool _isDragging;

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    // 记录鼠标相对于子窗口的起始位置
    _dragStartMousePos = e.GetPosition(this);
    _isDragging = true;
    CaptureMouse();
}

protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    if (_isDragging)
    {
        var currentMousePos = e.GetPosition(this);
        // 计算鼠标拖动的偏移量
        var offset = currentMousePos - _dragStartMousePos;

        // 计算子窗口的新位置
        double newLeft = Left + offset.X;
        double newTop = Top + offset.Y;

        // 获取父窗口的屏幕坐标和尺寸
        var parentScreenPos = Owner.PointToScreen(new Point(0, 0));
        double parentRight = parentScreenPos.X + Owner.ActualWidth;
        double parentBottom = parentScreenPos.Y + Owner.ActualHeight;

        // 限制子窗口在父窗口范围内
        newLeft = Math.Max(parentScreenPos.X, Math.Min(newLeft, parentRight - ActualWidth));
        newTop = Math.Max(parentScreenPos.Y, Math.Min(newTop, parentBottom - ActualHeight));

        // 更新子窗口位置
        Left = newLeft;
        Top = newTop;
    }
}

protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonUp(e);
    _isDragging = false;
    ReleaseMouseCapture();
}

方案对比

  • 方案一:兼容系统默认的标题栏拖动,无需修改窗口样式,但需要用到Win32 API,代码稍复杂。
  • 方案二:纯WPF实现,无需依赖原生API,但只适用于自定义拖动逻辑的窗口(比如自定义标题栏)。

这两个方案都是在窗口移动过程中实时限制位置,完全不会出现之前的闪烁问题,你可以根据自己的窗口样式选择合适的方案。

内容的提问来源于stack exchange,提问作者cravie

火山引擎 最新活动