第 28 章 萬國碼(2)


緣起

最近下載了一些 PDB 檔或是 UPDB 檔,這兩種檔案是 Palm 的資料庫檔案,可以藉由網站,Online-Convert ,把它們轉換成 *.TXT 檔。只是經由 Online-Convert 轉換而來的 TXT 檔,所採用的換行符號並不是高位元組為 0DH,低位元組為 0AH,而是 0AH,以致於以 Windows 的記事本閱讀時,沒有換行,很不方便。後來想寫個程式,轉換這種檔案變成 UTF-8 編碼的超文字連結檔 ( 就是 HTML 檔 ),又想是否能擴充成都可以把純文字檔都轉換成超文字連結檔?當然首要條件就是要能判斷,這些純文字檔的文字是以哪一種方式編碼?據小木偶所知,純文字檔大概有下面幾種編碼方式:

  1. ASCII 碼:每個字元僅用一個位元組 ( byte ) 表示,且小於 80h,因數量有限故只能使用英文、阿拉伯數字等。
  2. Big-5 ( 大五碼 ):每個中文正體字以一個字組 ( word,一個字組相當於兩個位元組 ) 表示,高位位元組必須在 81h∼0FEh 之間,可以是 81h 或 0FEh;低位位元組必須在 40h∼07Eh 或 0A1h∼0FEh 之間。
  3. UTF-16:在基本多語文字面 ( BMP ) 的字元以一個字組表示,若在輔助字面的字元以兩個字組表示。但是現今世界所用語言的百分之九十五,均可編入基本多語文字面,故僅需考慮前者即可。此外,還分「UTF-16 Little-Endian」與「UTF-16 Big-Endian」兩種。這是由於不同電腦平台,高位位元組放在較高位址或較低位址所致。
  4. UTF-8:依據萬國碼的範圍,會把在基本多語文字面的一個字元編成一到三個位元組不等,而編成數個位元組後,規定第一個位元組必定放在低位址,在網路上傳送時也是先傳送,因此沒有「Little-Endian」或「UTF-16 Big-Endian」的問題。

有關萬國碼的部份,請參閱組合語言附錄十。此外,還有一點要提出來。如果有一篇文章,全為英文或阿拉伯數字,那麼以 ASCII 編碼或 UTF-8 編碼結果是相同的。您可以參考組合語言附錄十的編碼方式,就可以知道,在 80h 以下的字元,以 UTF-8 編碼後的結果與 ASCII 相同。不過習慣上,我們把它歸類為 ASCII 編碼。


判斷一字串或一檔案的編碼方式

接下來,進入本章主題。這一章,小木偶打算寫個副程式,What_Encoding,來判斷某一個字串的編碼方式是 ASCII、Big-5、UTF-8 還是 UTF-16。最後把它變成一個動態連結程式庫。此外,小木偶還要說兩件事。一是這個副程式是有限制的,這是因為純文字檔出現的很早,也沒有組織為它規定標準格式,因此很難百分之百的準確判斷編碼方式。二是這個副程式也可以判斷某個檔案的編碼方式,就某種程度而言,字串可以說是存放在連續記憶體中的資料,如果把檔案讀入記憶體堙A也是存在連續的記憶體內,所以小木偶才這樣講。

原理

雖然剛剛說到,並沒有組織為純文字檔訂定標準統一的格式,但是微軟的「記事本」( notepad.exe ) 在儲存檔案時,使用 Unicode 編碼儲存時會自動在檔案前加上「識別碼」可供辨認。如果您用「記事本」另存新檔時,會看見可以選四種編碼方式,如下圖:

  1. ANSI:選擇以「ANSI」方式儲存,其實就是以「ASCII」編碼儲存,如果您的文章中含有正體中文,那麼會以「Big-5」的編碼方式儲存起來。這時「記事本」不會加上任何「識別碼」。
  2. Unicode:選擇以「Unicode」方式儲存,其實是以萬國碼 UTF-16 Little-Endian 編碼儲存。「記事本」會自動在檔案最前面加上一個字組,0FEFFh,當成識別碼。
  3. Unicode big endian:以這種方式儲存檔案,其實是以萬國碼 UTF-16 Big-Endian 方式儲存。「記事本」會自動在檔案最前面加上一個字組,0FFFEh。
  4. UTF-8:選擇以萬國碼 UTF-8 方式儲存時,「記事本」會自動在檔案最前面加上一個長三個位元組的數值,0BFBBEFh。

但是這只是微軟「記事本」有這樣的功能,其他的文書處理軟體未必遵循此規範。不過微軟財大氣粗,為了相容性,所以有些軟體也依照這種格式,如「UltraEdit-32」就是一個例子。下圖是「UltraEdit-32」存檔時的畫面:

上圖複合控制項 ( combobox ) 清單中的編碼方式有些是與「筆記本」相同,只是名稱不同,另外還多了好幾種編碼方式:

  1. ANSI/ASCII:與筆記本的「ANSI」相同。
  2. UTF-8:和記事本的「UTF-8」相同。
  3. UTF-16:與記事本的「Unicode」相同,會被編成 UTF-16 Little Endian。
  4. UTF-8 - NO BOM:被編碼成 UTF-8 而儲存起來,但是不含「0BFBBEFh」的「BOM」( BOM 中文意義為「位元組順序記號」,英語是 byte-order mark )。那什麼是「BOM」呢?原來「 BOM」指的就是微軟自行定義的「0BFBBEFh」、「0FEFFh」、「0FFFEh」這些位於檔案最前面,指示此檔案編碼方式的數值。
  5. UTF-16 - NO BOM:被儲存成「UTF-16 Little Endian」,但不含「0FEFFh」BOM。
  6. UTF-16 - Big Endian - NO BOM:被編碼成「UTF-16 Big Endian」,但不含「0FFFEh」BOM。
  7. UTF-16 Big Endian:儲存成與記事本的「Unicode big endian」相同格式,含有 BOM。
  8. Unicode - ASCII Escaped:是指字元萬國碼的數值超過 0FFh 的會被儲存成「/u十六進位數值」的形式,而未超過 0FFh 的則以 ASCII 表示。

在 UltraEdit-32 程式堙A你可以選擇把純文字檔存成「NO BOM」也可以選擇存成含有「BOM」的萬國碼。當然,如果選擇前者,那麼就不太容易判斷編碼方式了。言歸正傳,由上面的文書處理器所存成的純文字檔格式,大致可以把編碼方式區分成「ASCII」、「UTF-8」、「UTF-16 Little Endian」、「UTF-16 Big Endian」四種,再加上在萬國碼研發之前,中華民國、香港、新加坡等地,正體中文所用的「Big-5」碼,這樣共有五種編碼方式。

