1. 程式人生 > >Delphi 之 第九課 Windows程式設計

Delphi 之 第九課 Windows程式設計

  Delphi 利用Object Pascal 和可視控制元件庫(VCL)對底層的Windows API 進行了完美的封裝,所以很少需要使用基礎Pascal 語言來建立Windows應用程式,也無需直接呼叫Windows API 函式。儘管如此,如果遇到特殊情況,VCL 又不支援,Delphi程式設計師還得直接面對Windows程式設計。不過只有在極其特殊的情況下,例如:基於不尋常API 呼叫的Delphi新控制元件開發, 你才需要這樣做,這裡我不想討論這方面內容,我只想讓大家看一下與作業系統互動的幾個Delphi元素以及Delphi程式設計師能從中獲益的Windows程式設計技術。

Windows 控制代碼

Delphi從Windows 引入了不少資料型別,其中控制代碼最重要。這種資料型別名為THandle,該型別在Windows 單元中定義:

type
  THandle = LongWord;

控制代碼資料型別通過數字實現,但並不當數字用。在Windows 中,控制代碼是一個系統內部資料結構的引用。例如,當你操作一個視窗,或說是一個Delphi 窗體時,系統會給你一個該視窗的控制代碼,系統會通知你:你正在操作142號視窗,就此,你的應用程式就能要求系統對142號視窗進行操作——移動視窗、改變視窗大小、把視窗極小化為圖示,等等。實際上許多Windows API 函式把控制代碼作為它的第一個引數,如GDI (圖形裝置介面)控制代碼、選單控制代碼、例項控制代碼、點陣圖控制代碼等等,不僅僅侷限於視窗函式,。

換句話說,控制代碼是一種內部程式碼,通過它能引用受系統控制的特殊元素,如視窗、點陣圖、圖示、記憶體塊、游標、字型、選單等等。Delphi中很少需要直接使用控制代碼,因為控制代碼藏在窗體、點陣圖及其他Delphi物件的內部。當你要呼叫Delphi不支援的Windows API 函式時,控制代碼才會有用。

現在舉一個簡單的Windows控制代碼例子,完善這節內容。例WHandle 程式的窗體很簡單,只有一個按鈕。正如下面主窗體文字所定義的那樣,我在程式碼中添加了窗體的OnCreate 事件和按鈕的OnClick 事件:

object FormWHandle: TFormWHandle
  Caption = 'Window Handle'
OnCreate = FormCreate object BtnCallAPI: TButton Caption = 'Call API' OnClick = BtnCallAPIClick end end

窗體一建立,程式就會通過窗體本身的Handle 屬性,獲取窗體對應的視窗控制代碼。呼叫IntToStr ,把控制代碼數值轉換為一個字串,然後再把它新增到窗體標題中,如圖9.1:

procedure TFormWHandle.FormCreate(Sender: TObject);
begin
  Caption := Caption + ' ' + IntToStr (Handle);
end;

因為FormCreate 是窗體類的方法,它可直接訪問同類的其他屬性和方法。因此,在這個過程中我們能夠直接訪問窗體的Caption 屬性和Handle 屬性。

圖 9.1: 例 WHandle 顯示窗體控制代碼,每次執行程式得到的控制代碼值不同

 

如果你多此次執行該程式,通常會獲得不同的控制代碼值。這個值實際上是由Windows 作業系統確定並返回給應用程式的。(控制代碼從來不是由程式決定的,而且控制代碼沒有預定義值,控制代碼是由系統決定的,每執行一次程式,產生一個新值。)

當你單擊按鈕,程式將呼叫Windows API 函式SetWindowText,它會根據第一個傳遞引數改變視窗的標題。更準確地說,所用的API 函式其第一個引數是需要修改窗體的控制代碼:

procedure TFormWHandle.BtnCallAPIClick(Sender: TObject);
begin
  SetWindowText (Handle, 'Hi');
end;

