第 27 章 萬國碼(1)


本章內容

這一章,小木偶要談的是如何在 Win32 系統中,使用萬國碼撰寫組合語言程式。這些內容很多而且複雜,所以只能談一部份,可分為底下幾個段落:

  1. 在 MASM32 堥洏庛U國碼的方法。可直接閱讀。
  2. 使用資源描述檔的組合語言程式,怎麼使用萬國碼。可直接閱讀。
  3. 多語言界面程式,亦即可以讓使用者從多種語言中,選擇哪一種語言做為界面。可直接閱讀。
  4. 一個工具程式,能把 ANSI 編碼的字串轉變成萬國碼字串,而把組成此字串每個字元,以十六進位表示其數值。可直接閱讀。

在 MASM32 堥洏庛U國碼的方法

在組合語言所製作的程式中,要使用萬國碼 ( 有關萬國碼的介紹,請按這裡 ) 編碼,可分為幾方面來說:作業系統、組譯器、文書處理器、包含檔 ( *.INC ) 四方面來說明:

  1. 早期,微軟把 Win32 作業系統可分兩大類,一是供一般客戶使用的 Windows 9x/Me,另一個是供伺服器使用的 Windows NT。前者僅支援 ASCII,並不支援萬國碼,後者是支援萬國碼的。到了網際網路盛行的年代,萬國碼已經是必須發展的趨勢,否則難以與全世界說不同語言的人溝通。這時候,微軟的作業系統由 Windows 9x/Me/NT 晉陞為 Windows XP,也悄悄的把內碼換成萬國碼。不過為了與舊程式相容,Windows XP 仍然能使用 ANSI/ASCII ( ANSI 與 ASCII 的關係,請參考「組合語言附錄十」)。事實上,Windows XP 內含兩種處理字元的 API:ANSI 版與 UNICODE 版,例如 MessageBox 分為 MessageBoxA 與 MessageBoxW 兩種 ( 註一 ),前者是 ANSI 版,後者是 UNICODE 版。當應用程式所傳來的字串,如果是以 ANSI 編碼的,那麼應用程式會呼叫 ANSI 版的 API,這 ANSI 版的 API 會先轉換成萬國碼再呼叫 UNICODE 版的 API 加以處理;如果應用程式傳來的字串是以萬國碼編碼,就直接呼叫 UNICODE 版的 API 處理。所以由作業系統來看,組合語言是可以使用萬國碼的。
  2. MASM32 所使用的組譯器 ( assembler ) 是微軟的 MASM 6.x,它是民國 81 年發售的產品,已經是二十多年前的事了。那時候萬國碼才剛剛推動,所以 MASM 6.0 乃至到 MASM 6.15 都未考慮支援萬國碼。換句話說,用 MASM32/MASM 6.x 所組譯的原始程式必須是以 ANSI ( 也可以是說 ASCII ) 格式所儲存的純文字檔。就組譯器這點而言,在處理字元、字串時,得小心處理萬國碼。
  3. 文書處理器主要是用於編寫組合語言原始程式 ( source file )。在 Win32 系統所執行的文書處理器多能儲存成不同格式,包含萬國碼、ASCII/ANSI,甚至是 UFT-8、UFT-16 等等。不過因為組譯器只能處理 ASCII/ANSI 格式的原始程式,所以也只能儲存成 ASCII/ANSI 格式。
  4. 包含檔內定義了許多 Win32 內的常數與 API 原型。如果要使用萬國碼,推薦使用 MASM32 v11 及其以後的版本。早期的 MASM32 並不支援萬國碼,不過到了 MASM32 v11 以後,開始支援萬國碼。也就是說,以包含檔來看,您必須下載 MASM32 v11 或以後的版本,才能支援萬國碼。

由前段的說明,讀者應當可以明瞭,目前要在 Win32 組合語言中使用萬國碼,還是不太容易。主要原因是組譯器不支援,但是也並非無法以組合語言寫出萬國碼編碼的程式。要達成這個目的,首先應該下載 MASM32 v11 或以後的版本,其次文書處理軟體所撰寫的原始程式必須以 ASCII/ANSI 格式儲存成 *.ASM 檔。在撰寫原始程式時,還須注意幾件事,才能寫出支援萬國碼的程式。於是此章誕生。

在 MASM32 舊版本 ( 如 MASM32 v7.0 ),並沒有支援萬國碼,那時在 *.INC 檔內,處理字元的 API 都是先定義 ANSI 版本,再把去掉最後面的「A」所得的 API 名稱定義為 ANSI 版的 API。例如,在 USER32.INC 堙A有關 MessageBox 的定義像下面這樣:

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
MessageBox  equ <MessageBoxA>

因此,在組合語言原始程式中,只需寫「INVOKE MessageBox」就能呼叫 MessageBox 了,而且是呼叫 MessageBoxA。到了 MASM32 v11 時,MessageBox 的定義變成下面這樣:

MessageBoxA PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
IFNDEF __UNICODE__
  MessageBox equ <MessageBoxA>
ENDIF

MessageBoxW PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
IFDEF __UNICODE__
  MessageBox equ <MessageBoxW>
ENDIF

這兩段條件組譯是 MASM 的特色之一。「IFNDEF」是指如果未定義後面的變數,「__UNICODE__」,那麼 MessageBox 就等於 MessageBoxA,亦即如果呼叫 MessageBox,其實就是呼叫 MessageBoxA;「IFDEF」則表示如果定義了「__UNICODE__」,那麼呼叫 MessageBox 其實就是呼叫 MessageBoxW。而其他有牽涉到處理文字的 Win32 API,也都有類似的定義。

換句話說,在 MASM32 v11 堙A要實行呼叫 UNICODE 版本的 API,首先要在原始碼一開始,至少是在包含 INCLUDE 檔之前,先定義「__UNICODE__」變數。定義的方法如下:

__UNICODE__ EQU 1

第二個步驟就是要把字串改成以萬國碼編碼。對於英文字母與阿拉伯數字而言很簡單,因為英文字母與阿拉伯數字的萬國碼只是 ASCII 字串的每個位元組變成兩個位元組,而高位元組設為零,就完成了。這件事也不需要以人工方式完成,在 MASM32 v11 版堙A已經完成了一個巨集指令,可以幫我們完成這件事。那是在「c:\masm32\macros\macros.asm」堶悸滿uWSTR」巨集。使用時只需要把「macros.asm」含括進來。如果字串內容,全部都是英文字母或阿拉伯字母,改以「WSTR」定義,「WSTR」會自動的把 80H 以下的 ASCII 字串變成萬國碼字串,並且放在「.DATA」區段中,其用法是︰

WSTR    字串名稱,"字串內容"

請看以下範例︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        OPTION  CASEMAP:NONE
        .386
        .MODEL  FLAT,STDCALL
__UNICODE__     EQU     1
INCLUDE         WINDOWS.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDE         C:\masm32\macros\macros.asm
 
;***************************************************************************************************
.DATA
WSTR            szTitle,"Simplest Program"
WSTR            szMessage,"Hellow, MASM32 UNICODE."
;***************************************************************************************************
.CODE
start:          INVOKE  MessageBox,NULL,ADDR szMessage,ADDR szTitle,MB_OK OR MB_ICONINFORMATION
                INVOKE  ExitProcess,NULL
;***************************************************************************************************
END             start

因為「WSTR」巨集會用到 WINDOWS.INC 堛漫w義,所以 macros.asm 最好放在所有含括進來的 *.INC 檔的後面。上面四行以紅色字所標示的,就是使用萬國碼時,新增加的程式碼。

MASM32 中文使用萬國碼

如果是中文字串要使用萬國碼,就無法以 WSTR 巨集定義,因為中文字編碼方式可不像英文那樣單純。到目前為止,中文字串只能以數值的方法定義。請看底下程式的第 12 行定義了「測試萬國碼」字串,第 13∼15 行定義了「Win32 組合語言中,使用萬國碼」字串。這兩個字串皆以萬國碼編碼,不是一般的 BIG-5 碼。那小木偶又怎麼知道這些中文字的萬國碼呢?應該有許多種方法,小木偶提供兩種。一是由中華民國國家發展委員會的全字庫網站查詢,這網站可以由注音查萬國碼。第二種可以用 UltraEdit-32 查詢 ( 小木偶使用 UltraEdit-32 是 v12.10a 版 ),在 UltraEdit-32 中新建立一暫存檔,然後輸入中文字,例如下圖輸入「測試萬國碼」:

然後在選單選按「File」→「Save As ...」,接下來會彈出一對話盒,我們要在「Format:」複合框媬嚝颩n儲存的格式,您可以見到,有好幾種格式可以選擇,我們要選的是「Unicode - ASCII Escaped」。如下圖:
接著,畫面變為下圖,您就可以看到「測試萬國碼」這五個字變成了萬國碼:
這樣就可以在底下程式碼的第 12 行填入「測試萬國碼」這五個字的萬國碼。此處要特別小心,當您把底下的檔案存成 ASM 原始碼時,一定要以「ANSI/ASCII」的格式儲存,這是因為組譯器 ( ML.EXE ) 只認得 ANSI/ASCII 字元。此外,小木偶覺得,用這種方式查中文字的萬國碼,仍然麻煩。所以本章後面,小木偶發展出一個工具程式,A2U.ASM,能自動把 ANSI 字串,變成萬國碼字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        OPTION  CASEMAP:NONE
        .386
        .MODEL  FLAT,STDCALL
