附錄七 Win32 堛漁伅


據小木偶所知,在 Win32 作業系統堙A時間的記錄方式至少有四種:

  1. Tick ( 滴答? )
  2. FILETIME
  3. SYSTEMTIME
  4. HIGH-RESOLUTION PERFORMANCE COUNTER


Tick

Tick 是指作業系統啟動後,所經歷的毫秒數,可以用 GetTickCount 取得,GetTickCount 沒有任何參數,返回後 EAX 就是作業系統啟動後所經歷的毫秒數。EAX 長度為一個雙字組,最多到 49.7 日後,就會歸零。


FILETIME 結構體

GetFileTime API

第二個記錄時間的方式是 FILETIME,主要是用來記錄檔案時間。FILETIME 其實是一個結構體,許多與檔案資料有關的 API 都會牽涉到 FILETIME。例如 GetFileTime、GetFileInformationByHandle 等 Win32 API,底下是 GetFileTime API 的原型:

        INVOKE  GetFileTime,hFile,lpCreationTime,lpLastAccessTime,lpLastWriteTime

GetFileTime 是用來取得檔案的建立時間、最後存取時間、最後修改時間,其最後的三個參數,lpCreationTime、lpLastAccessTime、lpLastWriteTime 分別是 hFile 所代表檔案的建立時間、最後存取時間、最後修改時間 ( 有關這三種時間,請參考檔案時間 )。這三個參數均為位址指標,各指向一種稱為 FILETIME 結構體。呼叫 GetFileTime 之後,如果成功執行,系統會分別在 lpCreationTime、lpLastAccessTime、lpLastWriteTime 三個位址指標所指的 FILETIME 結構體中存入 hFile 的對應檔案時間,並返回非零值;如果失敗則返回 0。

有關 FILETIME 結構體的欄位是:

FILETIME        STRUCT
dwLowDateTime   DWORD   ?
dwHighDateTime  DWORD   ?
FILETIME        ENDS

事實上,FILETIME 結構體是一個 64 位元的數值,dwLowDateTime 是較低的 32 位元,dwHighDateTime 是較高的 32 位元,兩者組成一個 64 位元的數值 ( dwHighDateTime×100000000H+dwLowDateTime ),單位是 100 奈秒。從世界標準時間 ( coordinated universal time,簡稱 UTC,中文為世界協調時間,也叫世界標準時間 ) 西元 1601 年元月元日零時零分零分零秒,以 100 奈秒為單位,開始計時。FILETIME 計算方式是每天以 86400 秒計算 ( 60*60*24=86400 ),一個平年有 365 天,一個閏年有 366 天計算。一般而言,每四年有一個閏年,以西元年數能為 4 整除者為閏年,例如西元 1604、1608 年為閏年,而 1601、1602 為平年;但是如果是西元年數末兩位為「00」的,必須要能為 400 整除,才算是閏年,所以西元 2000 年是閏年,而西元 1700、1800、1900 是平年。按上面方法,每一個平年會使 FILETIME 增加 11ED178C6C000H,每個閏年使 FILETILE 增加 11F9AA3308000H,而 64 位元的最大正數是 7FFFFFFFFFFFFFFFH,可以推算 FILETIME 能記載到西元 30828 年 9 月 14 日。

如果檔案系統不支援某個檔案時間,則該 FILETIME 會變為 0,例如 DOS 作業系統的檔案系統,FAT12/16 以及 Windows 95/98/Me 等系統使用的是 FAT12/16/32 檔案作業系統,都僅僅記錄檔案建立時間,所以如果以 GetFileTime 取得位於 FAT12/16/32 的檔案之時間,最後存取時間就無意義,會被填入 0。另外 Windows NT/XP 所使用的檔案時間,均以世界標準時間的時間寫入磁碟中。例如,假使使用者於中華民國 103 年七月十三日九點 45 分整時,建立一個新檔案,那麼實際上在磁碟的記錄是西元 2014 年七月十三日凌晨一點 45 分整。

FileTimeToLocalFileTime API

眾所皆知,全球劃分成 24 個時區,世界標準時間是以本初子午線 ( 東經 0 度 ) 為標準時間。GetFileTime 所得到的是世界標準時間,如果要轉變成當地時間,需呼叫 FileTimeToLocalFileTime,FileTimeToLocalFileTime 的原型是

        INVOKE  FileTimeToLocalFileTime,lpFileTime,lpLocalFileTime

這兩個參數,lpFileTime、lpLocalFileTime 都是位址指標,應指向兩個不同的 FILETIME 結構體,前者是世界標準時間,後者是當地時間。呼叫前須把 lpFileTime、lpLocalFileTime 及 lpFileTime 所指的 FILETIME 結構體 ( 也就是要轉換的 UTC 時間 ) 填好,呼叫時 Windows 會計算好當地時間,並填入 lpLocalFileTime 所指的結構體內。如果成功完成,返回非零值;如果失敗,返回零。

那麼,FileTimeToLocalFileTime 又怎麼知道使用者所在時區呢?原來把滑鼠螢幕右下角顯示時間的地方,按下滑鼠右鍵,在彈出選單中選「調整日期和時間」,於彈出的對話盒中選「時區」( 參考下圖紅框圈起來的部份 ),就可以設定時區,因此 Windows 就可以得知當地時間與世界標準時間相差多少小時。參考下圖:

以中華民國台灣而言,比世界標準時間早了八個小時,所以假如中華民國當地時間是西元 2014 年七月十三日九點 45 分整 ( 01CF9E7F1CE77600H ),世界標準時間其實是西元 2014 年七月十三日凌晨一點 45 分整 ( 01CF9E3C0EC43600H ),兩者相差 28800 秒,即 8 小時。FileTimeToLocalFileTime 也是利用此原理,將 UTC 增減一定的秒數而換算成當地時間。有些國家或地區會實施日光節約時間 ( 亦即夏日時把時間調快一小時,以達節約能源的目的 ),會自動出現在上圖中的地圖下方,紫色框框圍起來的地方。


