0. 背景故事
??????現(xiàn)在的東西動不動就用G來算,一眨眼的功夫,我那100G的硬盤已擁擠不已了,但還有很多東西想放進來啊,怎么辦?好吧,現(xiàn)在 DVD 刻錄機的價格已經(jīng)平民化了,我買了一個來舒緩緊張的硬盤。這下好了,硬盤上的可用空間總是足以讓我下載想要的大塊頭了。沒過多久,我刻錄的 DVD 就堆積成山,成為我房間的一道景物。為了管理這座“山”,我決定寫一個 DVD 管理軟件,嗯,就叫它 Cupel 吧。不難想象,Cupel 將充分使用 TreeView 控件的各種功能,現(xiàn)在我把開發(fā) Cupel 的過程中使用 TreeView 的心得寫下來,希望能為那些尋找這方面內(nèi)容的朋友提供一些參考。
?
1. 填充節(jié)點
1.1 說說要求
圖 1-1 類別視圖
??????如上圖所示,根節(jié)點是光盤庫,它可以包含0個或多個類別節(jié)點,每個類別節(jié)點又包含0個或多個光盤節(jié)點。Cupel 通過 Cupel.Data.DiscLibrary 類來讀取和儲存相關(guān)數(shù)據(jù)。
1.2 進行填充
??????類別視圖的節(jié)點應該在 Cupel 的主窗體顯示之前填充好,于是我選擇在 Load 事件發(fā)生時進行填充:
//
?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();
}
??????填充節(jié)點的方法是很簡單的,上面的代碼有兩點需要說明:
- 1) 無論是 TreeNode 還是 TreeView,節(jié)點都是包含在 Nodes 屬性中的,通過該屬性的 Add() 方法可以添加新的節(jié)點。正如一個 TreeNode 可以包含多個子節(jié)點,一個 TreeView也可以包含多個根節(jié)點。
- 2) 節(jié)點填充完畢后,你應該使用 TreeView.ExpandAll() 方法展開所有節(jié)點。然而,當光盤節(jié)點過多時,展開全部節(jié)點可能不太合適,此時可以考慮只展開類別節(jié)點,即把 Code #01 的 m_CategoryView.ExpandAll(); 改為 libraryNode.Expand(); 就行了。
1.3 添加圖標
圖 1-2 文件夾視圖
??????對于 Windows 的用戶,上面這幅圖應該是很熟悉了,上面的每個節(jié)點都帶有一個圖標,這使得目錄試圖更直觀。Code #01 并沒有為每個節(jié)點添加圖標,運行結(jié)果是每個節(jié)點將只有文字。要為節(jié)點添加圖標,最簡單的方法就是在創(chuàng)建節(jié)點時通過構(gòu)造函數(shù)來指定,但在此之前,你得先創(chuàng)建一個 System.Windows.Forms.ImageList 實例,并用它來儲存圖標。這里介紹在 Visual Studio 里使用 ImageList 組件為 TreeView 提供圖像資源:
- 1) 在“工具箱”中拖動 ImageList 組件到主窗體;
- 2) 在“屬性”窗口中點擊 Images 屬性右邊的“...”按鈕打開“圖像集合編輯器”;
- 3) 按“添加”按鈕添加所需的圖標。
- 4) 選中 TreeView 控件,在“屬性”窗口中找到 ImageList 屬性,并把它的值設為剛才的 ImageList。
??????至此,相關(guān)的準備工作已經(jīng)完畢,接下來要做的就是修改 Code #01 為節(jié)點指定圖標,這可以通過使用 TreeNode 如下的構(gòu)造函數(shù)做到:
//
?Code?#02
public
?TreeNode(
string
?text,?
int
?imageIndex,?
int
?seletedImageIndex)
??????由于在 Cupel 中無論節(jié)點是否被選中,其圖標都是一樣的,所以上面構(gòu)造函數(shù)的后兩個參數(shù)值是一樣的。假設 category.ico 在 ImageList 中的索引是1,那么你可以這樣指定類別節(jié)點的圖標:
//
?Code?#03
TreeNode?categoryNode?
=
?
new
?TreeNode(category.Name,?
1
,?
1
);
1.4 繼續(xù)思考
??????前面說到,每個節(jié)點可以包含0個或多個字節(jié)點,于是在用戶第一次運行 Cupel 時,類別視圖將只有一個根節(jié)點。這顯然是不太友好的,因為面對著“一無所有”的類別視圖,用戶很可能會不知所措,尤其在他有很多光盤并且還沒決定如何對這些光盤分類時。此時我們不妨考慮為用戶提供一個默認分類,這樣他就可以在此基礎上構(gòu)想一個更合適自己的分類,這要比憑空想出一個分類容易的多。當然,有些用戶早已想出了一套很好的分類,此時我們就沒必要為他提供默認分類了,而是直接讓他應用自己的分類。可以看出,如果 Cupel 在第一次運行時顯示一個設置向?qū)В儐栍脩羰褂媚J分類還是應用自己的分類,則會使用戶感到更加友好。
??????無論多么好吃的東西,每天都吃也會使人感到厭倦。現(xiàn)今是一個個性化的時代,圖 1-1 無疑顯得有點單調(diào),如果用戶可以為每個類別指定一個不同的圖標,甚至隸屬不同類別的光盤也具有不同的圖標,這將會使得 Cupel 令人眼前一亮。進一步考慮,我們可以考慮把類別視圖的圖標設置儲存在一個配置文件,讓用戶可以選擇應用不同的圖標套裝。當然,有些用戶根本不在乎這點兒花樣,就像那些一直支持著“Windows 經(jīng)典”主題的用戶一樣。可以看出,如果 Cupel 在第一次運行時顯示一個設置向?qū)В儐栍脩羰褂媚膫€圖標套裝,則會使用戶感到更加友好。
?
2. 延遲填充
2.1 說說要求
圖 2-1 光盤結(jié)構(gòu)視圖
??????圖 2-1 分上下兩部分,上面是一個 TreeView,顯示了類別視圖選中的光盤節(jié)點所包含的目錄結(jié)構(gòu),下面是一個 ListView,顯示了光盤結(jié)構(gòu)視圖選中的節(jié)點的細節(jié)信息,此圖實質(zhì)上是一個主-從視圖。
??????當光盤所包含的目錄或文件節(jié)點比較多時,一次過填充光盤結(jié)構(gòu)視圖的所有節(jié)點很可能導致界面沒有響應,這顯然是不允許的。其實,我們沒有必要一開始就把所有節(jié)點都填充上去,而應該在用戶訪問到某節(jié)點時才填充它的子節(jié)點。
2.2 做好準備
??????TreeView 中的節(jié)點信息都包含在 TreeNode 中,為了使得光盤結(jié)構(gòu)視圖具備延遲填充特性,以及在節(jié)點信息視圖上顯示選中節(jié)點的細節(jié)信息,我們有必要自定義一個用于 TreeView 的節(jié)點類,該類將派生自 TreeNode,并且包含實現(xiàn)相關(guān)功能的信息。
??????節(jié)點可分為目錄節(jié)點和文件節(jié)點兩類,它們既有相同之處,也有不同之處,于是我們很容易聯(lián)想到建立一個繼承體系:
圖 2-2 節(jié)點繼承圖
??????FileSystemTreeNodeBase 類的 Properties 屬性是一個 List<FileSystemTreeNodeProperty> 集合,它包含了與節(jié)點的相關(guān)信息,這些信息將會顯示在節(jié)點信息視圖上,實現(xiàn)主-從視圖。另外,F(xiàn)ileSystemTreeNodeBase 類還包含了一個 FillSubNodes 抽象方法,用于協(xié)助光盤結(jié)構(gòu)視圖實現(xiàn)延遲填充特性。由于文件節(jié)點不會有子節(jié)點,所以 FileTreeNode.FillSubNodes() 的方法體是空的。現(xiàn)在我們來看一下 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();
????}
}
??????用戶有可能在展開某個節(jié)點后把它折疊起來,此時該節(jié)點的 Nodes 屬性就會包含它的子節(jié)點(一個例外情況就是原光盤的某個目錄是空目錄,即里面沒有包括任何子目錄和/或文件),所以我們應該首先檢查 Nodes.Count 是否為0。當條件滿足時,我們就對該節(jié)點進行填充,留意填充代碼包含在 TreeView.BeginUpdate() 和 TreeView.EndUpdate() 之間,這樣做是為了避免 TreeView 每填充一個節(jié)點就繪制一次,從而提高了效率。
2.3 按需填充
??????僅當某個節(jié)點包含了子節(jié)點時,我們才能展開該節(jié)點,所以在展開該節(jié)點時,就要對其子節(jié)點所包含的子節(jié)點進行填充。例如,在圖 2-1 中,當我們展開根節(jié)點(即“G:\”)時,“浪客劍心”所包含的子節(jié)點就得填充好了,否則它就無法被展開,它里面的目錄結(jié)構(gòu)也就無法顯示了。
??????回到 Cupel,當用戶選中類別視圖中的某個光盤節(jié)點,光盤結(jié)構(gòu)視圖就會顯示該光盤的根節(jié)點及其所包含的子節(jié)點:
//
?Code?#05
DirectoryTreeNode?rootDirectoryTreeNode?
=
?
rootDirectoryTreeNode.FillSubNodes();