那要如何判斷字元的編碼方式呢?在網路上有許多先進提出各種方法,但此處小木偶僅用最愚笨的方法來判斷。首先要說明的是,使用者要判斷的字串或檔案,全為一些數值 ( 可看成十六進位的數值 ),這些數值是由一連串的數個到很多很多的位元組集合起來,所以是各種字元編碼方式,也有可能根本不是純文字。因此小木偶把所有可能的五種編碼方式檢查完,若還不是那五種,就有可能不是純文字檔了。

原始碼

底下是 ENCODE.ASM 的原始碼,其內含有 what_encoding,是用來判斷一字串或檔案編碼方式,小木偶將把它製作成動態連結程式庫,真正呼叫時需用 Whate_Encoding。

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE
 
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
;進入/離開副程式
DLLEntry        PROC    hInstDLL,dwReason,dwReserved
                mov     eax,1   ;不需事前處理或事後清理,故僅返回TRUE
                ret
DLLEntry        ENDP
;-------------------------------------------------------------------------------------------------------------
;檢查DL是否為10abcdef(亦即第七、六位元分別是1、0),如果是,返回ZR;否則返回NZ
check_utf8_10   PROC
                and     dl,0c0h
                cmp     dl,80h
                ret
check_utf8_10   ENDP
;-------------------------------------------------------------------------------------------------------------
;判斷位址在pString,長度為cLen位元組的字串,為ASCII、Big5、UNICODE編碼方式
;返回時: EAX=0-ASCII
;      =1-BIG5
;      =2-ASCII與BIG5混合
;      =3-UTF-8
;      =4-UTF-16,Little-Endian
;      =5-UTF-16,Big-Endian
;      =6-非純文字檔
;此副程式先檢查是否有BOM,如果有的話設好相對應的EAX值,就離開副程式。如果沒有BOM的話,檢查是否為UTF-8編碼,檢
;查方法是檢驗第一個位元組的高位元是否為1110、0、110、11110,再檢查其後的2、0、1、3個位元組的最高兩位元是否均為
;10,若為否的話則跳到we08;若均符合的話,則再檢查是否整個pString所指字串的每個位元組的最高位元均為0,若均為0,
;表示是ASCII;否則為UTF-8。
 
;what_encoding需要check_utf8_10副程式。
what_encoding   PROC    USES esi pString:DWORD,cLen:DWORD
                LOCAL   cLT80H,cBig5,cOther,cZero,cCrLf:DWORD
                LOCAL   temp0,temp1:DWORD
                mov     esi,pString
;檢查編碼方式,先檢查是否為Window格式的萬國碼(Window格式的萬國碼有BOM,0feffh、0fffeh、0bfbbefh):
        .IF WORD PTR [esi]==0feffh      ;Window格式的萬國碼,以UTF-16,Little-Endian編碼
                mov     eax,4
        .ELSEIF WORD PTR [esi]==0fffeh  ;Window格式的萬國碼,以UTF-16,Big-Endian編碼
                mov     eax,5
        .ELSEIF WORD PTR [esi]==0bbefh
                cmp     BYTE PTR [esi+2],0bfh
                jne     we00
                mov     eax,3           ;Window格式的萬國碼,以UTF-8編碼
        .ELSE                           ;如果都不是以上三種,就得進一步檢查,跳到we00
                jmp     we00
        .ENDIF
                jmp     quit_check
;也有可能不是Window格式的萬國碼,那麼就沒有BOM(事實上,Window也可以儲存無BOM),先檢查是否為UTF-8
;編碼。檢查方法是取cLen個位元組檢查,如果均符合UTF-8編碼方式,那麼ECX為0,執行到we07處時,為ZR。
we00:           mov     ecx,cLen
                mov     cLT80H,0
                mov     cZero,0
                mov     cOther,0        ;儲存奇數位址為0的個數
                mov     cBig5,0         ;儲存偶數位址為0的個數
            ;第一個測試是,還原後的萬國碼是否在0h∼7Fh之間
we01:           mov     al,[esi]
                test    al,80h
                jnz     we04            ;若AL≧80h,則跳到we04
                or      al,al
                jnz     we02
                inc     cZero           ;若此位元組為0,cZero加一
                test    esi,1
                jz      we02
                inc     cOther          ;此位元組為0,且在奇數位址,則cOther加一(可能為Little Endian)
                jmp     we03
we02:           inc     cBig5           ;此位元組為0,且在偶數位址,則cBig5加一(可能為Big Endian)
we03:           inc     cLT80H          ;不管此位元組是否為0,均小於80H時,則cLT80H加一
                inc     esi
                dec     ecx
                jnz     we01
                mov     eax,cZero       ;若ECX=0,表示已經檢查完字串,應為UTF-8編碼,但須進一步確認
                mov     ecx,cLen
                mov     temp0,eax       ;計算EAX佔ECX的百分比,所得百分比存入EAX返回。
                mov     temp1,eax       ;ECX為總數,EAX為ECX的一部份個數
                shl     temp0,6
                shl     eax,2
                shl     temp1,5
                add     eax,temp0
                xor     edx,edx
                add     eax,temp1       ;EAX=EAX*100
                div     ecx             ;EAX=100*EAX/ECX
                cmp     eax,10          ;如果0所佔的比例超過10%以上,跳到we11
                ja      we11
                cmp     ecx,cLT80H
                jne     we07
                xor     eax,eax         ;如果cLen=cLT80H,表示所指字串,全部均小於80H,為ASCII編碼,但
                jmp     quit_check      ;也可以說是UTF-8(不過習慣上還是認為是ASCII)
            ;第二個測試是,還原後的萬國碼是否在80h∼7FFh之間
we04:           and     al,0e0h
                cmp     al,0c0h
                jne     we05
                mov     dl,[esi+1]
                call    check_utf8_10
                jne     we08
                add     esi,2
                sub     ecx,2
                jz      we07
                jmp     we01
            ;第三個測試是,原後的萬國碼是否在800h∼0FFFFh之間
we05:           mov     al,[esi]
                and     al,0f0h         
                cmp     al,0e0h
                jne     we06
                mov     dl,[esi+1]
                call    check_utf8_10
                jne     we08
                mov     dl,[esi+2]
                call    check_utf8_10
                jne     we08
                add     esi,3
                sub     ecx,3
                jz      we07
                jmp     we01
            ;第四個測試是,還原後的萬國碼是否在10000h∼1FFFFFh之間
we06:           mov     al,[esi]
                and     al,0f8h
                cmp     al,0f0h
                jne     we08
                mov     dl,[esi+1]
                call    check_utf8_10
                jne     we08
                mov     dl,[esi+2]
                call    check_utf8_10
                jne     we08
                mov     dl,[esi+3]
                call    check_utf8_10
                jne     we08
                add     esi,4
                sub     ecx,4
                jnz     we01
