Ch 11 對話盒(4)通用對話盒


通用對話盒簡介

在 Windows 系統堙A有一些系統已經設計好的對話盒可供程式設計師直接使用,這些對話盒稱為『通用對話盒』( common dialog ),有下面幾種:

通用對話盒API 名稱 使用結構體
設定顏色ChooseColor CHOOSECOLOR
搜尋FindText FINDREPLACE
取代ReplaceText FINDREPLACE
設定字型ChooseFont CHOOSEFONT
開啟舊檔GetOpenFileName OPENFILENAME
另存新檔GetSaveFileName OPENFILENAME
版面設定PageSetupDlg PAGESETUPDLG

在使用他們時,只需要在對應的結構體中填入適當的資料,然後再呼叫 API 就可以了,不用在資源檔中設定各種子控件,是一種很方便的做法。

通用對話盒函式原型是存在 comdlg32.inc 檔案內,因此在撰寫程式時必須在原始碼中加入

include     comdlg32.inc
includelib  comdlg32.lib

這兩行,這樣製造出的可執行檔在執行之後,會自動地連結到 『C:\WINDOWS\SYSTEM\COMDLG32.DLL』動態函數庫中。也因為如此,當微軟更新 COMDLG32.DLL 時,所呈現的畫面就會不同。同時使用過 Windows 9x 和 Windows XP 的人可能知道,同一個程式在這兩個系統中,同樣是執行『開啟舊檔』,其畫面是不同的。底下圖一是在 Windows 98 第二版中執行『開啟舊檔』的畫面:


圖一

底下圖二是同一個程式,在 Windows XP 執行『開啟舊檔』的畫面:

圖二


原理

這一章堙A小木偶想藉由撰寫閱讀檔案的方式來說明如何使用 GetOpenFileName。這個程式執行時的畫面如下圖:


圖三

當使用者按下『離開』選單時,會關閉視窗並結束程式。當使用者按下『開啟』選單時,會顯示『開啟舊檔』,如上面圖一或圖二的對話盒,這時使用者可以選擇一個檔案,程式將在工作區媗膆雰鉹漁e。如果使用者按下『清除』選單,程式會清除工作區的內容。

使用者要開啟檔案時,必定得先取得要開啟的檔名,一個簡單的方法就是利用 GetOpenFileName API 顯示『開啟舊檔』通用對話盒,讓使用者選取檔案。

GetOpenFileName 的用法

GetOpenFileName API 將會顯示一個系統預先定義好的通用對話盒 ( 如上面圖一、圖二 ),供使用者在清單控件選定檔名或是在編輯框中輸入檔名。當使用者選好檔名或由編輯框輸入檔名並按下『開啟舊檔』下壓式按鈕控件後,GetOpenFileName 傳回 TRUE,並在 OPENFILENAME 結構體的 lpstrFile 欄位所指定位址內,填入被選定的檔名;如果使用者按下『取消』下壓式按鈕,則傳回 FALSE。至於接下來的工作,如開啟檔案、讀取檔案、寫入檔案、關閉檔案……等工作,必須由其他程式負責,GetOpenFileName 不做開啟、讀取、寫入…檔案等工作。GetOpenFileName 之原型為:

BOOL GetOpenFileName(
   LPOPENFILENAME   lpofn   // address of structure with initialization data
   );

lpofn 是一個指標,指向一個稱為 OPENFILENAME 的結構體,這個結構體的欄位如下:

OPENFILENAME      struc
lStructSize       DWORD          ?    ;結構體大小
hwndOwner         HWND           ?    ;父視窗代碼
hInstance         HINSTANCE      ?    ;執行實例代碼
lpstrFilter       LPCTSTR        ?    ;指向檔名篩選字串位址
lpstrCustomFilter LPTSTR         ?    ;指向自訂檔名篩選字串位址
nMaxCustFilter    DWORD          ?    ;自訂檔名篩選字串大小
nFilterIndex      DWORD          ?    ;顯示於檔案類型的檔名篩選字串
lpstrFile         LPTSTR         ?    ;完整檔名位址
nMaxFile          DWORD          ?    ;完整檔名大小
lpstrFileTitle    LPTSTR         ?    ;主檔名位址
nMaxFileTitle     DWORD          ?    ;主檔名大小
lpstrInitialDir   LPCTSTR        ?    ;起始子目錄
lpstrTitle        LPCTSTR        ?    ;對話盒標題
Flags             DWORD          ?    ;建立對話盒時的各項設定
nFileOffset       WORD           ?    ;主檔名在完整檔名的第幾個位址
nFileExtension    WORD           ?    ;副檔名在完整檔名的第幾個位址
lpstrDefExt       LPCTSTR        ?    ;預設的副檔名
lCustData         DWORD          ?    ;傳給攔截函式的資料
lpfnHook          LPOFNHOOKPROC  ?    ;攔截函式位址
lpTemplateName    LPCTSTR        ?    ;自訂對話盒模版名
OPENFILENAME      ends

OPENFILENAME 的欄位說明如下:

開啟檔案與建立檔案:CreateFile API

在 Win32 系統中,開啟檔案和建立檔案所用的 API 都是 CreateFile,這一點和以前 DOS 用不同的中斷服務程式 ( AH=3CH 是建立新檔案,AH=3DH 是開啟檔案 ),是不一樣的。除此之外,CreateFile 不僅僅能建立或開啟檔案,它還可以開啟實體磁碟與邏輯磁碟 ( Physical Disks and Volumes,參見附錄六)、通信設備 ( communications resources )、控制台 ( consoles )……等等裝置,但此處僅僅談論檔案的開啟。CreateFile 的原型是:

HANDLE CreateFile(
 LPCTSTR                lpFileName,
 DWORD                  dwDesiredAccess,
 DWORD                  dwShareMode,
 LPSECURITY_ATTRIBUTES  lpSecurityAttributes,
 DWORD                  dwCreationDisposition,  // how to create 
 DWORD                  dwFlagsAndAttributes,   // file attributes 
 HANDLE                 hTemplateFile
);

參數 lpFileName 是指向以 NULL 為結尾的檔名字串位址,此字串最大數值是 260 位元組,檔名字串的格式一般是『'磁碟機名:\路徑名\主檔名.副檔名',0』。

dwDesiredAsess 是指要對檔案進行什麼操作。GENERIC_READ 是指對檔案進行讀取,GENERIC_WRITE 是指對檔案進行覆寫,假如要對檔案同時進行讀寫,必須使用 GENERIC_READ or GENERIC_WRITE。

dwShareMode 是共享屬性,指檔案被此程式開啟後,後來的程式是否能對此檔案進行操作。此參數若是 NULL 表示別的程式不能開啟;若是 FILE_SHARE_DELETE,表示別的程式能刪除;若是 FILE_SHARE_READ,表示別的程式也能讀取;若是 FILE_SHARE_WRITE,表示別的程式也能寫入。

lpSecurityAttributes 是安全屬性,表示是否能被子程式所繼承,若為 NULL 表示不能繼承;若不為 NULL 則指向一個稱為 SECURITY_ATTRIBUTES 的結構體。

dwCreationDistribution 參數用來決定 lpFileName 指向位址的檔名字串所代表的檔案存在或者不存在時,應如何處理,這個參數設定不同而有開啟和建立檔案不同的效果。可用的數值有:

dwFlagsAndAttributes 是用來指定所建立檔案之屬性以及對檔案的操作方式,常用的屬性有下面幾種:

常用的操作方式有下面數種,他們可以和檔案屬性搭配使用:

最後一個參數,hTemplateFile,是開啟檔案模版,這是給 Win NT/XP 使用的,Win 9x/Me 不支援,一般建議用 NULL,以維持相容。

