《网络吸管》开发手记

《网络吸管》开发手记
网络确实是个好东西,文章呀,图片呀什么的都很吸引人。每次上网都能满载而归,但是这些资料的收集过程却很麻烦。对于好文章,每次都要复制、粘贴地在记事本和 IE 之间切换多次才能保存下来,而且说不定什么时候遇到那种怎么复制也复制不下来的防复制网页;对于图片也要点右键,选择“图片另存为”,再点确定才可以,遇到文件重名问题还要重命名。上网的兴致全被打乱了。网上虽然也有“网文快捕”之类的小软件,但是由于不是为自己“量身定做”的,所以用起来也不是很顺手。既然这样,就自己动手做一个吧,“自己动手丰衣足食”嘛!说干就干!
设计思想很简单:监视剪贴板,当发现剪贴板中有新内容时,就根据内容是文字还是图片来决定不同的保存方式。
如何监视剪贴板呢?很自然地想到放一个定时器,每隔一段时间检测一个剪贴板,将剪贴板地内容于上次检测地内容相比较,如果不同,就说明剪贴板的内容有变化。但是这样效率太低了,并且定时器的时间间隔也不好把握,间隔太短会降低系统的效率,而间隔太长就有可能漏掉复制的内容。这让我想起了 CPU 与外设之间通讯方式中的查询方式,那么有没有一种像 CPU 与外设之间的中断方式的东西呢?启动 MSDN ,搜索 ClipBoard ,呵呵!终于找到了!是什么呢?听我慢慢道来!
为了使应用程序能自动感知剪贴板的变化, windows 提供了两个 API 函数。使用 SetClipBoard 可以将窗体注册到剪贴板观测链中,然后程序就能响应剪贴板的变化消息。剪贴板观察器是一个显示剪贴板当前内容的窗口。剪贴板观察链是一系列相互独立的剪贴板观察窗口,它们都能够接受当前发送到剪贴板的内容。
SetClipBoard 的原型是:
function SetClipBoard(hwndNewViewer:HWND):HWND;
hwndNewViewer 为要注册的窗体句柄。如果注册成功,则返回剪贴板观测链中下一个窗体的句柄;如果发生错误或无其他窗体,则返回 NULL
如果剪贴板发生变化, windows 会向窗体发送 WM_CHANGECCHAIN WM_DRAWCLIPBOARD 消息,观测链中每个窗体都会调用 SendMessage 将该消息传送给下一个窗体。当应用程序退出时,要利用 API 函数 ChangeClipboardChain 将窗体从剪贴板观测链中移去。其原型为:
function ChangeClipboardChain(hWndRemove, hWndNewNext:HWND):boolean;
hWndRemove 将要删除的窗口的句柄 , hWndNewNext SetClipBoard 返回的窗体的句柄。
这样我们只要在程序中等待剪贴板变化的消息即可。当消息到来时,我们应该怎样得到剪贴板中的内容呢? Delphi clipbrd.pas 单元中定义了一个类 TClipboard ,它封装了 Windows 剪贴板,简化了大量复杂的处理过程。我们在程序中可以直接调用全局函数 Clipboard ,该函数用于返回 TClipboard 对象实例,使用这个实例对剪贴板进行剪切、复制和粘贴等操作。下面是 TClipboard 对象的几个常用的方法和属性的简单介绍:
方法:
procedure Clear; 清空剪贴板。
function HasFormat(Format: Word): Boolean; 查询剪贴板中是否有指定格式的内容。可以有三种取值: CF_TEXT( 文字 ) CF_BITMAP( 位图 ) CF_METAFILEPICT( 元文件 )
属性:
AsText :用于读写剪贴板文字内容。
 
如何给用户保存下来的图片文件命名也是个问题。我们可以设置一个全局整型变量,每当保存一个图片文件时,就令这个变量增加 1 ,将这个整型变量转换成字符串做为文件名。如果指定的文件名已经存在,就要给文件重命名。最简单的办法就是在文件名之前(或之后)加上一个字符串(比如 'new' ),如果加上这个字符串后还是存在重名的文件呢?这就要用到学编程的人在一开始就学到的一个小技巧:递归。这个问题的解决办法见下面的代码:
procedure SaveToPic(APic: TJPegImage; AFileName: string);
Const PICPLUSSTR = 'new';
begin
  if FileExists(AFileName) then
    savetopic(ABmp, PICPLUSSTR+AFileName)
  else
      SaveBmpAsJpg(APic, AFileName);
end;
 
