Ch 17 文字

文字的大小 ( 點數 )

相信各位讀者曾經使用資源描述檔來描述對話盒堛滷惆謅艇韟w排方式,相信各位讀者也已經注意到了,在對話盒堛漱憒r大小,並不是以圖素為單位,那麼它與圖素究竟有何關係呢?。事實上不只是資源描述檔媢儭亄高漲r形大小,在許多文書處理軟體,如 UltraEdit-32、WORD、記事本等軟體的字形大小,也都不是以圖素為單位。例如下圖是在記事本堙A選擇『Courier New』字型,大小為『10』,但是實際上這些文字的大小並非 10 個圖素的大小 ( 此處的圖素是指 pixel,用以代替螢幕上的點,而文字的大小則以『點』表示,這篇文章堻ˋ磽u這個約定 )。它們在小木偶的 19 英寸液晶螢幕上大小是 16×16 圖素大,也可以說,我如果選擇『10』點字,但是在螢幕上卻是以 16 個圖素來表示。

在 Windows 作業系統堙A螢幕上的文書處理程式選擇 10 點的文字大小時,我們也會希望在列印時,印出的文字還是 10 點表示。但是我們知道螢幕的解析度與印表機的解析度是不同的 ( 此處的解析度是指每英寸有幾個圖素 ),一般液晶螢幕的解析度是每英寸約 86 個圖素;但是雷射印表機每英寸卻可達 600 圖素,兩者相差了 7 倍之多( 註一 ),假如 Windows 作業系統堛漱憒r大小 10 點就是在螢幕上的 10 個圖素,也是在印表機上的 10 個圖素,那麼在螢幕上可以很清楚得辨識 0.4 公分高、0.4 公分寬的字,在印表機印出來的字寬與高卻變成了 0.057 公分 ( 0.4÷7≒0.057 ),這樣印出來的字太小了,根本看不清楚。

換句話說,因為雷射印表機的解析度是每英寸 600 圖素,其圖素很小;而液晶螢幕解析度是每英寸 86 圖素,其圖素較大。也就是說,雷射印表機的每個圖素大小,大約是液晶螢幕的七分之一左右。如果作業系統把 10 點的字都當作 10 個圖素,而且同時應用在液晶螢幕或雷射印表機上,那麼在螢幕上看得很清楚的字,印出來就會小到縮成一團而看不清楚;或是印表機印出來適當大小的字,在螢幕顯示上卻是大到螢幕上只能顯示幾個字就容納不下了。

由以上的說明,我們應該可以明瞭,雖然同樣是 10 點字,在不同解析度的周邊設備上所使用的圖素多寡是不同的。然而,在一般傳統鉛字印刷堙A每英寸可以容納 72 點,這個值是在電腦、印表機等未發明之前就已經使用了數百年,電腦發明後,我們也遵守這項傳統。所以在傳統鉛字印刷堙A10 點字的高度是10/72英寸。我們也希望能在螢幕上,能顯示 10 點字也是10/72英寸,為了達到這個目的,在 Windows 作業系統堙A我們可以用 GetDeviceCaps 來取得周邊裝置的解析度,用法是:

int GetDeviceCaps(
    HDC hdc,    // device-context handle 
    int nIndex  // index of capability to query  
   );

這個 API 首先得取得該周邊裝置的設備內容 ( device context ),如果要取得水平方向的解析度 ( 此處的解析度是每英寸有多少圖素 ),nIndex 參數設為 LOGPIXELSX ;如果要取得垂直方向的解析度,就使 nIndex 設為 LOGPIXELSY ,接著呼叫 GetDeviceCaps 就可以了。nIndex 參數不只這兩個,還可以用其他參數,例如用 HORZRES、VERTRES 可以取得水平寬度和垂直高度有多少圖素;用 HORZSIZE、VERTSIZE 可以取得水平寬度和垂直高度有多少毫米等等,其他還有許多用法,可以參考 MSDN。

有了以上的觀念之後,我們就可以算出在不同周邊裝置中,不同文字點值所佔用的圖素大小,這個值等於該文字有多少英寸,再乘以該周邊裝置的解析度;而文字有多少英寸就是該文字的點數除以 72,如果以數學式表示,就是下面的式子:

文字所佔圖素=每英寸圖素×文字所佔英寸數=每英寸圖素×文字點數/72

萬年曆程式

小木偶利用上面所說的原理,撰寫一個萬年曆程式『perpetual calendar.asm』,它是一個以對話盒為主要界面的程式,使用者可以按主選單堛滿y放大』或『縮小』選項來放大或縮小字形,而此對話盒能夠自動計算所需的大小,調整對話盒大小,不致會截斷後面的文字。

這個程式處理 WM_INITDIALOG 訊息時,取得了一些資料,例如螢幕寬度、高度、當前時間,以及靜態控制項、編輯框、複合框及兩個按鈕的代碼,接著最重要的就是呼叫副程式,CalcFont,來計算字形大小。與取得時間有關的 API 有兩個,分別是 GetLocalTime 和 GetSystemTime,說明如下:

GetLocalTime 和 GetSystemTime API

在 Win32 API 堙A可以利用 GetSystemTime 來獲得系統時間,而這個系統時間就是協調世界時間 ( Coordinated Universal Time,縮寫成『UTC』註二 );而 GetLocalTime API 是用來獲得當地的時間。我們的程式呼叫這兩個 API 時,得把 SYSTEMTIME 結構體的位址傳給系統,而系統會把 SYSTEMTIME 結構體填好傳回來,它們都沒有傳回值,其語法是:

VOID GetLocalTime(
    LPSYSTEMTIME lpSystemTime   // address of system time structure  
   );