we07:           mov     eax,3   ;若ZR,則為UTF-8
                jmp     quit_check
;we08處,開始檢查是否為BIG5與ASCII混合。檢查方式是以cLT80H記錄有幾個ASCII字元,cBig5記錄有幾個BIG5字元(如果某
;個位元組在81H∼0FEH之間,而且下個位元組在40H∼7EH之間或0A1H∼0FEH之間,那麼個字組為BIG5碼)。
we08:           mov     esi,pString
                mov     cBig5,0
                mov     cOther,0
                mov     cLT80H,0
                mov     cCrLf,0
                mov     ecx,cLen
we09:           lodsb
    .IF (al==0dh)||(al==0ah)
                inc     cCrLf
    .ELSEIF al<80h
                inc     cLT80H
    .ELSEIF (al>=81h)&&(al<=0feh)
        .IF (BYTE PTR [esi]>=40h)&&(BYTE PTR [esi]<=7eh)
we10:           inc     esi
                inc     cBig5
                dec     ecx
        .ELSEIF (BYTE PTR [esi]>=0a1h)&&(BYTE PTR [esi]<=0feh)
                jmp     we10
        .ELSE
                inc     cOther
        .ENDIF
    .ELSE
                inc     cOther
    .ENDIF
                loop    we09
                cmp     cOther,0
                jnz     we14
        .IF (cBig5!=0)&&(cLT80H==0)
                mov     eax,1   ;pString所指的字串均由BIG5組成
        .ELSEIF (cBig5==0)&&(cLT80H!=0)
                mov     eax,0   ;pString所指的字串均由ASCII組成
       ;.ELSEIF (cBig5==0)&&(cLT80H==0)不可能發生這種情形,不可能不是ASCII,也不是BIG5,也不是其他編碼
        .ELSE
                mov     eax,2   ;pString所指的字串由ASCII與BIG5組成
        .ENDIF
                jmp     quit_check
;若執行到此,編碼方式為無BOM的UTF-16。判斷是UTF-16 Little-Endian(UTF-16LE)或是UTF-16 Big-Endian(UTF-16BE)
we11:           cmp     eax,50          ;若等於50%,表示有一半為零,即pString所指字串為無BOM的
                jne     we14            ;UTF-16,且均為80H以下的文字
                cmp     cOther,0        ;cOther=在奇數位址的位元組,為0的個數
                jz      we12
                cmp     cBig5,0         ;cBig5=在偶數位址的位元組,為0的個數
                jnz     we14
                mov     eax,4           ;若cOther≠0,且cBig5=0,則為UTF16 Little-Endian
                jmp     quit_check
we12:           cmp     cBig5,0
                jz      we14
                mov     eax,5           ;若cOther=0,且cBig5≠0,則為UTF16 Big-Endian
                jmp     quit_check
;如果是純文字檔,應有換行符號,檢查換行符號高位元組為0的為UTF16 LE;低位元組為0的為UTF16 BE
we14:           mov     cZero,0         ;借用來記錄高位元組中,為零的個數
                mov     cOther,0        ;借用來記錄低位元組中,為零的個數
                mov     ecx,cLen
                mov     esi,pString
                shr     ecx,1
we15:           lodsw
        .IF (ax==0dh)||(ax==0ah)
                inc     cZero
        .ELSEIF (ax==0d00h)||(ax==0a00h)
                inc     cOther
        .ENDIF
                loop    we15
        .IF (cZero!=0)&&(cOther==0)
                mov     eax,4   ;換行符號中,高位元組為0的個數很多,但沒有低位元組為0,為UTF16 Little-Endian
        .ELSEIF (cOther!=0)&&(cZero==0)
                mov     eax,5   ;換行符號中,低位元組為0的個數很多,但沒有高位元組為0,為UTF16 Big-Endian
        .ELSE
                mov     eax,6
        .ENDIF
quit_check:     ret
what_encoding   ENDP
;*************************************************************************************************************
                END     DLLEntry

底下是 ENCODE.DEF 檔案,小木偶把 what_encoding 包裝成 What_Encoding 副程式:

1
2
EXPORTS
        What_Encoding=what_encoding

底下是 ENCODE.INC 檔案:

1
What_Encoding   PROTO   :DWORD,:DWORD

組譯與呼叫 What_Encoding

底下說明組譯成動態連結程式庫的方法:

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

 Assembling: encode.asm

E:\HomePage\SOURCE\Win32\VIEWHEX>link /DLL /SUBSYSTEM:WINDOWS /DEF:encode.def encode.obj [Enter]
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

   Creating library encode.lib and object encode.exp

E:\HomePage\SOURCE\Win32\VIEWHEX>

到此,已經得到 ENCODE.DLL、ENCODE.LIB、ENCODE.INC 這三個檔案,當程式設計師要判斷某段字串是用哪一種編碼方式,只需把該段字串的位址及其長度 ( 以位元組為單位 ) 為參數,呼叫 What_Encoding,如下:

        INVOKE  What_Encoding,pString,cLen

返回後,EAX 會有該字串的編碼方式:

EAX編碼方式EAX編碼方式
0ASCII1Big-5
2ASCII 與 BIG5 混合3UTF-8
4UTF-16,Little Endian5UTF-16,Big Endian
6非純文字檔

當然,還要記得在呼叫前,要先宣告 What_Encoding 的原型,或把 ENCODE.INC 包含進來。如下程式碼:

INCLUDE         ENCODE.INC
INCLUDELIB      ENCODE.LIB

解說 What_Encoding 副程式

在 ENCODE.ASM 檔案堛熔 37∼49 行,檢查了所指檔案或字串的開頭是否有 BOM,如果有,依照 BOM 的指示,就能得到編碼方式了。如果沒有 BOM,則跳到第 53 行的 we00 標記處,繼續檢查。

we00 標記處要檢查的是此字串或檔案是否為 UTF-8 編碼。請參考組合語言附錄十或底下的說明,可知 UTF-8 編碼中,一個字元可能由一個位元組,也可能兩個位元組,或三個、四個位元組表示,視其所代表的字元的數值在哪一個範圍,如下表:

原來的 UCS-4
字元範圍
位元結構 經 UTF-8 轉換後
00000000

0000007F

第一個字組:
第二個字組:

第一個位元組:
00000080

000007FF

第一個字組:
第二個字組:

第一個位元組:
第二個位元組:
00000800

0000FFFF
第一個字組:
第二個字組:

第一個位元組:
第二個位元組:
第三個位元組:

00010000

001FFFFF
第一個字組:
第二個字組:

第一個位元組:
第二個位元組:
第三個位元組:
第四個位元組:

ENCODE.ASM 的第 53∼135 行,分四次測試是否均為 UTF-8 編碼,只要有一個位元組不符合上表的編碼方式,就會跳到第 138 行繼續檢查。您仔細觀察上面的表會發現,常常要檢查第 7、6 位元是否為 1、0,因此小木偶寫了個副程式,check_utf8_10,來負責此事。第 53∼135 行的程式碼中,ECX 代表還要測試的位元組數,所以每個測試結束,都會有「dec ecx」、「sub ecx,2」、「sub ecx,3」、「sub ecx,4」使還要檢查的位元組減少 ( 因為檢查過了 ),若一直到 ECX 為 0,都還沒跳到第 138 行處,那表示符合 UTF-8,此時都會跳躍到 we07 標記處。

在執行 What_Encoding 副程式時,並不知道編碼方式,所以也有可能是全篇為純英文文字檔,卻以 ASCII 或 UTF-16 編碼,第 62∼90 行的程式碼是為了檢查這種情形。如果整篇檔案或字串是以 ASCII 編碼的純文字檔,也就是組成檔案或字串的每個位元組均小於 80h 時,以 ASCII 編碼的結果與 UTF-8 編碼的結果是相同的,但習慣上會說是 ASCII ( 事實上,可以把 ASCII 碼當成是 UTF-8 的「子集合」)。小木偶把小於 80h 的位元組個數記錄於 cLT80H,再與檔案或字串長度,cLen,比較,如果兩者相等,表示為 ASCII 碼,見程式碼的 87∼90 行。如果是純英文文字卻以 UTF-16 編碼,那麼會有 50% 的位元組為 0。如果是高位位元組為 0 ( 高位位元組在奇數位址 ),為 UTF-16 Little Endian;如果是低位位元組為 0 ( 低位位元組在偶數位址 ),則為 UTF-16 Big Endian。第 74∼84 行計算 0 所佔的比率,大於 10% 時跳至第 175 行的 we11 處,然後判斷 0 是在奇數還是在偶數位址,以決定是 Little Endian 還是 Big Endian。小木偶以 cOther 記錄奇數位址的位元組為 0 的個數;以 cBig5 記錄偶數位址的位元組為 0 的個數。所以當 cOther 為 0,且 cBig5 不等於 0,表示為 UTF16 Big-Endian;反之為 Little Endian。這段程式在第 175∼186 行。

假如不是 UTF-8,會跳躍至 we08 或 we11 處執行,we08 標記處是判斷是否為 Big-5 編碼,we11 標記處是判斷整篇檔案或字串是以 UTF-16 編碼的純文字檔。檢查「Big-5」碼的方法是,採用判斷某個字組是否在「Big-5」的編碼範圍內。「Big-5」的編碼範圍如下,某個字組的低位元組 ( 也就是第一個位元組 )在 81h∼0FEh 之間,而且高位元組 ( 第二個位元組 ) 是在 40h∼7Eh 之間或在 0A1h∼0FEh 之間。假如一字串或一檔案除了換行符號外,其餘均符合「Big-5」的編碼範圍,那麼此字串或檔案全由「Big-5」碼的中文字編成;假如除了換行符號、以及「Big-5」的編碼範圍內的文字以外,還有小於 80h 的數值,那麼應該就是一個夾雜著中英文的字串或檔案;假如除了上面情形之外,還有其他範圍的數值,抑或是低位位元組符合「Big-5」的編碼範圍內,但高位位元組不符合,那就表示不是「Big-5」碼也不是中英文夾雜的情形,會跳到 we14 處。小木偶以 cBig5 記錄以 Big-5 編碼的字元個數,以 cLT80H 記錄 ASCII 編碼的字元個數,以 cOther 記錄非 ASCII 也非 Big5 的編碼個數。這段程式碼在第 136∼173 行。

如果執行到 we14 標記處,表示不是 UTF-8、純 ASCII、Big-5 編碼,有可能是 UTF-16,也有可能根本不是純文字檔。除非能把每個字元組織起來,並有其意義,才有可能判斷是 UTF-16 或非純文字檔。小木偶想了許久,純文字檔應該會分段,所以或許能用換行符號判斷純文字檔,而把它和執行檔、圖形檔、影音檔、壓縮檔等分開。第 188∼206 行,判斷換行符號是的排列方式為 00 0D 或 0D 00,決定為 Big Endian 或 Little Endian。

What_Encoding 的限制

What_Encoding 副程式無法百分之百的判斷純文字檔的編碼方式,尤其是一個很短的字串。如果字串夠長準確度就越高。


VIEWHEX 程式

紙上談兵不如實作,底下小木偶實作一個程式,VIEWHEX,它會呼叫 What_Encoding 副程式,把編碼方式顯示出來,如下圖所示。當使用者按下「開啟」時,會出現「開啟舊檔」通用對話盒,使用者可挑選一個檔案。VIEWHEX 能把該檔案的內容,以十六進位方式顯示在一個子視窗內。使用者可以按鍵盤上的「PgUp」、「PgDn」、「Home」、「End」鍵,就能顯示上一頁、下一頁、第一頁與最後一頁;使用者也可以把滑鼠游標移到子視窗右上角 ( 紫色框框的區域 ),按下滑鼠右鍵,可以切換萬國碼與 ASCII ( 萬國碼部份只能顯示 UTF-16 Little Endian )。

VIEWHEX 的原始碼

底下是 VIEWHEX.EXE.MANIFEST:

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

底下是 VIEWHEX.RC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "c:\masm32\include\resource.h"
#define IDB_OPEN        20000
#define IDS_FILENAME    20001
#define IDS_FILEDATA    20002
#define IDS_ENCODE      20003
#define IDC_HEX         20004
#define IDB_QUIT        20005
#define RT_MANIFEST     24
 
VIEWHEX DIALOG  200,100,416,203
STYLE   WS_CAPTION|WS_VISIBLE|WS_SYSMENU
FONT    9,"MS Sans Serif"
CAPTION "觀看檔案內容"
BEGIN
  PUSHBUTTON "開啟",IDB_OPEN, 4,  3, 50,14
  LTEXT      "",IDS_FILENAME,58,  3,354,10
  LTEXT      "",IDS_FILEDATA 58, 13,354,10
  LTEXT      "",IDS_ENCODE,  58, 23,354,10
  PUSHBUTTON "離開",IDB_QUIT, 4, 18, 50,14
END
 
EYE     ICON viewer02.ico
 
1       RT_MANIFEST MOVEABLE PURE "VIEWHEX.EXE.MANIFEST"

