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

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

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

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

if
?(
!
m_CategoryView.SelectedNode.IsEditing)

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

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

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