__UNICODE__     EQU     1
INCLUDE         WINDOWS.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
;***************************************************************************************************
.DATA
szTitle         DW      6e2ch,8a66h,842ch,570bh,78bch,0                         ;測試萬國碼
szMessage       DB      57h,0,69h,0,6eh,0,33h,0,32h,0,20h,0,44h,7dh,8,54h       ;Win32 組合
                DB      9eh,8ah,0,8ah,2dh,4eh,0ch,0ffh,7fh,4fh,28h,75h          ;語言中,使用
                DB      2ch,84h,0bh,57h,0bch,78h,0,0                            ;萬國碼
;***************************************************************************************************
.CODE
start:          INVOKE  MessageBox,NULL,ADDR szMessage,ADDR szTitle,MB_OK OR MB_ICONINFORMATION
                INVOKE  ExitProcess,NULL
;***************************************************************************************************
END             start

上面程式碼的第 12、13 行,定義了兩個以萬國碼編碼的字串,第 12 行直接以一個字組表示,第 13 行是以兩個位元組表示,均能使程式正確運作。當呼叫 MessageBox 時,其實是呼叫 MessageBoxW,而傳進去的字串結尾為 NULL,以萬國碼編碼是一個字組 ( 16 位元 )。所以第 15 行以「DB」定義字串,結尾為「0,0」;第 13 行以「DW」定義,結尾為「0」。


使用資源描述檔的程式,如何使用萬國碼

如果要撰寫對話盒程式,使用「資源描述檔」描述對話盒內的控制項,是極其方便的做法。當資源描述檔被 RC.EXE 編譯時,RC.EXE 會自動的把兩個「"」號內的文字編成萬國碼。因此撰寫以對話盒為主的程式,不必擔心資源描述檔的問題,一切交給 RC.EXE 去執行就可以了。即使您把資源描述檔存成 ASCII 格式,也沒有關係,「"」內的文字仍會被 RC.EXE 以萬國碼的方式編譯。

在 MASM32 v11 版內,就有一個程式,「masm32\examples\unicode_generic\multi_lingual\multi_lingual.exe」,執行後會在同一個對話盒內顯示十種文字。小木偶模仿這個程式,自行製作「MULTILG.ASM」,也能在同一對話盒,顯示「正體中文」、「簡體中文」、「英文」、「拉丁文」、「俄文」等文字。MULTILG 執行後如右。底下是「MULTILG.RC」的原始碼。小木偶是不懂拉丁文和俄文的,但是藉由 Google 大神的幫助,可輕而易舉的翻譯簡單的句子。只要在 Google 的網頁中輸入「翻譯」並搜尋後,第一個項目左邊就會出現您電腦內定的語文,右邊可選擇您想翻成哪一種語文。

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
#include "c:\masm32\include\resource.h"
#define IDS_TCHINESE    2000
#define IDS_SCHINESE    2001
#define IDS_ENGLISH     2002
#define IDS_LATIN       2003
#define IDS_RUSSIAN     2004
#define IDB_EXIT        2005
#define RT_MANIFEST     24
 
1   RT_MANIFEST MOVEABLE PURE "multilg.exe.manifest"
 
GLOBE   ICON    Globe.ico
 
MultiLG DIALOG  0,0,170,85
STYLE   WS_POPUP|WS_CAPTION|WS_VISIBLE|WS_SYSMENU|WS_MINIMIZEBOX
CAPTION "多語言對話盒"
FONT    9,"新細明體"
BEGIN
  LTEXT "正體中文:早安,組合語言。",              IDS_TCHINESE,5, 5,150,12
  LTEXT "簡體中文:早上好,汇编。",                IDS_SCHINESE,5,17,150,12
  LTEXT "英文:Good morning, assembly language.",IDS_ENGLISH, 5,29,150,12
  LTEXT "拉丁文:Bonum mane, contione lingua.",  IDS_LATIN,   5,41,150,12
  LTEXT "俄文:доброе утро на языке ассемблера.",IDS_RUSSIAN,5,53,150,12
  PUSHBUTTON "Exit",IDB_EXIT,106,66,50,12
END

最後把 MULTILG.RC 存成「UTF-16 - NO BOM」格式,如果您用 UltraEdit-32 編輯 MULTILG.RC 檔,所見畫面如下圖紅色框框:

MULTILG.ASM 的原始碼如下,但是必須把它存成「ANSI/ASCII」格式:

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
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
IDB_EXIT        EQU     2005
__UNICODE__     EQU     1
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDE         C:\masm32\macros\macros.asm
;*******************************************************************************
.DATA
hInstance       HANDLE  ?               ;模組代碼
hButton         HANDLE  ?               ;按鈕代碼
WSTR            szDlgName,"MultiLG"     ;對話盒面板名稱
WSTR            szIcon,"GLOBE"          ;圖示名稱
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_INITDIALOG
                INVOKE  GetDlgItem,hDlg,IDB_EXIT
                mov     hButton,eax
                INVOKE  LoadIcon,hInstance,OFFSET szIcon
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
 
.ELSEIF uMsg==WM_COMMAND
                mov     edx,wParam
                mov     eax,lParam
                shr     edx,10h
        .IF eax==hButton
            .IF edx==BN_CLICKED
                jmp     quit
            .ENDIF
        .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
quit:           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

底下是 MULTILG.EXE.MANIFEST 檔的內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>

要注意的是 MULTILG.EXE.MANIFEST 必須存成 ANSI/ASCII 格式,否則製作出來的可執行檔無法執行 ( 會出現「系統無法執行指定的程式。」的錯誤訊息 )。組譯及連結方式如下

E:\HomePage\SOURCE\Win32\UNICODE>rc multilg.rc [Enter]

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

 Assembling: multilg.asm

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

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

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

E:\HomePage\SOURCE\Win32\UNICODE>

MULTILG.ASM 的第 20、21 行定義對話盒面板及圖示的名稱,不論使用 ASCII 字元或萬國碼都無所謂,程式都能正確執行。原因是 Windows XP 在背後搞鬼,Windows XP 會自動把 ASCII 字串變成萬國碼,使之與資源內的文字搭配,所以不會有問題。另外第 38 行的無條件跳躍的目的標號,不可使用「exit」,似乎是因為 WSTR 巨集使用的緣故,所以小木偶改用「quit」。


多語言界面程式

MULTILG 在同一個視窗中顯示不同語言,然而能精通兩種或更多語言的人畢竟很少,所以實用性不高。大部分的人都只使用一種語言,所以大部分的情形是在一個視窗或程式堙A只要能顯示一種語文就可以了。問題是,同一程式要能提供不同的語言,給講不同語言的人選擇。例如在大陸地區可以讓人們選擇簡體中文、在台灣則選擇正體中文、在美國則是英文、在日本則是日文……,甚至可以在程式中,任意切換語言。如下圖的程式,能在選單中選擇以哪一種語言顯示,選好之後,選項文字、標題欄文字都會切換成該種語言。

  

上圖中,左邊的視窗原本以正體中文顯示,如果使用者選擇簡體中文後,視窗就變成右邊的樣子。要達到上面的目的,可以有許多方法,請參考「編寫 Win32 多語言用戶界面應用程序」。在這一篇文章中提到三種方法:

  1. 第一種是為每種語言撰寫來源檔 ( ASM 檔 ) 及資源描述檔 ( RC 檔 ),這是早先使用的方法,已逐漸淘汰。
  2. 用資源描述檔內的字串表實現多語言界面。
  3. 以僅含資源的動態連結程式庫實現多語言界面。

以字串表實現多語言界面

因為第一種方法以逐漸不使用,此處小木偶只介紹後兩種方法。先介紹第二種方法,這種方法是在資源描述檔 ( *.RC 檔 ) 堥洏峖r串表 ( string table )。

字串表 ( string table )

字串表是資源描述檔的成員之一。字串表在資源描述檔中的語法是:

STRINGTABLE
CHARACTERISTICS dword
LANGUAGE        language,sublanguage
VERSION         dword
BEGIN
  stringID,"string"
  ......
END

CHARACTERISTICS 是使用者定義的資料,可以為某些工具使用,可以省略。LANGUAGE 後面接著的是「language」與「sublanguage」,這兩個參數是指 STRINGTABLE 堛漲r串所使用語言。LANGUAGE 也可以省略,省略時即為系統所內定的語言。VERSION 為版本編號,也可以省略。字串表 ( STRINGTABLE ) 沒有識別碼,只有夾在 BEGIN/END 中間的字串,才有識別碼,稱為「字串識別碼」,字串識別碼可以從 0∼65535。每個字串前面的編號就是字串識別碼,字串以「"」含括起來,「"」與字串識別碼之間以「,」隔開。在兩個「"」之間的字串,也可以用「/x0數值」來表示十六進位數所代表的字元。例如下面的字串起頭是先換行兩次:

  20006,"\x0d\x0a\x0d\x0a現在顯示正體中文。"