底下是 VIEWHEX.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
IDB_OPEN        EQU     20000
IDS_FILENAME    EQU     20001
IDS_FILEDATA    EQU     20002
IDS_ENCODE      EQU     20003
IDC_HEX         EQU     20004
IDB_QUIT        EQU     20005
 
__UNICODE__     EQU     1
bgColor         EQU     0       ;顯示十六進位內容子視窗的背景色
frColor         EQU     0ff00h  ;顯示十六進位內容子視窗的文字顏色
HexWidth        EQU     610
HexHeight       EQU     268
FontHeight      EQU     15      ;邏輯字形高度
FontWidth       EQU     7       ;邏輯字形寬度
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         COMDLG32.INC
INCLUDE         GDI32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      COMDLG32.LIB
INCLUDELIB      GDI32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
 
INCLUDE         ENCODE.INC
INCLUDELIB      ENCODE.LIB
;*************************************************************************************************************
.CONST
szDlgName       DW      56h,49h,45h,57h,48h,45h,58h,0h                                  ;VIEWHEX
szIconName      DW      45h,59h,45h,0h                                                  ;EYE
szHexClassName  DW      56h,69h,65h,77h,48h,65h,78h,61h,64h,65h,63h,69h,6dh,61h,6ch,0   ;ViewHexadecimal
szFilter        DW      53efh,57f7h,884ch,6a94h,28h,2ah,2eh,45h,58h,45h,29h,0h,2ah      ;可執行檔(*.EXE),0,*
                DW      2eh,45h,58h,45h,0                                               ;.EXE,0
                DW      7d14h,6587h,5b57h,6a94h,28h,2ah,2eh,54h,58h,54h,29h,0h,2ah      ;純文字檔(*.TXT),0,*
                DW      2eh,54h,58h,54h,0                                               ;TXT,0
                DW      8d85h,6587h,5b57h,9023h,7d50h,6a94h,28h,2ah,2eh,48h,54h,4dh     ;超文字連結檔(*.HTM
                DW      4ch,2ch,2ah,2eh,48h,54h,4dh,29h,0h,2ah,2eh,48h,54h,4dh,4ch,3bh  ;L,*.HTM),0,*.HTML;
                DW      2ah,2eh,48h,54h,4dh,0h                                          ;*.HTM,0
                DW      6240h,6709h,6a94h,6848h,28h,2ah,2eh,2ah,29h,0,2ah,2eh,2ah,0,0   ;所有檔案(*.*),0,*.*
szCrtDateFmt    DW      5efah,6a94h,6642h,9593h,0ff1ah,67h,67h,79h,79h,79h,79h,5e74h    ;建檔時間:ggyyyy年
                DW      4dh,6708h,64h,64h,65e5h,0h                                      ;M月dd日
szCrtTimeFmt    DW      20h,48h,48h,3ah,6dh,6dh,3ah,73h,73h,3000h,3000h,0               ; HH:mm:ss
szFileSizeFmt   DW      6a94h,6848h,5927h,5c0fh,0ff1ah,25h,49h,36h,34h,64h,4f4dh        ;檔案大小:%I64d位
                DW      5143h,7d44h,28h,62h,79h,74h,65h,73h,29h,0h                      ;元組(bytes)
szASCII         DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,41h,53h,43h,49h,49h,0            ;ASCII
szBig5          DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,42h,49h,47h,2dh,35h,0            ;BIG-5
szASCII_Big5    DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,41h,53h,43h,49h,49h,3001h,42h,\  ;ASCII、BIG-5混合
                        49h,47h,2dh,35h,6df7h,5408h,0h
szUTF8          DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,55h,54h,46h,2dh,38h,0            ;UTF-8
szUTF16LE       DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,55h,54h,46h,2dh,31h,36h,20h,4ch,\;UTF-16 Little-Endian
                        69h,74h,74h,6ch,65h,2dh,45h,6eh,64h,69h,61h,6eh,0
szUTF16BE       DW      7de8h,78bch,65b9h,5f0fh,0ff1ah,55h,54h,46h,2dh,31h,36h,20h,42h,\;UTF-16 Big-Endian
                        69h,67h,2dh,45h,6eh,64h,69h,61h,6eh,0
szNotText       DW      975eh,7d14h,6587h,5b57h,6a94h,0h                                ;非純文字檔
pszEncode       LPSTR   OFFSET szASCII,OFFSET szBig5,OFFSET szASCII_Big5,OFFSET szUTF8
                LPSTR   OFFSET szUTF16LE,OFFSET szUTF16BE,OFFSET szNotText
szAddrFmt       DW      25h,30h,38h,58h,28h,25h,30h,39h,64h,29h,0h      ;%08X(%09d)
szFontFace      DW      43h,6fh,75h,72h,69h,65h,72h,20h,4eh,65h,77h,0h  ;Courier New
szNoFile        DW      5c1ah,672ah,9078h,64c7h,6a94h,6848h,0h  ;尚未選擇檔案
szOffsetAddr    DW      504fh,79fbh,4f4dh,5740h,0h              ;偏移位址
szHex           DW      5341h,516dh,9032h,4f4dh,5167h,5bb9h,0h  ;十六進位內容
szUnicodeTitle  DW      842ch,570bh,78bch,0     ;萬國碼
szASCIITitle    DW      41h,53h,43h,49h,49h,0   ;ASCII
rcTitle         RECT    <0,0,608,FontHeight>    ;標題欄範圍
rcTitleAddr     RECT    <0,0,133,FontHeight>    ;標題欄堛漲鴔}剪裁矩形
rcTitleHex      RECT    <134,0,484,FontHeight>  ;標題欄堛漱Q六進位數值剪裁矩形
rcTitleStr      RECT    <485,0,608,FontHeight>  ;標題欄堛漲r串剪裁矩形
;*************************************************************************************************************
.DATA
hInstance       DD      ?
hCtrlHex        HWND    ?
hMem            HGLOBAL ?
bgColorControl  DD      ?       ;子視窗控件的背景顏色
dwFileSize      DD      ?       ;檔案大小,以位元組為單位
;編碼方式:0-ASCII。1-BIG5。2-ASCII與BIG5混合。3-UTF-8。4-UTF-16。Little-Endian。5-UTF-16,Big-Endian。
dwEncode        DD      ?       ;6-非純文字檔
szFilename      DW      MAX_PATH DUP (0)
flag            DD      0
;位元0:0-表示尚未開啟檔案;1-表示已開啟某個檔案
;位元1:0-表示在IDC_HEX右邊為ASCII或BIG5;1-表示萬國碼
;位元2:0-表示未到檔案結尾;1表示已為檔案尾端
pFirstByte      DD      ?       ;顯示頁的第一個位址,此位址為相對於hMem的偏移位址
pMax1stByte     DD      ?       ;顯示頁中的第一個位元組的偏移位址不可超過pMax1stByte
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
display_page    PROC    USES esi hwnd:HWND,hdc:HDC
                LOCAL   hFont,hbrTitle:HANDLE
                LOCAL   rcDisplay:RECT  ;剪裁矩形
                LOCAL   cRow,cHex:DWORD ;每一顯示頁有16個橫列,每一橫列有16個位元組
                LOCAL   buffer[20h]:WORD
                LOCAL   string[20]:WORD
            ;建立邏輯字形
                INVOKE  CreateFont,FontHeight,FontWidth,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,\
                        CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH or FF_DONTCARE,OFFSET szFontFace
                mov     hFont,eax
                INVOKE  SelectObject,hdc,hFont
                INVOKE  DeleteObject,eax             ;設定標題欄顏色為黃底黑字
                INVOKE  CreateSolidBrush,0ffffh
                mov     hbrTitle,eax
                INVOKE  FillRect,hdc,OFFSET rcTitle,hbrTitle    ;黃色畫刷塗滿標題欄
                INVOKE  DeleteObject,hbrTitle
                INVOKE  SetBkColor,hdc,0ffffh
                INVOKE  SetTextColor,hdc,0
            ;若未開啟並讀取檔案
                test    flag,1
                jnz     dp00
                INVOKE  DrawText,hdc,OFFSET szNoFile,-1,OFFSET rcTitle,DT_CENTER
                jmp     dp99