這段程式碼與前面所講的事件處理程式等效,它通過給窗體的Caption 屬性賦一個新值,改變窗體的標題。對上面這種情況,呼叫一個API 函式沒有什麼意義,因為用Delphi來做更簡單。然而有些API在Delphi中沒有相應的函式,就需要直接呼叫API,這一點你會在後面的高階例子中看到。

外部宣告

Windows 程式設計中涉及的另一個重要元素是外部宣告。外部宣告原先用於在Pascal程式碼中連線組合語言寫的外部函式,現在外部宣告用於Windows程式設計,用來呼叫動態連線庫DLL函式。在Delphi的Windows 單元中有許多這種宣告:

// forward declaration
function LineTo (DC: HDC; X, Y: Integer): BOOL; stdcall;

// external declaration (instead of actual code)
function LineTo; external 'gdi32.dll' name 'LineTo';

這段宣告表示函式LineTo 的程式碼同名儲存在GDI32.DLL 動態連結庫中(最重要的Windows 系統庫之一)。實際應用時,外部宣告中的函式名與DLL中的函式名可以不同。

一般你不需要象剛才所例舉的那樣寫宣告,因為Windows 單元和一些Delphi 系統單元中已包含了這些宣告。只有在呼叫自定義DLL,或呼叫Delphi 中未定義的Windows 函式時,你才能需要寫外部宣告。

注意:在16位Delphi中,外部宣告使用不帶副檔名的庫名,後面跟name指令(如上所示)或是一個index指令,後面跟DLL中函式的序號。儘管Win32 仍然允許通過序號訪問DLL函式,但是微軟公司已經宣告未來將不支援這種訪問方式,這一改變反映了系統庫訪問方式的改變。還要注意的是:目前Delphi的Windows 單元已取代了16位Delphi的WinProcs 和WinTypes 單元。

回撥函式

從第六章已經瞭解到Objet Pascal 支援過程型別。過程型別常用於給Windows API函式傳遞迴調函式。

首先,什麼是回撥函式呢?回撥函式就是能對一系列系統內部元素執行給定操作的API函式,例如能對所有同類視窗進行操作的函式。這種函式也叫列舉函式,它是作為引數傳遞的函式,代表對所有內部元素執行的操作,該函式或過程的型別必須與給定的過程型別相容。Windows 回撥函式的應用不止上述一種,不過這裡僅研究以上簡單應用。

現在考慮 EnumWindows API 函式,它的原型如下(從Win32 幫助檔案拷貝而來):

BOOL EnumWindows(
  WNDENUMPROC lpEnumFunc,  // address of callback function
  LPARAM lParam // application-defined value
  );

當然,這是個C語言的定義。我們可以檢視Windows 單元,從中找到相應的Pascal 語言定義:

function EnumWindows (
  lpEnumFunc: TFNWndEnumProc;
  lParam: LPARAM): BOOL; stdcall;

查閱幫助檔案,我們發現作為引數傳遞的函式應該屬於下面的型別(也是在C中):

BOOL CALLBACK EnumWindowsProc (
  HWND hwnd, // handle of parent window
  LPARAM lParam // application-defined value
  );

這與下面的Delphi 過程型別定義一致:

type
  EnumWindowsProc = function (Hwnd: THandle;
    Param: Pointer): Boolean; stdcall;

其中第一個引數是各主窗體的控制代碼,第二個引數則是呼叫EnumWindows 函式時所傳遞的值。實際上,Pascal 中沒有相應的TFNWndEnumProc型別定義 ,它只是個指標。這意味著我們需要傳遞一個帶有合適引數的函式,將它用作一個指標,也就是取函式的地址而不是呼叫它。不幸的是,這也意味著如果某個引數型別出現錯誤時,編譯器不會給予提示。

每當呼叫Windows API函式或傳遞一個回撥函式給系統時,Windows 要求程式設計師遵循stdcall 呼叫協定。預設情況下,Delphi使用另一種更高效的呼叫協定,其關鍵字為register。