SYSTEMTIME 結構體的各欄位大小都是一個字組,這與其他 Win32 使用的變數、常數大小不同,這可能是因為表示時間以字組來表示,就綽綽有餘了 ( 字組可以表示 0∼65535 的範圍 )。底下是 SYSTEMTIME 結構體的各欄位意義:

SYSTEMTIME      STRUC
wYear           WORD    ?       ;西元年份 
wMonth          WORD    ?       ;月,1 為一月、2 為二月、3 為三月……
wDayOfWeek      WORD    ?       ;星期幾,0 為星期日、1 為星期一、2 為星期二……
wDay            WORD    ?       ;日
wHour           WORD    ?       ;時,採用 24 時制,由 0∼23
wMinute         WORD    ?       ;分
wSecond         WORD    ?       ;秒
wMilliseconds   WORD    ?       ;毫秒,從 0∼999
SYSTEMTIME      ENDS

接下來,在 WM_INITDIALOG 訊息堜I叫 CalcFont 副程式,這個副程式做了四件事。第一是依據字形的大小,dwDlgFontSize,算出字體的高度佔有幾個圖素,存入 dwFontHigh,然後依據 dwFontHigh 呼叫 CreateFont 建立邏輯字形,再用 GetTextMetrics 取得字形寬度佔有幾個圖素,存入 dwFontWidth 變數。這樣,程式就已經取得字形的高度與寬度有多少圖素。假如使用者更動了字形大小,那麼就會影響整個視窗版面,包含各控制項的大小及各控制項在對話盒的位置都會改變,這件事由呼叫 CalcCtlPosi 副程式完成,這是 CalcFont 副程式的第二項工作。CalcFont 做的第三件事是依據字形大小,計算對話盒的大小及把對話盒移到螢幕中央。CalcFont 副程式的最後一項工作便是把新建立完成的邏輯字形選入各控制項堙C

WM_SETFONT

此處值得一提的是,要改變對話盒堛漲r形,可以用 SelectObject;但這一招似乎對控制項無效,即使小木偶曾經取得控制項的裝置內容代碼,再以 SelectObject 改變其字形也無法成功。不過我們還是可以藉由 SendMessage 發送 WM_SETFONT 訊息給某個控制項,使其變更字形,此時 wParam 是字形代碼,lParam 是用來指示控制項在改變字形後是否立即重繪字形,TRUE 表示立即重繪,FALSE 表示否。到此算是已處理完 WM_INITDIALOG 訊息。

當使用者按下主選單中的『縮小』或『放大』選項時,dwDlgFontSize 會在 8 到 24 的範圍內隨之減一或增一,接著所做的事便是呼叫 CalcFont 副程式重重新計算字體高度、計算各控制項大小及位置、計算對話盒大小並移至螢幕中央、對對話盒選入新的邏輯字形這四件事,最後是呼叫 InvalidateRect 設定對話盒需要重新繪圖。

然後在 WM_PAINT 訊息堙A呼叫 DrawCalendarTbl 副程式,處理繪出該月份的月曆。在此月曆的第一列 ( 由左而右稱為列 ) 是星期『日、一、二……』等文字,這些文字以背景為黃色,當做欄位。如下圖所示,每個文字都是以 DrawText 輸出至設備內容:

萬年曆月曆表格說明文字框、間隔距離

由上圖中能夠看得出來,每個剪裁矩形左上角水平方向相距兩個字元的寬度 ( 圖中 w 表示一個字元寬 ),而上下方向則是相距一個半字元寬度 ( 圖中 h 表示一個字元高 ),

DrawText API

DrawText 是用來把字串以指定的方式顯示在某個的區域內,其原型為:

int DrawText(
    HDC     hDC,        // handle to device context
    LPCTSTR lpString,   // pointer to string to draw
    int     nCount,     // string length, in characters
    LPRECT  lpRect,     // pointer to structure with formatting dimensions
    UINT    uFormat     // text-drawing flags
   );

DrawText 會把字串顯示在剪裁矩形的第一行,如果字串含有換行字元 ( linefeed ) 及歸位字元 ( carriage return ),那麼就會換到下一行繼續顯示,我們可以用 DT_LEFT、DT_RIGHT、DT_CENTER 指定向左、右、中間對齊,當 DrawText 換行時,每行高度是 tmHeight,行距不包含 tmExternalLeading ( 參考 TEXTMETRIC 結構體 ),如果要行距包含外部間距,可以使用 DT_EXTERNALLEADING。

uFormat 參數可以用 DT_SINGLELINE 顯示一行字串,即使字串包含換行或歸位字元,DrawText 也會把這兩個字元當做一般文字顯示在螢幕上。在有 DT_SINGLELINE 的情形下,也可以搭配 DT_TOP、DT_BOTTOM、DT_VCENTER 來使字串位於剪裁矩形的頂端、底端或垂直的中間,內定值是 DT_TOP。小木偶這個萬年曆程式把星期『日、一、二……』等文字分七次呼叫 DrawText,每個剪裁矩形寬度是一個半字元寬 ( 圖中以 w 表示一個字元寬,以 h 表示一個字元高,圖中也以藍色矩形標示剪裁矩形,而剪裁矩形堶悸犖韘滽x形是文字框 ),而文字則是顯示在剪裁矩形中央 ( 使用了 DT_CENTER ),所以在文字兩邊各留有四分之一寬字元。

不管是多行或單行字串,如果剪裁矩形的寬度太小或字串太長,都會使字串右邊被截掉,這時 uFormat 可使用 DT_NOCLIP,使字串不受剪裁矩形的限制而超出剪裁矩形;因為 DT_NOCLIP 不須計算剪裁矩形的範圍,所以顯示速度當然會加快。但如果想要在剪裁矩形的範圍內,把超出的部份一到下一行顯示,可以使用 DT_WORDBREAK。