dp00:       ;顯示標題
                INVOKE  DrawText,hdc,OFFSET szOffsetAddr,-1,OFFSET rcTitleAddr,DT_CENTER
                INVOKE  DrawText,hdc,OFFSET szHex,-1,OFFSET rcTitleHex,DT_CENTER
                test    flag,2
            .IF ZERO?
                mov     edx,OFFSET szASCIITitle
            .ELSE
                mov     edx,OFFSET szUnicodeTitle
            .ENDIF
                INVOKE  DrawText,hdc,edx,-1,OFFSET rcTitleStr,DT_CENTER
            ;開始繪出整個顯示頁,先繪製各行的偏移位址,此偏移位址由檔案起頭開始算起
                INVOKE  SetBkColor,hdc,bgColor
                INVOKE  SetTextColor,hdc,frColor
                mov     cRow,10h
                mov     esi,pFirstByte
                mov     rcDisplay.top,FontHeight
                mov     rcDisplay.bottom,FontHeight*2
            ;印出最左邊的偏移位址
dp01:           INVOKE  wsprintf,ADDR buffer,OFFSET szAddrFmt,esi,esi
                mov     rcDisplay.left,0
                mov     rcDisplay.right,133
                INVOKE  DrawText,hdc,ADDR buffer,-1,ADDR rcDisplay,DT_CENTER
            ;印出中間部份的十六進位內容
                mov     rcDisplay.left,144
                mov     rcDisplay.right,144
                mov     ecx,FontWidth
                lea     edi,string
                shl     ecx,1
                add     rcDisplay.right,ecx
                mov     cHex,10h        ;每一行有16個位元組的資料
dp02:           mov     edx,esi
                cmp     esi,dwFileSize
                jne     dp03
                or      flag,4          ;如果已到檔案尾端,印出十六進位內容停止,並設定flag的第
                jmp     dp04            ;3位元為一,以便只印出右邊的字串一次即可
dp03:           add     edx,hMem
                movzx   eax,BYTE PTR [edx]
                lea     ecx,buffer
                stosb
                call    al_to_hex
                INVOKE  DrawText,hdc,ADDR buffer,2,ADDR rcDisplay,DT_LEFT
            ;計算下一個rcDisplay
                mov     ecx,FontWidth
                shl     ecx,1
                add     ecx,FontWidth   ;ECX=FontWidth*3
                add     rcDisplay.left,ecx
                add     rcDisplay.right,ecx
                inc     esi
                dec     cHex
                jnz     dp02
dp04:       ;印出右邊的萬國碼字串或ASCII字串
                sub     eax,eax
                stosw
                mov     rcDisplay.left,485
                mov     rcDisplay.right,485+FontWidth*16
                test    flag,2
            .IF ZERO?
                INVOKE  DrawTextA,hdc,ADDR string,-1,ADDR rcDisplay,DT_LEFT or DT_SINGLELINE
            .ELSE
                INVOKE  DrawTextW,hdc,ADDR string,-1,ADDR rcDisplay,DT_LEFT or DT_SINGLELINE
            .ENDIF
                test    flag,4
                jnz     dp99
                add     rcDisplay.top,FontHeight
                add     rcDisplay.bottom,FontHeight
                dec     cRow
                jnz     dp01
dp99:           and     flag,0fffffffbh
                ret
display_page    ENDP
;-------------------------------------------------------------------------------------------------------------
;把AL的內容變成十六進位萬國碼,存於EDI所指位址,每個AL需要四個位元組空間存放
al_to_hex       PROC
                mov     dl,al
                shr     al,4
                and     dl,0fh
                call    trans_and_save
                mov     al,dl
trans_and_save: add     al,"0"
                cmp     al,"9"
                jbe     ok00
                add     al,7
ok00:           mov     [ecx],ax
                add     ecx,2
                ret
al_to_hex       ENDP
;-------------------------------------------------------------------------------------------------------------
;開啟並讀取檔案內容,如果開啟成功,hMem會有檔案內容,並把檔案資料(建檔時間、檔案大小、編碼方式)存於szFileData
;堙A然後關閉檔案,返回的EAX=1;如果失敗,EAX=0
open_and_read   PROC    hdlg:HWND
                LOCAL   ofn:OPENFILENAME
                LOCAL   fi:BY_HANDLE_FILE_INFORMATION
                LOCAL   ft:FILETIME,syst:SYSTEMTIME
                LOCAL   hFile:HFILE
                LOCAL   NumberOfBytesRead:DWORD ;已讀入的檔案大小
                LOCAL   szFileData[100h]:WORD
                mov     eax,hdlg
                mov     ecx,hInstance
                mov     edx,OFFSET szFilename
                mov     ofn.lStructSize,SIZEOF OPENFILENAME
                mov     ofn.hwndOwner,eax
                mov     ofn.hInstance,ecx
                mov     ofn.lpstrFilter,OFFSET szFilter
                mov     ofn.lpstrCustomFilter,0
                mov     ofn.nMaxCustFilter,0
                mov     ofn.nFilterIndex,2
                mov     ofn.lpstrFile,edx
                mov     WORD PTR [edx],0
                mov     ofn.nMaxFile,MAX_PATH
                mov     ofn.lpstrFileTitle,0
                mov     ofn.nMaxFileTitle,MAX_PATH
                mov     ofn.lpstrInitialDir,0
                mov     ofn.lpstrTitle,0
                mov     ofn.Flags,OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_HIDEREADONLY
                INVOKE  GetOpenFileName,ADDR ofn
        ;如果使用者按開啟舊檔對話盒的「取消」按鈕
        .IF eax==0
                INVOKE  GlobalFree,hMem
                mov     hMem,0
                and     flag,0fffffffeh
                INVOKE  SetDlgItemText,hdlg,IDS_FILENAME,OFFSET szHex+12
                INVOKE  SetDlgItemText,hdlg,IDS_FILEDATA,OFFSET szHex+12
                INVOKE  SetDlgItemText,hdlg,IDS_ENCODE,OFFSET szHex+12
                jmp     @f
        .ENDIF
                INVOKE  CreateFile,OFFSET szFilename,GENERIC_READ,0,0,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
        .IF eax==INVALID_HANDLE_VALUE
