凌科网页精灵开发手记

凌科网页精灵开发手记
杨中科
现在网上有大量的网页特效软件供网页制作者使用,但是大多数网页特效软件只是罗列了如果将网页特效代码添加到 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>
显然我们可以提供三个参数供用户选择,那就是网址(如 [url]http://www.sohu.com[/url] )、网站名称(如 “搜狐网”)、超链接的标题(如“将本站加入收藏夹”)。
新建一个 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;
来还原到保存的值。
 

本文出自 “CowNew开源团队” 博客,转载请与作者联系!

你可能感兴趣的:(开发,休闲,精灵,手记,凌科网页)