m_DiscInfoView.Nodes.Clear();
m_DiscInfoView.Nodes.Add(rootDirectoryTreeNode);
??????接著,當用戶點擊可展開節(jié)點左邊的“+”時,將引發(fā) TreeView 的 BeforeExpand 事件,此時是填充該節(jié)點的子節(jié)點的子節(jié)點的最佳時機:
//
?Code?#06
private
?
void
?m_DiscInfoView_BeforeExpand(
object
?sender,?TreeViewCancelEventArgs?e)

{
????
foreach
?(FileSystemTreeNodeBase?subNode?
in
?e.Node.Nodes)

????
{
????????subNode.FillSubNodes();
????}
}
2.4 顯示細節(jié)
??????當用戶選中光盤結(jié)構(gòu)視圖中的某個節(jié)點時,節(jié)點信息視圖將顯示與該節(jié)點相關(guān)的信息,這兩個視圖共同組成一個主-從視圖:
//
?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 首先檢測是否為鼠標左鍵點擊以及點擊次數(shù)是否為1,這些信息都包含在類型為 TreeNodeMouseClickEventArgs 的 e 參數(shù)中。另外,e.Node 是當前選中的節(jié)點,你必須把它強制轉(zhuǎn)換成 FileSystem.TreeNodeBase 類型才能訪問其所包含的 Properties 屬性。
2.5 繼續(xù)思考
??????雖然我們使用了“延遲填充”,但在展開某些節(jié)點時依然會感覺到“遲鈍”,出現(xiàn)這種情況的主要原因是該節(jié)點的子節(jié)點包含著大量子節(jié)點。此時我們可以在展開之前把鼠標指針改為等待樣式,待節(jié)點展開完畢后再改為默認樣式:
//
?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”節(jié)點,而“浪客劍心”節(jié)點里面包含著數(shù)量可觀的子節(jié)點卻無需展開,那么 Cupel 的運行效率將受到影響。再者,預先填充這么多不需要的節(jié)點也會造成內(nèi)存空間的浪費。為了避免這些弊端,我們可以修改一下這個方案,用“偽子節(jié)點”代替真實子節(jié)點來進行填充。還是拿圖 2-1 來舉例,當用戶展開根節(jié)點時,填充“Bleach”、“Bleach OVA”和“浪客劍心”等子節(jié)點,接著分別為這些子節(jié)點填充一個“偽子節(jié)點”。當用戶繼續(xù)展開“浪客劍心”節(jié)點時,它所包含的“偽子節(jié)點”將被刪除,取而代之的是它原本包含的真實子節(jié)點。
?
3. 節(jié)點編輯
3.1 說說要求
??????這里所說的“節(jié)點編輯”是狹義的重命名現(xiàn)有節(jié)點的名字,廣義上它還包括添加新節(jié)點以及移除現(xiàn)有節(jié)點。下圖示范了 Cupel 把“Anime”節(jié)點重命名為“Cartoon”:
圖 3-1 編輯類別名
??????對節(jié)點進行重命名時需要注意:
- 1) 新名字不能為空字符串;
- 2) 新名字不能和已存在的名字相沖突;
- 3) 新名字不允許包含某些特殊字符(可選)。
3.2 開始編輯
??????TreeView.LabelEdit 屬性指示了節(jié)點是否允許編輯,默認情況下,它的值為 false。我們可以為類別節(jié)點提供一個上下文菜單,里面包含一個重命名菜單項,當用戶點擊該菜單項時,該類別節(jié)點進入編輯狀態(tài):
//
?Code?#09
m_CategoryView.LabelEdit?
=
?
true
;