SYSTEMTIME 結構體

第三種記錄時間的方法是 SYSTEMTIME 結構體。SYSTEMTIME 的時間欄位如下:

SYSTEMTIME      STRUCT
wYear           DW      ?   ;wYear 是西元年數,由 1601 到 30827
wMonth          DW      ?   ;wMonth 是月份,由 1 到 12,分別代表一月到十二月
wDayOfWeek      DW      ?   ;wDayOfWeek 是星期幾,由 0 到 6,分別代表星期日到星期六
wDay            DW      ?   ;wDay 表示日,由 1 最多可以到 31
wHour           DW      ?   ;wHour 是小時,由 0 到 23
wMinute         DW      ?   ;wMinute 是分鐘,由 0 到 59
wSecond         DW      ?   ;wSecond 是秒鐘,由 0 到 59
wMilliseconds   DW      ?   ;wMilliseconds 表示毫秒,由 0 到 999
SYSTEMTIME      ENDS

各欄位的意義顯而易見,要注意的是 wDay 的範圍。如果是小月 ( 如四月、六月等),只能到 30;二月是 28 或 29,視平年或閏年而定;大月 ( 如一月、三月等 ) 可到 31。由上面的介紹可知,SYSTEMTIME 的時間格式比起 FILETIME 格式好懂得多;但是如果要做運算,例如求兩事件相隔多久的時間,卻是以 FILETIME 較為方便。

FileTimeToSystemTime、SystemTimeToFileTime API

我們會很常在這 FILETIME 與 SYSTEMTIME 這兩種格式之間做轉換,但是如果真的以上面方式推算,實在太麻煩。微軟早已有了替代方案,那就是 FileTimeToSystemTime、SystemTimeToFileTime API,前者專門用來把 FILETIME 的時間格式轉換成 SYSTEMTIME 的時間格式;而後者則是做相反的過程。微軟也建議採用這兩個 API 轉換 FILETIME、SYSTEMTIME 兩種格式,而儘量少自行寫程式轉換。它們的原型是:

        INVOKE  FileTimeToSystemTime,lpFileTime,lpSystemTime
        INVOKE  SystemTimeToFileTime,lpSystemTime,lpFileTime

FileTimeToSystemTime 的第一個參數是 lpFileTime,為一位址指標,指向 FILETIME 結構體;第二個參數是 lpSystemTime,也是位址指標,指向 SYSTEMTIME 結構體。呼叫 FileTimeToSystemTime 前需將 lpFileTime、lpSystemTime 與 lpFileTime 所指的 FILETIME 填好,FileTimeToSystemTime 會把 FILETIME 的時間格式轉換成 SYSTEMTIME 的時間格式。如果 FILETIME 結構體內的時間是世界標準時間,轉換後變成 SYSTEMTIME 格式的世界標準時間;果是當地時間,轉換後變成 SYSTEMTIME 格式的當地時間。如果成功的轉換成 SYSTEMTIME 格式,返回值為非零,否則為零。

SystemTimeToFileTime 是用於把 SYSTEMTIME 格式轉換成 FILETIME。呼叫前,需把 lpSystemTime 所指的 SYSTEMTIME 結構體填好正確時間,各欄的範圍也需正確,呼叫成功後,系統會在 lpFileTime 所指位址填上 FILETIME 格式的時間,並返回非零值,如果呼叫失敗,則傳回零。SystemTimeToFileTime 比較有趣的一點是在 lpSystemTime 所指的 SYSTEMTIME 結構體中的 wDayOfWeek 即使是錯誤的,也不會影響到結果。


HIGH-RESOLUTION PERFORMANCE COUNTER

從字面上看,這是一種高解析度的時間計數器,利用這個高解析度時間計數器,可以測量出電腦執行一件事所花的時間,精確到微秒等級 ( 微秒是 10-6 秒 )。可惜的是,並不是所有的電腦都有安裝這種計數器。因此在使用這種計數器之前,必須先確定此電腦是否有安裝這種高解析度時間計數器。方法是呼叫 QueryPerformanceFrequency,QueryPerformanceFrequency 的原型如下

BOOL QueryPerformanceFrequency ( LARGE_INTEGER *lpFrequency
);

如果呼叫成功,傳回值為 TRUE;否則為 FALSE。QueryPerformanceFrequency 只有一個參數,lpFrequency。此參數指向一個 LARGE_INTEGER 的位址,LARGE_INTEGER 是一個 64 位元長 ( 即四字組,QWORD ) 的變數。換句話說,QueryPerformanceFrequency 的唯一一個參數是指向一個 64 位元長的變數位址。如果 QueryPerformanceFrequency 成功返回時,會把高解析度時間計數器的頻率,存入這個 64 位元長的變數堙A並使 EAX 返回值為 TRUE;如果,電腦中沒有安裝高解析度時間計數器,那麼這個 64 位元長的變數會存入 0,返回值仍為 TRUE。

高解析度時間計數器會在從系統啟動時設為 0,每隔一段極短的時間就增加一。高解析度時間計數器的頻率則是指,這個計數器每秒增加的數值,如果每秒增加的數值越多,就是代表每增加一所花時間越短。例如,小木偶有一台 DELL E5400 筆電,其高解析度時間計數器的頻率為 3579545,意思是每一秒鐘能使計數器的數值增加 3579545。換句話說,每增加一,只需花費 1/3579545 秒,大約是 2.794×10-7 秒。

那麼,要怎麼去測量電腦執行一個工作所花的時間是多少呢?小木偶得先介紹另一個 Win32 API,它是用來獲得高解析度時間計數器目前的數值,其原型為