如果要使用不同語言,我們只要慎選字串識別碼,就可以讓使用者選用不同的語言了。方法是遵守兩個原則:第一,同種語言的字串識別碼放在一起,例如正體中文從 20000 開始、簡體中文從 21000 開始、英文從 22000 開始……。第二,相對應的字串識別碼的字串意義應相同,例如識別碼 20000 的字串為「檔案」,那麼 21000 的字串應為「文件」,22000 的字串應為「File」。

LoadString API

要從資源檔中提取字串表中的字串,可呼叫 LoadString。LoadString 的原型是:

int LoadString ( HINSTANCE hInstance,
                 UINT      uID,
                 LPTSTR    lpBuffer,
                 int       nBufferMax
);

hInstance 是模組代碼,可以由 GetModuleHandle、LoadLibrary 等 API 得到。uID 是要取得的字串識別碼。lpBuffer 是取得的字串存放位址。nBufferMax 是要取得的字串長度,如果要取得的字串是 ASCII 字串,以位元組為單位;如果要取得的字串是寬位元組字串,以字組為單位。如果 nBufferMax 設為零,那麼 lpBuffer 會得到該字串的位址,這個位址是唯讀不能寫入。如果要設定 nBufferMax 為零,在編譯 *.RC 檔時,最好能輸入「-n」或「/n」參數,如下:

E:\HomePage\SOURCE\Win32\UNICODE>rc /n mtlg1.rc [Enter]

「/n」是會使字串表中的字串結尾處,加上「NULL」。這樣的好處是程式不必為載入的字串準備一塊記憶體。

返回時,LoadString 會傳回讀取的字串長度,單位如同 nBufferMax。LoadString 讀取字串時,會自動在字串最後面加上 NULL。如果 nBufferMax 不為零且小於資源檔內的字串,那麼字串會被截掉,所真正讀取的字串長度會比 nBufferMax 還少一,這是因為要在字串後容納 NULL。

原始檔

底下先來看看 MTLG1.EXE.MANIFEST 的內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>

底下是 MTLG1.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
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
#include "c:\masm32\include\resource.h"
#define RT_MANIFEST     24
#define IDC_EDIT        2000
#define IDM_OPEN        10000
#define IDM_EXIT        10001
#define IDM_HELP        10002
#define IDM_TCHINESE    20000
#define IDM_SCHINESE    21000
#define IDM_GERMAN      22000
#define IDM_ENGLISH     23000
#define IDM_RUSSIAN     24000
 
1   RT_MANIFEST MOVEABLE PURE "MTLG1.EXE.MANIFEST"
 
Unicode ICON    mini_unicode.ico
 
MTLG    DIALOG  300,200,200,150
STYLE   WS_POPUP|WS_CAPTION|WS_VISIBLE|WS_SYSMENU|WS_MINIMIZEBOX
CAPTION "多語言對話盒"
FONT    9,"新細明體"
BEGIN
  EDITTEXT IDC_EDIT,1,1,198,148,ES_MULTILINE|ES_AUTOHSCROLL|ES_AUTOVSCROLL
END
 
MainMenu        MENU
BEGIN
POPUP           "File"
  BEGIN
    MENUITEM    "Open",IDM_OPEN
    MENUITEM    "Exit",IDM_EXIT
  END
POPUP           "View"
  BEGIN
    MENUITEM    "正體中文",IDM_TCHINESE
    MENUITEM    "简体中文",IDM_SCHINESE
    MENUITEM    "Deutsch",IDM_GERMAN
    MENUITEM    "English",IDM_ENGLISH
    MENUITEM    "русский",IDM_RUSSIAN
  END
MENUITEM        "Help",IDM_HELP
END
 
STRINGTABLE
BEGIN
  20000,"檔案"
  20001,"開啟"
  20002,"離開"
  20003,"檢視"
  20004,"說明"
  20005,"這個程式能顯示不同語言的介面。"
  20006,"\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a現在顯示正體中文。"
  20007,"使用者可以由「檢視」選單中,\n挑選其中任一個語言顯示。"
 
  21000,"文件"
  21001,"开启"
  21002,"退出"
  21003,"检视"
  21004,"说明"
  21005,"这个程序能显示不同语言的介面。"
  21006,"\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a现在显示简体中文。"
  21007,"用户可以由“检视”选单中,\n挑选其中任一个语言显示。"
 
  22000,"Datei"
  22001,"Geöffnet"
  22002,"Ausgang"
  22003,"Ansicht"
  22004,"Hilfe"
  22005,"Dieses Programm kann in verschiedenen Sprachen Schnittstelle angezeigt werden."
  22006,"Zeigen Sie Deutsch jetzt gleich."
  22007,"Benutzer können über das Menü 'Ansicht',\nWählen Sie entweder eine Sprache aus."
 
  23000,"File"
  23001,"Open"
  23002,"Exit"
  23003,"View"
  23004,"Help"
  23005,"This program can be displayed in different languages."
  23006,"Now displayed in English."
  23007,"Users can from 'View' menu ,\n choose either a language."
 
  24000,"файл"
  24001,"открыто"
  24002,"выход"
  24003,"вид"
  24004,"помогите"
  24005,"Эта программа может быть отображен в различных Языки интерфейса."
  24006,"Дисплей на русском языке."
  24007,"Люди могут из меню 'Вид'\nВыберите либо язык."
END

注意到上面的資源描述檔,堶悸漲r串表中的字串識別碼,是經過有意的安排。同種語言都放在一起,而且順序相同。這樣的話,只要用一個變數,儲存不同語言的第一個字串識別碼,表示現在使用哪一種語言,就能很容易取得同種語言的其他字串。例如,如果此變數為 20000,那麼增加一,就能取得「檔案」;如果切換成英文,則該變數變為 23000,也是增加一,就能取得「File」。

底下,來看看 MTLG1.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
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
__UNICODE__     EQU     1
IDC_EDIT        EQU     2000
IDM_OPEN        EQU     10000
IDM_EXIT        EQU     10001
IDM_HELP        EQU     10002
IDM_TCHINESE    EQU     20000
IDM_SCHINESE    EQU     21000
IDM_GERMAN      EQU     22000
IDM_ENGLISH     EQU     23000
IDM_RUSSIAN     EQU     24000
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDE         C:\masm32\macros\macros.asm
 
;*************************************************************************************************************
.DATA
hInstance       HANDLE          ?       ;模組代碼
hMenu           HANDLE          ?       ;主選單代碼
hFileMenu       HANDLE          ?       ;檔案子選單代碼
hViewMenu       HANDLE          ?       ;檢視子選單代碼
now_language    DD              ?       ;對話盒現在所使用的語文
mii             MENUITEMINFO    <>
WSTR            szDlgName,"MTLG"        ;對話盒面板名稱
WSTR            szIconName,"Unicode"    ;圖示名稱
WSTR            szMenuName,"MainMenu"   ;主選單名稱
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
change_language PROC    USES esi hdlg:DWORD
                LOCAL   menu_name[200h]:BYTE
                mov     esi,now_language
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                mov     mii.fMask,MIIM_STRING
                lea     edx,menu_name
                mov     mii.dwTypeData,edx
                mov     mii.cch,SIZEOF menu_name
                INVOKE  SetMenuItemInfo,hMenu,0,TRUE,ADDR mii
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetMenuItemInfo,hFileMenu,0,TRUE,ADDR mii
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetMenuItemInfo,hFileMenu,1,TRUE,ADDR mii
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetMenuItemInfo,hMenu,1,TRUE,ADDR mii
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetMenuItemInfo,hMenu,2,TRUE,ADDR mii
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetWindowText,hdlg,ADDR menu_name
                inc     esi
                INVOKE  LoadString,hInstance,esi,ADDR menu_name,SIZEOF menu_name
                INVOKE  SetDlgItemText,hdlg,IDC_EDIT,ADDR menu_name
                INVOKE  DrawMenuBar,hdlg
                ret
change_language ENDP
;-------------------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   buffer[200h]:BYTE
.IF uMsg==WM_INITDIALOG
            ;設定圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
            ;取得主選單、檔案子選單、檢視子選單代碼
                INVOKE  LoadMenu,hInstance,OFFSET szMenuName
                mov     hMenu,eax
                INVOKE  SetMenu,hDlg,eax
                INVOKE  GetSubMenu,hMenu,0
                mov     hFileMenu,eax
                INVOKE  GetSubMenu,hMenu,1
                mov     hViewMenu,eax
            ;取得使用者使用的語言
                call    GetUserDefaultLangID
            .IF al==04h
              .IF ah==08h
                mov     now_language,IDM_SCHINESE
                mov     edx,1
              .ELSE
                mov     now_language,IDM_TCHINESE
                sub     edx,edx
              .ENDIF
            .ELSEIF al==07h
                mov     now_language,IDM_GERMAN
                mov     edx,2
            .ELSEIF al==19h
                mov     now_language,IDM_RUSSIAN
                mov     edx,4
            .ELSE
                mov     now_language,IDM_ENGLISH
                mov     edx,3
            .ENDIF
            ;依據使用者使用的語言,在hViewMenu設定被勾選的子選項
                mov     mii.cbSize,SIZEOF MENUITEMINFO
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,edx,1,OFFSET mii
            ;依據使用者使用的語言,在選單、對話盒名稱顯示不同語言
                INVOKE  change_language,hDlg
 
