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。这绕过了无数无用的布局计算并提供了一个活泼的用户体验。
为了获得灵感,我使用了以下线程:
为了使它的行为类似于装饰器,我不得不进行一些修改。希望这可以帮助其他人解决这个问题:
创建一个扩展 WPF 的新对象Window
调整构造函数如下
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;
}
- 使用 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.
我正在尝试在 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。这绕过了无数无用的布局计算并提供了一个活泼的用户体验。
为了获得灵感,我使用了以下线程:
为了使它的行为类似于装饰器,我不得不进行一些修改。希望这可以帮助其他人解决这个问题:
创建一个扩展 WPF 的新对象Window
调整构造函数如下
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;
}
- 使用 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.