@@:             xor     eax,eax
        .ELSE
                mov     hFile,eax
            .IF hMem!=0 ;如果使用者已經開啟過某一個檔案,hMem不為零,先釋放舊的記憶體區塊
                INVOKE  GlobalFree,hMem
            .ENDIF
                INVOKE  GetFileInformationByHandle,hFile,ADDR fi
                mov     eax,fi.nFileSizeLow
                mov     dwFileSize,eax
                mov     edx,eax
                add     eax,200h
                and     eax,0fffffff0h
                and     edx,0ffffff00h
                mov     pMax1stByte,edx
                INVOKE  GlobalAlloc,GPTR,eax
                mov     hMem,eax
                INVOKE  ReadFile,hFile,hMem,dwFileSize,ADDR NumberOfBytesRead,0
                INVOKE  CloseHandle,hFile
            ;把IDS_FILENAME靜態控制項標題設為檔案名稱
                INVOKE  SetDlgItemText,hdlg,IDS_FILENAME,OFFSET szFilename
            ;取得檔案建立時間與檔案大小
                push    edi
                INVOKE  FileTimeToLocalFileTime,ADDR fi.ftCreationTime,ADDR ft  ;把UTC時間變成當地時間(中華民國標準時間)
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst                  ;把FILETIME格式的當地時間變成SYSTEMTIME格式
                lea     edi,szFileData
                INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szCrtDateFmt,edi,SIZEOF szFileData
                dec     eax
                shl     eax,1
                mov     ecx,SIZEOF szFileData
                add     edi,eax
                sub     ecx,eax
                INVOKE  GetTimeFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szCrtTimeFmt,edi,ecx
                dec     eax
                shl     eax,1
                add     edi,eax
            ;把檔案大小存入EDI所指位址,此位址在szFileData
                INVOKE  wsprintf,edi,OFFSET szFileSizeFmt,fi.nFileSizeLow,fi.nFileSizeHigh
                INVOKE  SetDlgItemText,hdlg,IDS_FILEDATA,ADDR szFileData
                pop     edi
            ;檢查檔案編碼方式為ASCII、UNICODE(UTF-16、UTF-8)、……
                INVOKE  What_Encoding,hMem,dwFileSize
                mov     dwEncode,eax
                shl     eax,2
                mov     edx,pszEncode[eax]
                INVOKE  SetDlgItemText,hdlg,IDS_ENCODE,edx
                mov     eax,TRUE
                mov     pFirstByte,0
                or      flag,1
        .ENDIF
                ret
open_and_read   ENDP
;-------------------------------------------------------------------------------------------------------------
HexProc         PROC    hWndHex:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   ps:PAINTSTRUCT
.IF uMsg==WM_PAINT
                INVOKE  BeginPaint,hWndHex,ADDR ps
                INVOKE  display_page,hWndHex,ps.hdc
                INVOKE  EndPaint,hWndHex,ADDR ps
 
.ELSEIF uMsg==WM_KEYUP
                mov     ecx,100h
        .IF wParam==VK_PRIOR            ;按下Page Up鍵
                sub     pFirstByte,ecx
                jge     key_up_ok
                mov     pFirstByte,0
        .ELSEIF wParam==VK_NEXT         ;按下Page Down鍵
                add     pFirstByte,ecx
                mov     eax,pMax1stByte
                cmp     eax,pFirstByte
                jae     key_up_ok
                mov     pFirstByte,eax
        .ELSEIF wParam==VK_HOME         ;按下Home鍵
                mov     pFirstByte,0
        .ELSEIF wParam==VK_END          ;按下End鍵
                mov     eax,pMax1stByte
                mov     pFirstByte,eax
        .ENDIF
key_up_ok:      INVOKE  InvalidateRect,hCtrlHex,0,TRUE
 
.ELSEIF uMsg==WM_LBUTTONUP
                mov     eax,lParam
                mov     edx,lParam
                and     eax,0ffffh      ;EAX=滑鼠所點擊的X座標
                shr     edx,10h         ;EDX=滑鼠所點擊的Y座標
        .IF (eax>485)&&(eax<=608)&&(edx<=FontHeight)
                xor     flag,2
                INVOKE  InvalidateRect,hCtrlHex,0,TRUE
        .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
                INVOKE  DestroyWindow,hWndHex
 
.ELSEIF uMsg==WM_DESTROY
                INVOKE  PostQuitMessage,NULL
 
.ELSE
                INVOKE  DefWindowProc,hWndHex,uMsg,wParam,lParam
                ret
.ENDIF
                xor     eax,eax
                ret
HexProc         ENDP
;-------------------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   wc:WNDCLASSEX
.IF uMsg==WM_COMMAND
                mov     edx,wParam
                mov     eax,wParam
                shr     edx,10h         ;EDX=通知碼
                and     eax,0ffffh      ;EAX=控制元件識別碼
   .IF dx==BN_CLICKED
        .IF ax==IDB_OPEN
                INVOKE  open_and_read,hDlg
                INVOKE  InvalidateRect,hCtrlHex,0,TRUE
                INVOKE  SetFocus,hCtrlHex       ;把IDC_HEX設為具有輸入焦點
        .ELSEIF ax==IDB_QUIT
                jmp     quit
        .ENDIF
   .ENDIF
 