.ELSEIF uMsg==WM_COMMAND
   .IF lParam==0
                mov     eax,wParam
                and     eax,0ffffh              ;EAX=選單識別碼
        .IF eax==IDM_EXIT
                jmp     quit
        .ELSEIF eax==IDM_OPEN
        .ELSEIF eax==IDM_HELP
                mov     ecx,now_language
                add     ecx,4
                INVOKE  LoadString,hInstance,ecx,ADDR buffer,SIZEOF buffer
                mov     ecx,now_language
                lea     edx,buffer
                add     ecx,7
                add     edx,10h
                INVOKE  LoadString,hInstance,ecx,edx,SIZEOF buffer-10h
                lea     edx,buffer
                add     edx,10h
                INVOKE  MessageBox,hDlg,edx,ADDR buffer,MB_OK or MB_ICONINFORMATION
        .ELSE
            .IF eax!=now_language
                push    eax
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_UNCHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,now_language,0,OFFSET mii
                pop     now_language
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,now_language,0,OFFSET mii
                INVOKE  change_language,hDlg
            .ENDIF
        .ENDIF
   .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
quit:           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

請依下面方式組譯、連結:

E:\HomePage\SOURCE\Win32\UNICODE>rc mtlg1.rc [Enter]

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

 Assembling: mtlg1.asm

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

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

/SUBSYSTEM:WINDOWS
"mtlg.obj"
"/OUT:mtlg1.exe"
"mtlg.res"

E:\HomePage\SOURCE\Win32\UNICODE>

MTLG1 執行後,在處理 WM_INITDIALOG 訊息時,會先取得使用者的語言,見程式第 85 行,呼叫 GetUserDefaultLangID。GetUserDefaultLangID 沒有參數,可取得使用者使用的語言,返回時,EAX 為語言識別碼 ( language identifier )。語言識別碼是一個 16 位元的數值,較低的 8 個位元稱為「Primary Language ID」,較高的 8 個位元稱為「SubLanguage ID」,底下是幾種常見的語言:

Primary Language ID SubLanguage ID 數值說  明
數值符號 數值符號
01LANG_ARABIC 01SUBLANG_ARABIC_SAUDI_ARABIA 0101h阿拉伯語 ( 在沙烏地阿拉伯使用 )
01LANG_ARABIC 02SUBLANG_ARABIC_IRAQ 0201h阿拉伯語 ( 在伊拉克使用 )
04LANG_CHINESE 01SUBLANG_CHINESE_TRADITIONAL 0104h正體中文 ( 在中華民國台灣使用 )
04LANG_CHINESE 02SUBLANG_CHINESE_SIMPLIFIED 0204h簡體中文 ( 在中國大陸地區使用 )
04LANG_CHINESE 03SUBLANG_CHINESE_HONGKONG 0304h正體中文 ( 在香港使用 )
04LANG_CHINESE 04SUBLANG_CHINESE_SINGAPORE 0404h正體中文 ( 在新加坡使用 )
09LANG_ENGLISH 01SUBLANG_ENGLISH_US 0109h英文 ( 在美國使用 )
09LANG_ENGLISH 02SUBLANG_ENGLISH_UK 0209h英文 ( 在英國使用 )
09LANG_ENGLISH 03SUBLANG_ENGLISH_AUS 0309h英文 ( 在澳洲使用 )
07LANG_GERMAN 01SUBLANG_GERMAN 0107h德文 ( 在德國使用 )
07LANG_GERMAN 02SUBLANG_GERMAN_SWISS 0207h德文 ( 在瑞士使用 )

第 86∼103 行判斷使用者使用哪一種語言,把對應的選項識別碼填到 now_language 堙C例如,如果是使用正體中文,now_language 為 IDM_TCHINESE;如果是俄文,則是 IDM_RUSSIAN。接下來是把被選定的選項設為被勾選的,然後呼叫 change_language 副程式 ( 第 110 行 )。小木偶的電腦安裝的是 Windows XP SP3 正體中文版,照理來說,呼叫 GetUserDefaultLangID 後,Primary Language ID 應為 LANG_CHINESE,SubLanguage ID 應為 SUBLANG_CHINESE_TRADITIONAL,也就是說 EAX 內的返回值應為 0104H 才對,但卻返回 0404H。小木偶查閱許多資料,還是搞不清楚,如果有先進知道,請來信解惑,小木偶感激不盡。

change_language 副程式在 39∼67 行。在這副程式堙A呼叫 LoadString 取得字串,緊接著就是呼叫 SetMenuItemInfo 把所得的字串,變成選項文字。因為選項文字共有五個,所以這種情形共有五次。而每一次都使字串識別碼增加一,就能取得下一個字串,而最初的字串識別碼,則是取決於 now_language。最後的兩次呼叫 LoadString 則是要設定對話盒標題與編輯框文字。在結束 change_language 前,呼叫 DrawMenuBar 重新繪製選單,否則選單還是顯示原先 DC ( 設備內容,device context ) 的內容。

DrawMenuBar API

DrawMenuBar 是用在系統已繪製好整個視窗後,程式改變選單時,重新繪製選單之用。如果不呼叫 DrawMenuBar,那麼螢幕上的選單並不會即刻改變。DrawMenuBar 的原型是:

BOOL DrawMenuBar( HWND hWnd
);

hWnd 是須重繪的選單所在的視窗。如果呼叫成功,返回值為非零;否則呼叫失敗。

其他部份

當使用者點選主選單的「檢視」彈出選項中的任何一種語言時,程式會跳到第 132 行執行。此時 MTLG1 會先比較使用者所點選的語言是否就是當前顯示的語言,如果不是才會 133∼140。第 133 行,先把使用者新選定的語言存入堆疊,稍後會用著。第 134∼136 是設定當前語言選項變為「未勾選」;然後第 137 行由堆疊取出使用者新選定的語言,並存入 now_language。接著第 138∼139 行,則是把使用者新選定的語言選項設為「勾選」。最後呼叫 change_language 副程式,改變對話盒中的選項文字。

當使用者點選主選單的「說明」選項時,會彈出一個對話盒,其中的對話盒的標題在程式 120∼122 行取得,並把此字串存入 buffer 的前 10h 個位元組內。而對話盒顯示的文字,則是由區域變數 buffer 字串的第 10h 個位址開始存放。最後就是呼叫 MessageBox,把對話盒顯示出來。

以僅含資源的動態連結程式庫實現多語言界面

這個方法是要為每一種語言製作一個僅含字串表的動態連結程式庫 ( DLL )。當使用者於程式視窗內選擇某一種語言時,則載入相對應的動態連結程式庫即可。接著程式要做的事,就是把視窗上的文字以使用者選擇的語言取代,而使用者所選擇的語言則是取自於相對應的動態連結程式庫。

小木偶把上面的 MTLG1 用此方法重新改寫,改寫後的程式稱為 MTLG2。MTLG2 視窗提供五種語言,可供選擇。它們是正體中文、簡體中文、德文、英文、俄文。因為有五種語言,所以要先製作五個 DLL 檔,而這五個 DLL 是純資源,亦即僅含資源而不含程式碼。底下是這五個 DLL 檔案的原始碼及製作方式。首先建立五個資源描述檔:TW.RC、CN.RC、DE.RC、EN.RC、RU.RC,其內各含有這五種語言的字串表,每個 RC 檔都要存成「UTF-16 - NO BOM」格式。這五個 RC 檔的內容如下:

正體中文的資源描述檔:TW.RC
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "c:\masm32\include\resource.h"
 
STRINGTABLE
BEGIN
  20000,"檔案"
  20001,"開啟"
  20002,"離開"
  20003,"檢視"
  20004,"說明"
  20005,"這個程式能顯示不同語言的介面。"
  20006,"\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a現在顯示正體中文。"
  20007,"使用者可以由「檢視」選單中,\n挑選其中任一個語言顯示。"
END

簡體中文的資源描述檔:CN.RC
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "c:\masm32\include\resource.h"
 
STRINGTABLE
BEGIN
  20000,"文件"
  20001,"开启"
  20002,"退出"
  20003,"检视"
  20004,"说明"
  20005,"这个程序能显示不同语言的介面。"
  20006,"\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a现在显示简体中文。"
  20007,"用户可以由“检视”选单中,/n挑选其"
END