if
?(
!
m_CategoryView.SelectedNode.IsEditing)

{
????m_CategoryView.SelectedNode.BeginEdit();
}
??????注意,僅當 TreeView.LabelEdit 為 true 時,TreeNode.BeginEdit() 方法才可用,否則會拋出 InvalidOperationException 異常。
3.3 完成編輯
??????節(jié)點完成編輯后將引發(fā) TreeView.AfterLabelEdit 事件,該事件通過 NodeLabelEditEventHandler 委托來作用,該委托所包含的類型為 NodeLabelEditEventArgs 的參數(shù) e 包含了完成編輯所需的信息:
- 1) e.Node 是當前編輯的節(jié)點;
- 2) e.Label 是用戶為節(jié)點輸入的新名字。
??????根據(jù) 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 把類別節(jié)點影射到磁盤的目錄,而 Windows 規(guī)定某些字符不能用于命名目錄或文件的,此時就有必要添加相關(guān)的代碼來排錯了。
??????另外,如果編輯期間拋出異常,就有可能導致數(shù)據(jù)處于未定義狀態(tài),此時你可以用一個 try 塊包圍代碼:
//
?Code?#11
try
{
????
//
?
}
catch
?(
)

{
????e.Node.EndEdit(
true
);
}
finally
{
????m_CategoryView.LabelEdit?
=
?
false
;
}
3.4 繼續(xù)思考
??????提供快捷鍵可以提高應用程序的易用性,我們在 Windows 中重命名目錄或文件時通常按 F2 來進入編輯狀態(tài)而不是使用右鍵菜單的重命名菜單項,于是我們也可以考慮在 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);
????}
}
??????當你添加新的類別節(jié)點時,它會有一個默認的名字——New Category,并且處于編輯狀態(tài):
//
?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();
}
??????可以看出,它和重命名類別節(jié)點名字的代碼非常相似,實質(zhì)上,它等效于先添加一個新的節(jié)點,然后對該節(jié)點進行重命名。至于移除現(xiàn)有類別節(jié)點則更簡單:
//
?Code?#14
if
?(m_CategoryView.SelectedNode?
!=
?
null
)