當 CreateFile 返回時,如果失敗,則傳回 INVALID_HANDLE_VALUE;如果成功,則傳回檔案代碼。( 如果要進一步得到錯誤原因,還得呼叫 GetLastError )

讀取檔案:ReadFile

ReadFile API 是用來讀取檔案,所讀取的檔案內容,由檔案指位器所指之處開始讀取,而讀取的長度則在參數 nNumberOfBytesToRead 堳定 ( 這個觀念和以前 DOS 時代讀取檔案相同,請參考 )。ReadFile 的原型是:

BOOL ReadFile(
  HANDLE        hFile,                // handle of file to read 
  LPVOID        lpBuffer,             // address of buffer that receives data  
  DWORD         nNumberOfBytesToRead, // number of bytes to read 
  LPDWORD       lpNumberOfBytesRead,  // address of number of bytes read 
  LPOVERLAPPED  lpOverlapped          // address of structure for data 
);

hFile 參數指定要讀取那一個檔案,這個參數應填入該檔案代碼。lpBuffer 指向一個緩衝區位址,這個緩衝區用來存放所讀取到的內容。nNumberOfBytesToRead 是要讀取的長度,以位元組為單位。lpNumberOfBytesRead 指向一個 32 位元的變數位址,在 ReadFile 執行完後,這個變數會被填入實際讀取的位元組數。為什麼要有 nNumberOfBytesToRead 這個參數呢?原因有很多,例如當你的檔案指位器已經到檔案未端,如果再讀取,就會讀不到檔案。

lpOverlapped 指向一個 OVERLAPPED 結構體位址,這個結構體是用來表示如何處理非同步讀寫用的,但是 Win9x 並不支援非同步讀寫,所以如果在 Win 9x 中,必須設為 NULL。

ReadFile 返回時,如果傳回非零值表示讀取正確;如果傳回零,表示讀取錯誤。如果要進一步得到錯誤原因,還得呼叫 GetLastError。

CloseHandle API

顧名思義,這個 API 是用來關閉檔案的,原型為:

BOOL CloseHandle(
   HANDLE   hObject     // handle to object to close  
);

它只有一個參數,就是要關閉的檔案代碼。如果成功,傳回 TRUE;失敗,傳回 FALSE。( 其實它不只可關閉檔案,還可關閉行程、執行緒……)

把檔案內容顯示於子控件

當讀取檔案內容之後,把內容顯示於螢光幕上有許多種方法,此處小木偶採用建立一個編輯框控制元件,再把檔案內容顯示於這個編輯框控制元件中。為了畫面美觀,這個編輯框控制元件最好要能填滿整個視窗的工作區,而且當視窗改變大小時,編輯框的大小也能隨之變動且再度填滿整個工作區。

為達此目的,必須取得工作區的大小,然後改變子控件大小以符合工作區。取得工作區的大小可用 GetClientRect API,在第三章時已經提過,請自行參考。

改變子控件的大小,也不困難,要記得子控件也是視窗的一種,更嚴謹的說,他是一種子視窗,所以改變子視窗的大小也就是改變子控件的大小。底下的 MoveWindow API 可達此目的。

MoveWindow API

MoveWindow API 可以改變視窗的大小,它當然也可改變控制元件的大小,除此之外,它也可以移動視窗,使視窗位置改變。MoveWindow 原型如下:

BOOL MoveWindow(
    HWND    hWnd,     // handle of window
    int     X,        // horizontal position
    int     Y,        // vertical position
    int     nWidth,   // width
    int     nHeight,  // height
    BOOL    bRepaint  // repaint flag
   );

hWnd 是指要更動位置或視窗大小的視窗代碼。X、Y 是指要把視窗移動到那兒,nWidth、nHeight 是指更改後的視窗寬度與高度。bRepaint 是指是否重繪視窗,TRUE 表示重繪,FALSE 表示不重繪。如果重繪視窗,MoveWindow 會發出 WM_PAINT 訊息給系統,系統會把此訊息傳給視窗函式。不過 WM_PAINT 訊息不用程式處理,系統內的對話框管理器會負責重繪。