BOOL QueryPerformanceCounter ( LARGE_INTEGER *lpPerformanceCount
);

QueryPerformanceCounter 只有一個參數,用來指向一個 64 位元變數的位址。如果呼叫成功,QueryPerformanceCounter 的傳回值為 TRUE,同時,系統會把高解析度時間計數器目前的數值,存入此變數堙F如果呼叫失敗,傳回值為 FALSE。因此,使用高解析度時間計數器,去測量電腦執行一件事所花的時間,其步驟如下:

                INVOKE  QueryPerformanceFrequency,OFFSET freq
                INVOKE  QueryPerformanceCounter,OFFSET t1
                call    do_something
                INVOKE  QueryPerformanceCounter,OFFSET t2
                finit           ;--st0--;--st1--;
                fild    t2      ;   t2  ;       ;
                fild    t1      ;   t1  ;   t2  ;
                fsub            ; t2-t1 ;       ;
                fild    freq    ;  freg ;   Δt ;
                fdiv            ;Δt/freq;      ;

先呼叫 QueryPerformanceFrequency,取得一秒鐘增加的數值,即上面程式片段的 freq。在電腦執行一件事前後,分別呼叫 QueryPerformanceCounter,記下高解析度時間計數器在執行該件事前後的數值,t1、t2,其差值就是執行這件事時,計數器增加了多少數值,即 ( t2-t1 )。因每秒鐘增加 freq,故每增加一所花的時間為 1/freq,再乘上 ( t2-t1 ),就是電腦呼叫 do_something 副程式所花的時間。但此時間可能是很小的數,並以浮點數存於 FPU 的 ST(0) 暫存器堙C


與時間有關常用的 API

獲得現在的時間

要取得現在的時間,可呼叫 GetSystemTime 或 GetLocalTime,前者是得到現在的世界標準時間,後者是得到現在的當地時間。它們的原型是:

        INVOKE  GetSystemTime,lpSystemTime
        INVOKE  GetLocalTime,lpSystemTime

這兩個 API 都只有一個參數,lpSystemTime,為一位址指標,指向 SYSTEMTIME 結構體,呼叫後,系統會把現在世界標準時間或當地時間填入 SYSTEMTIME 堙C這兩個 API 都沒有傳回值,也沒有錯誤碼。

設定現在的時間

既然獲得現在時間,要呼叫 GetSystemTime 或 GetLocalTime;那麼設定現在的時間,應該就是呼叫 SetSystemTime 或 SetLocalTime 了?的確是如此,SetSystemTime 或 SetLocalTime 的原型如下:

        INVOKE  SetSystemTime,lpSystemTime
        INVOKE  SetLocalTime,lpSystemTime

前者是設定世界標準時間,後者是設定當地時間。這兩個 API 都只有一個參數,lpSystemTime,為一位址指標,指向 SYSTEMTIME 結構體,呼叫前要把 SYSTEMTIME 結構體填上正確的時間,但其中的 wDayOfWeek 可以忽略不填。如果成功,返回非零值;否則返回零。Win32 系統中實際計時的是世界標準時間,因此如果程式呼叫 SetLocalTime 成功的設定時間時,系統其實已經利用時區及日光節約時間的資料,暗地婼桴膍t統內部的世界標準時間。此外,這兩個 API 呼叫前,都必須使程式具有 SE_SYSTEMTIME_NAME 特權才可以。

在螢幕上印出日期與時間

假設在 SYSTEMTIME 中已有了時間,如果利用 wsprintf 分別把 SYSTEMTIME 的各欄位的資料寫在某塊記憶體中,再把該記憶體內容印在螢幕上,也是未嘗不可。但此處小木偶想呼叫 GetDateFormat、GetTimeFormat 兩個 API 處理,可能更為簡單。底下是這兩個 API 之原型:

        INVOKE  GetDateFormat,Locale,dwFlags,lpDate,lpFormat,lpDateStr,cchDate
        INVOKE  GetTimeFormat,Locale,dwFlags,lpTime,lpFormat,lpTimeStr,cchTime

