65.9K
CodeProject 正在变化。 阅读更多。
Home

打造你自己的压缩器 2 / 3

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (4投票s)

2008年12月5日

LGPL3

4分钟阅读

viewsIcon

25773

downloadIcon

327

本文介绍如何使用 CAKE3 创建虚拟文件列表、目录树以及实现拖放功能。

 


本教程包含 3 部分:

  1. 存档操作 
  2. 文件列表、目录树和拖放  (本文) 
  3. 线程支持

打造你自己的压缩器 2 

请注意,在附带的演示中,文件列表和目录树的代码分别位于 FileList.cs 和 FolderTree.cs 中。
我还添加了 IconDispenserMini.cs(用于检索图标)和 CakDataObject.cs(用于拖放)。

 什么是虚拟列表视图,为什么需要它。

尝试打开一个包含数千个文件的存档,使用 cyoa1 中的最后一个演示,你会注意到界面会冻结半分钟甚至更长时间。
实际上,系统在返回控件之前正在将所有文件加载到文件列表中。

虚拟列表视图使用另一种机制进行加载,你需要指定列表视图中的项目数量,当用户滚动到尚未创建的项目时,列表视图会(通过事件)请求一个项目。 几年前,当我使用 Delphi 进行开发时,我不得不使用一些第三方组件,现在随着 .NET 框架包含了这个功能,就容易多了。 

创建虚拟文件列表 

1) listView.VirtualMode = true;
2) listView.RetrieveVirtualItem += 
     new RetrieveVirtualItemEventHandler(FileList_RetrieveVirtualItem);
...
3) ContentList contentList = cakdir.Archive_Contents;
4) listView.VirtualListSize = contentList.Count;
....
5) void FileList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
6) {
7)   if (e.ItemIndex < cakdir.Archive_Contents.Count)
8)   { 
9)      ContentType ct = cakdir.Archive_Contents[e.ItemIndex]; 
0)      e.item = new ListViewItem(new string[] 
           {ct.fileName, Utils.SizeInK(ct.fileSize) });
a)   }    
b) }
  • 第 1 行启用虚拟模式,请记住,将其设置为 true 后,你将无法使用 listView.SelectedItem,(而是使用 SelectedIndices)。
  • 第 2 行附加一个事件,以便列表视图在需要时请求 ListViewItem。
  • 第 3-4 行,不逐个添加 ListViewItem,而是将 VirtualListSize 设置为最大尺寸。
    请注意,第 3 行之后,contentList 是 cakdir.Archive_Contents 的同一地址引用,如果你希望它们引用不同的地址,可以使用 Clone() 方法。
  • 第 7-10 行,根据 e.ItemIndex 创建一个新的 ListViewItem。

创建文件夹树


首先,我们需要为所有文件夹创建根节点。
1) folderViewNode = new TreeNode(folderView);
2) if (contentList.GetSubdirectoryList().Count > 0)
3)   folderViewNode.Nodes.Add("Uncached");
4) treeView.Nodes.Add(folderViewNode);
  • 第 1 行和第 4 行创建一个根节点并将其添加到 treeview 的节点列表中。
  • 第 2 行和第 3 行添加一个虚拟节点(dummy node),如果存在子目录,则会出现一个“+”按钮,并且该节点是可展开的。
然后,当一个节点(根节点或其他节点)被展开时,生成子目录节点。
1) void FolderTree_BeforeExpand(object sender, 
                     System.Windows.Forms.TreeViewCancelEventArgs e)
2) {
3)    e.Node.Nodes.Clear();
4)    string path = Utils.AppendSlash(e.Node.FullPath.Replace(folderView, ""));
5)    foreach (string s in contentList.GetSubdirectoryList(path))
6)    {
7)       TreeNode tn = new TreeNode(s);
8)       if (contentList.GetSubdirectoryList(path + s + "\\").Count > 0)
9)         tn.Nodes.Add("Uncached");
0)        e.Node.Nodes.Add(tn);
a)    }
b) }
  • 第 5 行,contentList.GetSubdirectoryList() 将返回指定路径的子目录。
    GetDirectoryList() 将返回所有目录,这些命令不会更改内容。
请注意,在此阶段,当你单击一个文件夹时,文件列表不会更新,我们需要将它们关联起来。
1) void FolderTree_AfterSelect(object sender, TreeViewEventArgs e)
2) {
3)    if (e.Node == null) return;
4)    contentList.Subdir = false;
5)    string path = Utils.AppendSlash(e.Node.FullPath.Replace(folderView, ""));
6)    contentList.Directory = path;
7)    contentList.Mask = "*";
8)    contentList.SearchParam = null;
9) }
  • 第 6-8 行,Directory、Mask 和 SearchParam 在设置后会更改内容。
    Mask 只支持 *。
    SearchParam 示例
    • n=*.txt(所有文本文件)
    • n=*.txt;d<08-01-08(2008 年 1 月 8 日之前的godic文本文档)
    • s>102400(大小 > 100kb)
    • n=*.txt;l=readme(包含至少一个“readme”的godic文本文档)