DrawText 還會把『&』字元視為前置字元,在此字元之後的字加上底線,而不會顯示『&』;如果要顯示『&』字元得使用『&&』。於是有底下三種用法:

  1. DT_NOPREFIX:忽略前置字元,把『&』字元當成普通字元看待。例如有一個字串是『A&BC&&D』,DrawText 會顯示為『ABC&D』;但是如果加上 DT_NOPREFIX,那麼 DrawText 會顯示為『A&BC&&D』。
  2. DT_HIDEPREFIX:隱藏前置字元,會把『A&BC&&D』顯示為『ABC&D』。
  3. DT_PREFIXONLY:只會在接著『&』後的字元畫出底線,而不會顯示出其他字元,例如會把『A&BC&&D』顯示為『    』。

如果 uFormat 使用了 DT_CALCRECT,那麼 DrawText 並不會把字串畫出來,而是計算所需要的剪裁矩形最小的大小,然後填入 lpRect 所指的 RECT 結構體內,這個功能可以在不知剪裁矩形的大小時使用,先使用 DT_CALCRECT 為參數呼叫 DrawText 計算好剪裁矩形之後,再呼叫一次 DrawText 真正的繪出文字來,算很常用的功能。假如這個字串是一行的文字,那麼程式設計師應先設定好剪裁矩形的左上角座標,亦即要先填好 top 和 left 欄位,然後以 DT_CALCRECT or DT_SINGLELINE 呼叫 DrawText,DrawText 返回時會把剪裁矩形的 bottom 和 right 填好。假如這個字串是多行文字,那麼程式設計師除了 top 和 left 要先填好外,還得指定 right,作為每一列的寬度,這樣 DrawText 才能決定此字串佔有幾列文字,進而決定剪裁矩形的高度,而傳回的 bottom 數值是剪裁矩形高度加上原先設定的 top。

DrawText 雖可以處理許多種狀況的字串,但是仍然無法改變文字顏色以及文字框的背景顏色。在 Windows 系統堙A要改變文字顏色須使用 SetTextColor ,要改變文字框的背景顏色,可以用 SetBkColor。請參考上圖,SetBkColor 只能改變文字框 ( 以綠色矩形框住的部份 ) 的背景顏色,至於文字框外的部份必須另外設法。在萬年曆這個程式堙A小木偶使星期『日、一、二……』等文字背景設為黃色,以凸顯它們是欄位,這堣p木偶是畫出一個填滿黃色的矩形,然後再以 SetBkColor 設定文字框背景為黃色。順帶一提,要取得文字框的背景顏色,可以用 GetBkColor。

欲畫出一個填滿的黃色矩形,可以用 FillRect API,在此之前,得先用 CreateSolidBrush 建立一個黃色的筆刷,CreateSolidBrush 會傳會一個筆刷代碼;接著再用 SelectObject 選擇此筆刷,FillRect 就會以這個筆刷填滿矩形。

SetBkMode

假如已經在星期『日、一、二……』等欄位上畫上塗滿顏色的黃色矩形,我們也可以設定背景模式為透明的,這樣系統就不會把文字框的背景塗滿顏色。事實上背景模式只有兩種可以選擇:TRANSPARENT、OPAQUE,前者是透明模式,後者是以現在的背景顏色把文字框塗滿。SetBkMode 語法是:

SetBkMode(
    HDC hdc,            // handle of device context
    int iBkMode         // flag specifying background mode
   );

當 Windows 在設備內容繪出文字時,是以內定的畫筆繪出文字的前景顏色,也用內定的筆刷繪出背景顏色。如上圖所示,是月曆的第一列星期『日、一、二……』的放大圖。文字的前景顏色是用設定,要而另外還有一個方法,可以設定文字框的背景為透明模式,這樣文字框的顏色不會顯示出來,就只會顯示出對話盒的顏色。底下的 SetBkMode 就是用來設定背景模式。

原始碼

底下是 perpetual_calendar.asm 的原始碼:

;perpetual calendar萬年曆:此程式利用模式對話盒為界面的應用程式
;             輸入西元幾年幾月,會把該月的月曆印在工作區
        .586
        .model  flat,stdcall
        option  casemap:none

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

IDC_EDIT        equ     100
IDC_COMBOBOX    equ     101
IDC_STATIC0     equ     102
IDC_STATIC1     equ     103
IDC_STATIC2     equ     104
IDC_BUTTON0     equ     105
IDC_BUTTON1     equ     106
IDM_EXIT        equ     107
IDM_LARGE       equ     108
IDM_SMALL       equ     109
IDM_CLEAR       equ     110
IDM_HELP        equ     111

DLGFONTSIZE     equ     12
FIELDBKCOLOR    equ     0ffffh  ;欄位背景顏色黃色
TODAYBKCOLOR    equ     0ff00h  ;今日的背景以綠色表示

;*******************************************************************************
.const
dwHalf          DWORD   0.5
wTen            WORD    10
w72             WORD    72
szDlgName       BYTE    'CalendarDlg',0
szFontFace      BYTE    '新細明體',0
szMonth         BYTE    '一',0,'二',0,'三',0,'四',0,'五',0,'六',0
                BYTE    '七',0,'八',0,'九',0,'十',0,'十一',0,'十二',0
szWeek          BYTE    '日',0,'一',0,'二',0,'三',0,'四',0,'五',0,'六',0
szFmt0          BYTE    '%04d',0
szFmt1          BYTE    '%2d',0
szFmt2          BYTE    '現在您選用的字形大小為 %2d 點。',0
nDayOfMonth     BYTE    31,28,31,30,31,30,31,31,30,31,30,31
szIcon          BYTE    'CalendarIcon',0
szTitle         BYTE    '現在字形大小',0