WM_SIZE 訊息與 WM_SIZING 訊息

當使用者用滑鼠右鍵拖曳視窗邊框,再放開滑鼠後,就能改變視窗大小,這時視窗會收到 WM_SIZE 訊息,而在拖曳的過程中會收到 WM_SIZING 訊息。

先談 WM_SIZING 訊息。WM_SIZING 訊息中,wParam 表示視窗的那一個邊框被拖曳,可以是下面的位元值之組合:

數值 說   明 數值說   明
WMSZ_BOTTOM底邊框 WMSZ_TOP上邊框
WMSZ_LEFT左邊框 WMSZ_RIGHT右邊框
WMSZ_TOPLEFT左上角落 WMSZ_TOPRIGHT右上角落
WMSZ_BOTTOMLEFT左下角落 WMSZ_BOTTOMRIGHT右下角落

lParam 指向一個名為 RECT 結構體之位址,此結構體存有視窗之大小。請參考第三章有關 RECT 之說明。如果程式要處理 WM_SIZING 訊息,在返回時,其傳回值必須設為 TRUE。

當視窗收到 WM_SIZE 訊息時,wParam 表示式窗是如何被改變的,常見情形是:

lParam 較低的 16 位元是視窗工作區的寬度,較高的 16 位元是工作區的高度。所以程式可藉由 WM_SIZE 訊息取得改變視窗大小後的資訊。

在對話盒標題欄顯示圖示:WM_SETICON

以前我們要在視窗的標題欄顯示圖示,一般是在 WNDCLASSEX 結構體中指定圖示,再藉由 RegisterClassEx API 『顯示』圖示;但是對話盒中,視窗類別已經由系統預設好,如何更改圖示呢?答案是把 WM_SETICON 訊息傳遞給對話盒。

WM_SETICON 訊息的 HWND 是想要改變圖示的視窗,其 lParam 是改變後的圖示代碼,這個圖示代碼可藉由 LoadIcon API 自資源中載入。而 wParam 則是表示大圖示或小圖示的旗標,可以用 ICON_BIG 或 ICON_SMALL。

好了,底下看看原始程式吧!


原始程式

ViewFile.ASM 原始程式如下:

        .386
        .model  flat,stdcall
        option  casemap:none

include         windows.inc
include         user32.inc
include         kernel32.inc
include         comdlg32.inc
includelib      user32.lib
includelib      kernel32.lib
includelib      comdlg32.lib

IDM_Exit        equ     100
IDM_Open        equ     200
IDM_Clear       equ     300
IDC_Edit        equ     400
MAX_FullFN      equ     260     ;017 含磁碟機、路徑、主、副檔名長度
MAX_OnlyFN      equ     100     ;018 僅含主檔名、副檔名之長度
MAX_Read        equ     4096    ;019 讀取檔案之位元組數

DlgProc         proto   :DWORD,:DWORD,:DWORD,:DWORD
O_and_R         proto   :DWORD

;**********************************************************
                .DATA
hInstance       HINSTANCE       ?
hFile           HFILE           ?
hwndEdit        HWND            ?
ofn             OPENFILENAME    <?>     ;029 OPENFILENAME 結構體
nReadBytes      dd   0                  ;030 讀取檔案的實際位元組數
szIcon          db   'eye',0            ;031 圖示名稱
szNoFile        db   '您沒有選定檔名。',0
szOpenErr       db   '讀取檔案錯誤。',0
szClearText     db   ' ',0
DlgName         db   'VFDlg',0
szFNFilter      db   'All Files (*.*)',0,'*.*',0
                db   'Text Files (*.txt)',0,'*.txt',0
                db   'HTML Files (*.html, *.htm)',0,'*.html;*.htm',0,0
