凌科网页精灵开发手记
杨中科
现在网上有大量的网页特效软件供网页制作者使用,但是大多数网页特效软件只是罗列了如果将网页特效代码添加到html文档中,比如:
“
第一步:把如下代码加入<body>区域中:
<span id=liveclock style=position:absolute;left:250px;top:122px>
</span> (这里可以调整时钟的方位。调用脚本时去掉括号中内容)
<SCRIPT language=javascript>
var minutes=Digital.getMinutes()
if(hours>12){dn="PM"
hours=hours-12
}
……………………
setTimeout("show5()",1000)
}
</SCRIPT>
第二步:把<body>中的内容改为
<body bgcolor="#fef4d9" ONLOAD=show5()>
”
这要求网页制作者必须了解html语言,而且即使对熟悉html的用户要想修改特效代码中的参数(比如上面例子中的“调整时钟的方位”)也是非常麻烦。这款软件则解决了这个问题,您只要选择一个要添加的特效,在弹出的对话框中填入几个相关参数,软件将自动将特效代码添加到网页代码的合适位置。
比如给软件添加一个旋转立体字的特效,只要选择“插件”->“旋转立体字”,就会弹出下面的对话框:
在对话框中填入各个参数后,网页精灵就自动帮您把代码插入到正确的位置了。
这款软件的技术核心就是插件化开发和程序自动升级技术。下面我将分别讲解这两项技术在“网页精灵”的应用。
一、插件化开发。
1、基本原理
凌科网页精灵中每个插件都是一个dll文件。插件这个名词大家都很熟悉。如PhotoShop等软件就是通过安装很多插件来实现某些特殊功能的。插件仅仅是从外部提供给应用程序的一个接口,通过调用约定的接口来实现插件所提供的功能。使用插件化开发的好处是明显的,它可以很轻松的实现软件的扩展,并且简化的软件设计的构架,使得开发程序变得更加简单。
插件化开发可以通过很多技术实现,比如COM,Dll等。我们这里采用Dll文件的形式实现插件。基本思想如下:程序每次启动时,在指定的目录下查找*.dll文件,然后将其加入到某个菜单下。在用户点击插件对应的菜单项时,只要调用接口函数中约定好的某个函数就可以。
2、插件导出的接口
凌科网页精灵中每个插件都必须导出下面三个接口函数:
GetPlugInHTML, GetPlugInName, GetPlugInDescription;
它们的函数原型的pascal描述如下
function GetPlugInHTML(AHandle: THandle;ASelectedText: PChar;
AResultHTHML: TResultHTML): Boolean;stdcall;
procedure GetPlugInName(AValue: PChar);stdcall;
procedure GetPlugInDescription(AValue: PChar);stdcall;
其中 TResultHTML的pascal定义如下
TResultHTML = record
ReplaceHTML: PChar; //替换文字
BodyHTML: PChar;//添加到<Body></Body>区的文字
BodyTagHTML: PChar;//添加到<Body >中的文字,如<Body onload="show()">
HeadHTML: PChar;//添加到<Head></Head>区中的文字
end;
接口函数描述:
(1) function GetPlugInHTML(AHandle: THandle;ASelectedText: PChar;
AResultHTHML: TResultHTML): Boolean;stdcall;
在用户点击插件对应的菜单时,主程序将调用此方法来得到插件返回的对网页的修改信息。
其中AHandle对应主窗口,也就是网页精灵的窗体句柄;ASelectedText代表用户此时在网页编辑器中选中的文本;AResultHTHML是返回值,将用ReplaceHTML将替换用户选择的文本,将把BodyHTML添加到网页的<Body></Body>区,将把BodyTagHTML添加到<Body >中,如在未调用插件的时候<Body> 在调用后bodyTagHTML=‘onload="show()"’则调用后<Body onload="show()">,将HeadHTML添加到<Head></Head>区;
返回值代表此插件的运行是否成功。如果返回False,则主程序会忽略插件对网页的修改信息。
(2)procedure GetPlugInName(AValue: PChar);stdcall;返回值是AValue,它将做为菜单的标题,代表插件的名称。
(3)void GetPlugInDescription(char* AValue);返回值是AValue,它将做为此插件的功能描述。
3、动态加载插件
加载插件信息到菜单的伪代码如下(关于FindFirst,FindNext的使用请参考Delphi的帮助, LoadLibrary, GetProcAddress, FreeLibrary的使用请参考MSDN):
var
LGetPlugInName: TGetPlugInName;
LSr: TSearchRec;
LHandle: THandle;
LName: PChar;
begin
FPlugIns.Clear;
if FindFirst(Adir+‘*.dll’, faAnyFile - faDirectory,LSr) = 0 then
// Adir是插件所在路径
repeat
LHandle := LoadLibrary(PChar(ADir + LSr.Name));
try
GetMem(LName, MAXNAMEDESCSIZE);
@LGetPlugInName := GetProcAddress(LHandle, 'GetPlugInName');
//调用GetPlugInName到插件的名称
LGetPlugInName(LName);
增加一个菜单,并设定菜单的标题为Lname;
设定菜单的OnClick事件句柄=OnPlugInClick;
// OnPlugInClick的定义在后边
将插件的文件名与菜单通过一定方法联系起来;
//联系起来以供在点击菜单的时候加载此插件
finally
FreeLibrary(LHandle);
FreeMem(LName);
end;
until (FindNext(LSr) <> 0);
end;
其中
TGetPlugInName = procedure(AValue: PChar);stdcall;
4、运行插件
在用户点击菜单之后将触发我们在上边设定的OnPlugInClick事件句柄。
procedure TFormMain.OnPlugInClick(Sender: TObject);
var
LHandle: THandle;
LGetPlugInHTML: TGetPlugInHTML;
LPlugInInfo: TPlugInInfo;
LRH: TResultHTML;
begin
LHandle := LoadLibrary(PChar(FPlugInsDir + LPlugInInfo.FileName));
try
@LGetPlugInHTML := GetProcAddress(LHandle, 'GetPlugInHTML');
LTmpStr := Trim(RichEditHTML.SelText);
GetMem(LRH.ReplaceHTML, MAXHTMLSIZE);
GetMem(LRH.BodyHTML, MAXHTMLSIZE);
GetMem(LRH.BodyTagHTML, MAXHTMLSIZE);
GetMem(LRH.HeadHTML, MAXHTMLSIZE);
//调用DLL中的GetPlugInHTML
LGetPlugInHTML(self.Handle, PAnsiChar(LTmpStr),LRH);
根据 LRH中的信息更改HTML页面中的相应区域;
finally
释放资源;
end;
5、开发插件示例
下面以开发一个“添加到收藏夹”插件为例来展示一下插件的开发。
实现这个功能的HTML如下:
<a href="javascript:window.external.AddFavorite('http://www.sohu.com', '搜狐
网')">将本站加入收藏夹</a>
显然我们可以提供三个参数供用户选择,那就是网址(如http://www.sohu.com)、网站名称(如 “搜狐网”)、超链接的标题(如“将本站加入收藏夹”)。
新建一个Dll工程,在工程文件中导出Dll输出的函数
exports
GetPlugInHTML, GetPlugInName, GetPlugInDescription;
新建一个窗体,布局如下:
将一个TbigStringContainer控件(我开发的可以存储大字符串的控件,在开发包中),放到窗体中,双击strings属性,输入一下的文本:
<a href="javascript:window.external.AddFavorite('<!url>', '<!favoritename>')"><!text></a>
其中“'<!url>”、“<!favoritename>”、“<!text>”是我们要根据用户输入的值替换的字符串。
在窗体的public中定义如下的方法:
function GetReplaceHTML: string;
代码如下
var
t: string;
begin
t := bigStringContainer1.GetString;
t := AnsiReplaceStr(t, '<!url>', EdtUrl.Text);
//将t字符串中的<!url>用, EdtUrl.Text替换
t := AnsiReplaceStr(t, '<!favoritename>', EdtFaName.Text);
t := AnsiReplaceStr(t, '<!text>', EdtText.Text);
result := t;
end;
Dll导出的三个函数的主要代码如下:
function GetPlugInHTML(AHandle: THandle;ASelectedText: PChar;
AResultHTHML: TResultHTML): Boolean;stdcall;
var
Dlg: TFormFavorite;
begin
result := false;
AResultHTHML.ReplaceHTML[0] := #0;
AResultHTHML.BodyHTML[0] := #0;
AResultHTHML.BodyTagHTML[0] := #0;
AResultHTHML.HeadHTML[0] := #0;
if ASelectedText <> nil then
StrLCopy(AResultHTHML.ReplaceHTML, ASelectedText, MAXHTMLSIZE);
Dlg := TFormFavorite.Create(nil);
Dlg.ParentWindow := AHandle;
if Dlg.ShowModal = mrOK then
begin
result := true;
FillMemory(AResultHTHML.ReplaceHTML, MAXHTMLSIZE, 0);
StrLCopy(AResultHTHML.ReplaceHTML, PChar(Dlg.GetReplaceHTML),MAXHTMLSIZE);
end;
Dlg.free;
end;
procedure GetPlugInName(AValue: PChar);stdcall;
begin
FillMemory(AValue, MAXNAMEDESCSIZE, 0);
StrLCopy(AValue, PChar('添加到收藏夹功能'), MAXNAMEDESCSIZE);
end;
procedure GetPlugInDescription(AValue: PChar);stdcall;
begin
FillMemory(AValue, MAXNAMEDESCSIZE, 0);
StrLCopy(AValue, PChar('本插件将为在网页中添加到收藏夹功能'),MAXNAMEDESCSIZE);
end;
编译后将插件放到“网页精灵”的插件目录下,启动“网页精灵”就可以看到这个插件已经被加载到了菜单中。
二、软件自动升级技术
当我们开发了新插件后,肯定希望用户能尽快得到此插件。应用程序升级的方法有两种:一是通知用户让用户到指定网站下载插件,然后由用户将插件放到插件目录下面;二是由程序负责从服务器上下载安装插件,用户唯一要做的就是决定是否愿意安装新插件。显然后一种方法比较好。
1、基本原理
在本地有一个存储已安装插件的信息的列表,在服务器端也维护一个服务器上的所有插件信息的列表。当要升级插件的时候,程序从服务器上下载此列表,与本地的列表比较,如果发现本地没有的插件,就将此插件下载下来,安装到插件目录下即可。
2、列表的结构
由于列表中要保存所有插件的文件名、名称、版本、描述等信息,所以用XML文件来保存比较合适。我定义XML文档格式如下:
<PlugInsList>
<PlugIn>
<FileName>插件的文件名</FileName>
<Name>插件的名称</Name>
<Version>版本</Version>
<Description>插件描述</Description>
</PlugInsList>
3、定义XML文件映射
我们可以使用DOM、SAX等解析XML文档,但是写起来很麻烦。好在咱们伟大的女神Delphi为我们提供了XML Data Binding Wizard这个强大的工具。XML数据绑定向导(XML Data Binding Wizard)可以将XML文件映射成类,这样程序员能够用它生成相应的接口和类来访问与修改XML文件数据,完全没有陌生感,用起来就好像使用普通的类一样。
运行Delphi7,点击“File”->”Other”,选择“New”页面中的“XML Data Binding Wizard”。用记事本建立如下文件:
<PlugInsList>
<PlugIn>
<FileName>文件名</FileName>
<Name>名称</Name>
<Version>版本</Version>
<Description>插件描述</Description>
</PlugIn>
<PlugIn>
<FileName>插件的文件名</FileName>
<Name>插件的名称</Name>
<Version>版本</Version>
<Description>插件描述</Description>
</PlugIn>
</PlugInsList>
注意:
<PlugIn></PlugIn>必须要写多于两组(包含两组),否则向导会认为<PlugIn></PlugIn>是只能有一组的元素,从而生成的映射类无法供我们使用。而写成多于两组的时候向导会将<PlugIn>属性映射成<PlugInsList>的一个数组属性。
以下是生成的代码的接口部分:
IXMLPlugInsListType = interface;
IXMLPlugInType = interface;
{ IXMLPlugInsListType }
IXMLPlugInsListType = interface(IXMLNodeCollection)
['{5D777B2B-E265-472B-8035-ADCED92E0F65}']
{ Property Accessors }
function Get_PlugIn(Index: Integer): IXMLPlugInType;
{ Methods & Properties }
function Add: IXMLPlugInType;
function Insert(const Index: Integer): IXMLPlugInType;
property PlugIn[Index: Integer]: IXMLPlugInType read Get_PlugIn; default;
end;
{ IXMLPlugInType }
IXMLPlugInType = interface(IXMLNode)
['{76ED7F51-20FF-4A4A-87B7-CFB9BB280F80}']
{ Property Accessors }
function Get_FileName: WideString;
function Get_Name: WideString;
function Get_Version: WideString;
function Get_Description: WideString;
procedure Set_FileName(Value: WideString);
procedure Set_Name(Value: WideString);
procedure Set_Version(Value: WideString);
procedure Set_Description(Value: WideString);
{ Methods & Properties }
property FileName: WideString read Get_FileName write Set_FileName;
property Name: WideString read Get_Name write Set_Name;
property Version: WideString read Get_Version write Set_Version;
property Description: WideString read Get_Description write Set_Description;
end;
{ Forward Decls }
TXMLPlugInsListType = class;
TXMLPlugInType = class;
{ TXMLPlugInsListType }
TXMLPlugInsListType = class(TXMLNodeCollection, IXMLPlugInsListType)
protected
{ IXMLPlugInsListType }
function Get_PlugIn(Index: Integer): IXMLPlugInType;
function Add: IXMLPlugInType;
function Insert(const Index: Integer): IXMLPlugInType;
public
procedure AfterConstruction; override;
end;
{ TXMLPlugInType }
TXMLPlugInType = class(TXMLNode, IXMLPlugInType)
protected
{ IXMLPlugInType }
function Get_FileName: WideString;
function Get_Name: WideString;
function Get_Version: WideString;
function Get_Description: WideString;
procedure Set_FileName(Value: WideString);
procedure Set_Name(Value: WideString);
procedure Set_Version(Value: WideString);
procedure Set_Description(Value: WideString);
end;
{ Global Functions }
function GetPlugInsList(Doc: IXMLDocument): IXMLPlugInsListType;
function LoadPlugInsList(const FileName: WideString): IXMLPlugInsListType;
function NewPlugInsList: IXMLPlugInsListType;
我们直接使用的两个接口是:
IXMLPlugInsListType = interface(IXMLNodeCollection);
IXMLPlugInType = interface(IXMLNode);
向导还提供了三个全局方法可以简化我们的操作:
function GetPlugInsList(Doc: IXMLDocument): IXMLPlugInsListType;
function LoadPlugInsList(const FileName: WideString): IXMLPlugInsListType;
function NewPlugInsList: IXMLPlugInsListType;
(1)、我们一般将一个TXMLDocument组件(实现了IXMLNode接口)做为GetPlugInsList的参数,GetPlugInsList将返回这个TXMLDocument组件对应的XML文档的根元素。
(2)、也可以将XML文件的文件名传递给function LoadPlugInsList(const FileName: WideString): IXMLPlugInsListType;返回值同样是对应的XML文档的根元素。
(3)、function NewPlugInsList: IXMLPlugInsListType;
在内存生成新文件。我们这里不直接用到它。
我们可以用IXMLPlugInsListType操纵文件中的<PlugInsList>中的<PlugIn>列表。
function Get_PlugIn(Index: Integer): IXMLPlugInType;
返回列表中指定位置Index的节点。
function Add: IXMLPlugInType;
将在列表中的最后位置添加一个节点,返回刚添加的节点。
function Insert(const Index: Integer): IXMLPlugInType;
将在列表中的Index后位置添加一个节点,返回刚添加的节点。
property PlugIn[Index: Integer]: IXMLPlugInType read Get_PlugIn;
则是一个以数组方式展现的所有节点的列表。
IXMLPlugInType的所有属性FileName、Version等,都是对<PlugIn>的属性的映射。
4、XML文件的读写操作
以前在Delphi中想操作XML文档要频繁调用XML API,十分的烦人。现在 XML Data Binding Wizard加上TXMLDocument(在Internet面板上)简化了我们的操作。双剑合壁,谁与争风!
在XML中添加新的节点的方法如下:
(1)var
ALocalList: IXMLPlugInsListType;
ALocalNode: IXMLPlugInType;
begin
ALocalList := GetPlugInsList(XMLDocLocal);
// XMLDocLocal为TXMLDocument控件
ALocalNode := ALocalList.Add;
ALocalNode.FileName := ‘DllFilename’;
ALocalNode.Name := ‘name’;
ALocalNode.Version := ‘version’;
ALocalNode.Description := ‘description’
XMLDocLocal.SaveToFile();
//如果XMLDocLocal的AutoSave=True则不用SaveToFile;
End;
(2)读取一个节点的方法可以参考添加节点的代码。
5、自动升级
有了上边的知识相信开发一个自动升级的系统已经不难了,您可以参考源代码自己分析,我就不多费口舌了。忘了说一点,从服务器上下载XML列表可以使用WinInet或IdHTTP控件,我用的就是IdHTTP控件(方便呀,调用一个Get方法就可以做到,还支持代理服务器!嘻嘻!)。得到列表后赋值给TXMLDocument的XML.Text属性就可以。
三、其他经验总结
1、预览功能的实现
预览功能可以使用WebBrowser控件,在切换到“预览”页面的时候,将HTML代码保存到文件中,然后调用WebBrowser.Navigate()方法将此页面加载即可。
但是这里有一个如果保存网页文件的问题,如果指定一个文件名,如”tmp.htm”,这样在程序打开一个的时候没问题,如果打开多个程序就会造成混乱:一个程序保存的”tmp.htm”被另一个程序加载了,会令人感到莫名其妙。
我解决此问题的方法是使用程序的句柄做为文件名。Windows每个程序都有一个句柄,即使是同一个软件的两个实例它们的句柄也不同。这个句柄本质上是一个整形数,所以可以把它转换成一个字符串做为文件名,在程序退出时删除此文件(如果保存在系统临时文件夹下就不用删除,Windows会自动替您删除)。代码如下:
FileName := 'tmp'+IntToStr(Integer(Application.Handle))+'.htm';
2、“撤销”功能的实现
很多文本编辑器、网页编辑器都有“撤销”功能,这样在用户想返回编辑前的某个状态时就会非常方便。我在这里用一个特殊的“堆栈”解决了这个问题(数据结构还是很管用的呀,一定要好好学呀)。
这个堆栈的特殊之处就在于这个堆栈有个最大的容量(也就等于允许最大的撤销次数,如果不限制最大的撤销次数就会导致系统资源越来越少,最后很可能就崩溃了),最特殊的地方就是当堆栈增加到满的后再压入新元素的时候就删除栈底元素,所有上边的元素都自动下移一个位置,新压入的元素放在栈顶。
我们当然可以使用动态数组或链表解决这个问题,但是我发现Contnrs单元中有一个TobjectList类非常好用,我就用这个类来实现这个堆栈吧!代码如下:
//字符串的Wrapper类
TStringObject = class
public
Value: string;
end;
TUndoStack = class(TObject)
private
FList: TObjectList;
FMaxSize: Integer;
protected
procedure DelBottom;virtual;//删除底端一个
function FGetCurSize: Integer;
public
constructor Create(ASize: Integer);overload;virtual;
constructor Create;overload;virtual;
destructor Destroy;override;
procedure Push(AObj: string);virtual;
function Pop: string;virtual;
function IsFull: Boolean;virtual;//是否满
function IsEmpty: Boolean;virtual;//是否空
procedure ClearStack;virtual;//清空
property MaxSize: Integer read FMaxSize; //最大容量
property CurSize: Integer read FGetCurSize;//现在的堆栈中的数量
end;
constructor TUndoStack.Create(ASize: Integer);
begin
inherited Create;
FList := TObjectList.Create;
FList.Capacity := ASize;
FMaxSize := ASize;
FList.Count := 0;
end;
constructor TUndoStack.Create;
begin
inherited;
Create(10);//default size
end;
procedure TUndoStack.DelBottom;
begin
if CurSize <= 0 then
raise Exception.Create('Stack already empty!');
FList.Delete(0);
FList.Capacity := FList.Capacity - 1;
FList.Capacity := FList.Capacity + 1;
end;
destructor TUndoStack.Destroy;
begin
FList.Free;
inherited;
end;
function TUndoStack.IsEmpty: Boolean;
begin
result := (CurSize <= 0);
end;
function TUndoStack.IsFull: Boolean;
begin
result := (CurSize = MaxSize);
end;
function TUndoStack.Pop: string;
begin
result := '';
if IsEmpty then
raise Exception.Create('Stack already Empty!')
else
begin
result := TStringObject(FList.Last).Value;
FList.Delete(CurSize - 1);
FList.Capacity := FList.Capacity - 1;
FList.Capacity := FList.Capacity + 1;
end;
end;
procedure TUndoStack.Push(AObj: string);
var
a: TStringObject;
begin
if IsFull then
begin
DelBottom;
end;
a := TStringObject.Create;
a.Value := AObj;
FList.Add(a);
end;
procedure TUndoStack.ClearStack;
begin
FList.Clear;
end;
function TUndoStack.FGetCurSize: Integer;
begin
FList.Pack;
result := FList.Count;
end;
使用方法如下:
定义一个FUndoStack: TUndoStack;撤销堆栈变量,在用户对文本做一个修改动作后调用
FUndoStack.Push(RichEditHTML.Lines.Text);
原来的文本压入撤销堆栈 。
在用户点击“撤销”后,调用
RichEditHTML.Lines.Text := FUndoStack.Pop;
来还原到保存的值。