;*******************************************************************************
.data
hInstance       HINSTANCE       ?
hDialog         HANDLE          ?
hEdit           HANDLE          ?
hComboBox       HANDLE          ?
hBtn0           HANDLE          ?               ;『查詢』按鈕的代碼
hBtn1           HANDLE          ?               ;『離開』按鈕的代碼
hStatic0        HANDLE          ?               ;靜態控制項『西元』的代碼
hStatic1        HANDLE          ?               ;靜態控制項『年』的代碼
hStatic2        HANDLE          ?               ;靜態控制項『月』的代碼
hNewFont        HFONT           ?
dwDlgFontSize   DWORD           DLGFONTSIZE     ;第幾號字體,相當於OFFICE堛漲r型大小
dwFontHigh      DWORD           ?               ;字體高度,以圖素為單位
dwFontWidth     DWORD           ?               ;字體寬度,以圖素為單位
dwScrWidth      DWORD           ?               ;螢幕寬度,以圖素為單位
dwScrHigh       DWORD           ?               ;螢幕高度,以圖素為單位
ptCalendarTbl   POINT           
rectText        RECT            
stLocTime       SYSTEMTIME      
ps              PAINTSTRUCT     
tm              TEXTMETRIC      
szBuffer        BYTE    40 dup (0)
;*******************************************************************************
.code
;-------------------------------------------------------------------------------
;求西元EAX年ECX月一日是星期幾
;返回時EAX=星期幾
CalculateDate   PROC    USES ebx esi
                LOCAL   m:DWORD
                mov     esi,eax ;保存年於ESI
                mov     eax,14
                sub     eax,ecx
                sub     edx,edx
                mov     ebx,12
                div     ebx     ;EAX=a=int((14-month)/12)
                sub     esi,eax
                mul     ebx
                sub     eax,2
                add     eax,ecx
                mov     ebx,100
                mov     m,eax   ;EAX=month+12a-2
                mov     eax,esi
                div     ebx
                mov     esi,eax ;C=世紀=(year-a)的千位數與百位數
                shr     esi,2
                shl     eax,1
                sub     esi,eax
                add     esi,edx
                shr     edx,2
                inc     esi
                add     esi,edx ;ESI=C/4-2C+Y+Y/4+D
                mov     eax,m
                mov     ecx,m
                shl     ecx,2
                add     eax,ecx
                mov     ebx,5
                shl     ecx,1
                sub     edx,edx
                add     eax,ecx
                dec     eax     ;EAX=13m-1
                div     ebx
                mov     ecx,7
                xor     edx,edx
                add     eax,esi ;EAX=C/4-2C+Y+Y/4+D+int((13m-1)/5)
@@:             cmp     eax,0
                jge     positive
                add     eax,ecx
                jmp     @b
positive:       div     ecx
                xchg    eax,edx
                ret
CalculateDate   ENDP
;-------------------------------------------------------------------------------
;設定好文字顏色與剪裁矩形,每個中文字的剪裁矩形是一個半字元寬、一個字元高
CalcTextRect    PROC    hdc:HDC,dwWeek:DWORD
    .IF dwWeek==0
                invoke  SetTextColor,hdc,0ffh           ;星期日文字紅色
    .ELSEIF dwWeek==6
                invoke  SetTextColor,hdc,0ff0000h       ;星期六文字藍色
    .ELSE
                invoke  SetTextColor,hdc,0              ;黑色
    .ENDIF
                mov     ecx,dwFontWidth
                shl     ecx,1
                mov     eax,dwWeek
                mul     ecx
                add     eax,ptCalendarTbl.x
                mov     rectText.left,eax
                add     ecx,dwFontWidth         ;ECX=一個半字組寬
                mov     rectText.right,eax
                shr     ecx,1
                add     rectText.right,ecx
                ret
CalcTextRect    ENDP
;-------------------------------------------------------------------------------
DrawCalendarTbl PROC    hdc:HDC
                LOCAL   dwWeek:DWORD            ;星期日:0,星期一:1,星期二:2,……星期六:6
                LOCAL   dwYear:DWORD,dwMonth:DWORD,dwDay:DWORD
                LOCAL   nMaxDay:DWORD           ;該月有幾天
                LOCAL   colorTextBk:DWORD

;設定填滿黃色的矩形大小,存入rectText堙A此矩形左上角在ptCalendarTbl處,寬13個半字元,一個字元高
                mov     edx,dwFontWidth
                mov     eax,edx
                mov     ecx,edx
                shl     edx,3
                shl     eax,2
                add     edx,ecx
                add     edx,eax
                shr     ecx,1
                add     edx,ecx                 ;EDX=13個半字元寬
                mov     eax,ptCalendarTbl.x
                mov     rectText.left,eax
                add     edx,eax
                mov     rectText.right,edx
                mov     ecx,ptCalendarTbl.y
                mov     rectText.top,ecx
                add     ecx,dwFontHigh
                mov     rectText.bottom,ecx

;畫出星期『日』、『一』∼『六』的欄位的背景顏色,即填滿黃色的矩形
                invoke  CreateSolidBrush,FIELDBKCOLOR
                push    eax
                invoke  SelectObject,hdc,eax
                invoke  DeleteObject,eax
                pop     edx
                invoke  FillRect,hdc,OFFSET rectText,edx

;畫出星期『日』、『一』∼『六』的欄位名
                invoke  SelectObject,hdc,hNewFont
                invoke  DeleteObject,eax
                invoke  SetBkColor,hdc,FIELDBKCOLOR
                mov     dwWeek,0
                mov     edx,ptCalendarTbl.y
                mov     rectText.top,edx
                add     edx,dwFontHigh
                mov     rectText.bottom,edx
@@:             invoke  CalcTextRect,hdc,dwWeek
                mov     ecx,dwWeek
                shl     ecx,1
                add     ecx,dwWeek
                add     ecx,OFFSET szWeek
                invoke  DrawText,hdc,ecx,-1,OFFSET rectText,DT_CENTER or DT_VCENTER or DT_SINGLELINE
                mov     ecx,dwFontWidth
                inc     dwWeek
                shl     ecx,2
                add     rectText.left,ecx
                cmp     dwWeek,7
                jb      @b

