風人園

          弱水三千,只取一瓢,便能解渴;佛法無邊,奉行一法,便能得益。
          隨筆 - 99, 文章 - 181, 評論 - 56, 引用 - 0
          數據加載中……

          TreeView 四技

          0. 背景故事

          ??????現在的東西動不動就用G來算,一眨眼的功夫,我那100G的硬盤已擁擠不已了,但還有很多東西想放進來啊,怎么辦?好吧,現在 DVD 刻錄機的價格已經平民化了,我買了一個來舒緩緊張的硬盤。這下好了,硬盤上的可用空間總是足以讓我下載想要的大塊頭了。沒過多久,我刻錄的 DVD 就堆積成山,成為我房間的一道景物。為了管理這座“山”,我決定寫一個 DVD 管理軟件,嗯,就叫它 Cupel 吧。不難想象,Cupel 將充分使用 TreeView 控件的各種功能,現在我把開發 Cupel 的過程中使用 TreeView 的心得寫下來,希望能為那些尋找這方面內容的朋友提供一些參考。

          ?

          1. 填充節點

          1.1 說說要求


          圖 1-1 類別視圖

          ??????如上圖所示,根節點是光盤庫,它可以包含0個或多個類別節點,每個類別節點又包含0個或多個光盤節點。Cupel 通過 Cupel.Data.DiscLibrary 類來讀取和儲存相關數據。

          1.2 進行填充

          ??????類別視圖的節點應該在 Cupel 的主窗體顯示之前填充好,于是我選擇在 Load 事件發生時進行填充:

          // ?Code?#01

          private ? void ?MainForm_Load( object ?sender,?EventArgs?e)
          {
          ????TreeNode?libraryNode?
          = ? new ?TreeNode( " My?Disc?Library " );

          ????
          foreach ?(DiscCategory?category? in ?m_MyDiscLibrary.GetCategories())
          ????
          {
          ????????TreeNode?categoryNode?
          = ? new ?TreeNode(category.Name);

          ????????
          foreach ?( string ?label? in ?category.GetDiscLabels())
          ????????
          {
          ????????????categoryNode.Nodes.Add(
          new ?TreeNode(label));
          ????????}


          ????????libraryNode.Nodes.Add(categoryNode);
          ????}


          ????m_CategoryView.Nodes.Add(libraryNode);

          ????m_CategoryView.ExpandAll();
          }

          ??????填充節點的方法是很簡單的,上面的代碼有兩點需要說明:

          • 1) 無論是 TreeNode 還是 TreeView,節點都是包含在 Nodes 屬性中的,通過該屬性的 Add() 方法可以添加新的節點。正如一個 TreeNode 可以包含多個子節點,一個 TreeView也可以包含多個根節點。
          • 2) 節點填充完畢后,你應該使用 TreeView.ExpandAll() 方法展開所有節點。然而,當光盤節點過多時,展開全部節點可能不太合適,此時可以考慮只展開類別節點,即把 Code #01 的 m_CategoryView.ExpandAll(); 改為 libraryNode.Expand(); 就行了。

          1.3 添加圖標


          圖 1-2 文件夾視圖

          ??????對于 Windows 的用戶,上面這幅圖應該是很熟悉了,上面的每個節點都帶有一個圖標,這使得目錄試圖更直觀。Code #01 并沒有為每個節點添加圖標,運行結果是每個節點將只有文字。要為節點添加圖標,最簡單的方法就是在創建節點時通過構造函數來指定,但在此之前,你得先創建一個 System.Windows.Forms.ImageList 實例,并用它來儲存圖標。這里介紹在 Visual Studio 里使用 ImageList 組件為 TreeView 提供圖像資源:

          • 1) 在“工具箱”中拖動 ImageList 組件到主窗體;
          • 2) 在“屬性”窗口中點擊 Images 屬性右邊的“...”按鈕打開“圖像集合編輯器”;
          • 3) 按“添加”按鈕添加所需的圖標。
          • 4) 選中 TreeView 控件,在“屬性”窗口中找到 ImageList 屬性,并把它的值設為剛才的 ImageList。

          ??????至此,相關的準備工作已經完畢,接下來要做的就是修改 Code #01 為節點指定圖標,這可以通過使用 TreeNode 如下的構造函數做到:

          // ?Code?#02

          public ?TreeNode( string ?text,? int ?imageIndex,? int ?seletedImageIndex)

          ??????由于在 Cupel 中無論節點是否被選中,其圖標都是一樣的,所以上面構造函數的后兩個參數值是一樣的。假設 category.ico 在 ImageList 中的索引是1,那么你可以這樣指定類別節點的圖標:

          // ?Code?#03

          TreeNode?categoryNode?
          = ? new ?TreeNode(category.Name,? 1 ,? 1 );

          1.4 繼續思考

          ??????前面說到,每個節點可以包含0個或多個字節點,于是在用戶第一次運行 Cupel 時,類別視圖將只有一個根節點。這顯然是不太友好的,因為面對著“一無所有”的類別視圖,用戶很可能會不知所措,尤其在他有很多光盤并且還沒決定如何對這些光盤分類時。此時我們不妨考慮為用戶提供一個默認分類,這樣他就可以在此基礎上構想一個更合適自己的分類,這要比憑空想出一個分類容易的多。當然,有些用戶早已想出了一套很好的分類,此時我們就沒必要為他提供默認分類了,而是直接讓他應用自己的分類。可以看出,如果 Cupel 在第一次運行時顯示一個設置向導,詢問用戶使用默認分類還是應用自己的分類,則會使用戶感到更加友好。

          ??????無論多么好吃的東西,每天都吃也會使人感到厭倦。現今是一個個性化的時代,圖 1-1 無疑顯得有點單調,如果用戶可以為每個類別指定一個不同的圖標,甚至隸屬不同類別的光盤也具有不同的圖標,這將會使得 Cupel 令人眼前一亮。進一步考慮,我們可以考慮把類別視圖的圖標設置儲存在一個配置文件,讓用戶可以選擇應用不同的圖標套裝。當然,有些用戶根本不在乎這點兒花樣,就像那些一直支持著“Windows 經典”主題的用戶一樣。可以看出,如果 Cupel 在第一次運行時顯示一個設置向導,詢問用戶使用哪個圖標套裝,則會使用戶感到更加友好。

          ?

          2. 延遲填充

          2.1 說說要求


          圖 2-1 光盤結構視圖

          ??????圖 2-1 分上下兩部分,上面是一個 TreeView,顯示了類別視圖選中的光盤節點所包含的目錄結構,下面是一個 ListView,顯示了光盤結構視圖選中的節點的細節信息,此圖實質上是一個主-從視圖。

          ??????當光盤所包含的目錄或文件節點比較多時,一次過填充光盤結構視圖的所有節點很可能導致界面沒有響應,這顯然是不允許的。其實,我們沒有必要一開始就把所有節點都填充上去,而應該在用戶訪問到某節點時才填充它的子節點。

          2.2 做好準備

          ??????TreeView 中的節點信息都包含在 TreeNode 中,為了使得光盤結構視圖具備延遲填充特性,以及在節點信息視圖上顯示選中節點的細節信息,我們有必要自定義一個用于 TreeView 的節點類,該類將派生自 TreeNode,并且包含實現相關功能的信息。

          ??????節點可分為目錄節點和文件節點兩類,它們既有相同之處,也有不同之處,于是我們很容易聯想到建立一個繼承體系:


          圖 2-2 節點繼承圖

          ??????FileSystemTreeNodeBase 類的 Properties 屬性是一個 List<FileSystemTreeNodeProperty> 集合,它包含了與節點的相關信息,這些信息將會顯示在節點信息視圖上,實現主-從視圖。另外,FileSystemTreeNodeBase 類還包含了一個 FillSubNodes 抽象方法,用于協助光盤結構視圖實現延遲填充特性。由于文件節點不會有子節點,所以 FileTreeNode.FillSubNodes() 的方法體是空的。現在我們來看一下 DirectoryTreeNode.FillSubNodes():

          // ?Code?#04

          public ? override ? void ?FillSubNodes()
          {
          ????
          if ?(Nodes.Count? == ? 0 )
          ????
          {
          ????????
          this .TreeView.BeginUpdate();

          ????????
          foreach ?(DirectoryNode?subDirectoryNode? in ?m_DirectoryNode.SubDirectoryNodes)
          ????????
          {
          ????????????DirectoryTreeNode?subDirectoryTreeNode?
          = ? new ?DirectoryTreeNode(subDirectoryNode.Name,?subDirectoryNode);
          ????????????subDirectoryTreeNode.Properties.Add(
          new ?FileSystemTreeNodeProperty( " Path " ,?subDirectoryNode.FullName));
          ????????????Nodes.Add(subDirectoryTreeNode);
          ????????}


          ????????
          foreach ?(FileNode?fileNode? in ?m_DirectoryNode.FileNodes)
          ????????
          {
          ????????????FileTreeNode?fileTreeNode?
          = ? new ?FileTreeNode(fileNode.Name);
          ????????????fileTreeNode.Properties.Add(
          new ?FileSystemTreeNodeProperty( " Directory " ,?fileNode.Directory));
          ????????????fileTreeNode.Properties.Add(
          new ?FileSystemTreeNodeProperty( " File?Name " ,?fileNode.Name));
          ????????????Nodes.Add(fileTreeNode);
          ????????}


          ????????
          this .TreeView.EndUpdate();
          ????}

          }

          ??????用戶有可能在展開某個節點后把它折疊起來,此時該節點的 Nodes 屬性就會包含它的子節點(一個例外情況就是原光盤的某個目錄是空目錄,即里面沒有包括任何子目錄和/或文件),所以我們應該首先檢查 Nodes.Count 是否為0。當條件滿足時,我們就對該節點進行填充,留意填充代碼包含在 TreeView.BeginUpdate() 和 TreeView.EndUpdate() 之間,這樣做是為了避免 TreeView 每填充一個節點就繪制一次,從而提高了效率。

          2.3 按需填充

          ??????僅當某個節點包含了子節點時,我們才能展開該節點,所以在展開該節點時,就要對其子節點所包含的子節點進行填充。例如,在圖 2-1 中,當我們展開根節點(即“G:\”)時,“浪客劍心”所包含的子節點就得填充好了,否則它就無法被展開,它里面的目錄結構也就無法顯示了。

          ??????回到 Cupel,當用戶選中類別視圖中的某個光盤節點,光盤結構視圖就會顯示該光盤的根節點及其所包含的子節點:

          // ?Code?#05

          DirectoryTreeNode?rootDirectoryTreeNode?
          = ?
          rootDirectoryTreeNode.FillSubNodes();

          m_DiscInfoView.Nodes.Clear();
          m_DiscInfoView.Nodes.Add(rootDirectoryTreeNode);

          ??????接著,當用戶點擊可展開節點左邊的“+”時,將引發 TreeView 的 BeforeExpand 事件,此時是填充該節點的子節點的子節點的最佳時機:

          // ?Code?#06

          private ? void ?m_DiscInfoView_BeforeExpand( object ?sender,?TreeViewCancelEventArgs?e)
          {
          ????
          foreach ?(FileSystemTreeNodeBase?subNode? in ?e.Node.Nodes)
          ????
          {
          ????????subNode.FillSubNodes();
          ????}

          }

          2.4 顯示細節

          ??????當用戶選中光盤結構視圖中的某個節點時,節點信息視圖將顯示與該節點相關的信息,這兩個視圖共同組成一個主-從視圖:

          // ?Code?#07

          private ? void ?m_DiscInfoView_NodeMouseClick( object ?sender,?TreeNodeMouseClickEventArgs?e)
          {
          ????
          if ?(e.Button? == ?MouseButtons.Left? && ?e.Clicks? == ? 1 )
          ????
          {
          ????????m_NodeInfoView.Items.Clear();

          ????????FileSystemTreeNodeBase?fileSystemTreeNode?
          = ?(FileSystemTreeNodeBase)e.Node;
          ????????
          foreach ?(FileSystemTreeNodeProperty?property? in ?fileSystemTreeNode.Properties)
          ????????
          {
          ????????????m_NodeInfoView.Items.Add(
          new ?ListViewItem(
          ????????????????
          new ? string []
          ????????????????
          {
          ????????????????????property.Name,
          ????????????????????property.Value
          ????????????????}

          ????????????????)
          ????????????);
          ????????}

          ????}

          }

          ??????值得注意的是,Code #07 首先檢測是否為鼠標左鍵點擊以及點擊次數是否為1,這些信息都包含在類型為 TreeNodeMouseClickEventArgs 的 e 參數中。另外,e.Node 是當前選中的節點,你必須把它強制轉換成 FileSystem.TreeNodeBase 類型才能訪問其所包含的 Properties 屬性。

          2.5 繼續思考

          ??????雖然我們使用了“延遲填充”,但在展開某些節點時依然會感覺到“遲鈍”,出現這種情況的主要原因是該節點的子節點包含著大量子節點。此時我們可以在展開之前把鼠標指針改為等待樣式,待節點展開完畢后再改為默認樣式:

          // ?Code?#08

          private ? void ?m_DiscInfoView_BeforeExpand( object ?sender,?TreeViewCancelEventArgs?e)
          {
          ????m_DiscInfoView.Cursor?
          = ?Cursors.WaitCursor;

          ????
          // ?
          }


          private ? void ?m_DiscInfoView_AfterExpand( object ?sender,?TreeViewEventArgs?e)
          {
          ????m_DiscInfoView.Cursor?
          = ?Cursors.Default;
          }

          ??????另外,這里所提出的延遲填充方案并不是最佳方案。試想一下,如果我只展開圖 2-1 中的“Bleach OVA”節點,而“浪客劍心”節點里面包含著數量可觀的子節點卻無需展開,那么 Cupel 的運行效率將受到影響。再者,預先填充這么多不需要的節點也會造成內存空間的浪費。為了避免這些弊端,我們可以修改一下這個方案,用“偽子節點”代替真實子節點來進行填充。還是拿圖 2-1 來舉例,當用戶展開根節點時,填充“Bleach”、“Bleach OVA”和“浪客劍心”等子節點,接著分別為這些子節點填充一個“偽子節點”。當用戶繼續展開“浪客劍心”節點時,它所包含的“偽子節點”將被刪除,取而代之的是它原本包含的真實子節點。

          ?

          3. 節點編輯

          3.1 說說要求

          ??????這里所說的“節點編輯”是狹義的重命名現有節點的名字,廣義上它還包括添加新節點以及移除現有節點。下圖示范了 Cupel 把“Anime”節點重命名為“Cartoon”:


          圖 3-1 編輯類別名

          ??????對節點進行重命名時需要注意:

          • 1) 新名字不能為空字符串;
          • 2) 新名字不能和已存在的名字相沖突;
          • 3) 新名字不允許包含某些特殊字符(可選)。

          3.2 開始編輯

          ??????TreeView.LabelEdit 屬性指示了節點是否允許編輯,默認情況下,它的值為 false。我們可以為類別節點提供一個上下文菜單,里面包含一個重命名菜單項,當用戶點擊該菜單項時,該類別節點進入編輯狀態:

          // ?Code?#09

          m_CategoryView.LabelEdit?
          = ? true ;

          if ?( ! m_CategoryView.SelectedNode.IsEditing)
          {
          ????m_CategoryView.SelectedNode.BeginEdit();
          }

          ??????注意,僅當 TreeView.LabelEdit 為 true 時,TreeNode.BeginEdit() 方法才可用,否則會拋出 InvalidOperationException 異常。

          3.3 完成編輯

          ??????節點完成編輯后將引發 TreeView.AfterLabelEdit 事件,該事件通過 NodeLabelEditEventHandler 委托來作用,該委托所包含的類型為 NodeLabelEditEventArgs 的參數 e 包含了完成編輯所需的信息:

          • 1) e.Node 是當前編輯的節點;
          • 2) e.Label 是用戶為節點輸入的新名字。

          ??????根據 3.1 中提到的三點要求的前兩點,我們可以寫出如下代碼:

          // ?Code?#10

          private ? void ?m_CategoryView_AfterLabelEdit( object ?sender,?NodeLabelEditEventArgs?e)
          {
          ????
          if ?(e.Label? != ? null )
          ????
          {
          ????????
          if ?(e.Label.Length? > ? 0 )
          ????????
          {
          ????????????
          if ?( ! m_MyDiscLibrary.IsCategoryNameExisting(e.Label))
          ????????????
          {
          ????????????????e.Node.EndEdit(
          false );

          ????????????????
          // ?
          ????????????}

          ????????????
          else
          ????????????
          {
          ????????????????e.CancelEdit?
          = ? true ;
          ????????????????MessageBox.Show(
          " 類別名已存在。 " );
          ????????????????e.Node.BeginEdit();
          ????????????}

          ????????}

          ????????
          else
          ????????
          {
          ????????????e.CancelEdit?
          = ? true ;
          ????????????MessageBox.Show(
          " 類別名不能為空。 " );
          ????????????e.Node.BeginEdit();
          ????????}

          ????}


          ????m_CategoryView.LabelEdit?
          = ? false ;
          }

          ??????在某些情況下,第三點要求是必須的,例如 Cupel 把類別節點影射到磁盤的目錄,而 Windows 規定某些字符不能用于命名目錄或文件的,此時就有必要添加相關的代碼來排錯了。

          ??????另外,如果編輯期間拋出異常,就有可能導致數據處于未定義狀態,此時你可以用一個 try 塊包圍代碼:

          // ?Code?#11

          try
          {
          ????
          // ?
          }

          catch ?()
          {
          ????e.Node.EndEdit(
          true );
          }

          finally
          {
          ????m_CategoryView.LabelEdit?
          = ? false ;
          }

          3.4 繼續思考

          ??????提供快捷鍵可以提高應用程序的易用性,我們在 Windows 中重命名目錄或文件時通常按 F2 來進入編輯狀態而不是使用右鍵菜單的重命名菜單項,于是我們也可以考慮在 Cupel 中提供類似的便捷:

          // ?Code?#12

          private ? void ?m_CategoryView_KeyUp( object ?sender,?KeyEventArgs?e)
          {
          ????
          if ?(e.KeyCode? == ?Keys.F2? && ?m_CategoryView.SelectedNode? != ? null ? && ?m_CategoryView.SelectedNode.Level? == ? 1 )
          ????
          {
          ????????RenameDiscCategory(
          null ,?EventArgs.Empty);
          ????}

          }

          ??????當你添加新的類別節點時,它會有一個默認的名字——New Category,并且處于編輯狀態:

          // ?Code?#13

          TreeNode?newTreeNode?
          = ? new ?TreeNode( " New?Category " ,? 1 ,? 1 );
          m_CategoryView.Nodes[
          0 ].Nodes.Add(newTreeNode);

          m_CategoryView.LabelEdit?
          = ? true ;

          if ?( ! newTreeNode.IsEditing)
          {
          ????newTreeNode.BeginEdit();
          }

          ??????可以看出,它和重命名類別節點名字的代碼非常相似,實質上,它等效于先添加一個新的節點,然后對該節點進行重命名。至于移除現有類別節點則更簡單:

          // ?Code?#14

          if ?(m_CategoryView.SelectedNode? != ? null )
          {
          ????m_CategoryView.SelectedNode.Remove();
          }

          ??????當然,在實際的應用中,這是遠遠不夠的,因為用戶可能只想移除該類別,而不希望丟失其所包含的光盤節點。對于用戶來說,正確的做法應該是把待移除的類別所包含的光盤節點移到別的類別節點下,然后再移除類別節點。但沒有人能夠保證用戶一定會這樣做,于是你就要有一些措施來避免不必要麻煩了,這里我介紹兩個措施:

          • 1) 顯示一個對話框提示用戶把待移除的類別節點所包含的光盤節點移動到別的類別節點,再執行刪除操作,這個對話框通常是一個向導;
          • 2) 類別節點移除后,原本隸屬該類別的光盤節點將被移到一個“Uncategorized”類別節點下,等待用戶做進一步的處理。

          ?

          4. 節點拖放

          4.1 說說要求

          ??????節點拖放可以用來實現更改某一光盤節點的所屬類別,例如,我把圖 1-1 中“Music”下的“MC0001”移到“Mix”下,就改變“MC0001”的類別了。由于每個光盤節點都必須隸屬某一個分類,于是你不能把它拖放到“My Disc Library”下和類別節點并列。你更不能把一個光盤節點拖放到另一個光盤節點下。換言之,只有光盤節點是可拖動的,并且只能置于類別節點下。

          4.2 基礎知識

          ??????要使得控件接受用戶拖放到它上面的數據,你必須把 AllowDrop 屬性設為 true,這是第一步。

          ??????接下來,你要了解 TreeView 拖放操作所涉及的三個事件:ItemDrag、DragEnter 和 DragDrop。舉個例子,我要把圖 1-1 中“Music”下的“MC0001”移到“Mix”下,那么當我們在“MC0001”上按下鼠標左鍵并開始拖動時,ItemDrag 事件就觸發了,然后,當“MC0001”被拖到“Music”的“地盤”上時,DragEnter 事件就觸發了,最后,當我們在“Music”上松開鼠標左鍵時,DragDrop 事件就觸發了。

          ??????從名字上很容易聯想到這三個事件的用途:

          • 1) ItemDrag 用于判斷對象是否允許拖動,如果允許則用 DoDragDrop() 方法初始化拖放操作。例如,如果被拖動的是“Music”而不是“MC0001”,我們應該中止拖放操作;
          • 2) DragEnter 則用于判斷拖過來的數據是否可以接受,用戶極有可能把非預期的數據拖過來,于是你有責任確保控件只接受那些可解釋的數據。例如,如果用戶拖過來的是一段文本,那么拖放操作就不能繼續了;
          • 3) 在 DragDrop 中,我們要做的就是解釋用戶拖放過來的數據,并對這些數據做適當的處理,當然,數據無法正確解釋也是有可能發生的,所以你有責任確保這些數據不會影響到現有的正常數據。

          4.3 實現拖放

          ??????首先是處理 ItemDrag 事件:

          // ?Code?#15

          private ? void ?m_CategoryView_ItemDrag( object ?sender,?ItemDragEventArgs?e)
          {
          ????
          if ?(e.Button? == ?MouseButtons.Left)
          ????
          {
          ????????TreeNode?sourceTreeNode?
          = ?(TreeNode)e.Item;

          ????????
          if ?(sourceTreeNode.Level? == ? 2 )
          ????????
          {
          ????????????DoDragDrop(sourceTreeNode,?DragDropEffects.Move);
          ????????}

          ????}

          }

          ??????需要說明的是,我們只接受左鍵拖動,而且源節點必須是光盤節點。在類別視圖上,節點只有三種類型:根節點、類別節點和光盤節點,它們分別是樹的第一層、第二層和第三層。而 TreeNode.Level 屬性恰好表達了節點的層次索引,只是它是從0開始的。于是,光盤節點的 Level 屬性值就是2。一切順利的話,我們就可以用 DoDragDrop 方法來初始化拖放操作了。

          ??????然后是處理 DragEnter 事件:

          // ?Code?#16

          private ? void ?m_CategoryView_DragEnter( object ?sender,?DragEventArgs?e)
          {
          ????Point?targetPoint?
          = ?m_CategoryView.PointToClient( new ?Point(e.X,?e.Y));
          ????TreeNode?targetNode?
          = ?m_CategoryView.GetNodeAt(targetPoint);

          ????
          if ?(targetNode? != ? null ? && ?targetNode.Level? == ? 1 ? && ?e.Data.GetDataPresent( typeof (TreeNode)))
          ????
          {
          ????????e.Effect?
          = ?e.AllowedEffect;
          ????????m_CategoryView.SelectedNode?
          = ?targetNode;
          ????}

          ????
          else
          ????
          {
          ????????e.Effect?
          = ?DragDropEffects.None;
          ????}

          }

          ??????在這里,我們必須確保目標節點是類別節點,并且用戶拖過來的數據類型是 TreeNode,否則不受理。要判斷目標節點是否為類別節點,首先得獲得目標節點的實例,這可以通過向 TreeView.GetNodeAt(Point pt) 方法傳遞目標節點的工作區坐標做到。然而,DragEventArgs.X 和 DragEventArgs.Y 所給出的是屏幕坐標,于是我們就需要使用 TreeView.PointToClient(Point pt) 方法把目標節點的屏幕坐標轉換成工作區坐標了。要判斷拖過來的數據類型是否為 TreeNode,我們只需把 typeof(TreeNode) 傳遞給 e.Data.GetDataPresent 并判斷其返回值就可以了。

          ??????最后是處理 DragDrop 事件:

          // ?Code?#17

          private ? void ?m_CategoryView_DragDrop( object ?sender,?DragEventArgs?e)
          {
          ????Point?targetPoint?
          = ?m_CategoryView.PointToClient( new ?Point(e.X,?e.Y));
          ????TreeNode?targetNode?
          = ?m_CategoryView.GetNodeAt(targetPoint);
          ????TreeNode?sourceNode?
          = ?(TreeNode)e.Data.GetData( typeof (TreeNode));

          ????
          if ?(sourceNode.Parent.Text? != ?targetNode.Text)
          ????
          {
          ????????sourceNode.Remove();
          ????????targetNode.Nodes.Add(sourceNode);
          ????}

          }

          ??????要處理拖過來的數據,當然就得先獲取目標節點,這點和處理 DragEnter 事件的一樣。由于通過了 DragEnter 事件的檢測,我們就可以在這里直接解釋拖過來的數據了,這可以通過 e.Data.GetData(Type t) 方法做到。把一個光盤節點拖放到一個類別節點相當于把該光盤節點從原來的類別節點上刪除,并添加到目標類別節點,當然我們應該判斷與拖放操作相關的兩個類別節點是否為同一個節點。

          4.4 繼續思考

          ??????“得一想二”是用戶的本性,我在這里介紹的拖放操作僅存在于同一個控件中的,難免日后用戶會期望得到跨控件/程序的拖放支持,例如,我把某張 DVD 放進光驅,然后打開我的電腦,把光驅的圖標拖到類別視圖上的某個類別節點,期望著 Cupel 會自動為我處理后續事宜。當 DVD 上的目錄/文件數量可觀時,用戶可能期望有一個進度條以便做到心中有數,甚至用戶還期望在此過程中可以隨時中止 DVD 信息獲取操作......噢,你還能想到什么?

          ?

          5. 后面的事

          ??????為了更好的管理我那座“山”,我引入了 Cupel,同時也引入了開發 Cupel 時的種種問題,這使我想起杰拉爾德·溫伯格在《你的燈亮著嗎?》中提到的一句話:每種解決方案都會帶來新的問題。回顧上面所說的一切,我們很容易想到把類別視圖封裝成一個自定義控件,而光盤結構視圖和節點信息視圖則組合封裝成一個用戶控件,這樣做的好處不言自明,然而這又會引入什么新的問題呢?說到這里,我又不禁想起另一句話:終點其實是另一個起點。

          posted on 2006-07-07 08:03 風人園 閱讀(1397) 評論(0)  編輯  收藏 所屬分類: DotNet

          主站蜘蛛池模板: 平罗县| 乐东| 靖西县| 西青区| 自贡市| 杭州市| 天祝| 玛多县| 大同市| 五台县| 邵阳市| 金川县| 卓资县| 大关县| 鹤岗市| 镇安县| 岗巴县| 泰宁县| 阳高县| 白山市| 江油市| 同心县| 镇宁| 芦溪县| 平泉县| 修水县| 堆龙德庆县| 云安县| 婺源县| 德惠市| 临潭县| 垫江县| 辽中县| 大足县| 元江| 双城市| 邵武市| 遵义县| 盐山县| 政和县| 新干县|