德文的資源描述檔:DE.RC
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "c:\masm32\include\resource.h"
 
STRINGTABLE
BEGIN
  20000,"Datei"
  20001,"Geöffnet"
  20002,"Ausgang"
  20003,"Ansicht"
  20004,"Hilfe"
  20005,"Dieses Programm kann in verschiedenen Sprachen Schnittstelle angezeigt werden."
  20006,"Zeigen Sie Deutsch jetzt gleich."
  20007,"Benutzer können über das Menü 'Ansicht',\nWählen Sie entweder eine Sprache aus."
END

英文的資源描述檔:EN.RC
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "c:\masm32\include\resource.h"
 
STRINGTABLE
BEGIN
  20000,"File"
  20001,"Open"
  20002,"Exit"
  20003,"View"
  20004,"Help"
  20005,"This program can be displayed in different languages."
  20006,"Now displayed in English."
  20007,"Users can from 'View' menu ,\n choose either a language."
END

俄文的資源描述檔:RU.RC
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "c:\masm32\include\resource.h"
 
STRINGTABLE
BEGIN
  20000,"файл"
  20001,"открыто"
  20002,"выход"
  20003,"вид"
  20004,"помогите"
  20005,"Эта программа может быть отображен в различных Языки интерфейса."
  20006,"Дисплей на русском языке."
  20007,"Люди могут из меню 'Вид'\nВыберите либо язык."
END

接下來是以 RC.EXE 編譯這五個資源描述檔 ( *.RC ),然後再以 LINK.EXE 連結並指定製作成 DLL 檔,方法如下:

E:\HomePage\SOURCE\Win32\UNICODE>rc /n tw.rc [Enter]

E:\HomePage\SOURCE\Win32\UNICODE>link /DLL /NOENTRY /SUBSYSTEM:WINDOWS /MACHINE:IX86 /OUT:TW.DLL tw.res [Enter]
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


E:\HomePage\SOURCE\Win32\UNICODE>

加上 RC.EXE 的參數「/n」會在編譯資源描述檔時,使字串表中的每個字串結尾自動加上「NULL」字元。這樣呼叫 LoadString 時,可以使最後一個參數設為 0,而在 lpBuffer 位址得到字串所在位址。連結時,加上「/NOENTRY」是建立僅含資源而不含執行碼的動態連結程式庫時,所必須加上的參數,它指定此動態連結程式庫無進入點。參數「/MACHINE:IX86」是指定所要建立的檔案在哪一種平台使用,可以有 ALPHA、ARM、IX86、MIPS、MIPS16、MIPSR41XX、PPC、SH3、SH4 等選擇,其中 IX86 就是 IBM 相容機種。

每一個 RC 檔都依照上面的方法,製造出 DLL 檔。接下來要製作主視窗,主視窗所需的檔案有 MTLG2.ASM、MTLG2.RC、MTLG.EXE.MANIFEST 三個檔案,底下是這三個檔案的內容,這三個檔案 MTLG2.ASM 一定得存成「ANSI/ASCII」格式,MTLG2.RC 要存成「UTF-16 - NO BOM」格式,MTLG2.EXE.MANIFEST 可以存成「ANSI/ASCII」,也是「UTF-16 - NO BOM」格式。

MTLG2.EXE.MANIFEST 的內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>

底下是 MTLG2.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "c:\masm32\include\resource.h"
#define RT_MANIFEST     24
#define IDC_EDIT        2000
#define IDM_OPEN        10000
#define IDM_EXIT        10001
#define IDM_HELP        10002
#define IDM_TW          20000
#define IDM_CN          21000
#define IDM_DE          22000
#define IDM_EN          23000
#define IDM_RU          24000
 
1   RT_MANIFEST MOVEABLE PURE "MTLG2.EXE.MANIFEST"
 
Unicode ICON    mini_unicode.ico
 
MTLG2   DIALOG  300,200,200,150
STYLE   WS_POPUP|WS_CAPTION|WS_VISIBLE|WS_SYSMENU|WS_MINIMIZEBOX
CAPTION "多語言對話盒"
FONT    9,"新細明體"
BEGIN
  EDITTEXT IDC_EDIT,1,1,198,148,ES_MULTILINE|ES_AUTOHSCROLL|ES_AUTOVSCROLL
END
 
MainMenu        MENU
BEGIN
POPUP           "File"
  BEGIN
    MENUITEM    "Open",IDM_OPEN
    MENUITEM    "Exit",IDM_EXIT
  END
POPUP           "View"
  BEGIN
    MENUITEM    "正體中文",IDM_TW
    MENUITEM    "简体中文",IDM_SCHINESE
    MENUITEM    "Deutsch",IDM_GERMAN
    MENUITEM    "English",IDM_ENGLISH
    MENUITEM    "русский",IDM_RUSSIAN
  END
MENUITEM        "Help",IDM_HELP
END

底下是 MTLG2.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
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
__UNICODE__     EQU     1
IDC_EDIT        EQU     2000
IDM_OPEN        EQU     10000
IDM_EXIT        EQU     10001
IDM_HELP        EQU     10002
IDM_TW          EQU     20000   ;正體中文
IDM_CN          EQU     21000   ;簡體中文
IDM_DE          EQU     22000   ;德文
IDM_EN          EQU     23000   ;英文
IDM_RU          EQU     24000   ;俄文
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDE         C:\masm32\macros\macros.asm
 
;*************************************************************************************************************
.CONST
pDLL            DD              OFFSET szTWDLL,OFFSET szCNDLL,OFFSET szDEDLL,OFFSET szENDLL,OFFSET szRUDLL
;*************************************************************************************************************
.DATA
hInstance       HANDLE          ?       ;模組代碼
hMenu           HANDLE          ?       ;主選單代碼
hFileMenu       HANDLE          ?       ;檔案子選單代碼
hViewMenu       HANDLE          ?       ;檢視子選單代碼
hDLLModule      HANDLE          ?       ;DLL的模組代碼
now_language    DD              ?       ;對話盒現在所使用的語文。正體中文時為20000;簡體中文時為21000;德文時為22000…
mii             MENUITEMINFO    <>
szHelpTitle     DW              ?       ;顯示在說明視窗的標題欄字串位址
pHelpText       DD              ?       ;顯示在說明視窗的字串位址
WSTR            szErrorTitle,"Error"    ;找不到某種語言DLL時,錯誤視窗的標題欄字串位址
WSTR            szError,"Not found DLL.";找不到某種語言DLL時,錯誤視窗的字串位址
WSTR            szDlgName,"MTLG2"       ;對話盒面板名稱
WSTR            szIconName,"Unicode"    ;圖示名稱
WSTR            szMenuName,"MainMenu"   ;主選單名稱
WSTR            szTWDLL,"TW.DLL"        ;正體中文的DLL檔名
WSTR            szCNDLL,"CN.DLL"        ;簡體中文的DLL檔名
WSTR            szDEDLL,"DE.DLL"        ;德文的DLL檔名
WSTR            szENDLL,"EN.DLL"        ;英文的DLL檔名
WSTR            szRUDLL,"RU.DLL"        ;俄文的DLL檔名
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
;載入DLL檔,如果找不到DLL,返回時CY;否則NC
change_language PROC    hdlg:DWORD
                INVOKE  FreeLibrary,hDLLModule
                mov     eax,now_language
                sub     edx,edx         ;pDLL為五個位址所組成的陣列,這五個位址分別指向各語言的DLL檔名,因此要
                mov     ecx,1000        ;取得各語言的DLL檔名,只需用EAX當成指標即可,EAX與選項識別碼的關係如下
                div     ecx             ;語文: 正體中文 簡體中文 德文 英文 俄文
                sub     eax,20          ;EAX :  0     4    8  12  16
                shl     eax,2           ;識別碼:20000   21000  22000 23000 24000
                mov     ecx,pDLL[eax]   ;故先把now_language內的選單識別碼除以1000,即得二十幾,這個位數再乘以
                INVOKE  LoadLibrary,ecx ;四就得EAX
                mov     hDLLModule,eax  ;若EAX=0,表示找不到DLL檔
   .IF eax==0
                stc
   .ELSE
                mov     mii.fMask,MIIM_STRING
                INVOKE  LoadString,hDLLModule,20000,OFFSET mii.dwTypeData,0     ;取得「檔案」字串的位址
                INVOKE  SetMenuItemInfo,hMenu,0,TRUE,ADDR mii
                INVOKE  LoadString,hDLLModule,20001,OFFSET mii.dwTypeData,0     ;取得「開啟」字串的位址
                INVOKE  SetMenuItemInfo,hFileMenu,0,TRUE,ADDR mii
                INVOKE  LoadString,hDLLModule,20002,OFFSET mii.dwTypeData,0     ;取得「離開」字串的位址
                INVOKE  SetMenuItemInfo,hFileMenu,1,TRUE,ADDR mii
                INVOKE  LoadString,hDLLModule,20003,OFFSET mii.dwTypeData,0     ;取得「檢視」字串的位址
                INVOKE  SetMenuItemInfo,hMenu,1,TRUE,ADDR mii
                INVOKE  LoadString,hDLLModule,20005,OFFSET mii.dwTypeData,0     ;取得「這個程…」字串的位址
                INVOKE  SetWindowText,hdlg,mii.dwTypeData
                INVOKE  LoadString,hDLLModule,20006,OFFSET mii.dwTypeData,0     ;取得「現在顯…」字串的位址
                INVOKE  SetDlgItemText,hdlg,IDC_EDIT,mii.dwTypeData
                INVOKE  LoadString,hDLLModule,20004,OFFSET mii.dwTypeData,0     ;取得「說明」字串的位址
                mov     edx,mii.dwTypeData
                mov     pHelpTitle,edx
                INVOKE  SetMenuItemInfo,hMenu,2,TRUE,ADDR mii
                INVOKE  LoadString,hDLLModule,20007,OFFSET pHelpText,0  ;取得顯示在說明視窗的字串位址
                INVOKE  DrawMenuBar,hdlg
                clc
   .ENDIF
                ret
