《網(wǎng)絡(luò)吸管》開發(fā)手記
網(wǎng)絡(luò)確實(shí)是個好東西,文章呀,圖片呀什么的都很吸引人。每次上網(wǎng)都能滿載而歸,但是這些資料的收集過程卻很麻煩。對于好文章,每次都要復(fù)制、粘貼地在記事本和IE之間切換多次才能保存下來,而且說不定什么時候遇到那種怎么復(fù)制也復(fù)制不下來的防復(fù)制網(wǎng)頁;對于圖片也要點(diǎn)右鍵,選擇“圖片另存為”,再點(diǎn)確定才可以,遇到文件重名問題還要重命名。上網(wǎng)的興致全被打亂了。網(wǎng)上雖然也有“網(wǎng)文快捕”之類的小軟件,但是由于不是為自己“量身定做”的,所以用起來也不是很順手。既然這樣,就自己動手做一個吧,“自己動手豐衣足食”嘛!說干就干!
設(shè)計思想很簡單:監(jiān)視剪貼板,當(dāng)發(fā)現(xiàn)剪貼板中有新內(nèi)容時,就根據(jù)內(nèi)容是文字還是圖片來決定不同的保存方式。
如何監(jiān)視剪貼板呢?很自然地想到放一個定時器,每隔一段時間檢測一個剪貼板,將剪貼板地內(nèi)容于上次檢測地內(nèi)容相比較,如果不同,就說明剪貼板的內(nèi)容有變化。但是這樣效率太低了,并且定時器的時間間隔也不好把握,間隔太短會降低系統(tǒng)的效率,而間隔太長就有可能漏掉復(fù)制的內(nèi)容。這讓我想起了CPU與外設(shè)之間通訊方式中的查詢方式,那么有沒有一種像CPU與外設(shè)之間的中斷方式的東西呢?啟動MSDN,搜索ClipBoard,呵呵!終于找到了!是什么呢?聽我慢慢道來!
為了使應(yīng)用程序能自動感知剪貼板的變化,windows提供了兩個API函數(shù)。使用SetClipBoard可以將窗體注冊到剪貼板觀測鏈中,然后程序就能響應(yīng)剪貼板的變化消息。剪貼板觀察器是一個顯示剪貼板當(dāng)前內(nèi)容的窗口。剪貼板觀察鏈?zhǔn)且幌盗邢嗷オ?dú)立的剪貼板觀察窗口,它們都能夠接受當(dāng)前發(fā)送到剪貼板的內(nèi)容。
SetClipBoard的原型是:
function SetClipBoard(hwndNewViewer:HWND):HWND;
hwndNewViewer為要注冊的窗體句柄。如果注冊成功,則返回剪貼板觀測鏈中下一個窗體的句柄;如果發(fā)生錯誤或無其他窗體,則返回NULL。
如果剪貼板發(fā)生變化,windows會向窗體發(fā)送WM_CHANGECCHAIN或WM_DRAWCLIPBOARD消息,觀測鏈中每個窗體都會調(diào)用SendMessage將該消息傳送給下一個窗體。當(dāng)應(yīng)用程序退出時,要利用API函數(shù)ChangeClipboardChain將窗體從剪貼板觀測鏈中移去。其原型為:
function ChangeClipboardChain(hWndRemove, hWndNewNext:HWND):boolean;
hWndRemove將要刪除的窗口的句柄, hWndNewNext為SetClipBoard返回的窗體的句柄。
這樣我們只要在程序中等待剪貼板變化的消息即可。當(dāng)消息到來時,我們應(yīng)該怎樣得到剪貼板中的內(nèi)容呢?Delphi的clipbrd.pas單元中定義了一個類TClipboard,它封裝了Windows剪貼板,簡化了大量復(fù)雜的處理過程。我們在程序中可以直接調(diào)用全局函數(shù)Clipboard,該函數(shù)用于返回TClipboard對象實(shí)例,使用這個實(shí)例對剪貼板進(jìn)行剪切、復(fù)制和粘貼等操作。下面是TClipboard對象的幾個常用的方法和屬性的簡單介紹:
方法:
procedure Clear; 清空剪貼板。
function HasFormat(Format: Word): Boolean; 查詢剪貼板中是否有指定格式的內(nèi)容。可以有三種取值:CF_TEXT(文字)、CF_BITMAP(位圖)、CF_METAFILEPICT(元文件)。
屬性:
AsText:用于讀寫剪貼板文字內(nèi)容。
如何給用戶保存下來的圖片文件命名也是個問題。我們可以設(shè)置一個全局整型變量,每當(dāng)保存一個圖片文件時,就令這個變量增加1,將這個整型變量轉(zhuǎn)換成字符串做為文件名。如果指定的文件名已經(jīng)存在,就要給文件重命名。最簡單的辦法就是在文件名之前(或之后)加上一個字符串(比如'new'),如果加上這個字符串后還是存在重名的文件呢?這就要用到學(xué)編程的人在一開始就學(xué)到的一個小技巧:遞歸。這個問題的解決辦法見下面的代碼:
procedure SaveToPic(APic: TJPegImage; AFileName: string);
Const PICPLUSSTR = 'new';
begin
if FileExists(AFileName) then
savetopic(ABmp, PICPLUSSTR+AFileName)
else
SaveBmpAsJpg(APic, AFileName);
end;
在實(shí)際應(yīng)用的時候,還應(yīng)該加上異常處理(如磁盤空間已滿,文件名過長等)。圖片的保存的基本問題已經(jīng)解決,我們再來看看文字的保存。為了增強(qiáng)程序的靈活性,我們應(yīng)該使用用戶能方便地將不同地文字保存到不同的文件。繼續(xù)沿用上面保存圖片的方式用數(shù)字做文件名嗎?當(dāng)然不可以。一是因?yàn)槲谋疚募幌駡D片那樣在資源管理器中可以預(yù)覽,用戶必須打開文件才能知道文件中保存的是什么內(nèi)容,如果用戶想在一大堆“1.txt”、“2.txt”……中找自己想要的內(nèi)容就太麻煩了;二是因?yàn)橛脩舨⒉灰竺看螐?fù)制下來的內(nèi)容都保存到單一的文件中,而是要將相關(guān)的內(nèi)容保存到一個文件中。我對這個問題的解決方法是這樣的:
用戶可以先復(fù)制一段文字,然后再按一個熱鍵(比如Ctrl+Alt+S,為什么要選Ctrl+Alt+S做熱鍵呢?后面再說!),這樣用戶以后復(fù)制下的文字就保存到以用戶復(fù)制的文字做為文件名的文件中。
記得無數(shù)位大師說過:“要將用戶界面與業(yè)務(wù)邏輯分開。”好吧,就將上面的東西封裝一下,也算是我向OO邁進(jìn)的第一步吧!(下面之列出了類的部分成員)
TWebPageSaver = class(TObject)
private
FImagePath: string;
FTextPath: string;
FImageCount: Integer;
FTextFileName: string;
procedure SetImagePath(const Value: string);
procedure SetTextPath(const Value: string);
public
function Save: Boolean;//result is whether the content is saved
procedure NewTextFile(AFileName:string);
property ImagePath: string read FImagePath write SetImagePath;
property TextPath: string read FTextPath write SetTextPath;
end;
在用戶界面中,當(dāng)用戶按下熱鍵Ctrl+Alt+S時,就調(diào)用TWebPageSaver.NewTextFile更改文字保存的文件名FTextFileName;當(dāng)收到剪貼板變化的消息時就調(diào)用TWebPageSaver.Save保存剪貼板中的內(nèi)容。另外還有ImagePath、TextPath等屬性,可以由用戶來更改圖片、文字的保存路徑。
核心代碼已經(jīng)完成,來做一下用戶界面吧!仿照著“windows優(yōu)化大師”我做了如下的界面:

