天天看點

WPF拖放功能實作

轉至:https://www.cnblogs.com/loveis715/archive/2011/12/05/2277384.html

寫在前面:本文為即興而作,是以難免有疏漏和詞不達意的地方。在這裡,非常期望您提供評論,分享您的想法和建議。

  這是一篇介紹如何在WPF中實作拖放功能的短文。

  首先要讀者清楚的一件事情是:拖放主要分為拖放源和拖放目标兩個組成。拖放源和拖放目标各自擁有不同的事件。軟體開發人員需要在适當的事件中完成相應功能。

  試想拖放是如何操作的:使用者選中一個界面元素,并在滑鼠左鍵按下的情況下移動滑鼠,最後,在到達拖放目标時松開滑鼠左鍵,進而完成資料拖放的全過程。從程式編寫的角度來看,使用者需要在左鍵選中項目并按下的情況下移動以啟動拖放,并在滑鼠移動的過程中給出目前拖放狀态的外觀回饋,并在松開滑鼠時嘗試将項目添加到目标中。

  對于任意的軟體界面,滑鼠左鍵按下并移動的行為并不一定會導緻拖放的開始。是以軟體開發人員需要自行編寫代碼啟動拖放功能,而不是由WPF決定。這也就是軟體開發人員在特定條件下需要自行調用DragDrop.DoDragDrop()以啟動拖放操作的原因。DragDrop.DoDragDrop()函數接受三個參數:dragSource、data以及allowedEffects。特别需要注意的是dragSource參數。該參數标示了拖拽操作的消息源,也決定了所有的消息源事件由誰發出。參數data則用來包裝Drag&Drop所操作的資料。一般情況下,其都是一個DataObject類型的執行個體。該執行個體内部應包裝拖拽所實際操作的資料。最後,allowedEffects可以用來指定拖拽操作的效果。調用該函數的片斷可以如下所示:

1 DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy);
           

啟動了拖拽操作以後,軟體開發人員就需要處理拖拽過程中所發生的一系列事件了。在這種情況下,軟體開發人員不能寄望于通過響應Mouse.MouseMove等事件完成拖拽行為的響應。這是因為DragDrop.DoDragDrop()函數實際上是一個阻塞函數。在拖拽行為終止之前,這些事件都不會被發送。取而代之的是,軟體開發人員可以使用拖拽源和目标所提供的事件。拖拽源提供的事件為QueryContinueDrag、GiveFeedback以及對應的Preview-事件。QueryContinueDrag事件用來決定是否繼續拖放操作。該事件發生的時機為鍵盤或滑鼠按鈕狀态發生變化時。GiveFeedback則用來為使用者提供拖放的回報資訊,如令被拖拽的界面元素的圖像随滑鼠變化,或提供一個Tooltip提示使用者目前的拖放效果等。該消息會在拖拽過程中定時發出,是以軟體開發人員也可以将其作為Timer事件使用。在這裡強調一下,因為在拖拽過程中軟體開發人員不能使用其它事件,是以了解各拖拽事件發生時機是一個很重要的事情。

  另外,在您看到Preview-事件的時候,相信您一定能夠想到這是一個隧道/冒泡路由事件。是以,對這些事件的處理并不一定需要向拖放源添加事件處理函數,而可以在較高層次中重寫相應函數即可,如重寫Window的OnGiveFeedback()函數。這樣做的好處在于,其能提供較為集中的拖拽處理邏輯,并在需要更改較高層次界面元素,如視窗的狀态欄狀态時擁有較好的語義特征。這樣做的不足之處也有,如其并不太适合視窗中擁有多個拖拽源的情況。當然,在高層次偵聽消息源發出的消息也是非常好的選擇。

  如果我們需要實作在拖拽過程中将拖拽的界面元素的樣子作為預覽這一功能,那麼該功能的實作如下所示:

mListBox.PreviewMouseMove += OnPreviewListBoxMouseMove;
mListBox.QueryContinueDrag += OnQueryContinueDrag;

private void OnQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
mAdornerLayer.Update();
…
}