這兩個 API 是把 lpDate、lpTime 所指 SYSTEMTIME 結構體內的時間,以特定的格式變成以零結尾的字串,儲存在 lpDateStr、lpTimeStr 所指的記憶體內。此特定的格式是由 Locale、dwFlags、lpFormat 三個參數共同決定。其中最為重要的是 lpFormat 參數,如果 lpFormat 為零,表示由 Locale、dwFlags 決定格式;如果 lpFormat 不為零,則 lpFormat 為以 NULL 結尾的格式字串之位址,這時 dwFlags 必須設為零。GetDateFormat 的 lpFormat 所指的格式字串會用到「y」、「M」、「d」、「g」等特殊的格式字元;GetTimeFormat 的 lpFormat 所指的格式字串會用到「t」、「h」、「H」、「m」、「s」等特殊的格式字元 ( 需注意大小寫有別 ),Win32 系統看到這四個字元,會將他們翻譯成「年」、「月」、「日」、「星期」、「紀元前」等等。至於顯示中文或英文,是依據「自訂地區選項」對話盒堛滿u日期」和「時間」標籤中的「月曆型態」和時間上下午符號決定 ( 。( 「自訂地區選項」對話盒可由「開始」「控制台」「地區及語言選項」的「地區選項」標籤堛滿u自訂」按鈕按下而彈出,如下圖所示 )


圖一

圖二

底下表格是 lpFormat 所使用格式字元的意義:
格式字元說  明 以 AD 2009/07/15 20:04:59 為例
中華民國國曆西曆
以下是 GetDateFormat 中,lpFormat 所指字串可用的格式字元
y只儲存年份的末兩位數,如果十位數為 0,那麼就儲存一位數 989
yy只儲存年份的末兩位數,如果十位數為 0,仍然儲存兩位數,如此便會以 0 為起頭9809
yyyy把全部的紀元儲存出來,如果紀元有五位數 ( 如佛教曆法 ) 也會把五位數顯示出來982009
yyyyy同「yyyy」98 2009
gg此函數使用與指定區域設置相關的 CAL_SERASTRING 值。如果要格式化的日期不帶有相關的年代或時期字符串,此元素將被忽略。右欄的例子,格式是「ggyyyy」 中華民國98年2009
M儲存月份,如果十位數為 0,那麼就儲存一位數 77
MM儲存月份,如果十位數為 0,那麼仍然儲存兩位數,如此便會以 0 為起頭0707
MMM儲存月份的縮寫 七月Jul
MMMM儲存月份的完整名稱 七月July
d儲存日,如果十位數為 0,那麼就儲存一位數 1515
dd儲存日,如果十位數為 0,那麼前面補 0 1515
ddd儲存星期幾,但以縮寫表示 星期三Wed
dddd儲存星期幾,但以全名表示 星期三Wednesday
以下是 GetTimeFormat 中,lpFormat 所指字串可用的格式字元
t儲存上下午,但只儲存一個字母 下午P
tt儲存上下午,但只儲存兩個字母 下午PM
h儲存十二小時制的小時,如果十位數為 0,那麼就只儲存一位數 88
hh儲存十二小時制的小時,如果十位數為 0,十位數補上 0 0808
H儲存二十四小時制的小時,如果十位數為 0,那麼就只儲存一位數 2020
HH儲存二十四小時制的小時,如果十位數為 0,那麼十位數補上 0 2020
m儲存分鐘,如果十位數為 0,那麼就只儲存一位數 44
mm儲存分鐘,如果十位數為 0,那麼十位數補上 0 0404
s儲存秒,如果十位數為 0,那麼就只儲存一位數 5959
ss儲存秒,如果十位數為 0,那麼十位數補上 0 5959
上面表格中,最右一欄假設使用者以時間,2009/7/15 20:4:59,為例,所得的結果。您會發現,雖然 GetDateFormat、GetTimeFormat 雖然方便,但使用者設定不同時,結果也不同。例如,程式均為

szDateFormat    DB      "ggyyyyMMdd, ddd",0
INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,ADDR szDateFormat....

當「日期」標籤中的「月曆型態」選「中華民國國曆」,結果是「中華民國98年07月15日, 星期三」;如果是「西曆 ( 英文 )」,結果是「2009Jul15, Wed」

Locale 是區域選項,可以是以下三種:

Local十六進位數值 說明
LOCALE_SYSTEM_DEFAULT800H 系統預設。在 WINDOWS.INC 1.6 版並未收錄此常數,所以要在程式中自行定義。
LOCALE_USER_DEFAULT400H 使用者或應用程式設定。在 Windows XP 堙A使用者設定可以由「控制台」「地區及語言選項」堻]定;應用程式可呼叫 SetLocalInfo 設定。
LOCALE_INVARIANT7FH 這是給接近作業系統等級的程式使用的,它會使得與區域無關,一般程式很少使用。如果使用此格式,不管使用者設定哪一種紀元,均以西元紀元表示。在 WINDOWS.INC 1.6 版並未收錄此常數,所以要在程式中自行定義。

如果 lpFormat 為零,dwFlags 可以是下面數值:

dwFlags十六進位數值說明
以下是 GetDateFormat 中,lpFormat 所指字串可用的格式字元
DATE_SHORTDATE1H 儲存簡短日期,其格式是由「自訂地區選項」對話盒堛滿u日期」標籤中的「簡短日期」決定,參考上圖一。不可和 DATE_LONGDATE 或 DATE_YEARMONTH 合用。如果 dwFlags 沒有指定 DATE_YEARMONTH、DATE_SHORTDATE、DATE_LONGDATE 三者之中的一個,同時 lpFormat 為 NULL,那麼系統自動使用 DATE_SHORTDATE 格式。
DATE_LONGDATE2H 儲存完整日期,其格式是由下圖的「完整日期」,決定,不可與 DATE_SHORTDATE 或 DATE_YEARMONTH 合用。
DATE_YEARMONTH8H 只儲存年份和月份。不可和 DATE_LONGDATE 或 DATE_SHORTDATE 合用。
DATE_USE_ALT_CALENDAR4H 使用第二種曆法,例如中華民國民間常用農曆,但是 Windows 並未收錄
DATE_LTRREADING10H 按照 MSDN 記載,會加上一個由左向右的記號,但在小木偶的 Windows XP SP3 中會造成程式錯誤。
DATE_RTLREADING20H 同上,只是改成由右向左。
LOCALE_NOUSEROVERRIDE80000000H 強制使用系統內定的區域格式,如果沒有設定此旗標,則使用使用者自訂的格式。因為此旗標會忽視使用者的設定,或系統更新、升級時都會使 LOCALE_NOUSEROVERRIDE 格式不同,因此最好少使用此旗標。MASM32 堛 WINDOWS.INC 1.6 版未收錄此旗標。
LOCALE_USE_CP_ACP40000000H MASM32 堛 WINDOWS.INC 1.6 版未收錄此旗標。此旗標使用系統內的 ANSI 碼取代各地的區域內碼。
以下是 GetTimeFormat 中,lpFormat 所指字串可用的格式字元
TIME_NOMINUTESORSECONDS1H 不儲存分鐘與秒鐘,只儲存小時
TIME_NOSECONDS2H 不儲存秒鐘,只儲存小時與分鐘
TIME_NOTIMEMARKER4H 不使用時間標記,例如「AM」、「PM」
TIME_FORCE24HOURFORMAT8H 以二十四小時制儲存
LOCALE_NOUSEROVERRIDE80000000H 同 GetDateFormat 的 LOCALE_NOUSEROVERRIDE
LOCALE_USE_CP_ACP40000000H 同 GetDateFormat 的 LOCALE_USE_CP_ACP

