WPF DragDrop Adorner 性能不佳/滞后

WPF DragDrop Adorner poor performance / laggy

我正在尝试在 WPF 中创建一个自定义控件,它是一个支持将项目从一个容器拖动到另一个容器的 ItemsControl(具有可自定义的数据模板)。拖动逻辑非常简单,我已经成功实现了。

问题 是我试图显示一个简单的拖动装饰器(这实际上是 item/datatemplate 被拖动的屏幕截图)。虽然我已设法显示装饰器并使其跟随鼠标光标,但它非常 滞后。我试过两种构建装饰器的方法——第一种是将内容呈现器附加到我的自定义装饰器上;第二个实际上是覆盖 OnRender 方法并自己绘制它。这两种方法的性能都非常差。

这就是我实现装饰器的方式:

public class ActionDragAdorner: Adorner
{
    private VisualCollection _Visuals;
    private ContentPresenter _ContentPresenter;
    private Rectangle _rect;

    public FrameworkElement AdornedElement { get; protected set; }
    public Point InitialClickLocation { get; set; }

    public Point CentralOffset
    {
        get
        {
            return new Point(-_rect.Width / 2, -_rect.Height / 2);
        }
    }

    public ActionDragAdorner(FrameworkElement adornedElement) 
        : base(adornedElement)
    {
        _Visuals = new VisualCollection(this);
        _ContentPresenter = new ContentPresenter();
        _Visuals.Add(_ContentPresenter);

        AdornedElement = adornedElement;

        _rect = new Rectangle();
        _rect.Width = adornedElement.ActualWidth;
        _rect.Height = adornedElement.ActualHeight;
        _rect.Fill = new VisualBrush(adornedElement);
        IsHitTestVisible = false;
        Content = _rect;

        _ContentPresenter.Arrange(new Rect(0, 0, _rect.Width, _rect.Height));

        this.Width = _rect.Width;
        this.Height = _rect.Height;
    }

    public ActionDragAdorner(FrameworkElement adornedElement, Visual content) 
        : this(adornedElement)
    { 
        Content = content; 
    }

    protected override Size MeasureOverride(Size constraint)
    {
        _ContentPresenter.Measure(constraint);
        return new Size(AdornedElement.ActualWidth, AdornedElement.ActualHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        _ContentPresenter.Arrange(new Rect(0, 0,
                finalSize.Width, finalSize.Height));
        return new Size(AdornedElement.ActualWidth, AdornedElement.ActualHeight);
    }

    protected override Visual GetVisualChild(int index)
    { return _Visuals[index]; }

    protected override int VisualChildrenCount
    { get { return _Visuals.Count; } }

    public object Content
    {
        get { return _ContentPresenter.Content; }
        set { _ContentPresenter.Content = value; }
    }
}

当按下左键时,我在 PreviewMouseMove 事件中开始拖动操作。由于 DragDrop.DoDragDrop 被阻塞,更新装饰器位置(跟踪鼠标光标)的唯一方法是覆盖 我的自定义控件的 OnGiveFeedback 事件:

protected override void OnGiveFeedback(GiveFeedbackEventArgs e)
{
    base.OnGiveFeedback(e);
    GetCursorPos(ref pointRef);

    Point relPos = this.PointFromScreen(pointRef.GetPoint());
    Point elementPos = dragAdorner.AdornedElement.TranslatePoint(new Point(0, 0), this);
    Point initialClick = dragAdorner.InitialClickLocation;
    Point pos = new Point(relPos.X - initialClick.X,
        relPos.Y - elementPos.Y - initialClick.Y);
    Rect target = new Rect(pos, dragAdorner.DesiredSize);
                
    dragAdorner.Arrange(target);
}

着眼于用户体验,我们需要在光标之后有一个流畅的装饰器 - 特别是因为装饰器本身非常简单 - 一个带有内部文本块的边框。 在性能分析期间,似乎 UI 线程的 FPS 丢失为零,这似乎是由过多的布局更新引起的(由于用于重新定位装饰器的 Arrange 调用)。我已经尝试了所有我能想到的方法,包括手动进行渲染转换。如果缓慢拖动项目,性能似乎还可以 - 但是如果我更快地移动鼠标,UI 线程将下降到零 FPS - 可能试图过快地进行太多布局更新;这也得到性能分析器的支持,因为在这些零 FPS 时刻,只处理布局更新调用(没有渲染调用)

我还在网上查看了其他使用装饰器进行拖放操作的示例,但这些示例似乎也很滞后。

问题:如何让装饰器以流畅的方式跟随鼠标光标,而不会出现断断续续的动作和不错的 FPS?

我的第一个猜测是不是您的装饰器变慢了,而是整个应用程序变慢了。它正在与被拖动的装饰器交互,触发大量事件,并且涉及 UI 的许多层。所以拖慢一点就OK了。

要验证假设 - 将 BitmapCache 应用于您拖过的 window。这是一个多么简单的例子:.

经过长时间的研究和测试,我放弃了以流畅的方式显示拖动装饰器的尝试,转而使用视觉画笔将装饰器显示为单独的无边框 window。这绕过了无数无用的布局计算并提供了一个活泼的用户体验。

为了获得灵感,我使用了以下线程:

为了使它的行为类似于装饰器,我不得不进行一些修改。希望这可以帮助其他人解决这个问题:

  1. 创建一个扩展 WPF 的新对象Window

  2. 调整构造函数如下

public ActionDragAdornerWindow(Visual dragElement) : base()
{
    WindowStyle = WindowStyle.None;
    AllowsTransparency = true;
    AllowDrop = false;
    Background = null;
    IsHitTestVisible = false;
    SizeToContent = SizeToContent.WidthAndHeight;
    Topmost = true;
    ShowInTaskbar = false;
    Opacity = 0.75;
    ShowActivated = false;

    Rectangle r = new Rectangle();
    r.Width = ((FrameworkElement)dragElement).ActualWidth;
    r.Height = ((FrameworkElement)dragElement).ActualHeight;
    r.IsHitTestVisible = false;
    r.Fill = new VisualBrush(dragElement);
    Content = r;       
}
  1. 使用 PInvoke 使 window 完全透明(因此它会干扰放置事件)- 并在覆盖的 OnSourceInitialized 方法中调用它。
public const int WS_EX_TRANSPARENT = 0x00000020;
public const int GWL_EXSTYLE = (-20);

[DllImport("user32.dll")]
public static extern int GetWindowLong(IntPtr hwnd, int index);

[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);

    // Get this window's handle
    IntPtr hwnd = new WindowInteropHelper(this).Handle;

    // Change the extended window style to include WS_EX_TRANSPARENT
    int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
    SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT);
}

 4. Make sure to update the adorner/window position in the GiveFeedback event, by capturing the mouse using GetCursorPos (PInvoke). Depending on the use cases and desired effect, some coordinate transformations will be required.