一、引言
Windows操作系统用户日常使用最多的程序,资源管理器(explorer.exe)。相对于命令行方式,它可以方便地对文件系统进行管理,如查看、运行、删除等操作。不过,虽然explorer也提供了文件搜索功能,而且在微软Windows后续版(如 Vista)本中不断加强文件检索功能。但仍不能很好地满足用户在大量文件中检索某些具有特定特征文件的需要。其中一个原因就是,用户也不可能将所有的类似于检索关键字的信息全都写在文件名上,供检索程序检索。
另一方面,我们知道Windows下不同类型文件的属性页是可以不一样的,也就是特定文件类型可以有自己特定的属性页。如PDF格式的文件就有一个名为 PDF 的属性页。在这个属性页上,可以填写如“标题”、“作者”、“主题”等信息。用户填写这些信息,查找文件会比较方便。而且搜索文件时也可以对这些内容进行搜索。
那么能不能让所有的文件都能具有用户可以自己定义、而不是某种特定类型文件才有的属性呢?比如,任何一种文件都可以定义一种叫做“主题”或“摘要”的属性。能不能将文件搜索和数据库系统联系在一起呢?因为数据库系统具有强大的搜索引擎,相对于文件系统,检索是数据库系统的优势。如果这两个功能结合到一起,用户既可以自己定义文件属性,又能对属性进行快速检索。这样不光会提高系统的文件管理性能,尤其是搜索性能,还会给用户提供很大的便利。
基于这个想法,笔者进行了实验,证明这是可以实现的。本文将讨论该功能实现方案。所给出的例程是基于Delphi 7 和Access 2003开发的。选择这两个工具原因是(1)Delphi开发shell接口、开发数据库都很方便。(2)Access是微软Windows操作系统默认就配有ADO(Active Data Object),不需要额外安装数据引擎。
二、实现
简要地说实现方案是这样的:扩展explorer的IContextMenu 接口,在右键菜单中加入一个新的菜单项,这里命名为“<文件备注>”,并将实现的dll插件(这里命名为“Contextmenu.dll”),插入到explorer中。当用户在右键菜单中选择该项后,调用应用程序。使用该程序,用户可以显示、添加、删除、修改自定义文件属性的项目(这里我们称为“文件备注项目”)和内容。所有用户填写的内容保存到数据库中。并用一个额外的应用程序专门负责查询和一致性维护(下文详细讨论)。这样一个完备、开放的系统可以很好地完成上文所提出的各项功能。
1.扩展IContextMenu 接口
关于用Delphi扩展IContextMenu 接口的资料太多,这里就不详细讨论其原理了,只给出创建这个Project的关键步骤、并加以简要说明。要在explorer中插入菜单,必须实现IShellExtInit、IContextMenu和IComObjectFactory三个接口。IShellExtInit实现接口的初始化,IContextMenu接口对象实现上下文相关菜单,IComObjectFactory接口实现对象的创建。
首先,在Delphi中新建一个ActvieX Library 项目(File->New->Others->ActiveX)
Delphi自动生成4个ActiveX Dll 标准输出函数。保存项目文件名为Contextmenu 。新建一个单元文件(unit文件,命名为Contextmenuhandle),在Interface部分作如下代码所示的声明,需要指出的是Class_ContextMenu:TGUID =‘{53E038BA-5DF3-410D-8BE3-D832C82078DC}'一行,是声明该接口的GUID,在Delphi中是使用Ctrl+Shift+G快捷键产生的。在Private部分声明了一个保存文件名的ASCII码字符串FFileName。 Protected部分的4个函数都是实现接口所必须的,其函数签名都可以在MSDN或SDK中找到。最后声明了一个全局变量FileList:TStringList,用来保存用户在explorer中所选中的文件名称。
interface
uses
Windows,ActiveX,ComObj,ShlObj,Classes;
type
TContextMenu = class(TComObject,IShellExtInit,IContextMenu)
private
FFileName: array[0..MAX_PATH] of Char;
protected
function IShellExtInit.Initialize = SEIInitialize;
function SEIInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall;
function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast,
uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;
const
Class_ContextMenu: TGUID ='{53E038BA-5DF3-410D-8BE3-D832C82078DC}';
var
FileList:TStringList;
限于篇幅,对以上4个接口的实现本文正文中不再给出源代码,读者可以参考杂志网站上源码。
如上所述:在explorer中插入自定义的菜单项目,用户选择该菜单时调用应用程序,并传入文件名和路径名。程序首先在数据库中检索该文件已经保存的用户自定义文件备注项目,并显示。用户可以修改文件备注项目的属性值,然后由程序保存提交到数据库中。
2.数据库设计
本文的数据库是采用Access 2003,因为Access是典型的桌面数据库管理系统。而且Windows操作系统上默认有ADO 数据引擎。即使没有安装Access软件,用Access + ADO开发的程序也能正常运行。这样发布程序时就不用特别考虑数据库客户端问题。
为了既说明问题也不至于使例程过于复杂,本文数据库中只使用3个相对简单的表。具体设计分别如表1、表2和表3,其关系如图1所示。
表1 Item表的属性
Item |
属性 |
类型 |
备注 |
ItemID |
int |
自动编号,主键 |
ItemCap |
char(20) |
备注项目标题 |
表2 File表的属性
File |
属性 |
类型 |
备注 |
FileID |
int |
自动编号,主键 |
FileName |
memo |
文件全名 |
表3 Memo表的属性
Memo |
属性 |
类型 |
备注 |
FileID |
int |
外键,指向File.FileID; 主键 |
ItemID |
int |
外键,指向Item.ItemID; 主键 |
Value |
memo |
文件备注内容 |

