凌科網頁精靈開發手記
楊中科
現在網上有大量的網頁特效軟件供網頁制作者使用,但是大多數網頁特效軟件只是羅列了如果將網頁特效代碼添加到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,十分的煩人?,F在 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方法就可以做到,還支持代理服務器!嘻嘻?。5玫搅斜砗筚x值給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;
來還原到保存的值。