并且文件列表在更新时应该得到通知。
3) ContentList contentList = cakdir.Archive_Contents;
3.1) contentList.OnItemChanged += new EventHandler(ContentList_OnItemChanged);
4) listView.VirtualListSize = contentList.Count;
...
5) public void ContentList_OnItemChanged(Object sender, EventArgs e)
6) {
7)    listView.VirtualListSize = 0;  
8)    listView.VirtualListSize = contentList.Count;
9) }
  • 第 7-8 行,VirtualListSize 被设置了两次,以避免新旧 contentList.Count 相等。

实现拖放  


拖动:从 CYOA 将文件拖到其他应用程序。

1) string[] fileNames = extractFile();
2) DataObject dataObj =new DataObject();
3) dataObj.SetData(DataFormats.FileDrop, fileNames);
4) listView.DoDragDrop(dataObj, DragDropEffects.Move | DragDropEffects.Copy);
  • 第 3 行,指定拖动的数据是 FileDrop,以及文件名。
  • 第 4 行,执行后,UI 会冻结,直到拖放完成。
实际上,这是支持此功能的代码,但文件名必须在执行此命令时存在,这会导致每次用户尝试拖动时应用程序都会冻结。
经过一些搜索,我找到了一个解决方案。


你需要通过**创建虚拟文件**和文件夹(对于文件夹,你只需要创建根文件夹)来欺骗 DataObjects,让你拥有将要拖放的文件,虚拟文件将在实际放置时被替换。
1) void ListView_ItemDrag(object sender, System.Windows.Forms.ItemDragEventArgs e)
2) {
3)   string[] tempPaths = getTempFilePaths();
4)   foreach (string path in tempPaths)
5)     CreateTemporaryFileName(path);
6)   dataObj.SetData(DataFormats.FileDrop, tempPaths);      
7)   listView.DoDragDrop(dataObj, DragDropEffects.Move | DragDropEffects.Copy);
8) }
然后,你需要创建一个新的 DataObject,并重写其 GetData() 方法。
1) public override object GetData(String format)
2) {
3)    Object obj = base.GetData(format);
4)    bool InDragDrop = (0 != (int)GetData(ShellClipboardFormats.CFSTR_INDRAGLOOP));
5)    if (System.Windows.Forms.DataFormats.FileDrop == format &&
6)        !InDragLoop && !extracted)
7)      {
8)         foreach (string item in TempFileList())
9)           System.IO.File.Delete(item);
0)         cakdir.Extract(....)
a)         extracted = true;
b)      }
c)    return obj;
d) }
  • 第 4 行,InDragDrop 是一个开关,它在 dataObj 中查找 CFSTR_INDRAGLOOP 数据。
    当用户放置文件时,它会返回 false,因此需要提取项目,这个 CFSTR_INDRAGLOOP 需要你设置它(见下文)。
然后,你必须通知新的 DataObject 在用户放置文件时进行提取。
1) void ListView_QueryContinueDrag(object sender, 
      System.Windows.Forms.QueryContinueDragEventArgs e)
2) {
3)   if (e.KeyState == 0)
4)   {
5)      dataObj.SetData(ShellClipboardFormats.CFSTR_INDRAGLOOP, 0);
6)      e.Action = DragAction.Drop;
7)      this.AllowDrop = true;
8)      return;
9)   }
0)   else e.Action = DragAction.Continue;
a) }
  • QueryContinueDrag 在拖放操作期间发生,并允许拖动源确定是否应取消拖放操作(msdn)。
    因此,如果你的项目拖到不可放置的表面(例如 Panel),它将不会被调用。
  • 第 5 行,如上所述,如果 KeyState = 0(没有按下任何键),则将 CFSTR_INDRAGLOOP 设置为 dataObj。
    来自 msdn
    • 1 - 鼠标左键,
    • 2 - 鼠标右键。 
    • 4 - SHIFT 键。 
    • 8 - CTL 键。 
    • 16 - 鼠标中键。 
    • 32 - ALT 键
  • 因此,当用户释放左键时,
    • 没有按下任何键,
    • 将 CFSTR_INDRAGLOOP 设置为 0, 
    • 虚拟文件被移除(在 dataObj.GetData 中),
    • 文件被提取(在 dataObj.GetData 中),
    • 放置操作完成。

放置:从其他应用程序将文件放置到 CYOA。


与拖动相比,这相对简单。

1) void FileList_DragOver(object sender, DragEventArgs e)
2) {
3)   if (!e.Data.GetDataPresent(DataFormats.FileDrop) || 
            (cakdir == null) || (cakdir.ArchiveName == "")
4)      || (!cakdir.CanAdd))
5)   {
6)      e.Effect = DragDropEffects.None;                
7)      return;
8)   }
9)   e.Effect = DragDropEffects.Copy;
0) }
  • 第 3 行,GetDataPresent 方法确定要放置的项目是否为文件(DataFormats.FileDrop)。
    回顾上面的 dataObj.SetData(DataFormats.FileDrop, ....)。
1) void ListView_DragDrop(object sender, DragEventArgs e)
2) {  
3)   if (e.Data.GetDataPresent(DataFormats.FileDrop))
4)   { 
5)     cakdir.Add(....)
6)     cakdir.List("*");
7)   }
8) }

完成这部分后,大部分基本功能应该已经完成,但你可能希望使存档操作(提取/添加)更具响应性,下一篇文章将介绍 cakdir 的线程支持。

历史 

2008-05-12 - 首次提交到 CodeProject。 



© . All rights reserved.