change_language ENDP
;-------------------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_INITDIALOG
            ;設定圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
            ;取得主選單、檔案子選單、檢視子選單代碼
                INVOKE  LoadMenu,hInstance,OFFSET szMenuName
                mov     hMenu,eax
                INVOKE  SetMenu,hDlg,eax
                INVOKE  GetSubMenu,hMenu,0
                mov     hFileMenu,eax
                INVOKE  GetSubMenu,hMenu,1
                mov     hViewMenu,eax
            ;取得使用者使用的語言
                call    GetUserDefaultLangID
            .IF al==04h
              .IF ah==08h
                mov     now_language,IDM_CN
                mov     edx,1
              .ELSE
                mov     now_language,IDM_TW
                sub     edx,edx
              .ENDIF
            .ELSEIF al==07h
                mov     now_language,IDM_DE
                mov     edx,2
            .ELSEIF al==19h
                mov     now_language,IDM_RU
                mov     edx,4
            .ELSE
                mov     now_language,IDM_EN
                mov     edx,3
            .ENDIF
            ;依據使用者使用的語言,在hViewMenu設定被勾選的子選項
                mov     mii.cbSize,SIZEOF MENUITEMINFO
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,edx,1,OFFSET mii
            ;依據使用者使用的語言,在選單、對話盒名稱顯示不同語言
                INVOKE  change_language,hDlg
                jc      not_found_dll
 
.ELSEIF uMsg==WM_COMMAND
   .IF lParam==0
                mov     eax,wParam
                and     eax,0ffffh      ;EAX=選單識別碼
        .IF eax==IDM_EXIT
                jmp     quit
        .ELSEIF eax==IDM_OPEN
        .ELSEIF eax==IDM_HELP           ;使用者按「說明」選項時,顯示說明視窗
                INVOKE  MessageBox,hDlg,pHelpText,pHelpTitle,MB_OK or MB_ICONINFORMATION
        .ELSEIF eax!=now_language
                push    eax
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_UNCHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,now_language,0,OFFSET mii
                pop     now_language
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewMenu,now_language,0,OFFSET mii
                INVOKE  change_language,hDlg
                jnc     finish
not_found_dll:  INVOKE  MessageBox,hDlg,OFFSET szError,OFFSET szErrorTitle,MB_OK or MB_ICONEXCLAMATION
        .ENDIF
   .ENDIF
.ELSEIF uMsg==WM_CLOSE
quit:           INVOKE  EndDialog,hDlg,NULL
 
.ELSE           ;其他未處理的訊息返回 FALSE
                mov     eax,FALSE
                ret
 
.ENDIF          ;已處理的訊息,返回 TRUE
finish:         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

上面三個檔案,MTLG2.RC 因含有萬國碼才能使用不同語言文字,所以必須存成「UTF-16 - NO BOM」格式,而 MTLG2.ASM 與 MTLG2.EXE.MANIFEST 一定要存成「ANSI/ASCII」格式才能使 ML.EXE 組譯,所製作的執行檔才可執行。然後以下面方式組譯、連結:

E:\HomePage\SOURCE\Win32\UNICODE>rc mtlg2.rc [Enter]

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

 Assembling: mtlg2.asm

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

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

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

E:\HomePage\SOURCE\Win32\UNICODE>

這程式與 MTLG1 差不多,最大的差別在 change_language 副程式。change_language 呼叫 LoadLibrary 載入含有使用者選定語言的 DLL 檔,這個 DLL 檔含有字串表。至於要載入哪一個 DLL 檔,在程式的第 54∼60 行計算其檔名位址。如果失敗,則設定進位旗標,返回視窗函式;如果成功,則載入各字串,並設定相關選項文字或標題欄,最後清除進位旗標,返回視窗函式。

另外此程式值得一提的是,一進入 change_language,第一個執行的是呼叫 FreeLibrary。當程式頭一次呼叫 change_language 時,事實上並沒有載入任何語言的動態連結庫,所以這一次執行會有錯誤,不過並不必處理。此後,每次當使用者改變語言時,都會呼叫 change_language,一進此副程式,首先做的就會釋放原先語言的動態連結庫。為何不把釋放動態連結庫寫在已經載入所有字串之後呢?原因是如果寫在載入所有字串之後,那麼當使用者選擇「說明」選項時,動態連結程式庫已被釋放,就會出現違反存取規則。


A2U.ASM:把 Big-5 碼轉換成萬國碼

有鑑於在編輯組合語言原始碼時,如要使用萬國碼,必須把中文以數值的方式表示。老實說,這實在有點兒麻煩。小木偶希望能像英文一樣,有個巨集就能完成。但左思右想,似乎很困難。不過如果寫個對話盒程式,讓使用者輸入中文、英文,甚至中英文夾雜的字串,中文以 Big-5 編碼,英文以 ASCII 編碼。然後按個「轉換」按鈕,就能把輸入的字串,以萬國碼編碼的方式呈現在另一個編輯框,不也減輕了麻煩嗎?實際執行的畫面如下圖:

使用者在上圖的輸入編輯框輸入「Win32組合語言」,按「轉換」按鈕後,在另一個唯讀編輯框 ( 小木偶稱之為輸出編輯框 ) 顯示「Win32組合語言」的萬國碼,即「                DW      057h,069h,06eh,033h……」。前面多了「                DW     」是為了方便寫入組合語言原始碼中。也可以按下「複製」按鈕,把這些萬國碼複製到剪貼簿堙C

撰寫 A2U.ASM 所遇到的困難

小木偶撰寫 A2U.ASM 的想法本來是這樣的:讓使用者在輸入編輯框輸入字串後按下「轉換」按鈕,A2U 會先取得該字串長度,然後以此字串長度向系統申請記憶體,再呼叫 GetWindowText 就可以把字串讀到這個記憶體內,這個字串為 ASCII 字串或 Big-5 字串,甚至可能是 Big-5 與 ASCII 中英文夾雜的字串。然後再呼叫 MultiByteToWideChar 兩次,第一次可取得要多大的記憶體才能容納轉換後的字串,第二次便真正的把 ASCII 字串、Big-5 字串,甚至可能是 Big-5 與 ASCII 中英文夾雜的字串轉換成萬國碼字串。

小木偶依照上面方法,寫好了 A2U.ASM,一切都算順利,也能實際工作。但是有些字,例如珉、煊、堃、喆、嚞、瀞、斈…等字無法轉換。原因是 Big-5 沒有收錄這些字,所以即使能在輸入編輯框中輸入這些字,也無法轉換。A2U 呼叫 GetWindowText 得到字串,所得到的字串是以 ASCII 或 Big-5 編碼,所以這些字無法正常被轉換成萬國碼。來源字串就是錯誤的,您又怎能期待會正確的轉換呢?所以不能用 GetWindowText 取得 ASCII 或 Big-5 字串,也無法用 MultiByteToWideChar 轉換字串。雖然如此,小木偶還是希望能介紹 MultiByteToWideChar 的用法,畢竟這個 API 也跟文字編碼有關,離這章主題不遠。有關 MultiByteToWideChar 的用法,請參考註二

言歸正傳。既然不能用 GetWindowText 取得 ASCII 或 Big-5 字串,那該怎麼辦呢?事實上,可以利用 Windows XP 的性質,改呼叫 GetWindowTextW 就可以了。要注意,呼叫 GetWindowTextW 後,所得的字串就是以萬國碼編碼的字串,也就不用再呼叫 MultiByteToWideChar 了。

A2U.ASM 原始碼與 A2U.RC

底下是 A2U.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
25
26
27
#include "c:\masm32\include\resource.h"
 
#define DEF_STYLE       ES_AUTOHSCROLL|ES_AUTOVSCROLL|ES_MULTILINE
#define IDC_QUIT        3000
#define IDC_INPUT       3001
#define IDC_OUTPUT      3002
#define IDC_COPY        3003
#define IDC_CHANGE      3004
#define RT_MANIFEST     24
 