很漂亮吧?左邊我用的是TSpeedButton組件,右邊是TNotePage組件。當(dāng)用戶點(diǎn)擊一個TSpeedButton時,調(diào)用TNotePage.ActivePage := '頁面的代號'就可以激活相應(yīng)的配置界面。這個軟件需要在后臺運(yùn)行,那么就讓它在平時縮小到系統(tǒng)托盤吧!將程序縮小到系統(tǒng)托盤很容易做到,網(wǎng)上有很多這樣的示例代碼。我手頭有一個控件cooltray4.3可以用來實(shí)現(xiàn)系統(tǒng)托盤的功能,我就懶得自己再去寫代碼了。
軟件運(yùn)行一切良好。不過一直令我耿耿于懷的就是網(wǎng)上那種防復(fù)制的網(wǎng)頁:不管你怎么拖動鼠標(biāo),那些文字就是無法被選定。仔細(xì)想一想,既然文字能夠在IE上顯示就一定可以得到它們。在MSDN中找了半天,才找到解決方法。可以通過ShellWindows集合來代表屬于shell 的當(dāng)前打開的窗口的集合,而IE就是屬于shell的一個應(yīng)用程序。用CoShellWindows.Create得到當(dāng)前打開的shell的接口(IShellWindows),調(diào)用接口的Count屬性得到當(dāng)前打開的shell的數(shù)量,然后遍歷這些窗口,嘗試從接口中取出IWebbrowser2接口(通過ShellWindow.Item(I) as IWebbrowser2這樣的接口類型轉(zhuǎn)換方式),如果結(jié)果不為nil說明這個窗口是IE窗口。之后只要調(diào)用IWebBrowser2接口的相應(yīng)方法即可得到窗口中的文字、URL、標(biāo)題等內(nèi)容了。
示例代碼如下:
{需要使用mshtml,SHdocvw兩個單元}
var
ShellWindow : IShellWindows;
WebBrowser : IWebBrowser2;
I, ShellWindowCount: integer;
HTMLdocument : IHTMLdocument2;
URL, Title, Text:string;
begin
ShellWindow := CoShellWindows.Create;
ShellWindowCount := ShellWindow.Count;
for I := 0 to ShellWindowCount-1 do
begin
WebBrowser := ShellWindow.Item(I) as IWebbrowser2;
if WebBrowser <> nil then
begin
HTMLDocument := WebBrowser.Document as IHtmlDocument2;
URL := URL;
Title := HTMLDocument.title;
Text := HTMLDocument.body.outerText ;
ShowMessage(URL+Title+Text);
end;
end;
ShellWindow := nil;
end;
我們定義一個記錄類型:
TWebPageRecord = record
URL: string; //保存網(wǎng)頁的URL
Title: string;//保存網(wǎng)頁的標(biāo)題
Text: string; //保存網(wǎng)頁的文字
end;
然后定義一個TWebPageRecord類型的數(shù)組FWebPageRecordArray,大小定位20吧(我想一般人不會打開20個以上的IE吧):
Const MAXPAGECOUNT = 20;
……
FWebPageRecordArray : array [0..MAXPAGECOUNT-1] of TWebPageRecord;
在遍歷IE窗口時,向數(shù)組中的元素的相應(yīng)字段復(fù)制即可。
對這個復(fù)制防復(fù)制(好拗口呀:))網(wǎng)頁的功能也封裝成一個類吧!
type
TWebCracker = class(TObject)
private
FWebPageRecordArray : array [0..MAXPAGECOUNT-1] of TWebPageRecord;
FWebPageCount: Integer;
public
procedure SnapShot;
function GetWebText(AIndex:integer): string;
function GetWebTitle(AIndex:integer): string;
function GetWebURL(AIndex:integer): string;
procedure Clear;
procedure Refresh;
function GetWebPageCount: Integer;
end;
在用戶界面中,可以通過調(diào)用TWebCracker.SnapShot;來對打開的IE窗口進(jìn)行遍歷,并保存到FWebPageRecordArray這個數(shù)組中。通過TWebCracker.GetWebPageCount方法可以得到FWebPageRecordArray中保存的頁面的個數(shù),通過GetWebText、GetWebTitle、GetWebURL就可以得到指定頁面的文字、標(biāo)題或是URL。
一切都已經(jīng)搞定了!爽!
通過編寫這個小軟件,我是收獲頗豐呀!除了學(xué)到了上邊這些技巧外,我還有一些小的經(jīng)驗(yàn),愿意與大家分享:
1、為用戶著想,讓用戶舒服
用戶是上帝嘛!以那個Ctrl+Alt+S熱鍵來說吧:一般用戶上網(wǎng)都是右手握鼠標(biāo),空下來的只有左手。小拇指按Ctrl,大拇指按Alt,食指剛好能按到S鍵,不費(fèi)一點(diǎn)力氣!
2、 良好的編碼習(xí)慣
(1)不要出現(xiàn)魔術(shù)數(shù)
以TWebCracker定義的那個FWebPageRecordArray數(shù)組來說:
Const MAXPAGECOUNT = 20;
……
FWebPageRecordArray : array [0..MAXPAGECOUNT-1] of TWebPageRecord;
別人一看MAXPAGECOUNT就知道是什么意思,而如果你寫成:
FWebPageRecordArray : array [0..19] of TWebPageRecord;
估計除了你自己沒有人能夠知道19到底是什么意思。
(2)用sender的方式增強(qiáng)代碼的健壯性
procedure TMainfrm.CBAutoRunClick(Sender: TObject);
Const
SIGNINREGISTRY = 'WebSuction';
begin
if (Sender as TCheckBox).Checked then
AddToAutoRun(Application.ExeName,SIGNINREGISTRY)
else DelAutoRun(SIGNINREGISTRY);
end;
這樣即使Checkbox1改了名字也不怕。
又如:
procedure TMainfrm.N1Click(Sender: TObject);
begin
if (Sender as TMenuItem).Caption = '暫停(&S)' then
begin
(Sender as TMenuItem).Caption := '開始(&R)';
FWebPageSaver.Pause;
end
else
begin
(Sender as TMenuItem).Caption := '暫停(&S)';
FWebPageSaver.ReStart;
end;
end;
(3)不要直接使用Tform2單元的全局Form2變量,那樣就破壞了封裝性
procedure TMainfrm.SBNextClick(Sender: TObject);
var
LSelectedIndex : integer;
FormDisplay : Tform2;
begin
LSelectedIndex := LBWebPage.ItemIndex;
if LSelectedIndex <> -1 then
begin
FormDisplay := Tform2.Create(self);
FormDisplay.SetContent(FWebCracker.GetWebText(LSelectedIndex));
FormDisplay.Show;
end;
end;
在TForm2中定義 SetContent方法
procedure TWebCrackfrm.SetContent(AText:string);
begin
Memo.Clear;
Memo.Lines.Add(AText);
end;