在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。
自定義 Panel 的優點有很多。首先,我們可以根據自己的需求來設計 Panel 的外觀和行為。其次,我們可以使用代碼來控制拖放操作的細節,比如拖放的開始和結束位置、拖放過程中控制項的顯示方式等等。最後,我們可以將自定義 Panel 作為一個控制項,方便地應用到不同的應用程式中。
在本教程中,我們將一步一步地創建一個自定義 Panel 來實現更簡單的拖拽操作。我們將學習如何定義 Panel 的佈局、如何處理拖放事件,以及如何將自定義 Panel 應用到不同的應用程式中。按照本教程的步驟操作,您將能夠創建一個功能強大且易於使用的自定義 Panel,從而使您的 WPF 應用程式更加友好和易用。
1.定義一個繼承自Panel的類。
public class DragStackPanel : Panel { /// <summary> /// 獲取或設置方向 /// </summary> public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(DragStackPanel), new PropertyMetadata(Orientation.Vertical)); }
2.重寫Panel類的MeasureOverride方法測量控制項Size。
public class DragStackPanel : Panel { protected override Size MeasureOverride(Size availableSize) { var panelDesiredSize = new Size(); foreach (UIElement child in InternalChildren) { child.Measure(availableSize); if (this.Orientation == Orientation.Horizontal) { panelDesiredSize.Width += child.DesiredSize.Width; panelDesiredSize.Height = double.IsInfinity(availableSize.Height) ? child.DesiredSize.Height : availableSize.Height; } else { panelDesiredSize.Width = double.IsInfinity(availableSize.Width) ? child.DesiredSize.Width : availableSize.Width; panelDesiredSize.Height += child.DesiredSize.Height; } } return panelDesiredSize; } }
3.重寫Panel類的ArrangeOverride方法排列控制項位置。
public class DragStackPanel : Panel { protected override Size ArrangeOverride(Size finalSize) { double x = 0, y = 0; foreach (FrameworkElement child in InternalChildren) { // 坐標 var position = new Point(x, y); // 寬度 var width = child.DesiredSize.Width; // 高度 var height = child.DesiredSize.Height; // 通過排列方向計算寬度和高度 if (this.Orientation == Orientation.Vertical) { width = finalSize.Width; } else { height = finalSize.Height; } // 尺寸 var size = new Size(width, height); // 排列位置及尺寸 child.Arrange(new Rect(position, size)); // 計算位置 if (this.Orientation == Orientation.Horizontal) { x += child.DesiredSize.Width; } else { y += child.DesiredSize.Height; } } return finalSize; } }
查看運行效果
<UniformGrid Rows="2"> <local:DragStackPanel Orientation="Horizontal"> <Button>test1</Button> <Button>test2</Button> </local:DragStackPanel> <local:DragStackPanel Orientation="Vertical"> <Button>test3</Button> <Button>test4</Button> </local:DragStackPanel> </UniformGrid>
4.重寫PreviewMouseLeftButtonDown方法。
該方法在按下滑鼠左鍵時觸發,我們需要在該方法中獲取第一次按下滑鼠的坐標,並且通過命中測試找到我們要拖拽的控制項,最後還要在裝飾層中添加一個元素,該元素的背景用原控制項的外觀來填充(VisualBrush),這樣就可以覆蓋原來的控制項,以便在拖拽控制項時能跨越控制項的邊界。以下為參考代碼:
public class DragStackPanel : Panel { private FrameworkElement draggingElement; private Point mouseRelativePosition; private int draggingElementzIndex; protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { // 獲取滑鼠相對於Panel的坐標 var mousePosition = e.GetPosition(this); // 通過命中測試獲取當前滑鼠位置下的元素 var hitTestResult = this.InputHitTest(mousePosition) as FrameworkElement; // 通過命中測試結果找到當前拖拽的控制項子項 draggingElement = FindChild(hitTestResult); if (draggingElement != null && this.InternalChildren.Contains(draggingElement)) { // 記錄滑鼠相對位置,以供後續使用 mouseRelativePosition = e.GetPosition(draggingElement); // 暫存ZIndex draggingElementzIndex = Panel.GetZIndex(draggingElement); // 將ZIndex置頂 Panel.SetZIndex(draggingElement, this.InternalChildren.Count); // 添加遮罩,防止拖拽時覆蓋 AddOverlay(draggingElement); e.Handled = true; } base.OnPreviewMouseLeftButtonDown(e); } }
5.重寫PreviewMouseMove方法。
該方法在滑鼠移動時觸發,我們需要在滑鼠被按下移動時,根據當前的坐標與第一次按下的坐標實時計算出被拖拽元素的偏移量,這樣該元素就能跟隨滑鼠移動,實現拖拽效果。以下為參考代碼:
public class DragStackPanel : Panel { private FrameworkElement draggingElement; private Point mouseRelativePosition; private int draggingElementzIndex; protected override void OnPreviewMouseMove(MouseEventArgs e) { var mousePosition = e.GetPosition(this); if (e.LeftButton == MouseButtonState.Pressed && draggingElement != null) { // 當前拖拽控制項置為不可滑鼠命中,以供命中下一層的換位控制項 draggingElement.IsHitTestVisible = false; // 判斷當前拖拽的控制項是否為頂層控制項 if (Panel.GetZIndex(draggingElement) == this.InternalChildren.Count) { // 計算出當前拖拽控制項相對於this的位置(控制項左上角) var targetPosition = new Point(mousePosition.X - mouseRelativePosition.X - draggingElement.Margin.Left, mousePosition.Y - mouseRelativePosition.Y - draggingElement.Margin.Top); // 獲取當前拖拽控制項在this中的原始位置 var draggingElementOriginalPosition = GetDraggingElementOriginalPosition(draggingElement); // 計算拖拽控制項移動時的偏移量 var offset = new Point(targetPosition.X - draggingElementOriginalPosition.X, targetPosition.Y - draggingElementOriginalPosition.Y); // 應用位移 draggingElement.RenderTransform = new TranslateTransform(offset.X, offset.Y); } e.Handled = true; } base.OnPreviewMouseMove(e); } }
6.重寫PreviewMouseLeftButtonUp方法。
該方法在滑鼠左健抬起時觸發,我們需要在該方法中將一些參數重置。
public class DragStackPanel : Panel { private FrameworkElement draggingElement; private Point mouseRelativePosition; private int draggingElementzIndex; protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) { mouseRelativePosition = default; RemoveOverlay(draggingElement); Panel.SetZIndex(draggingElement, draggingElementzIndex); draggingElement.IsHitTestVisible = true; draggingElement.RenderTransform = null; draggingElement = null; e.Handled = true; base.OnPreviewMouseLeftButtonUp(e); } }
以下為運行效果:
7.處理控制項的拖拽換位。
拖拽換位的思路就是將當前正在拖拽的元素放置到新的Index中,並把該Index後面的所有元素整體後移一位。該功能在PreviewMouseMove方法中實現。
public class DragStackPanel : Panel { private FrameworkElement draggingElement; private FrameworkElement hitElement; private Point mouseRelativePosition; private int draggingElementzIndex; protected override void OnPreviewMouseMove(MouseButtonEventArgs e) { ... // 命中當前拖拽控制項的下一層控制項 var hitTestResult = this.InputHitTest(mousePosition) as FrameworkElement; // 查找被命中的下一層換位控制項 hitElement = FindChild(hitTestResult); // 判斷是否有效 if (hitElement != null && this.InternalChildren.Contains(hitElement)) { // 應用換位 MoveChild(draggingElement, hitElement); } } private void MoveChild(FrameworkElement element1, FrameworkElement element2) { var index1 = this.InternalChildren.IndexOf(element1); var index2 = this.InternalChildren.IndexOf(element2); if (index1 >= 0 && index2 >= 0) { this.InternalChildren.RemoveAt(index1); this.InternalChildren.Insert(index2, element1); } } }
在ArrangeOverride方法中處理重新排列時當前拖拽元素的坐標。
public class DragStackPanel : Panel { private FrameworkElement draggingElement; private FrameworkElement hitElement; private Point mouseRelativePosition; private int draggingElementzIndex; protected override Size ArrangeOverride(Size finalSize) { double x = 0, y = 0; foreach (FrameworkElement child in InternalChildren) { ... // 獲取當前正在拖拽元素的位置坐標 var dragElementPosition = GetDraggingElementMovingPosition(child); if (dragElementPosition != default) { // 處理拖拽元素坐標 var offset = new Point(dragElementPosition.X - position.X, dragElementPosition.Y - position.Y); child.RenderTransform = new TranslateTransform(offset.X, offset.Y); SetDraggingElementMovingPosition(child, dragElementPosition); } ... } return finalSize; } }
運行效果
8.處理跨Panel拖拽。
到目前為止已經實現了本Panel內的控制項隨意拖拽換位,處理從A控制項拖到B控制項也類似,這裡需要用到一個靜態變數來保存正在拖拽的控制項,當B控制項檢測到滑鼠進入時,只需要在A控制項移除正在拖拽的控制項,在B控制項添加正在拖拽的控制項就可以實現了。以下為核心代碼:
public class DragStackPanel : Panel { // 通過拖拽傳遞到下一個Panel的控制項 private static FrameworkElement draggingTransferElement; private void Control_MouseEnter(object sender, MouseEventArgs e) { panel.Children.Remove(draggingTransferElement); panel.DraggingElement = null; Panel.SetZIndex(draggingTransferElement, this.InternalChildren.Count + 1); this.Children.Add(draggingTransferElement); this.AddOverlay(draggingTransferElement); } }
以下為運行效果:
9.在ListBox、ListView、DataGrid等ItemsControl中使用拖拽功能。
所有繼承自ItemsControl的控制項,都有一個ItemsPanel屬性,該屬性可以指定一個Panel類型的控制項來對ItemsControl進行排列。理論上只要將ItemsControl.ItemsPanel設置為我們自己開發的Panel控制項就可以實現排列及拖拽功能,但是這裡直接使用的話並不會有效果。原因就是我們並沒有對數據綁定的情況下做處理。它的處理邏輯也與上面的類似,首先找到ItemsControl控制項,通過對ItemsSource進行操作就可以實現排列功能,由於代碼大同小異這裡就不再贅述。以下為ListBox控制項拖拽的案例效果。
<ListBox ItemsSource="{Binding Items}"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <DragStackPanel AllowCrossBorderDrag="True" CanDragAndSort="True" IsItemsHost="True"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Property1}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
10.添加動畫效果。
至此基本功能已經開發完成了,下麵我們為它添加上動畫效果,讓它更具有觀賞性。動畫的核心思想就是記錄每個元素舊位置的坐標,當元素移動到新位置時啟動一個動畫,從舊坐標過渡到新坐標,由於代碼太過基礎,這裡就不展示了,直接上效果。
<DragStackPanel AllowCrossBorderDrag="True" CanDragAndSort="True" IsItemsHost="True"> <DragStackPanel.ChildMoveBehavior> <ChildMoveBehavior Duration="0:0:0.5"> <ChildMoveBehavior.EaseX> <QuinticEase EasingMode="EaseOut" /> </ChildMoveBehavior.EaseX> <ChildMoveBehavior.EaseY> <QuinticEase EasingMode="EaseOut" /> </ChildMoveBehavior.EaseY> </ChildMoveBehavior> </DragStackPanel.ChildMoveBehavior> </DragStackPanel>
技術交流群
聯繫方式