.ELSEIF uMsg==WM_INITDIALOG
              ;註冊子視窗類別
                mov     ecx,hInstance
                mov     wc.cbSize,SIZEOF WNDCLASSEX
                mov     wc.style,CS_HREDRAW or CS_VREDRAW
                mov     wc.lpfnWndProc,OFFSET HexProc
                mov     wc.cbClsExtra,0
                mov     wc.cbWndExtra,0
                mov     wc.hInstance,ecx
                mov     wc.hIcon,0
                INVOKE  LoadCursor,NULL,IDC_ARROW
                mov     wc.hCursor,eax
                INVOKE  CreateSolidBrush,bgColor
                mov     wc.hbrBackground,eax
                mov     wc.lpszMenuName,0
                mov     wc.lpszClassName,OFFSET szHexClassName
                mov     wc.hIconSm,0
                INVOKE  RegisterClassEx,ADDR wc
              ;以上面的視窗類別,建立子視窗
                INVOKE  CreateWindowEx,0,OFFSET szHexClassName,0,WS_CHILD or WS_BORDER or WS_TABSTOP,6,54,\
                        HexWidth,HexHeight,hDlg,IDC_HEX,hInstance,0
                mov     hCtrlHex,eax
                INVOKE  ShowWindow,hCtrlHex,SW_SHOWDEFAULT
              ;載入圖示並以此設定對話盒圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
                INVOKE  GetSysColor,COLOR_3DFACE
                mov     bgColorControl,eax
 
.ELSEIF uMsg==WM_CLOSE
quit:           INVOKE  GlobalFree,hMem
                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
                INVOKE  InitCommonControls
;*************************************************************************************************************
END             start

把圖示檔 VIEWER02.ico 與 VIEWHEX.ASM、VIEWHEX.RC、VIEWHEX.EXE.MANIFEST、ENCODE.INC、ENCODE.LIB、ENCODE.DLL 七個檔案全放在同一子目錄堙A然後依下面步驟組譯並連結:

E:\HomePage\SOURCE\Win32\VIEWHEX>rc viewhex.rc [Enter]

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

 Assembling: viewhex.asm

*************
UNICODE Build
*************

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

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

E:\HomePage\SOURCE\Win32\VIEWHEX>

最後可得 VIEWHEX.EXE 可執行檔,它須與 ENCODE.DLL 放在同一子目錄才可正常運作。

解說 VIEWHEX.ASM

在對話盒函式中建立 hCtrlHex 子視窗

VIEWHEX 由第 406 行開始執行,到第 408 行則建立一個對話盒,此對話盒依據 VIEWHEX.RC 內的對話盒模板建立了兩個按鈕與三行靜態控件,而佔對話盒最大區域、綠字黑底的子視窗,則是在對話盒函式堙A處理 WM_INITDIALOG 時建立的 ( 第 365∼386 行 ),建立好後把它的視窗代碼,存入 hCtrlHex 堙ChCtrlHex 子視窗,並不是 Win32 系統內所預建的控件,因為它們不符合小木偶的需求。編輯框控件在載入大檔案時,會花很多時間處理,才能顯示出來,因此小木偶才自行建立這個子視窗。hCtrlHex 的產生,也證明我們可以在對話盒程式中自己設計子視窗

對話盒函式主要處理兩個訊息:一是 WM_COMMAND,另一個是 WM_INITDIALOG 訊息。在 WM_COMMAND 訊息中,主要是處理當使用者按下「開啟」按鈕的過程。首先會呼叫 open_and_read 副程式,此副程式會呼叫 GetOpenFileName API,取得將要開啟的檔案名稱 ( 第 213∼231 行 ),如果成功,則開啟舊檔案 ( 第 242 行 )。在讀取檔案內容之前,先釋放原先的記憶體區塊 ( 第 247∼249 行 )。為何要先釋放記憶體區塊呢?這是因為當使用者開啟一檔案後,如又開啟另一個檔案,則存有原先的檔案內容的記憶體區塊便無用了,為不耗費多餘資源,故先將其釋放。等一切完成後,才取得檔案大小、建立時間、依檔案大小配置稍多一點新的記憶體、讀取檔案內容到此記憶體內、呼叫 What_Encoding 判斷編碼方式,最後使三個靜態控件顯示檔名檔案大小與編碼方式 ( 第 247∼291 行 )。

hCtrlHex 子視窗的視窗函式-- HexProc

hCtrlHex 子視窗的視窗函式在第 369 行指定,為 HexProc 副程式。它負責處理 WM_PAINT、WM_KEYUP、WM_LBUTTONUP 三個訊息。WM_PAINT 會呼叫 display_page,display_page 會依據是否已經開啟檔案,在 hCtrlHex 子視窗內顯示檔案內容或者使 hCtrlHex 空白 ( 第 113∼115 行 )。其方法是設定 flag 變數的第 0 位元,為一時表示已開啟檔案 ( 在 open_and_read 副程式中,如果成功讀取檔案內容時,此位元會被設定,第 291 行 );為零時表示未開啟檔案 ( 第 236 行 )。如果已開啟檔案,那麼 display_page 會根據 pFirstByte 變數,把包含此變數以後的 256 個位元組的資料,顯示在 hCtrlHex 視窗堙C

在 dispaly_page 堨i分成三部份顯示,左邊的偏移位址,中間的十六進位內容,右邊的字串。左邊的偏移位址的基準點是由檔案起頭開始,因此在成功開啟檔案後,需要把 pFirstByte 設為 0 ( 第 290 行 )。右邊的字串,會依據黃色標題欄顯示「ASCII」或「萬國碼」來解釋字串。這也是利用 flag 中的第一個位元決定,如果此位元為零,表示顯示 ASCII;否則顯示萬國碼 ( 此處的萬國碼僅能顯示 UCS-2 字集。每顯示中間的十六進位內容一橫列的資料,display_page 就會把該列的資料存入 string 區域變數 ( 第 155 行 ),等中間的十六進位內容顯示完畢,就會以呼叫 DrawTextA 或 DrawTextW 的方式顯示 string ( 第 172∼177 行 )。

HexProc 也得處理 WM_KEYUP 訊息,這是為了讓使用者按下鍵盤上的「PgUp」、「PgDn」、「Home」、「End」鍵,能顯示上一頁、下一頁、第一頁與最後一頁。只要改變 pFirstByte 變數即可,例如當按下「Home」鍵時,使 pFirstByte 變為 0;按下「PgDn」鍵時,使 pFirstByte 增加 100h。當然要注意 pFirstByte 是有範圍的,不可小於零,也不會超過 pMax1stByte。pMax1stByte 的大小顯然與檔案大小有關,在開啟檔案時就可求得,見第 251∼257 行。在處理完按鍵後,呼叫 InvalidateRect 使 hCtrlHex 重新繪製內容。另外,要使 HexProc 能接收到來自按鍵訊息,必須使 hCtrlHex 具有輸入焦點,這一程式片段應在使用者開啟檔案之後,即刻呼叫 SetFocus ( 見第 385 行 )。

在 HexProc 堙A還要處理滑鼠左鍵的事件。這個事件是當使用者在 hCtrlHex 右上角的黃色標題欄中點按時,切換右邊顯示的字串為「ASCII」或「萬國碼」。見第 323∼331 行。


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