GetDateFormat、GetTimeFormat 最後一個參數分別是 cchDate、cchTime,它們必須在呼叫前先存入 lpDateStr、lpTimeStr 所指記憶體大小,以容納所得的字串不會造成緩衝區溢位。如果 cchDate、cchTime 設為零,那麼 GetDateFormat、GetTimeFormat 將會傳回所需記憶體大小。GetDateFormat、GetTimeFormat 如果成功的返回,會傳回所得字串大小,如果失敗傳回 0。


範例:FI ( File Information )

上面提到了一些有關檔案的觀念,底下就是實作的部份。小木偶撰寫了 FI 程式,執行後按下「瀏覽」按鈕,產生「開啟舊檔」的通用對話盒,使用者可以任意選擇其內的檔案,FI 會把該檔案的大小、建立時間、最後存取時間、最後修改時間顯示在靜態控件上,如下圖:

原始檔

底下是 FI.RC 的原始檔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "c:\masm32\include\resource.h"
 
#define  RT_MANIFEST    24
#define  IDS_TEXT      50000
#define  IDB_BROWSE    50001
#define  IDB_EXIT      50002
 
FilInfo DIALOG  200,100,210,190
STYLE   DS_MODALFRAME|WS_POPUP|WS_VISIBLE|WS_CAPTION|WS_SYSMENU
FONT    9,"MS Sans Serif"
CAPTION "檔案資料"
BEGIN
  LTEXT          "按「瀏覽」選擇檔案",  IDS_TEXT,  5, 10,200,140
  PUSHBUTTON     "瀏覽",            IDB_BROWSE, 20,160, 80, 20
  PUSHBUTTON     "離開",              IDB_EXIT,110,160, 80, 20
END
 
1       RT_MANIFEST MOVEABLE PURE "FI.EXE.MANIFEST"
 
FilInfo ICON    INFO.ICO

底下是 FI.EXE.MANIFEST 的原始檔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
    <description>Test application for xp style.</description>
    <dependency>
        <dependentAssembly>
            <assemblyIdentity
                type="win32"
                name="Microsoft.Windows.Common-Controls"
                version="6.0.0.0"
                processorArchitecture="*"
                publicKeyToken="6595b64144ccf1df"
                language="*"
            />
        </dependentAssembly>
    </dependency>
</assembly>

底下是 FI.ASM 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
INCLUDE         WINDOWS.INC
INCLUDE         COMDLG32.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMDLG32.LIB
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
 
IDS_TEXT        EQU     50000
IDB_BROWSE      EQU     50001
IDB_EXIT        EQU     50002
 
;***************************************************************************************************
.CONST
szIcon          EQU     THIS BYTE
szDlgName       DB      "FilInfo",0                     ;對話盒面板名稱
szFilter        DB      "所有檔案 (*.*)",0,"*.*",0,0
szTitle         DB      "選擇檔案,以顯示被選擇的檔案資料",0
szError         DB      "開啟檔案錯誤",0
szFileSizeFmt   DB      "檔案大小:%I64d位元組(bytes)",0dh,0ah,0
szTimeFmt       DB      " HH:mm:ss",0dh,0ah,0
szCreateDateFmt DB      "建檔時間:ggyyyy年M月dd日ddd",0
szAccessDateFmt DB      "最近存取時間:ggyyyy年M月dd日ddd",0
szWriteDateFmt  DB      "最近修改時間:ggyyyy年M月dd日ddd",0
;***************************************************************************************************
.DATA
hInstance       HANDLE                          ?
hFile           HANDLE                          ?
ofn             OPENFILENAME                    <>
fi              BY_HANDLE_FILE_INFORMATION      <>
szFullName      DB                              MAX_PATH DUP (0)
szInfo          DB                              200h DUP (0)
;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
;輸入:EDI-GetDateFormat/GetTimeFormat會把字串填入EDI所指位址之處
;   lpDateFmt-GetDateFormat參數lpFormat位址
;   lpFT-三種檔案時間所在位址
;輸出:EDI將分別填入三種檔案時間字串
save_time       PROC    lpDateFmt:LPSTR,lpFT:LPSTR
                LOCAL   ft:FILETIME
                LOCAL   syst:SYSTEMTIME
                INVOKE  FileTimeToLocalFileTime,lpFT,ADDR ft    ;把UTC時間變成當地時間(中華民國標準時間)
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst  ;把FILETIME格式的當地時間變成SYSTEMTIME格式
                mov     ecx,OFFSET szInfo+SIZEOF szInfo
                sub     ecx,edi
                INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,lpDateFmt,edi,ecx
                dec     eax
                add     edi,eax
                mov     ecx,OFFSET szInfo+SIZEOF szInfo
                sub     ecx,edi
                INVOKE  GetTimeFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szTimeFmt,edi,ecx
                dec     eax
                add     edi,eax
                ret
save_time       ENDP
;---------------------------------------------------------------------------------------------------
get_file_info   PROC    USES esi edi
            ;把完整檔名移到szInfo處
                mov     esi,ofn.lpstrFile
                mov     edi,OFFSET szInfo
next_byte:      lodsb
                cmp     al,0
                je      ok
                stosb
                jmp     next_byte
            ;換行
ok:             mov     ax,0a0dh
                stosw
            ;把檔案大小存入szInfo堙A緊接著檔名之後
                INVOKE  wsprintf,edi,OFFSET szFileSizeFmt,fi.nFileSizeLow,fi.nFileSizeHigh
                dec     eax
                add     edi,eax
            ;把建立檔案時間存入szInfo堙A緊接著檔案大小之後
                INVOKE  save_time,OFFSET szCreateDateFmt,OFFSET fi.ftCreationTime
            ;把最近存取檔案時間存入szInfo堙A緊接著建立檔案時間之後
                INVOKE  save_time,OFFSET szAccessDateFmt,OFFSET fi.ftLastAccessTime
            ;把最近修改檔案時間存入szInfo堙A緊接著最近存取檔案時間之後
                INVOKE  save_time,OFFSET szWriteDateFmt,OFFSET fi.ftLastWriteTime
                ret