图1 数据库各表之间的关系
3.主程序设计
由于本文所说的文件属性是可以用户自定义的,所以界面设计一定要简洁、灵活、方便扩展。此处采用的方法是动态生成属性页TTabSheet和TMemo控件。基本界面如图2所示。其他的属性页(TTabSheet)和TMemo控件由代码动态维护。其他主要控件是TPageControl、TADOConnection、TADOQuery、TImageList和TToolBar等辅助性和修饰性控件。
程序的其核心思想是:程序启动时首先查询数据库Item表,得到所有的备注项目名称作为属性页标题、生成全部属性页。同时在每个属性页的客户区中生成一个TMemo控件。然后程序根据文件名和备注项目名分别查找到FileID和ItemID,得到这两个之后查询memo表的vallue属性,将用户备注显示到每个属性页上的TMemo控件上。
用户可以新建、删除文件备注项目,可以填写每个备注项目(属性页和TMemo)要保存的信息,点击确定后可以将这些信息提交到数据库中。

图2 主程序界面设计
限于篇幅本文正文中只给出几个核心功能的代码。其他的代码请参考杂志网站发布的源代码。
首先要说明的是主类的成员变量和成员函数,各个成员用途,参考如下声明的注释:
.....
public
{ Public declarations }
strFileNames : TstringList ; // 保存文件名称
strItemStr :TStringList ; //保存属性页名称
protected
//MemArr :Array of TMemo ;
MemList :TList ; //用于保存动态生成的Memo
procedure InitSheet; //初始化TTabsheet,动态创建所有的TabSheet
procedure GetMemItemStrings();//获得所有Item表中的 ItemCap
procedure ShowAllItems(); //显示每个Tabsheet上Memo的字符串
function GetItemValue(Item , FileName :string) :string;//获得指定文件和备注项目的字符串 //(也就是用户保存的属性值)
function AddItem(strItem :string) :boolean;//添加新的备注项目
function DelItem(strItem :string) :boolean ;//删除备注项目
function SaveMemo(Item,FileName,value:string):boolean ;//将指定文件名\备注项目的字符
//串写入数据库
procedure ShowFileProp(FileName :string);//显示文件基本属性
end;
(1) 动态创建控件
如前所述,主程序TPageControl上的TTabSheet和TMemo大部分都是动态生成的,要从数据库中加载,然后再构造控件。首先在FormCreate时,使用GetMemItemStrings(自定义的)函数从数据库中检索出表Item中所有的ItemCap(备注项目标题)。动态构造是在InitSheet过程中完成的,主要使用了TList容器MemList。顺便指出,为了保存动态生成对象还可以使用TObjectList型容器。当然这要求所保存的对象需要从超类TObject派生下来的。使用TList容器不存在这样问题,但要程序员自己负责内存管理。具体代码参考InitSheet过程,如下:
procedure TForm2.InitSheet;
var i :integer ;
var t1 :TTabsheet ;
var p : ^TMemo ;
begin
for i := 0 to self.strItemStr.Count -1 do
begin
t1:=TTabsheet.CreateParented(self.PageControl1.Handle );
t1.Caption := self.strItemStr[i] ;
t1.Show ;
new(p);
P^:= TMemo.Create(self);
P^.ParentWindow :=t1.Handle ;
P^.Width :=self.Memo1.Width ;
P^.Height :=self.Memo1.Height ;
P^.Font :=self.Memo1.Font ;
P^.ScrollBars :=ssVertical ;
P^.Show ;
MemList.Add(p);
t1.PageControl :=self.PageControl1 ;
end;
self.PageControl1.ActivePageIndex := 0 ;
end;
(2)显示文件备注
这部分工作主要是在FormShow时参考下面代码。InitSheet函数上文已经给出。ShowFileProp是在第一个属性页中显示一些文件属性的基本信息。并不是很重要的。最主要的是ShowAllItems函数,用来显示文件备注项目的内容(值地字符串)。该函数中也显示了如何从抽象容器中取出具体对象的方法:即类型转换。
procedure TForm2.FormShow(Sender: TObject);
begin
self.InitSheet ;
self.ShowFileProp(self.strFileNames.Strings[0]);
self.ShowAllItems ;
end;
procedure TForm2.ShowAllItems;
var i :integer ;
begin
for i := 0 to self.strItemStr.Count -1 do
begin
TMemo(MemList[i]^).Text :=(self.GetItemValue(self.strItemStr.Strings[i] ,self.strFileNames[0]));
end;
end;
(3) 响应用户操作
主要讨论(1)用户新建文件备注项目,和(2)响应“确定”按钮单击事件。
要注意的是用户新建文件备注项目时要动态维护MemList容器,响应“确定”按钮单击事件时使用了自定义函数SaveMemo,其功能是向数据库保存所有更改过的信息,限于篇幅代码没有给出,但其核心思想就是使用SQL代码进行数据维护。
1)用户新建文件备注项目
procedure TForm2.ToolButton1Click(Sender: TObject);
var t1 :TTabsheet ;
var memArrtmp : array of TMemo ;
var strCap :string ;
var i :integer ;
var p : ^ Tmemo ;
begin
if( (false= InputQuery('新增备注项', '输入有意义的备注项名称', strCap)) or (trim(strCap)='')) then
begin
exit ;
end ;
if not self.AddItem(strCap) then exit ;
t1:=TTabsheet.CreateParented(self.PageControl1.Handle );
t1.Caption := strCap ;
t1.Show ;
new(p);
P^ := TMemo.Create(self);
P^.ParentWindow :=t1.Handle ;
P^.Width :=self.Memo1.Width ;
P^.Height :=self.Memo1.Height ;
P^.Font :=self.Memo1.Font ;
P^.ScrollBars :=ssVertical ;
P^.Show ;
P^.SetFocus ;
MemList.Add(p);
self.PageControl1.ActivePageIndex :=self.PageControl1.PageCount ;
t1.PageControl :=self.PageControl1 ;
end;
2)响应“确定”按钮单击事件
procedure TForm2.BitBtn2Click(Sender: TObject);
var i : integer ;
var strFile :string ;
begin
strFile:= strFileNames[0] ;
for i := 1 to self.PageControl1.PageCount-1 do
begin
SaveMemo(self.PageControl1.Pages[i].Caption ,strFile, TMemo(self.MemList[i-1]^).Text);
end;
close;
end;
三、运行结果
由于这里开发的是插入到explorer中的插件,所以编译之后,要进行注册。方法是菜单Run->Register ActiveX server ,然后在explorer中用鼠标选中一个文件,右键单击,可以看到弹出的右键菜单中多了一项<文件备注>,这正是我们插入的,如图3所示。单击执行“<文件备注>”菜单命令,弹出主程序界面。这样用户就可以根据自己的需要,添加、删除、修改文件属性备注项目和内容(值,value)了。图4所示了,新建一个文件备注项目前后。限于篇幅,其他操作执行效果不再给出。