szTitle         db   '閱讀檔案',0       ;039 父視窗標題
szFullFN        db   MAX_FullFN dup (0) ;040 完整檔名存放處
szOnlyFN        db   MAX_OnlyFN dup (0) ;041 主檔名存放處
szDefExt        db   'txt',0            ;042 內定副檔名
buffer          db   MAX_Read dup (0),0 ;043 檔案內容存放處
;**********************************************************
                .CODE
;----------------------------------------------------------
;開啟與讀取檔案
O_and_R proc    hwnd:HWND
        mov     eax,hwnd
        mov     ofn.lStructSize,sizeof ofn      ;050 OPENFILENAME 長度
        mov     ofn.hwndOwner,eax
        mov     ofn.lpstrFilter,offset szFNFilter
        mov     ofn.lpstrCustomFilter,NULL
        mov     ofn.nFilterIndex,2              ;054 預設副檔名為 *.TXT
        mov     ofn.lpstrFile,offset szFullFN   ;055 完整檔名存放處
        mov     ofn.nMaxFile,MAX_FullFN
        mov     ofn.lpstrFileTitle,offset szOnlyFN
        mov     ofn.nMaxFileTitle,MAX_OnlyFN
        mov     ofn.lpstrInitialDir,NULL        ;059 用程式所在子目錄
        mov     ofn.lpstrTitle,NULL             ;060 用預設標題
        mov     ofn.Flags,OFN_READONLY          ;061 唯讀
        mov     ofn.lpstrDefExt,offset szDefExt
        mov     ofn.lCustData,NULL
        mov     ofn.lpTemplateName,NULL
        invoke  GetOpenFileName,addr ofn
        mov     edx,offset szTitle+8
        mov     byte ptr [edx],0
.if eax                                 ;069 按下開啟舊檔的確定控件
        invoke  CreateFile,offset szFullFN,GENERIC_READ,\
                FILE_SHARE_READ,NULL,OPEN_EXISTING,\
                FILE_ATTRIBUTE_NORMAL,NULL
  .if eax!=INVALID_HANDLE_VALUE
        mov     hFile,eax               ;073 正確地開啟檔案
        invoke  ReadFile,hFile,offset buffer,MAX_Read,\
                addr nReadBytes,NULL
        mov     edx,nReadBytes
        mov     buffer[edx],0
        mov     edx,offset szTitle+8
        mov     byte ptr [edx],'-'
        invoke  CloseHandle,hFile
        mov     ecx,offset buffer
  .else
        mov     ecx,offset szOpenErr    ;083 開啟檔案錯誤
  .endif
.else
        mov     ecx,offset szNoFile     ;086 按下開啟舊檔的取消控件
.endif
        invoke  SetDlgItemText,hwnd,IDC_Edit,ecx        ;088 改變編輯框文字
        invoke  SetWindowText,hwnd,offset szTitle       ;089 改變標題欄
        ret
O_and_R endp
;----------------------------------------------------------
DlgProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        LOCAL   nWinSize:RECT
.if uMsg==WM_INITDIALOG
        invoke  LoadIcon,hInstance,offset szIcon        ;096 載入圖示
        invoke  SendMessage,hWnd,WM_SETICON,ICON_SMALL,eax
        invoke  GetDlgItem,hWnd,IDC_Edit
        mov     hwndEdit,eax
        invoke  GetClientRect,hWnd,addr nWinSize        ;100 取得工作區大小
        mov     ecx,nWinSize.bottom
        mov     eax,nWinSize.right
        sub     ecx,nWinSize.top
        sub     eax,nWinSize.left
        invoke  MoveWindow,hwndEdit,0,0,eax,ecx,TRUE    ;105 改變編輯框大小
        invoke  SetWindowText,hWnd,offset szTitle