;設定文字背景顏色與對話盒背景同色
                invoke  GetSysColor,COLOR_BTNFACE       ;取得對話盒背景色
                mov     colorTextBk,eax
                invoke  SetBkColor,hdc,eax

;由編輯框、複合框分別取得西元幾年幾月,並計算該月的一日是星期幾
                invoke  GetDlgItemInt,hDialog,IDC_EDIT,NULL,FALSE
                mov     dwYear,eax
                invoke  SendMessage,hComboBox,CB_GETCURSEL,0,0
                mov     dwMonth,eax
                mov     ecx,eax
                inc     ecx
                mov     eax,dwYear
                call    CalculateDate   ;計算星期幾,傳回於EAX
                mov     dwWeek,eax      ;星期日:0,星期一:1,……

;計算該月有幾天,並存於nMaxDay
                mov     ecx,dwMonth
                movzx   edx,nDayOfMonth[ecx]
                mov     nMaxDay,edx
    .IF ecx==1                          ;檢查二月份的天數
                mov     eax,dwYear      ;若為閏年則二月有29天,nMaxDay=29
                and     eax,011b        ;否則為平年,二月有28天,nMaxDay=28
                jnz     ok
                xor     edx,edx
                mov     ecx,100
                mov     eax,dwYear
                div     ecx
        .IF edx==0
                mov     eax,dwYear
                mov     ecx,400
                div     ecx
                or      edx,edx
                jz      leap_year
        .ELSE
leap_year:      inc     nMaxDay
        .ENDIF
    .ENDIF

;畫出月曆表格
ok:             mov     dwDay,1         ;每個月由一日開始到nMaxDay止
                mov     ecx,ptCalendarTbl.y
                mov     rectText.top,ecx
next_week:      mov     eax,dwFontHigh ;每一列(由左而右)代表一個星期
                shl     eax,1          ;每列之間相隔半個字元高,再加上每列一個字元高
                add     eax,dwFontHigh ;,故每列文字矩形左上角要加上一個半字元高,才
                shr     eax,1          ;是下一列文字矩形左上角的位置所在
                add     eax,ecx
                mov     rectText.top,eax
                add     eax,dwFontHigh
                mov     rectText.bottom,eax
next_day:       mov     eax,dwDay
                cmp     eax,nMaxDay
                ja      finish
                invoke  wsprintf,OFFSET szBuffer,OFFSET szFmt1,eax
                invoke  CalcTextRect,hdc,dwWeek
                mov     ecx,dwDay
    .IF cx==stLocTime.wDay              ;若為今日,則以綠色背景表示
                mov     eax,TODAYBKCOLOR
    .ELSE
                mov     eax,colorTextBk
    .ENDIF
                invoke  SetBkColor,hdc,eax
                invoke  DrawText,hdc,OFFSET szBuffer,-1,ADDR rectText,DT_CENTER or DT_VCENTER or DT_SINGLELINE
                inc     dwDay
                inc     dwWeek
                cmp     dwWeek,7
                jb      next_day
                mov     dwWeek,0
                mov     ecx,rectText.top
                jmp     next_week
finish:         ret
DrawCalendarTbl ENDP
;-------------------------------------------------------------------------------
;依據dwFontWidth、dwFontHigh計算各控制項的大小,並把各控制項移動到對話盒適當的位置
;傳回值:EDX為工作區寬度,EAX為工作區高度
CalcCtlPosi     PROC
                LOCAL   dwHalfFntWidth:DWORD,ptCtl:POINT
                LOCAL   dwBtnWidth:DWORD,dwBtnHigh:DWORD
                LOCAL   dwClientWidth:DWORD,dwClientHigh:DWORD  ;工作區的寬度與高度

;依據dwFontWidth、dwFontHigh計算按鈕的寬度與高度,寬八個字元,高7/4字元
                mov     edx,dwFontWidth
                shl     edx,3
                mov     dwBtnWidth,edx
                mov     ecx,dwFontHigh
                shl     ecx,3
                sub     ecx,dwFontHigh
                shr     ecx,2
                mov     dwBtnHigh,ecx