下面是一個與定義相容的回撥函式,此函式把視窗的標題讀到字串中,然後新增到給定窗體的一個列表框中:

function GetTitle (Hwnd: THandle; Param: Pointer): Boolean; stdcall;
var
  Text: string;
begin
  SetLength (Text, 100);
  GetWindowText (Hwnd, PChar (Text), 100);
  FormCallBack.ListBox1.Items.Add (
    IntToStr (Hwnd) + ': ' + Text);
  Result := True;
end;

窗體有一個幾乎覆蓋整個窗體的列表框,窗體頂部有一個小面板,面板上有一個按鈕。當按下按鈕時,EnumWindows API函式被呼叫,並且GetTitle 函式作為引數傳遞給它:

procedure TFormCallback.BtnTitlesClick(Sender: TObject);
var
  EWProc: EnumWindowsProc;
begin
  ListBox1.Items.Clear;
  EWProc := GetTitle;
  EnumWindows (@EWProc, 0);
end;

你可以直接呼叫GetTitle函式,不必先把值儲存到過程型別臨時變數中,上例這麼做是為了使回撥過程更清楚。程式執行結果確實很有意思,正如你在圖9.2中看到的那樣,結果顯示了系統中正在執行的所有主視窗,其中大部分是隱藏的,你通常看不到,許多實際上沒有標題。

圖 9.2: 例CallBack輸出結果--當前所有主窗體,其中包括可見及隱藏的窗體

最小的Windows 程式

為了完整介紹Windows 程式設計及Pascal 語言,現在我展示一個簡單但完整的應用程式,建立該程式沒有使用VCL庫。這個程式只是簡單地採用命令列引數(儲存在系統全程變數cmdLine中),並利用ParamCount 和 ParamStr 這兩個Pascal 函式從引數中提取資訊。其中第一個函式返回引數的個數,第二個返回給定位置的引數。

儘管在圖形使用者介面環境下使用者很少操縱命令列引數,但是Windows 命令列引數對系統本身卻很重要。例如,一旦你定義了副檔名和應用程式的關聯,只要雙擊所關聯的檔案就能執行這個程式。實際上,當你雙擊一個檔案,Windows 即啟動關聯程式並把選定的檔案作為命令列引數傳遞給它。

下面是工程檔案的完整原始碼(一個DPR 檔案,不是PAS 檔案):

program Strparam;

uses
  Windows;

begin
  // show the full string
  MessageBox (0, cmdLine, 
    'StrParam Command Line', MB_OK);

  // show the first parameter
  if ParamCount > 0 then
    MessageBox (0, PChar (ParamStr (1)), 
      '1st StrParam Parameter', MB_OK)
  else
    MessageBox (0, PChar ('No parameters'), 
      '1st StrParam Parameter', MB_OK);
end.

輸出程式碼使用MessageBox API 函式,很容易就避開了VCL庫。實際上,象上面那樣純粹的Windows 程式,其優點就是佔的記憶體少,程式執行檔案大約才16k位元組。

為了給程式提供命令列引數,你可以用Delphi的 Run > Parameters 選單命令。另一個方法是:開啟Windows 資源管理器,查詢包含程式執行檔案的目錄,然後把你要執行的檔案拖到可執行檔案上,Windows 資源管理器會把拖放的檔名用作命令列引數,開始執行程式。圖9.3顯示了資源管理器及相應的輸出。

圖9.3: 把一個檔案拖放到執行檔案上,給例StrParam提供命令列引數 

結束語

在這一章中,我們對Windows 程式設計的底層內容進行了介紹,討論了控制代碼和簡單的Windows 程式。對於常規的Windows 程式設計任務,通常只需使用Delphi 提供的可視開發工具及VCL可視控制元件庫。但是這超出了本書討論的範圍,因為本書討論的是Pascal 語言