A2U     DIALOG  200,200,176,110
STYLE   WS_CAPTION|WS_VISIBLE|WS_SYSMENU
CAPTION "BIG-5 碼轉換成萬國碼"
FONT    10,"新細明體"
BEGIN
  LTEXT         "輸入字串:",100,   5,  8, 40, 12
  EDITTEXT      IDC_INPUT,         42,  5,125, 36,DEF_STYLE|ES_WANTRETURN
  LTEXT         "輸出字串:",101,   5, 47, 40, 12
  EDITTEXT      IDC_OUTPUT,        42, 46,125, 36,DEF_STYLE|ES_READONLY
  PUSHBUTTON    "轉換",IDC_CHANGE,  5, 90, 50, 13
  PUSHBUTTON    "複製",IDC_COPY,   63, 90, 50, 13
  PUSHBUTTON    "離開",IDC_QUIT,  121, 90, 50, 13
END
 
1       RT_MANIFEST MOVEABLE PURE "A2U.EXE.MANIFEST"
 
Search  ICON    Help2.ico

底下是 A2U.EXE.MANIFEST 檔案的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>

底下是 A2U.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
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
 
IDC_QUIT        EQU     3000
IDC_INPUT       EQU     3001
IDC_OUTPUT      EQU     3002
IDC_COPY        EQU     3003
IDC_CHANGE      EQU     3004
 
;*************************************************************************************************************
.CONST
szDlgName       DB      "A2U",0         ;對話盒面板名稱
szIconName      DB      "Search",0      ;圖示名稱
szDW            DB      "                DW      "
szWordFmt       DB      "0%xh,",0
;*************************************************************************************************************
.DATA
hInstance       DD      ?       ;模組代碼
hEdtInput       DD      ?       ;輸入編輯框代碼
hEdtOutput      DD      ?       ;輸出編輯框代碼
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
;把hEdtInput編輯框內的文字變成十六進位數值,顯示在hEdtOutput內
;輸入:EAX-hEdtInput編輯框內的文字編輯框的個數,以字組為單位
;   hEdtInput-hEdtInput編輯框的代碼
;   hEdtOutput-hEdtOutput編輯框的代碼
;輸出:hEdtOutput編輯框會顯示十六進位數值
get_hex_code    PROC    USES esi edi
                LOCAL   n,hMemIn,hMemOut,cbPerLine:DWORD
                mov     n,eax
                and     eax,0fffffff0h
                inc     n
                shl     eax,1           ;EAX=2*EAX
                add     eax,20h
                INVOKE  GlobalAlloc,GPTR,eax
                mov     hMemIn,eax
                INVOKE  GetWindowTextW,hEdtInput,hMemIn,n
                mov     eax,n
                sub     edx,edx
                shl     eax,3
                mov     ecx,100- SIZEOF szDW
                sub     eax,n           ;EAX=7n
                push    eax
                div     ecx             ;EAX=INT( 7n/(100-SIZEOF szDW) )
                inc     eax             ;EAX=EAX+1=在輸出編輯框內的字串被分成的行數=Line
                sub     edx,edx
                mov     ecx,SIZEOF szDW+2
                mul     ecx             ;x=x=Line*( SIZEOF szDW+2 )=因szDW及換行所造成增多的位元組個數
                pop     edx
                add     edx,eax
                add     edx,800h        ;EDX=x+7n+800h,多出來的800h是為了以防萬一
                INVOKE  GlobalAlloc,GPTR,edx
                mov     hMemOut,eax
                mov     edi,eax
                inc     n               ;因為一開始便減一,所以此處要先加一
                xor     eax,eax
                mov     esi,hMemIn
                jmp     @f
        .WHILE n>0
                lodsw
                INVOKE  wsprintf,edi,OFFSET szWordFmt,eax
                add     edi,eax
                add     cbPerLine,eax
            .IF cbPerLine>100           ;如果超過100個位元組,則換行
                mov     ax,0a0dh
                dec     edi
                stosw
@@:             push    esi
                mov     ecx,SIZEOF szDW ;把szDW字串存入EDI所指位址
                mov     esi,OFFSET szDW
                rep     movsb
                mov     cbPerLine,SIZEOF szDW
                pop     esi
            .ENDIF
                dec     n
        .ENDW
                mov     BYTE PTR [edi-1],0
                INVOKE  SetWindowText,hEdtOutput,hMemOut
                INVOKE  GlobalFree,hMemIn
                INVOKE  GlobalFree,hMemOut
                ret
get_hex_code    ENDP
;-------------------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   cbOutput,hMemTitle:DWORD
.IF uMsg==WM_INITDIALOG
        ;載入圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
        ;取得輸入與輸出編輯框代碼
                INVOKE  GetDlgItem,hDlg,IDC_INPUT
                mov     hEdtInput,eax
                INVOKE  GetDlgItem,hDlg,IDC_OUTPUT
                mov     hEdtOutput,eax
 
.ELSEIF uMsg==WM_COMMAND
                mov     ecx,wParam
                mov     edx,wParam
                shr     ecx,10h         ;ECX=通知碼
                and     edx,0ffffh      ;EDX=控制元件識別碼
    .IF ecx==BN_CLICKED
        .IF edx==IDC_QUIT
                jmp     quit
        .ELSEIF edx==IDC_CHANGE
                INVOKE  GetWindowTextLengthW,hEdtInput
            .IF eax!=0
                call    get_hex_code
            .ENDIF
        .ELSEIF edx==IDC_COPY
                INVOKE  GetWindowTextLength,hEdtOutput
            .IF eax!=0
                mov     cbOutput,eax
                and     eax,0fffffff0h
                add     eax,20h
                inc     cbOutput        ;需加上NULL字元
                INVOKE  GlobalAlloc,GPTR,eax
                mov     hMemTitle,eax
                INVOKE  GetDlgItemText,hDlg,IDC_OUTPUT,eax,cbOutput
                INVOKE  OpenClipboard,hDlg
                call    EmptyClipboard
                INVOKE  SetClipboardData,CF_TEXT,hMemTitle
                INVOKE  GlobalFree,hMemTitle
                call    CloseClipboard
            .ENDIF
        .ENDIF
    .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
quit:           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

解說

底下先看看 A2U 的畫面,然後說明 A2U 程式。

當使用者在 A2U 對話盒的輸入編輯框輸入好字串後,按下「轉換」按鈕,程式便會得到 Windows 系統傳來的 WM_COMMAND 訊息,其 wParam 為 IDC_CHANGE,因此會執行第 107 行的程式碼。經過一連串比對按下哪個按鈕,來到第 115 行呼叫 GetDlgItemTextLengthW,取得輸入編輯框內容的長度,這堜珨〞漱漁e長度是以字組 ( 字組英文是 word,一個字組長 16 位元 ) 為單位,且不含 NULL。例如輸入編輯框內容為「Win32組合語言」,那麼輸入編輯框內容的長度為 9 個字元,亦即 18 個位元組。接下來第 116 行,檢查輸入編輯框內容的長度是否為零,如果是的話,表示使用者尚未輸入任何字串,直接結束處理 WM_COMMAND 訊息,返回系統。如果不為零,就要呼叫 get_hex_code 副程式 ( 第 117 行 )。

get_hex_code 副程式在 33∼92 行。這個副程式是把使用者輸入的字串,變成萬國碼編碼的十六進位數值,顯示在輸出編輯框堙C例如,使用者輸入「Win32組合語言」,就會在輸出編輯框顯示「                DW      057h,069h,06eh,033h,032h,07d44h,05408h,08a9eh,08a00h,00h」,「W」字母的萬國碼數值是 57h、「i」的萬國碼數值是 69h……。get_hex_code 副程式執行時,DlgProc 會把要處理的字元個數存在 EAX 堙A傳給 get_hex_code,但是這個字元個數不包含 NULL。所以 get_hex_code 會先把 EAX,存在區域變數,n,堙A並使 n 增加一 ( 第 43 行 ),這樣就包含了 NULL 字元。接下來要向系統申請配置記憶體,申請記憶體的大小是 EAX 的兩倍,因為一個萬國碼字元需要兩個位元組,又為了希望申請的記憶體大小是以 para 為單位 ( 16 個位元組為一個 para ),以及以防萬一,因此申請較大一點的記憶體。第 41、42、44 行的程式碼實現上述目的。申請配置好記憶體後,把記憶體位址儲存在 hMemIn 堙C第 47 行,就是把輸入編輯框的字串移到 hMemIn 堙A這時候 hMemIn 堛漲r串已經是萬國碼編碼的字串了。