;依據dwFontWidth、dwFontHigh決定每個控制項位置,方式如下:
;  1.『西元』在工作區左上角右方三個字元、下方兩個字元處,寬兩個字元,高一個字元
;  2.編輯框在『西元』右方半個字元處,寬四個字元,高一個字元再加六點
;  3.『年』在編輯框右方半個字元處,寬一個字元,高一個字元
;  4.複合框在『年』右方半個字元處,寬四個字元,高
;  5.『月』在複合框半個字元處,
;  6『查詢』、『離開』按鈕在『西元』下方一個字元處
;返回時,EAX傳回新的視窗大小,低字組為視窗寬度,高字組為視窗高度
                mov     ecx,dwFontHigh
                mov     eax,dwFontWidth
                shl     ecx,1
                mov     dwHalfFntWidth,eax
                mov     ptCtl.y,ecx
                shr     dwHalfFntWidth,1
                mov     ptCtl.x,eax
                shl     eax,1
                add     ptCtl.x,eax
                invoke  MoveWindow,hStatic0,ptCtl.x,ptCtl.y,ecx,dwFontHigh,TRUE
                mov     eax,dwFontWidth
                shl     eax,1
                mov     ecx,eax
                add     eax,dwHalfFntWidth
                shl     ecx,1
                add     ptCtl.x,eax
                mov     edx,ptCtl.y
                mov     eax,dwFontHigh
                push    ecx
                sub     edx,3
                add     eax,6
                invoke  MoveWindow,hEdit,ptCtl.x,edx,ecx,eax,TRUE
                pop     ecx
                mov     eax,dwHalfFntWidth
                add     ptCtl.x,ecx
                add     ptCtl.x,eax
                invoke  MoveWindow,hStatic1,ptCtl.x,ptCtl.y,dwFontWidth,dwFontWidth,TRUE
                mov     ecx,dwFontWidth
                mov     eax,dwHalfFntWidth
                add     ptCtl.x,ecx
                add     ptCtl.x,eax
                shl     ecx,2
                mov     edx,ptCtl.y
                mov     eax,dwFontHigh
                sub     edx,3
                add     eax,6
                invoke  MoveWindow,hComboBox,ptCtl.x,edx,ecx,eax,TRUE
                mov     edx,dwHalfFntWidth
                mov     ecx,dwFontWidth
                add     ptCtl.x,edx
                shl     ecx,2
                add     ptCtl.x,ecx
                invoke  MoveWindow,hStatic2,ptCtl.x,ptCtl.y,dwFontWidth,dwFontWidth,TRUE
                mov     eax,ptCtl.x
                mov     edx,dwFontWidth
                shl     edx,2
                add     eax,edx         ;EAX=工作區寬度=西元到月的寬度再加上左右各三字元寬
                mov     ecx,dwFontHigh
                mov     dwClientWidth,eax
                shl     ecx,1
                add     ptCtl.y,ecx
                mov     edx,dwBtnWidth
                mov     ecx,3
                shl     edx,1           ;兩個按鈕
                sub     eax,edx
                cwd
                div     cx
                mov     ptCtl.x,eax
                push    eax             ;EAX=兩按鈕之間隔=『查詢』離左邊框之間隔
                invoke  MoveWindow,hBtn0,eax,ptCtl.y,dwBtnWidth,dwBtnHigh,TRUE
                mov     edx,dwBtnWidth
                pop     eax
                add     ptCtl.x,edx
                add     eax,ptCtl.x
                invoke  MoveWindow,hBtn1,eax,ptCtl.y,dwBtnWidth,dwBtnHigh,TRUE

;計算表格位置,並存於ptCalendarTbl結構體堙C此表格是用來顯示某個月的月曆,
;每一行寬一個半字元,另外行與行之間相隔一個字元,每列之間相隔半個字元。
                mov     ecx,dwFontHigh
                mov     eax,dwBtnHigh
                shl     ecx,1
                add     ptCtl.y,eax
                add     ecx,ptCtl.y
                mov     dwClientHigh,ecx
                mov     ptCalendarTbl.y,ecx     ;傳回月曆表格的Y位置,在按鈕底緣下兩個字元高
                mov     edx,dwFontWidth
                mov     eax,edx
                mov     ecx,edx
                shl     edx,3
                shl     eax,2
                add     edx,ecx
                add     edx,eax
                shr     ecx,1
                add     edx,ecx                 ;EDX=13個半字元寬
                mov     eax,dwClientWidth
                sub     eax,edx
                shr     eax,1
                mov     ptCalendarTbl.x,eax     ;傳回月曆表格的X位置
                mov     ecx,dwFontHigh
                mov     eax,ecx
                shr     eax,1
                add     eax,ecx
                mov     ecx,eax
                shl     eax,2
                shl     ecx,1
                add     eax,ecx                 ;EAX=1.5個字元高乘以6
                add     eax,dwFontHigh          ;EAX=最後一列底緣
                add     eax,dwFontHigh
                add     eax,dwFontHigh
                add     dwClientHigh,eax
                mov     eax,dwClientHigh
                mov     edx,dwClientWidth
                ret
CalcCtlPosi     ENDP
;-------------------------------------------------------------------------------
CalcFont        PROC    hdc:HDC,hWin:HWND
                LOCAL   dwTemp:DWORD
                LOCAL   dwWinWidth:DWORD,dwWinHigh:DWORD        ;視窗的寬度與高度
                invoke  DeleteObject,hNewFont   ;刪除舊的字形
;依據dwDlgFontSize大小,建立邏輯字體,其高度為
;int(fabs(FontSize*GetDeviceCaps(hdc,LOGPIXELSY)/72)/10.0+0.5)
;並把字體高度及寬度分別存於dwFontHigh、dwFontWidth變數
                invoke  GetDeviceCaps,hdc,LOGPIXELSY ;取得沿Y軸方向的每邏輯英吋的點數
                mov     dwTemp,eax      ;--st0--
                fild    dwTemp          ;T=每邏輯英吋的點數
                fimul   dwDlgFontSize   ;TF
                fimul   wTen            ;10TF
                fidiv   w72             ;10TF/72
                fidiv   wTen            ;TF/72
                fadd    dwHalf          ;TF/72+0.5
                fabs                    ;|TF/72+0.5|
                fistp   dwFontHigh
                invoke  CreateFont,dwFontHigh,0,0,0,500,0,0,0,0,0,0,0,0,OFFSET szFontFace
                mov     hNewFont,eax
                invoke  SelectObject,hdc,eax
                invoke  DeleteObject,eax
                invoke  GetTextMetrics,hdc,OFFSET tm
                mov     eax,tm.tmMaxCharWidth
                mov     dwFontWidth,eax
;用CalcCtlPosi計算各控制項位置及改變大小,並移動各控制項到對話盒堛瑣A當位置
                call    CalcCtlPosi