get_file_info   ENDP
;---------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_INITDIALOG
                INVOKE  LoadIcon,hInstance,OFFSET szIcon
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
 
.ELSEIF uMsg==WM_COMMAND
                mov     eax,wParam
                mov     edx,wParam
                shr     eax,10h         ;EAX=通知碼
                and     edx,0ffffh      ;EDX=控制元件識別碼
    .IF eax==BN_CLICKED
        .IF edx==IDB_EXIT
                jmp     exit
        .ELSEIF edx==IDB_BROWSE
                mov     ecx,hDlg
                mov     ofn.lStructSize,SIZEOF ofn
                mov     ofn.hwndOwner,ecx
                mov     ofn.lpstrFilter,offset szFilter
                mov     ofn.lpstrCustomFilter,0
                mov     ofn.nFilterIndex,0
                mov     ofn.lpstrFile,offset szFullName
                mov     ofn.nMaxFile,SIZEOF szFullName
                mov     ofn.lpstrFileTitle,0
                mov     ofn.lpstrInitialDir,0
                mov     ofn.lpstrTitle,OFFSET szTitle
                mov     ofn.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
                INVOKE  GetOpenFileName,OFFSET ofn
            .IF eax==0
                mov     edx,OFFSET szError
            .ELSE
                INVOKE  CreateFile,OFFSET szFullName,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                mov     hFile,eax
                INVOKE  GetFileInformationByHandle,eax,OFFSET fi
                INVOKE  CloseHandle,hFile
                call    get_file_info
                mov     edx,OFFSET szInfo
            .ENDIF
                INVOKE  SetDlgItemText,hDlg,IDS_TEXT,edx
        .ENDIF
    .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
exit:           INVOKE  EndDialog,hDlg,NULL
 
.ELSE           ;其他未處理的訊息返回 FALSE
                mov     eax,FALSE
                ret
 
.ENDIF          ;已處理的訊息,返回 TRUE
                mov     eax,TRUE   
                ret
DlgProc         ENDP
;---------------------------------------------------------------------------------------------------
start:          INVOKE  GetModuleHandle,NULL
                mov     hInstance,eax
                INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,OFFSET DlgProc,NULL
                INVOKE  ExitProcess,eax
                call    InitCommonControls
;***************************************************************************************************
        END     start

下載 INFO.ICO,然後把它與 FI.ASM、FI.RC、FI.EXE.MANIFEST 放在同一目錄,然後由「開始」「附屬應用程式」中開啟「命令提示字元」,切換到剛剛的目錄下,依下面方式組譯 ( 黃色字是您要下的指令,[Enter]表示按下 Enter 鍵 ):

E:\HomePage\SOURCE\Win32\AP07_FI>rc fi.rc [Enter]

E:\HomePage\SOURCE\Win32\AP07_FI>ml fi.asm /link fi.res [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: fi.asm

***********
ASCII build
***********

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"fi.obj"
"/OUT:fi.exe"
"fi.res"

E:\HomePage\SOURCE\Win32\AP07_FI>

至此,已組譯成功,可以得到 FI.EXE,您可自行執行看看。底下是一些說明。

檔案的時間

DOS 作業系統使用的檔案系統是 FAT12/16,僅僅記錄檔案建立或修改的時間,或者是子目錄建立的時間 ( 參考組合語言第 18 章 )。但是在 NTFS 檔案系統下,檔案或資料夾的時間就複雜得多了,可分為建立的時間 ( create time )、最後存取的時間 ( access time ) 與最後修改的時間 ( modify time ) 三種。底下說明這三種檔案時間:

在 NTFS 記錄的這三種時間,都是以世界標準時間記錄成 FILETIME 格式。這麼做的好處是,假如您的隨身碟埵陪蚗仵蛂A今天您搭飛機由台灣飛到澳洲雪梨,在電腦上所得到的時間是東澳標準時間 ( AEST,比 UTC 快 10 小時 ),而不是中華民國標準時間。

副程式:save_time

第 49∼50 行是

                INVOKE  FileTimeToLocalFileTime,lpFT,ADDR ft    ;把UTC時間變成當地時間(中華民國標準時間)
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst  ;把FILETIME格式的當地時間變成SYSTEMTIME格式

這兩行,先把 UTC 時間變成當地時間 ( 中華民國標準時間 ),這兩個時間都是以 FILETIME 的格式來表示的。lpFT 是由父程式,get_file_info,傳來的參數,在 get_file_info 第 80∼85 行呼叫三次 save_time,分別把建立檔案的時間、最近修改的時間、最後存取的時間位址當成 lpFT 傳給 save_time。要注意的是,這三種時間都是世界標準時間,且格式為 FILETIME。第 49 行先把這些時間轉換成當地時間,當地時間仍是 FILETIME 格式。第 50 行則是把剛剛轉換後的當地時間,變成 SYSTEMTIME 格式,存在區域變數 syst 堙C

接下來的工作是呼叫 GetDateFormat、GetTimeFormat 把 syst 堛漁伅ˍ雃谷r串,存到 szInfo 堙CszInfo 是一個字串,在返回後,即第 124 行,對話盒函式會使 EDX 指向 szInfo 字串的位址,然後在第 126 行,呼叫 SetDlgItemText 把靜態控件,IDS_TXET,的標題設為 szInfo 字串,於是就可以在螢幕上顯示檔案的資料了。

另外,因為 szInfo 字串堜狴]含的是檔名、檔案大小、建立檔案的時間、最近修改的時間、最後存取的時間這些資料所形成的字串,一個接著一個,連接起來的。因此每次呼叫 GetDateFormat、GetTimeFormat 後,所計算出來的字串都必須接在前一次字串結尾,並且要把結尾的 NULL 字元除去。

