我正试图在WPF中创建一个自定义控件,它是一个ItemsControl(具有可自定义的数据模板(,支持将项目从一个容器拖动到另一个容器。拖动逻辑是非常直接的,我已经设法使它发挥作用。
问题是我试图显示一个简单的拖动装饰器(本质上是被拖动的项目/数据模板的屏幕截图(。虽然我已经成功地显示了装饰器并使其跟随鼠标光标,但它非常落后。我曾尝试过两种构建装饰器的方法——第一种是在我的自定义装饰器上附加一个内容呈现器;第二种方法实际上是重写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的许多层。这就是为什么当缓慢拖动时,一切都会好起来。
若要检查假设,请将BitmapCache应用于正在拖动的窗口。下面是一个简单的例子:https://stackoverflow.com/a/62635978/275330.
经过长时间的研究和测试,我放弃了以流畅的方式显示拖动装饰器的尝试,转而使用视觉画笔将装饰器显示为一个单独的无边界窗口。这绕过了大量无用的布局计算,提供了一个快速的用户体验。
为了获得灵感,我使用了以下线索:https://stackoverflow.com/a/27975085/15010804
为了让它表现得像装饰物,我不得不做了一些修改。希望这能帮助其他人解决这个问题:
-
创建一个扩展WPF窗口的新对象
-
将构造函数调整为以下
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使窗口真正透明(这样它会干扰丢弃事件(,并在重写的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.