;修改對話盒大小,並把對話盒移到螢幕中央
                mov     dwWinWidth,edx  ;EDX為對話盒的工作區寬度
                mov     dwWinHigh,eax   ;EAX為對話盒的工作區高度,以圖素為單位
                invoke  GetSystemMetrics,SM_CYMENU
                add     dwWinHigh,eax
                invoke  GetSystemMetrics,SM_CYCAPTION
                add     dwWinHigh,eax
                invoke  GetSystemMetrics,SM_CYFIXEDFRAME
                shl     eax,1           ;對話盒高度=工作區高度+主選單高度+
                add     dwWinHigh,eax   ;      標題欄高度+2×外框高度
                invoke  GetSystemMetrics,SM_CXFIXEDFRAME
                shl     eax,1
                add     dwWinWidth,eax  ;對話盒寬度=工作區寬度+2×外框寬度
                mov     edx,dwScrWidth
                sub     edx,dwWinWidth
                mov     ecx,dwScrHigh
                sub     ecx,dwWinHigh
                shr     edx,1           ;EDX=對話盒X座標=(螢幕寬度-對話盒寬度)/2
                shr     ecx,1           ;ECX=對話盒Y座標=(螢幕高度-對話盒高度)/2
                invoke  MoveWindow,hWin,edx,ecx,dwWinWidth,dwWinHigh,TRUE
;利用把已建立好的字形選入各控制項
                invoke  SendMessage,hStatic0,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hEdit,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hStatic1,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hComboBox,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hStatic2,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hBtn0,WM_SETFONT,hNewFont,TRUE
                invoke  SendMessage,hBtn1,WM_SETFONT,hNewFont,TRUE
                ret
CalcFont        ENDP
;-------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HANDLE,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   dwTemp:DWORD
.IF uMsg==WM_PAINT
                invoke  BeginPaint,hDlg,OFFSET ps
                invoke  DrawCalendarTbl,ps.hdc
                invoke  EndPaint,hDlg,OFFSET ps

.ELSEIF uMsg==WM_COMMAND
                mov     eax,wParam
    .IF lParam==0
    ;若lParam=0,表示選定某選單選項
        .IF ax==IDM_EXIT
                invoke  EndDialog,hDlg,NULL
        .ELSEIF ax==IDM_HELP
                invoke  wsprintf,OFFSET szBuffer,OFFSET szFmt2,dwDlgFontSize
                invoke  MessageBox,hDlg,OFFSET szBuffer,OFFSET szTitle,MB_OK or MB_ICONQUESTION
        .ELSEIF ax==IDM_CLEAR
                invoke  SetDlgItemText,hDlg,IDC_EDIT,NULL
        .ELSEIF ax==IDM_LARGE
                inc     dwDlgFontSize
            .IF dwDlgFontSize>24
                mov     dwDlgFontSize,24
                jmp     no_change
            .ENDIF
                jmp     @f
        .ELSEIF ax==IDM_SMALL
                dec     dwDlgFontSize
            .IF dwDlgFontSize<8
                mov     dwDlgFontSize,8
                jmp     no_change
            .ENDIF
@@:             invoke  GetDC,hDlg
                mov     dwTemp,eax
                invoke  CalcFont,eax,hDlg
                invoke  ReleaseDC,hDlg,dwTemp
                invoke  InvalidateRect,hDlg,NULL,TRUE
        .ENDIF
    .ELSE
    ;若lParam不為0,表示選定某子控制元件
                mov     edx,wParam
                shr     edx,16
        .IF dx==BN_CLICKED
            .IF ax==IDC_BUTTON1         ;表示按下『離開』按鈕
                invoke  SendMessage,hDlg,WM_COMMAND,IDM_EXIT,0
            .ELSEIF ax==IDC_BUTTON0     ;表示按下『查詢』按鈕
                jmp     look_up
            .ENDIF
        .ELSEIF dx==CBN_SELCHANGE       ;表示使用者已選定月份了
            .IF ax==IDC_COMBOBOX
look_up:        invoke  InvalidateRect,hDlg,NULL,TRUE
            .ENDIF
        .ENDIF
    .ENDIF
no_change:

.ELSEIF uMsg==WM_INITDIALOG
                invoke  LoadIcon,hInstance,OFFSET szIcon
                invoke  SendMessage,hDlg,WM_SETICON,ICON_BIG,eax
                mov     eax,hDlg
                mov     hDialog,eax
;取得螢幕的寬度及高度,分別存於dwScrWidth、dwScrHigh
                invoke  GetSystemMetrics,SM_CXSCREEN
                mov     dwScrWidth,eax
                invoke  GetSystemMetrics,SM_CYSCREEN
                mov     dwScrHigh,eax
;取得靜態控制項、編輯框、複合框及兩個按鈕的代碼
                invoke  GetDlgItem,hDlg,IDC_EDIT
                mov     hEdit,eax
                invoke  SetFocus,eax    ;把輸入焦點設在編輯框
                invoke  GetDlgItem,hDlg,IDC_COMBOBOX
                mov     hComboBox,eax
                invoke  GetDlgItem,hDlg,IDC_BUTTON0
                mov     hBtn0,eax
                invoke  GetDlgItem,hDlg,IDC_BUTTON1
                mov     hBtn1,eax
                invoke  GetDlgItem,hDlg,IDC_STATIC0
                mov     hStatic0,eax
                invoke  GetDlgItem,hDlg,IDC_STATIC1
                mov     hStatic1,eax
                invoke  GetDlgItem,hDlg,IDC_STATIC2
                mov     hStatic2,eax
;底下的程式片段是用來把『一』、『二』…加入複合框
                mov     dwTemp,0
@@:             mov     ecx,dwTemp
                mov     edx,ecx
                shl     ecx,1
                add     ecx,edx
        .IF dl==11
                add     ecx,2
        .ENDIF
                add     ecx,OFFSET szMonth
                invoke  SendMessage,hComboBox,CB_ADDSTRING,NULL,ecx
                inc     dwTemp
                cmp     dwTemp,12
                jb      @b
;使當前的西元年、月變成複合框的內定選項
                invoke  GetLocalTime,OFFSET stLocTime
                movzx   ecx,stLocTime.wMonth
                dec     ecx
                invoke  SendMessage,hComboBox,CB_SETCURSEL,ecx,0
                movzx   ecx,stLocTime.wYear
                invoke  wsprintf,OFFSET szBuffer,OFFSET szFmt0,ecx
                invoke  SetWindowText,hEdit,OFFSET szBuffer
