WPF自定義Panel:讓拖拽變得更簡單

来源:https://www.cnblogs.com/qushi2020/p/18098514
-Advertisement-
Play Games

在 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>

 

技術交流群

 

聯繫方式


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹基於R語言中的Ternary包,繪製三元圖(Ternary Plot)的詳細方法;其中,我們就以RGB三色分佈圖為例來具體介紹~ ...
  • 本文介紹瞭如何快速搭建一個基於大型語言模型(LLM)的混元聊天應用。強調了開發速度的重要性,並指出了使用Streamlit這一工具的優勢,特別是對於不熟悉前端代碼的開發者來說,Streamlit提供了一種快速構建聊天應用的方法。 ...
  • 前言 aardio中有些經常使用的庫,換個項目總需要複製一下,還不便於修改。雖然可以直接把它放到aardio\lib目錄下,也是不便於共用給其他人使用。 最近偶然翻到編輯器里的工具->開發環境->擴展庫發佈工具,就想著可以像官方一樣,發佈自己的擴展庫,也便於分享給大家使用,最好能像官方擴展庫一樣線上 ...
  • 隨著汽車的普及和使用頻率的增加,車輛的維修保養成為了車主們經常需要面對的問題。為了提供更好的服務,挖數據平臺提供了一個維修保養記錄統計介面,讓用戶可以方便地查詢車輛的保養記錄和維修記錄。本文將對該介面進行詳細解析,並介紹其使用方法和應用場景。 首先,我們來看一下該介面的具體功能。該介面可以查詢車輛的 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...