在实际应用的时候,还应该加上异常处理(如磁盘空间已满,文件名过长等)。图片的保存的基本问题已经解决,我们再来看看文字的保存。为了增强程序的灵活性,我们应该使用用户能方便地将不同地文字保存到不同的文件。继续沿用上面保存图片的方式用数字做文件名吗?当然不可以。一是因为文本文件不像图片那样在资源管理器中可以预览,用户必须打开文件才能知道文件中保存的是什么内容,如果用户想在一大堆“ 1.txt ”、“ 2.txt ”……中找自己想要的内容就太麻烦了;二是因为用户并不要求每次复制下来的内容都保存到单一的文件中,而是要将相关的内容保存到一个文件中。我对这个问题的解决方法是这样的:
用户可以先复制一段文字,然后再按一个热键(比如 Ctrl+Alt+S ,为什么要选 Ctrl+Alt+S 做热键呢?后面再说!),这样用户以后复制下的文字就保存到以用户复制的文字做为文件名的文件中。
记得无数位大师说过:“要将用户界面与业务逻辑分开。”好吧,就将上面的东西封装一下,也算是我向 OO 迈进的第一步吧!(下面之列出了类的部分成员)
  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;
在用户界面中,当用户按下热键 Ctrl+Alt+S 时,就调用 TWebPageSaver.NewTextFile 更改文字保存的文件名 FTextFileName ;当收到剪贴板变化的消息时就调用 TWebPageSaver.Save 保存剪贴板中的内容。另外还有 ImagePath TextPath 等属性,可以由用户来更改图片、文字的保存路径。
 
核心代码已经完成,来做一下用户界面吧!仿照着“ windows 优化大师”我做了如下的界面:
 
 
 
 
 
 
很漂亮吧?左边我用的是 TSpeedButton 组件,右边是 TNotePage 组件。当用户点击一个 TSpeedButton 时,调用 TNotePage.ActivePage := ' 页面的代号 ' 就可以激活相应的配置界面。这个软件需要在后台运行,那么就让它在平时缩小到系统托盘吧!将程序缩小到系统托盘很容易做到,网上有很多这样的示例代码。我手头有一个控件 cooltray4.3 可以用来实现系统托盘的功能,我就懒得自己再去写代码了。
软件运行一切良好。不过一直令我耿耿于怀的就是网上那种防复制的网页:不管你怎么拖动鼠标,那些文字就是无法被选定。仔细想一想,既然文字能够在 IE 上显示就一定可以得到它们。在 MSDN 中找了半天,才找到解决方法。可以通过 ShellWindows 集合来代表属于 shell 的当前打开的窗口的集合,而 IE 就是属于 shell 的一个应用程序。用 CoShellWindows.Create 得到当前打开的 shell 的接口( IShellWindows ),调用接口的 Count 属性得到当前打开的 shell 的数量,然后遍历这些窗口,尝试从接口中取出 IWebbrowser2 接口(通过 ShellWindow.Item(I) as IWebbrowser2 这样的接口类型转换方式),如果结果不为 nil 说明这个窗口是 IE 窗口。之后只要调用 IWebBrowser2 接口的相应方法即可得到窗口中的文字、 URL 、标题等内容了。
示例代码如下:
{ 需要使用 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;  // 保存网页的 URL
    Title: string;// 保存网页的标题
    Text: string; // 保存网页的文字
  end;
 
然后定义一个 TWebPageRecord 类型的数组 FWebPageRecordArray ,大小定位 20 吧(我想一般人不会打开 20 个以上的 IE 吧) :
Const  MAXPAGECOUNT = 20;
……
FWebPageRecordArray : array [0..MAXPAGECOUNT-1] of TWebPageRecord;
在遍历 IE 窗口时,向数组中的元素的相应字段复制即可。
对这个复制防复制(好拗口呀 :) )网页的功能也封装成一个类吧!
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;
在用户界面中,可以通过调用 TWebCracker.SnapShot; 来对打开的 IE 窗口进行遍历,并保存到 FWebPageRecordArray 这个数组中。通过 TWebCracker.GetWebPageCount 方法可以得到 FWebPageRecordArray 中保存的页面的个数,通过 GetWebText GetWebTitle GetWebURL 就可以得到指定页面的文字、标题或是 URL
一切都已经搞定了!爽!
 
通过编写这个小软件,我是收获颇丰呀!除了学到了上边这些技巧外,我还有一些小的经验,愿意与大家分享:
1 、为用户着想,让用户舒服
用户是上帝嘛!以那个 Ctrl+Alt+S 热键来说吧:一般用户上网都是右手握鼠标,空下来的只有左手。小拇指按 Ctrl ,大拇指按 Alt ,食指刚好能按到 S 键,不费一点力气!
2 良好的编码习惯
1 )不要出现魔术数
TWebCracker 定义的那个 FWebPageRecordArray 数组来说:
Const  MAXPAGECOUNT = 20;
……
FWebPageRecordArray : array [0..MAXPAGECOUNT-1] of TWebPageRecord;
别人一看 MAXPAGECOUNT 就知道是什么意思,而如果你写成:
FWebPageRecordArray : array [0..19] of TWebPageRecord;
估计除了你自己没有人能够知道 19 到底是什么意思。
2 )用 sender 的方式增强代码的健壮性
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;

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

你可能感兴趣的:(开发,职场,休闲,手记,《网络吸管》)