;用副程式,CalcFont,計算字體大小;並用副程式,SetCtlPosition,設置控制項位置
                invoke  GetDC,hDlg
                mov     dwTemp,eax
                invoke  CalcFont,eax,hDlg
                invoke  ReleaseDC,hDlg,dwTemp

.ELSEIF uMsg==WM_CLOSE
                invoke  SendMessage,hDlg,WM_COMMAND,IDM_EXIT,0

.ELSE
                mov     eax,FALSE
                ret
.ENDIF
                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
;*******************************************************************************
                end     start

底下是 perpetual_calendar.rc 原始碼:

#include "c:\masm32\include\resource.h"

#define IDC_EDIT        100     //西元年份編輯框
#define IDC_COMBOBOX    101     //月份複合框
#define IDC_STATIC0     102
#define IDC_STATIC1     103
#define IDC_STATIC2     104
#define IDC_BUTTON0     105     //查詢按鈕
#define IDC_BUTTON1     106     //離開按鈕
#define IDM_EXIT        107
#define IDM_LARGE       108
#define IDM_SMALL       109
#define IDM_CLEAR       110
#define IDM_HELP        111

#define DLGFONTSIZE     12

CalendarMenu    MENU
BEGIN
  MENUITEM      "離開",IDM_EXIT
  MENUITEM      "放大",IDM_LARGE
  MENUITEM      "縮小",IDM_SMALL
  MENUITEM      "清除",IDM_CLEAR
  MENUITEM      "說明",IDM_HELP
END

CalendarIcon    ICON    Moon.ico

CalendarDlg     DIALOG MOVEABLE PURE LOADONCALL DISCARDABLE 100,0,200,300
STYLE           DS_FIXEDSYS|DS_SETFONT|WS_POPUP|WS_VISIBLE|WS_SYSMENU|
                WS_MAXIMIZEBOX|WS_MINIMIZEBOX|WS_CAPTION 
CAPTION         "萬年曆"
FONT            DLGFONTSIZE,"細明體"
MENU            CalendarMenu
BEGIN
  CONTROL "西元",IDC_STATIC0, "STATIC",  WS_CHILD|SS_LEFT|WS_GROUP|WS_VISIBLE,5,13,33,11
  CONTROL "",    IDC_EDIT,    "EDIT",    WS_CHILD|ES_LEFT|WS_BORDER|WS_TABSTOP|WS_VISIBLE|ES_NUMBER,40,10,47,12
  CONTROL "年",  IDC_STATIC1, "STATIC",  WS_CHILD|SS_LEFT|WS_GROUP |WS_VISIBLE,90,13,35,11
  CONTROL "",    IDC_COMBOBOX,"COMBOBOX",WS_CHILD|WS_TABSTOP|WS_VISIBLE|CBS_DROPDOWNLIST|WS_VSCROLL,120,10,48,90
  CONTROL "月",  IDC_STATIC2, "STATIC",  WS_CHILD|SS_LEFT|WS_GROUP |WS_VISIBLE,170,13,25,11
  CONTROL "查詢",IDC_BUTTON0, "BUTTON",  WS_CHILD|BS_PUSHBUTTON|WS_TABSTOP,40,28,40,12
  CONTROL "離開",IDC_BUTTON1, "BUTTON",  WS_CHILD|BS_PUSHBUTTON|WS_TABSTOP,120,28,40,12
END

底下是 perpetual_calendar.mak 檔的內容:

# 產生 perpetual_calendar.EXE
ALL : perpetual_calendar.EXE

# 組譯器及連結器
perpetual_calendar.EXE : perpetual_calendar.ASM perpetual_calendar.RES
# 組譯器會自動傳 perpetual_calendar.OBJ 給連結器,故 /link 之後不用再加上 perpetual_calendar.OBJ
    ml /coff perpetual_calendar.ASM /link /SUBSYSTEM:WINDOWS perpetual_calendar.RES 

# 資源編譯器
perpetual_calendar.RES : perpetual_calendar.RC Moon.ico
    rc perpetual_calendar.RC

圖示檔,Moon.ico 在下載。


註一:小木偶使用 ViewSonic VA916 19 英寸液晶螢幕,可視區域是 37.6×30.1 cm2,也就是 14.8×11.85 平方英寸,以寬 1280 圖素、高 1024 圖素顯示時,沿著水平方向的解析度是每英寸 86.5 個圖素,沿著垂直方向的解析度是 86.4 個圖素 ( 1280÷14.8≒86.5,1024÷11.85≒86.4,600/86≒6.98 )。

註二:因為地球是圓形的,在太陽直射的地區只有一小塊區域,在這塊區域是正午 12 時;而在這塊區域的東方早已過了正午 12 時;而在這塊區域的西方,卻尚未到正午。我們把地球分成 24 個時區,並以英國倫敦的格林威治天文台當做標準時間,稱為格林威治標準時間 ( Greenwich Mean Time,縮寫為『GMT』),當太陽在格林威治天文台的最高點時,即為格林威治標準時間正午 12:00,而中華民國在比格林威治標準時間快 8 個小時的地區,就以 GMT+8 表示;夏威夷的時間是比格林威治標準時間時間慢 10 小時,以 GMT-10 表示。

但是因為地球並非正球體,每天的自轉是有些不規則的,而且正在緩慢減速中,所以,格林威治標準時間已經不再被作為標準時間使用,現在的標準時間,是由銫原子鐘報時的協調世界時間 ( UTC )。所以在中華民國,用 GetSystemTime 所得到的是協調世界時間是清晨兩點整;而用 GetLocalTime 取得的時間是是上午十點整。


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