private void OnPreviewListBoxMouseMove(object sender, MouseEventArgs e)
{
    ListBoxItem listBoxItem = … // Find your actual visual you want to drag
…
    DragDropAdorner adorner = new DragDropAdorner(listBoxItem);
    mAdornerLayer = AdornerLayer.GetAdornerLayer(mTopLevelGrid);
    mAdornerLayer.Add(adorner);

    DataItem dataItem = listBoxItem.Content as DataItem;
    DataObject dataObject = new DataObject(dataItem.Clone());
    // Here, we should notice that dragsource param will specify on which 
// control the drag&drop event will be fired
    System.Windows.DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy);
…
}
           

其中DragDropAdorner用來顯示被拖拽的界面元素的預覽。其使用了Adorner。如果讀者對該控件的實作有興趣,請自行下載下傳示例程式檢視。有關Adorner的使用,我會在有時間的時候專門撰寫一篇文章。

  接下來就是拖拽目标事件了。拖拽目标會發送DragEnter、DragOver、DragLeave、Drop以及相應的Preview-事件。這些事件的意義十厘清晰,相信您從名字中就能看出這些事件發生的時機。在這些事件中需要注意的則是傳入的DragEventArgs。通過設定它的Effects成員,軟體開發人員可以控制滑鼠的狀态,以提示使用者目前拖拽動作的光标回報。同時通過它的Data屬性,軟體開發人員可以獲得DoDragDrop()函數調用時所傳入的資料。

  下面就是一段響應拖拽目标事件的代碼:

private void OnDragOver(object sender, DragEventArgs e)
{
    e.Effects = DragDropEffects.None;

    // Find the corresponding treeview item in mTreeView and select it
    Point pos = e.GetPosition(mTreeView);
    HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos);
    if (result == null)
        return;

    TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit);
    if (selectedItem != null)
        selectedItem.IsSelected = true;

    e.Effects = DragDropEffects.Copy;
}

private void OnDrop(object sender, DragEventArgs e)
{
    // Drop the data item into corresponding treeview item
    Point pos = e.GetPosition(mTreeView);
   HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos);
    if (result == null)
        return;

    TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit);
    if (selectedItem == null)
        return;

    DataItem parent = selectedItem.Header as DataItem;
    DataItem dataItem = e.Data.GetData(typeof(DataItem)) as DataItem;
    if (parent != null && dataItem != null)
        parent.Items.Add(dataItem);
}
           

需要讀者注意的則是該段代碼中對GetData()函數的調用。Drag&Drop過程中,如果希望從DataObject中擷取資料,那麼必須使用原有類型。如A是B的基類,而DataObject中封存的則是類型B的執行個體,那麼軟體開發人員需要在DataObject.GetData()中使用typeof(B),而不能是typeof(A)。

  在實作拖拽功能的時候,軟體開發人員需要注意一系列問題。

  首先是DragDropEffect。該枚舉中的每個值都對應着拖放過程的一種特定行為。這些外觀在UI設計和使用者使用中擁有特定的慣用法。是以在開發過程中要想好到底希望對拖拽目标執行何種操作,以防止使用者在使用過程中産生疑惑。

  另外,外部拖拽源也是一種常見的拖拽功能,如将檔案拖拽到應用程式内以進行加載。在某些情況下,拖拽目标事件并不能提供所需的資訊。如在拖拽多個檔案到應用程式内的時候,IDataObject接口隻提供了GetData()函數。在這種情況下,軟體開發人員可以嘗試将其轉化為DataObject類型執行個體,并通過GetFileDropList()函數傳回所有被拖拽的檔案。

  同時需要注意的是滑鼠位置的擷取方法。在拖拽過程中,滑鼠的位置不能通過Mouse類等WPF标準方法獲得。在某些情況下,該方法将會傳回一個錯誤的位置。這是因為在拖拽過程中,滑鼠的控制權是由拖拽源所管理的。該管理過程中會使用Win32函數,進而使WPF無法正确地傳回滑鼠的位置資訊。一個變通的方法則是使用PInvoke調用Win32 API GetCursorPos()。

  另一個需要提及的小技巧則是如何禁用拖拽。标準控件包括一些預設情況下可作為拖拽目标的控件,如TextBox。為了禁止該功能,軟體開發人員可以将OnPreviewDragEnter()和OnPreviewDragOver()重載中DragEventArgs的Handled屬性設定為true,并設定Effects為None,以模拟禁止拖放的效果。