图3 在explorer 右键菜单中插入的菜单项目“<文件备注>”

图4 新建文件备注项目,(a)(b)分别为新建前后
四、结语
文中讨论和实现了一个基于数据库系统的用户自定义文件属性(文中称为文件备注项目)的方案。并给出了一个实际可以正常运行的例程。其中关键技术在于:
(1) 实现扩展IContextMenu 接口,在explorer的右键菜单中加入自定义的菜单项目。
(2) 以动态生成控件,并自己负责内存管理的方式,组织一个简洁易用而又便于扩展的界面。本文中采用的动态生成TTabsheet和TMemo对象的方法。当然,由于文件备注项目的属性值都是保存在数据中的,界面有很大的独立性和灵活性。可以较容易对程序进行改进和更新。
基于本文提出的思想,笔者再提出几点认为可以完善的小地方,供读者参考。
(1) 引言部分指出,要将数据库的搜索功能和文件系统联系起来。所以,还要作的工作当然要有一个快速、方便的搜索引擎。由于所有的信息都保存在数据库中,所以这样的搜索引擎主要是设计SQL查询,利用各种开发工具都可以实现,没有原则性困难和问题。
(2) 关于数据一致性问题。考虑这样一种情况:用户在填写好文件备注,并提交的数据库后,将文件删除,或者重新命名,这样保存到数据库中的数据便成了“死”数据。不可能再被程序读出,因为这个文件名已经不存在了。或者,恰好,用户删除之后又新建了一个与被删除文件同名的文件。这样,新建的文件明明没有填写文件备注,但却可以从数据库中读出该文件备注信息。这两种情况都会破坏数据库系统和文件系统的一致性。解决这个问题的方案可以有两种:①设计一个单独的程序,负责一致性检查。它根据数据库中的文件路径和文件名去测试该文件是否存在。如果不存在,就将数据库中的数据删除。②利用Shell监视功能构造一个Shell事件监视器,监视文件系统的删除和重命名事件,如果这样的事件发生,并且数据库中已经该文件的备注信息,那么首先删除数据库中的信息后,再执行shell操作,在Delphi中很容易使用Shell监视的API或控件来实现这个功能。相对来说,方案①比较容易实现,但不能完全保证数据库系统和文件系统的一致性。方案②可以很好地保证数据库系统和文件系统的一致性,但要求这个程序一直不能停止运行。
(3) 本文给出的例程所采用的是所有文件的备注项目都是相同的,也可以修改程序使每个文件或某种类型文件具有单独的备注项目。当然,数据库也要相应修改。
总的来说,以上讨论的三个细节都不存在原则上困难和问题。所以,本文不再详细给出代码例程。
|