{
????m_CategoryView.SelectedNode.Remove();
}
??????當然,在實際的應用中,這是遠遠不夠的,因為用戶可能只想移除該類別,而不希望丟失其所包含的光盤節(jié)點。對于用戶來說,正確的做法應該是把待移除的類別所包含的光盤節(jié)點移到別的類別節(jié)點下,然后再移除類別節(jié)點。但沒有人能夠保證用戶一定會這樣做,于是你就要有一些措施來避免不必要麻煩了,這里我介紹兩個措施:
- 1) 顯示一個對話框提示用戶把待移除的類別節(jié)點所包含的光盤節(jié)點移動到別的類別節(jié)點,再執(zhí)行刪除操作,這個對話框通常是一個向?qū)В?
- 2) 類別節(jié)點移除后,原本隸屬該類別的光盤節(jié)點將被移到一個“Uncategorized”類別節(jié)點下,等待用戶做進一步的處理。
?
4. 節(jié)點拖放
4.1 說說要求
??????節(jié)點拖放可以用來實現(xiàn)更改某一光盤節(jié)點的所屬類別,例如,我把圖 1-1 中“Music”下的“MC0001”移到“Mix”下,就改變“MC0001”的類別了。由于每個光盤節(jié)點都必須隸屬某一個分類,于是你不能把它拖放到“My Disc Library”下和類別節(jié)點并列。你更不能把一個光盤節(jié)點拖放到另一個光盤節(jié)點下。換言之,只有光盤節(jié)點是可拖動的,并且只能置于類別節(jié)點下。
4.2 基礎知識
??????要使得控件接受用戶拖放到它上面的數(shù)據(jù),你必須把 AllowDrop 屬性設為 true,這是第一步。
??????接下來,你要了解 TreeView 拖放操作所涉及的三個事件:ItemDrag、DragEnter 和 DragDrop。舉個例子,我要把圖 1-1 中“Music”下的“MC0001”移到“Mix”下,那么當我們在“MC0001”上按下鼠標左鍵并開始拖動時,ItemDrag 事件就觸發(fā)了,然后,當“MC0001”被拖到“Music”的“地盤”上時,DragEnter 事件就觸發(fā)了,最后,當我們在“Music”上松開鼠標左鍵時,DragDrop 事件就觸發(fā)了。
??????從名字上很容易聯(lián)想到這三個事件的用途:
- 1) ItemDrag 用于判斷對象是否允許拖動,如果允許則用 DoDragDrop() 方法初始化拖放操作。例如,如果被拖動的是“Music”而不是“MC0001”,我們應該中止拖放操作;
- 2) DragEnter 則用于判斷拖過來的數(shù)據(jù)是否可以接受,用戶極有可能把非預期的數(shù)據(jù)拖過來,于是你有責任確保控件只接受那些可解釋的數(shù)據(jù)。例如,如果用戶拖過來的是一段文本,那么拖放操作就不能繼續(xù)了;
- 3) 在 DragDrop 中,我們要做的就是解釋用戶拖放過來的數(shù)據(jù),并對這些數(shù)據(jù)做適當?shù)奶幚恚斎唬瑪?shù)據(jù)無法正確解釋也是有可能發(fā)生的,所以你有責任確保這些數(shù)據(jù)不會影響到現(xiàn)有的正常數(shù)據(jù)。
4.3 實現(xiàn)拖放
??????首先是處理 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);
????????}
????}
}
??????需要說明的是,我們只接受左鍵拖動,而且源節(jié)點必須是光盤節(jié)點。在類別視圖上,節(jié)點只有三種類型:根節(jié)點、類別節(jié)點和光盤節(jié)點,它們分別是樹的第一層、第二層和第三層。而 TreeNode.Level 屬性恰好表達了節(jié)點的層次索引,只是它是從0開始的。于是,光盤節(jié)點的 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;
????}
}
??????在這里,我們必須確保目標節(jié)點是類別節(jié)點,并且用戶拖過來的數(shù)據(jù)類型是 TreeNode,否則不受理。要判斷目標節(jié)點是否為類別節(jié)點,首先得獲得目標節(jié)點的實例,這可以通過向 TreeView.GetNodeAt(Point pt) 方法傳遞目標節(jié)點的工作區(qū)坐標做到。然而,DragEventArgs.X 和 DragEventArgs.Y 所給出的是屏幕坐標,于是我們就需要使用 TreeView.PointToClient(Point pt) 方法把目標節(jié)點的屏幕坐標轉(zhuǎn)換成工作區(qū)坐標了。要判斷拖過來的數(shù)據(jù)類型是否為 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);
????}
}
??????要處理拖過來的數(shù)據(jù),當然就得先獲取目標節(jié)點,這點和處理 DragEnter 事件的一樣。由于通過了 DragEnter 事件的檢測,我們就可以在這里直接解釋拖過來的數(shù)據(jù)了,這可以通過 e.Data.GetData(Type t) 方法做到。把一個光盤節(jié)點拖放到一個類別節(jié)點相當于把該光盤節(jié)點從原來的類別節(jié)點上刪除,并添加到目標類別節(jié)點,當然我們應該判斷與拖放操作相關(guān)的兩個類別節(jié)點是否為同一個節(jié)點。
4.4 繼續(xù)思考
??????“得一想二”是用戶的本性,我在這里介紹的拖放操作僅存在于同一個控件中的,難免日后用戶會期望得到跨控件/程序的拖放支持,例如,我把某張 DVD 放進光驅(qū),然后打開我的電腦,把光驅(qū)的圖標拖到類別視圖上的某個類別節(jié)點,期望著 Cupel 會自動為我處理后續(xù)事宜。當 DVD 上的目錄/文件數(shù)量可觀時,用戶可能期望有一個進度條以便做到心中有數(shù),甚至用戶還期望在此過程中可以隨時中止 DVD 信息獲取操作......噢,你還能想到什么?
?
5. 后面的事
??????為了更好的管理我那座“山”,我引入了 Cupel,同時也引入了開發(fā) Cupel 時的種種問題,這使我想起杰拉爾德·溫伯格在《你的燈亮著嗎?》中提到的一句話:每種解決方案都會帶來新的問題。回顧上面所說的一切,我們很容易想到把類別視圖封裝成一個自定義控件,而光盤結(jié)構(gòu)視圖和節(jié)點信息視圖則組合封裝成一個用戶控件,這樣做的好處不言自明,然而這又會引入什么新的問題呢?說到這里,我又不禁想起另一句話:終點其實是另一個起點。