.elseif uMsg==WM_CLOSE
exit:   invoke  EndDialog,hWnd,NULL
.elseif uMsg==WM_COMMAND
        mov     eax,wParam
  .if lParam==0
     .if ax==IDM_Open                                   ;112 選擇『開啟』
        invoke  O_and_R,hWnd
     .elseif ax==IDM_Clear                              ;114 選擇『清除』
        invoke  SetDlgItemText,hWnd,IDC_Edit,offset szClearText
        mov     edx,offset szTitle+8
        mov     byte ptr [edx],0
        invoke  SetWindowText,hWnd,offset szTitle
     .elseif ax==IDM_Exit                               ;119 選擇『離開』
        jmp     exit
     .endif
  .endif
.elseif uMsg==WM_SIZE
        mov     eax,lParam
        mov     ecx,eax
        cwd
        shr     ecx,16
        invoke  MoveWindow,hwndEdit,0,0,eax,ecx,TRUE    ;128 改變編輯框大小
.else
        mov     eax,FALSE
        ret
.endif
        mov     eax,TRUE
        ret
DlgProc endp
;----------------------------------------------------------
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        invoke  DialogBoxParam,hInstance,addr DlgName,NULL,\
                addr DlgProc,NULL
        invoke  ExitProcess,eax
;**********************************************************
        end     start

ViewFile.RC 資源檔如下:

#include "c:\masm32\include\resource.h"
#define IDM_Exit        100
#define IDM_Open        200
#define IDM_Clear       300
#define IDC_Edit        400
#define VF_MENU         500

EYE     ICON    eye.ico

VF_MENU MENU
{
MENUITEM    "離開(&E)",IDM_Exit
MENUITEM    "開啟(&O)",IDM_Open
MENUITEM    "清除(&C)",IDM_Clear
}

VFDlg   DIALOG  0,0,320,220
STYLE   DS_SETFONT|WS_VISIBLE|WS_OVERLAPPEDWINDOW
FONT    9,"新細明體"
MENU    VF_MENU
{
EDITTEXT IDC_Edit,0,0,300,200,ES_READONLY|ES_MULTILINE|WS_HSCROLL|
         WS_VSCROLL|ES_AUTOHSCROLL|ES_AUTOVSCROLL
}

ViewFile.MAK 內容如下:

ALL : ViewFile.EXE

viewfile.exe : ViewFile.ASM ViewFile.RES
        ml /coff ViewFile.ASM /link ViewFile.RES

ViewFile.RES : ViewFile.RC EYE.ICO
        rc ViewFile.RC

圖示為:


說明

在模式對話盒加上選單

事實上,這個程式就是一個模式對話盒, 因為建立模式對話盒所用的程式碼短,它只需要呼叫 DialogBoxParam 即可,不需要註冊視窗類別,也不需要建立訊息迴圈,是一個很簡便的做法,許多程式設計師都很喜歡用這種做法。但是使用 DialogBoxParam 就少了註冊視窗類別,就不得不用另外的方法為模式對話盒加上『圖示』以及『選單』。加上圖示的方法,前面已經敘述了,至於加上選單方法的奧祕,藏於資源檔中,底下就先來檢查資源檔吧。

這個程式的資源檔堨]含三部份,圖示、選單以及對話盒。其中定義圖示、選單的方式沒變,但是在定義對話盒模版時,多了一個 MENU 關鍵字 ( 在 ViewFile.RC 檔案中紅色的部分 ),而這個 MENU 關鍵字之後則是要加上的選單名稱,這個選單則是以像以前一樣定義在一對的 BEGIN/END 或 {、} 之間,並以

選單名稱  MENU

為其名稱。但此處這個選單名稱無法以字串表示,只能以數字表示,這點也和以前定義選單的方式不同。

在資源檔中還有另一處值得一提,就是 EDITTEXT 控件,這個編輯框子控件的長與寬是小木偶任意選定的,因為建立此子視窗之後,立即會被 MoveWindow API 修改成和工作區的大小一樣。


到第十章回到首頁到第十二章