第 48∼61 行,是計算 hMemIn 堛爾U國碼字串中的每個字變成一個個十六進位數值時,所需的記憶體大小。程式第 55 行算出在輸出編輯框需要有幾行的原始碼。這個行數,可以用 7n÷(100-SIZEOF szDW) 得到,此處的 n 是使用者輸入的字串所含字元數,必須包含 NULL 字元。不論中文或英文,以萬國碼編碼的每個字,在組合語言原始碼中都需要 7 個位元組表示。例如組合語言的「組」,萬國碼是「07d44h」,再加上一個「,」,所以需要 7 個位元組。在第 50 行把 EAX 左移三個位元,就相當於乘以 8,再減去一個 n,就得到 7n。小木偶希望每一行字不超過 110 個位元組 ( 此處一個字佔據一個位元組 ),所以超過 100 個字就得換行。因此,在 51 行,把 (100-SIZEOF szDW) 存入 ECX 堙A當成除數。算出行數之後,再乘上 ( SIZEOF szDW+2 ),就得到「                DW      」以及換行的兩個字元,總共要佔用多少位元組。

接著,以剛剛計算所得的位元組個數,向系統申請配置記憶體 ( 第 62 行 ),並把記憶體代碼記錄於 hMemOut 堙C然後進入迴圈,迴圈執行的次數,即為萬國碼字串的字元數,以 n 表示。因為每一行的前幾個位元組都是 szDW 字串,所以先跳到 78 行,這樣的話還沒處理任何一個萬國碼,就已經減一了,所以第 65 行先加一。進入迴圈前,把 EDI 當做指標,指向 hMemOut ( 第 64 行 ),要填入的位址;ESI 則是指向 hMemIn 字串,即將處理的萬國碼字串的位址。每一行現有多少位元組,存於 cbPerLine 堙A因此每處理完一個字元,就檢查是否超過 100,如果是的話就換行。換行要做的事有把 0a0dh 存入 EDI 所指字串、把 szDW 字串填入 EDI 所指位址、重設 cbPerLine。


註一:寬字元

ANSI 每個字元僅佔一個位元組;而 UNICODE 則佔兩個位元組,因此稱 UNICODE 為寬字元 ( wide character )。

註二:MultiByteToWideChar API

要把使用者輸入的 ASCII 碼或 Big-5 碼,甚至兩者夾雜的字串,轉換成萬國碼,所使用的 API 稱為 MultiByteToWideChar。字面上的意思是把多個位元組字串轉換成寬字組,多個位元組字串的意思是指,這種字串包含全為英文字、中英夾雜或全為中文字;而所謂的寬字組,則是指萬國碼。所以可以猜想得到,這個 API 相當複雜,光是要分辨中文或英文就已經很複雜了。幸好這些事情,都由微軟解決了,並且已經實作完成了。MultiByteToWideChar 的原型是:

int MultiByteToWideChar(
  UINT   CodePage,
  DWORD  dwFlags,
  LPCSTR lpMultiByteStr,
  int    cbMultiByte,
  LPWSTR lpWideCharStr,
  int    cchWideChar
);

第一個參數,CodePage,是指多個位元組字串的「頁碼」,意即要轉換字串的頁碼,可以是下面的一種:

頁碼數值說  明
CP_ACP0 ANSI 頁碼。對於 Big-5 碼而言,也是使用 CP_ACP。事實上,對於多位元組編碼的環境 ( 如中日韓泰文等 ),CP_ACP 與 CP_OEMCP 是相同的。CP_ACP 與 CP_OEMCP 更詳細的說明,請按
CP_OEMCP1 OEM 頁碼
CP_MACCP2 Macintosh 頁碼 ( 麥金塔頁碼 )
CP_THREAD_ACP3 在執行緒中轉換 ANSI 頁碼。
CP_SYMBOL42 Symbol 頁碼 ( 42 頁碼 ),只能在 Windows 2000 以後的系統使用。
CP_UTF765000 UTF-7。
CP_UTF865001 把 UTF-8 轉換成萬國碼,這時第二個參數,dwFlags,必須設為 0 或 MB_ERR_INVALID_CHARS,否則會產生 ERROR_INVALID_FLAGS 錯誤碼。

第二個參數,dwFlags,用來指定一些額外的設定,以指示系統如何轉換,可以是下面數值:

dwFlags數值說  明
MB_PRECOMPOSED1 內定值,不可和 MB_COMPOSITE 合用。有些含有重音符號的西洋字母,例如Ä,使用 MB_PRECOMPOSED 時,會變成一個萬國碼的字元,「Ä」( U+00C4 )。
MB_COMPOSITE2 不可和 MB_PRECOMPOSED 合用。有些含有重音符號的西洋字母,例如Ä,使用 MB_PRECOMPOSED 時,會變成分開的兩個萬國碼的字元,「A ̈」( LATIN CAPITAL LETTER A (U+0041) + COMBINING DIAERESIS (U+0308) )。
MB_USEGLYPHCHARS4 用字形代替控制字元,例如  代替 U+0008。
MB_ERR_INVALID_CHARS8 如果 lpMultiByteStr 所指字串包含無效的字元,轉換發生失敗。如果設定此旗標,就會傳回 ERROR_NO_UNICODE_TRANSLATION;否則可以接著呼叫 GetLastErro 傳回 ERROR_NO_UNICODE_TRANSLATION。只能在 Windows 2000 SP4 或 XP 以後使用。

第三個參數,lpMultiByteStr,是指向多位元組字串的位址,意即來源字串位址;而在此位址的字串佔有多少位元組,則是由第四個參數,cbMultiByte,指定。cbMultiByte 有幾種情形:

  1. 如果為正數,MultiByteToWideChar 就只把所指定的長度變成寬位元組字串。
  2. 如果是 0,會產生呼叫錯誤。
  3. 如果 cbMultiByte 為 -1,系統會自行決定多位元組字串的長度,就是以 0 為結尾;返回時,會傳回來需要多少位元組,以容納轉換後的寬位元組字串,而此字串包含 0。

第五個參數,lpWideCharStr,是指向轉換後存放寬位元組字串的位址,也就是存放萬國碼字串的緩衝區位址,不可和 lpMultiByteStr 指向相同位址或重疊,這樣會產生 ERROR_INVALID_PARAMETER 錯誤碼。最後一個參數,cchWideChar 則是 lpWideCharStr 所能容納字串的最大字元個數。如果 cchWideChar 設為零,那麼 MultiByteToWideChar 會傳回轉換後所需字元多寡,才能容納寬位元組字串,這時不需要 lpWideCharStr,所以 lpWideCharStr 也應設為 0。

lpMultiByteStr 所指的來源字串可以不是以 NULL 結尾,如果是這樣的話,就必須在 cbMultiByte 指定要轉換的位元組個數,而轉換後的寬位元組字串也不是以 NULL 結尾。當然,如果來源字串是以 NULL 結尾,這時候就可以把 cbMultiByte 設為 -1,而轉換後的寬位元組字串就是以 NULL 結尾。

返回時,如果呼叫成功,並且 cchWideChar 不為零,返回值,EAX,是寫入的寬字元個數,寫入的寬字組字串位於 lpWideCharStr 指向的緩衝區;如果呼叫成功,並且 cchWideChar 為零,返回值是接收到待轉換字串的緩衝區所需的寬字元個數大小,但不把寬字組字串寫入 lpWideCharStr 所指位址。如果呼叫失敗,返回值為零,可以呼叫 GetLastError 取得錯誤碼,錯誤碼可能是下面之一:ERROR_INSUFFICIENT_BUFFER、ERROR_INVALID_FLAGS、ERROR_INVALID_PARAMETER 或 ERROR_NO_UNICODE_TRANSLATION。

來源字串可能不合法的原因有下面兩種。一是雙位元組字元 ( DBCS ) 以正常位元組開始,但結尾位元組卻是錯的。另一種可能是沒有設定 MB_ERR_INVALID_CHARS 旗標,而且來源字串沒有使用內定字元卻翻譯成內定字元。

說明完 MultiByteToWideChar 後,應可了解,要把多位元組串轉換成萬國碼字串,步驟如下:

  1. 呼叫 MultiByteToWideChar 時,把最後兩個參數,lpWideCharStr 和 cchWideChar,設為 0。這樣就能傳回 cchWideChar 應該是多少。如果 lpMultiByteStr 所指之字串以 NULL 結尾,那麼就把 cbMultiByte 設為 -1,讓 MultiByteToWideChar 自行決定字串結尾。
  2. 呼叫 GlobalAlloc 配置記憶體,所需記憶體大小為第一步驟傳回值的兩倍。因為上一步驟傳回的是字元大小,並非位元組大小。
  3. 再一次呼叫 MultiByteToWideChar,這時候 lpWideCharStr 就得指向緩衝區位址,cchWideChar 為第一步驟的傳回值。

底下是實現的步驟:

1
2
3
4
5
6
7
        INVOKE  MultiByteToWideChar,CP_ACP,0,要轉換的字串位址,-1,0,0          ;取得轉換後的字串長度
        mov     dwLen,eax
        shl     eax,1                                                       ;把字串長度乘 2,變成位元組
        INVOKE  GlobalAlloc,GPTR,eax                                        ;配置記憶體
        mov     hMem,eax                                                    ;轉換後的字串將存於hMem所指位址
        INVOKE  MultiByteToWideChar,CP_ACP,0,要轉換的字串位址,-1,hMem,dwLen
        INVOKE  GlobalFree,hMem

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