小木偶用的方法很簡單,每次呼叫 GetDateFormat、GetTimeFormat 前,以 EDI 暫存器記錄將要在 szInfo 字串的哪一個位址開始填入計算後的字串;呼叫之後,EDI 再加上寫入 szInfo 的位元組個數,就變成下次呼叫的開始填入字串位址。而寫入 szInfo 的位元組數,就存於 GetDateFormat、GetTimeFormat 的返回值,EAX 堙C因此您會看到

                dec     eax
                add     edi,eax

這兩行,前一行是去掉 NULL 字元,後一行則是加上填入的位元組個數。在程式第 54∼55、59∼60 行,甚至 77∼78 行都可以見到這種用法。

至於 GetDateFormat、GetTimeFormat 的最後一個參數,是 szInfo 字串還剩多少空間可以容納它們所計算出的結果。方法也很簡單,只要知道在 szInfo 字串的哪一個位址開始填入計算後的字串 ( 記錄在 EDI ),然後以 szInfo 的最後位址去減掉此位址,就可以得到還剩多少空間。至於 szInfo 的最後位址,則是用 szInfo 的起始位址加上 szInfo 的總長度,記錄於 ECX 堙C因此您可以在 51∼52、56∼57 見到像下面的程式碼:

                mov     ecx,OFFSET szInfo+SIZEOF szInfo
                sub     ecx,edi

附註:目錄的時間

小木偶曾查過一些資料,似乎沒有 Win32 API 能夠提供目錄的建立時間、最近修改時間、最近存取的時間。想來想去,只有用變通的方法。這個變通的方法是搜尋符合某個指定字串的檔名或目錄名。那就是 FindFirstFile。

FindFirstFile

FindFirstFile 是用來在一個目錄奡M找符合指定名稱的第一個檔案或目錄名,並取得相關資料。其原型是:

        INVOKE  FindFirstFile,lpFileName,lpFindFileData

lpFileName 是某個字串的位址,這個字串,一般含有萬用字元且以 0 結尾,它代表要為 FindFirstFile 所搜尋的檔名或目錄名。萬用字元是指「*」與「?」兩個字元,「*」是指可以以任意字元代入且不限長度,「?」只限於以一個任意字元代入。「*」與「?」兩個字元,好像是數學上的代數,可以代進任何數字一樣。此外,檔名分為主檔名與副檔名,「.」的左邊為主檔名,右邊為副檔名。下面所搜尋的目標都是在「C:\WINDOWS」目錄下或是「C:\」目錄下的檔案,請看以下說明,lpFileName,所指位址的字串可能是像底下的樣子:

指定檔名或目錄字串說  明
C:\WINDOWS\*.*所搜尋的是任何一個檔案或子目錄都符合
C:\WINDOWS\*.EXE搜尋到第一個副檔名是「EXE」的檔案或子目錄就符合
C:\WINDOWS\????.EXE搜尋第一個副檔名為「EXE」且檔名或子目錄名長度為四個字元的檔案 ( 注意,「?」也可代表空字元,所以主檔名長度為一個、兩個、三個字元的名稱也符合「????.EXE」)
C:\WINDOWS\?i*.*搜尋檔名的第一或第二個字元是「i」的第一個檔案
C:\WINDOWS\N*.*搜尋檔名第一個字是「N」的檔案,就符合搜尋
C:\WINDOWS\NOTEPAD.EXE搜尋 NOTEPAD.EXE 檔案。FindFirstFile 搜尋符合的第一個檔案時,並不嚴格限制大小寫,亦即 lpFileName 所指字串為「NOTEPAD.*」,則檔名為「Notepad.EXE」「notepad.exe」、「NotePad.Exe」都是符合的。
C:\WINDOWS就是指「C:\WINDOWS」目錄
C:\僅指明子目錄底下的檔案,但沒指明檔名,就會發生 INVALID_HANDLE_VALUE 錯誤。如果呼叫 GetLastError,會得到 ERROR_FILE_NOT_FOUND 錯誤碼,表示未找到符合的檔案。
C:\WINDOWS\同上

FindFirstFile 的第二個參數,lpFindFileData,是一個位址,此位址指向一個稱為 WIN32_FIND_DATA 的結構體,此結構體的欄位是:

WIN32_FIND_DATA         STRUCT
dwFileAttributes        DWORD           ?               ;屬性
ftCreationTime          FILETIME        <>              ;檔案建立時間
ftLastAccessTime        FILETIME        <>              ;最近存取時間
ftLastWriteTime         FILETIME        <>              ;最近修改時間
nFileSizeHigh           DWORD           ?               ;檔案大小,高字組
nFileSizeLow            DWORD           ?               ;檔案大小,低字組
dwReserved0             DWORD           ?
dwReserved1             DWORD           ?
cFileName               BYTE            MAX_PATH DUP (?);檔名 ( 不包含路徑名 )
cAlternateFileName      BYTE            14 DUP (?)      ;短檔名
WIN32_FIND_DATA         ENDS

第一個欄位,dwFileAttributes,是指檔案屬性,如下表所示,dwFileAttributes 欄位可以是下表中的一個或數個的聯集:

屬性數值說明
FILE_ATTRIBUTE_ARCHIVE20H 保存,表示檔案需保存,通常是提供給備份軟體或備份命令使用的,當使用者建立或修改檔案時,會自動標識 FILE_ATTRIBUTE_ARCHIVE 屬性,以提示備份軟體這個檔案尚未備份,當備份後,此屬性自動取消。如果使用者再次修改檔案,會再次標識 FILE_ATTRIBUTE_ARCHIVE
FILE_ATTRIBUTE_COMPRESSED800H 壓縮,表示檔案或目錄為壓縮的。對檔案來說,表示此檔案被壓縮過。對目錄來說,有了此屬性後,此目錄媟s建的檔案會被壓縮
FILE_ATTRIBUTE_DEVICE40H 保留
FILE_ATTRIBUTE_DIRECTORY10H 目錄
FILE_ATTRIBUTE_ENCRYPTED4000H 加密。對檔案來說,是內容加密。對目錄來說,是對將來新建的檔案自動設定加密屬性
FILE_ATTRIBUTE_HIDDEN2H 隱藏,一般而言,具有此屬性的檔案或目錄,在檔案總管堥ㄓㄤ菕A但可以設定「工具」「資料夾選項」「檢視」「顯示所有檔案及資料夾」堻]定,即使具有 FILE_ATTRIBUTE_HIDDEN 屬性,也能見著
FILE_ATTRIBUTE_NORMAL80H 檔案沒有設置其他屬性,此屬性只能單獨使用
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED2000H
FILE_ATTRIBUTE_OFFLINE1000H 離線。檔案內容暫時不可用,此屬性被遠端儲存裝置 ( remote storage ) 軟體所用,不能任意更改。
FILE_ATTRIBUTE_READONLY1H 唯讀。對檔案而言,應用程式只能讀取,不能修改或刪除。對目錄而言,應用程式不能刪除。
FILE_ATTRIBUTE_REPARSE_POINT400H
FILE_ATTRIBUTE_SPARSE_FILE200H
FILE_ATTRIBUTE_SYSTEM4H 系統檔,表示此檔案或目錄是作業系統的一部份,
FILE_ATTRIBUTE_TEMPORARY100H
FILE_ATTRIBUTE_VIRTUAL10000H

WIN32_FIND_DATA 的第二、三、四個欄位,分別表示檔案建立時間、最近存取的時間、最近修改時間,均以 FILETIME 格式記錄。nFileSizeHigh、nFileSizeLow 合起來是一個六十四位元的數,代表檔案大小,以位元組為單位,因此檔案大小為 nFileSizeHigh×100000000H+nFileSizeLow 位元組;如果是目錄,則此欄位為 0。接下來的 dwReserved0 只有在 dwFileAttributes 欄位中包含了 FILE_ATTRIBUTE_REPARSE_POINT 屬性才有意義,否則此欄位無用。dwReserved1 是保留的欄位,留待將來使用。cFileName 是一個長 260 位元組的字串,表示檔名 ( 僅含主檔名與副檔名,不包含路徑 )。cAlternateFileName 為一個 14 位元組長的字串,其內記錄著短檔名名稱,短檔名是指 DOS 使用的 8.3 形態,亦即 8 個位元組長的主檔名,再加上 3 個位元組長的副檔名,其間以「.」隔開。

呼叫 FindFirstFile 前需把 lpFileName 及 lpFindFileData 這兩個位址填好,如果成功的執行完 FindFirstFile,系統會在 lpFindFileData 所指的 WIN32_FIND_DATA 結構體內填好找著的第一個檔案的資料,並返回一個搜尋代碼 ( search handle ),此搜尋代碼可供 FindNextFile 使用,繼續尋找下一個符合的檔案。搜尋完成後,也要用這個搜尋代碼呼叫 FindClose 結束搜尋。如果呼叫 FindFirstFile 失敗,返回 INVALID_HANDLE_VALUE,可以呼叫 GetLastError 得到更進一步的錯誤碼,一般會得到 ERROR_FILE_NOT_FOUND,表示找不到符合的檔案;或是 ERROR_PATH_NOT_FOUND,表示 lpFileName 所指的名稱是錯誤的名稱。

FindNextFile

FindNextFile 是用來搜尋下一個符合特定名稱的檔案或目錄,原型如下:

        INVOKE  FindNextFile,hFindFile,lpFindFileData

hFindFile 是呼叫 FindFirstFile 後傳回來的搜尋代碼,lpFindFileData,是指向 WIN32_FIND_DATA 的結構體的位址。呼叫 FindNextFile 前需先填好 hFindFile 及 lpFindFileData,如果成功的執行完 FindNextFile,系統會在 lpFindFileData 結構體填好找著的檔案的資料,並返回非零值;如果返回零值時,應該呼叫 GetLastError,取得錯誤碼,如果錯誤碼是 ERROR_NO_MORE_FILES,表示已經沒有符合的檔案了。一般而言,程式應該進入一個迴圈,重複呼叫 FindNextFile,直到返回零,然後呼叫 GetLastError,檢查 ERROR_NO_MORE_FILES 是否出現為止。這樣就能在目錄中,把符合特定檔名的所有檔案找出來。

FindClose

FindClose 用於搜尋特定檔名時,已完全找出所有符合此特定檔名的檔案,或者不想再繼續尋找檔案了。這時就要呼叫 FindClose,它的原型是:

        INVOKE  FindClose,hFindFile

它只有一個參數,hFindFile,就是 FindFirstFile 所得到的搜尋代碼。如果呼叫成功,返回非零值;失敗則返回 0,可以呼叫 GetLastError 得到進一步的錯誤碼。程式關閉搜尋代碼後,就無法再呼叫 FindNextFile 搜尋下一個符合的檔案了。

目錄的時間

本節一開始提到,似乎沒有直接的方法得到目錄的建立時間、最近存取的時間、最近修改時間,只能用變通的方法。這個變通的方法就是利用 FindFirstFile 或 FindNextFile 找到目錄,然後在 WIN32_FIND_DATA 結構體內會有目錄建立時間、最近存取的時間與最近修改時間。