2013年6月11日 星期二

從轉譯 ZoomIn 工具講講撰寫組語的一些樂趣與困難

常常用 Visual C++ 寫古典 Win32 程式的人應該都知道,經典的第六版之所以歷久不衰
除了本身整合開發環境高效率,體積小,還可以將專案 dsp 檔轉成標準的 makefile 檔之外
就是第六版還附有所謂 17 大經典工具,這些工具被稱為 SDK Tools,它們會被稱為
經典不僅僅是各個程式小巧,而且每一支都很實用,更棒的就是這些工具本身原始碼
也是公開可以研究,這些工具不僅輔助開發程式的利器,本身每支工具的原始碼就是
示範 Win32 許多重要 API 的經典寶庫,我學習 Windows 程式設計的歷史中,絕大部分的
功底就是念透這些工具的原始碼,這個比買一本書看還有用,其中有六支值得仔細閱讀



1. aniedit    : 可以創造與編輯動畫游標 ( animated cursors ),從原始碼會學習到游標相關 API

2. fontedit  : 可以造字,從原始碼會學習到不少重要控制字型檔的 API

3. imagedit : 這個工具很實用,可以創造、修改與讀取 Icon,會學習到一個編輯工具要怎麼寫

4. spy        : 此工具可以截取視窗的訊息如何流動是很重要的工具,順便學會 Hook DLL 怎麼寫

5. windiff   : 可以比較兩個讀進來的文字檔有甚麼地方不同,可以用視覺化方式呈現

6. zoomin  : 恩,這是今天要講的工具,它可以對螢幕局部作像素級放大還可以複製入剪貼簿

這六支工具的原始碼都相當重要,除了針對特殊用途的 API,還會看到很多常用不斷重複
與視窗有關的常用 API,另外除了學習 API 之外,還要好好看這些程式的架構,培養撰寫
建構結構化視窗程式框架的能力,那原始碼哪裡找?看看有沒有人還有 MSDN 6 的光碟,
SDK Tools 原始碼通通都在裡面,另外它們的環境都 "非常標準",就是說它們其實跟環境
無關,在指令列下 nmake 就可以直接產生可執行檔,所以它們連 "環境" 都很經典,也是學習
打造 makefile 指令檔的好對象,每支我都曾經在 VS2008 下重新編譯過,老程式馬上變成
新版本,這些經典工具都有後繼傳人,例如 spy 的強化版 spy++,可惜第六版之後
這些好工具就各自分家了,你還是可以一支一支慢慢收集,但是有一些新的進化版已經
不在提供原始碼了,蠻可惜的事情 。

有趣的地方就出現了,前一陣子我重新轉譯了 ZoomIn 這支工具,整個程式重新用
FASM 寫了一遍,其實發現了不少好玩的地方與困難點,我並不想講解 ZoomIn 工具
原始碼本身的原理,而是要談談,我在轉譯過程中遇到的一些困難與樂趣,那我要拿
那一部分呢?就是整個 ZoomIn 的 C 程式裡面最難轉譯的副程式 CreatePhysicalPalette
這個副函式的用途我不想多談,反正就是一種 GDI 物件,可以創造實體調色盤,利用
調色盤 ( Palette ) 這種玩意,可以讓 ZoomIn 工具達到快速縮放像素級畫面的功能,有些
人仿 ZoomIn 這類的工具只學半套,以為只要會 StretchBlt 這個 API 就可以達成,但是發現
怎麼做出來的效率跟 ZoomIn 差了一大截,那就是因為還忘了要使用 Palette 這種 GDI 物件
沒有用 Palette,StretchBlt 就會很頓,因為 Palette 可是直接把顏色作查表阿,這種速度當然
不是 RGBA 顏色系統可以拼的囉,原理講到這,我們應該把重點擺在,怎麼把條副函式
的 C 版本改用 FASM 實現才對,現在讓我們看看這條 C 函式原本的面貌

原始 ZoomIn 中 CreatePhysicalPalette 的 C 原始碼 :

HPALETTE CreatePhysicalPalette(VOID)
{
    PLOGPALETTE ppal;
    HPALETTE hpal = NULL;
    INT i;

    ppal = (PLOGPALETTE)LocalAlloc(LPTR,
            sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * NPAL);
    if (ppal) {
        ppal->palVersion = 0x300;
        ppal->palNumEntries = NPAL;

        for (i = 0; i < NPAL; i++) {
            ppal->palPalEntry[i].peFlags = (BYTE)PC_EXPLICIT;
            ppal->palPalEntry[i].peRed   = (BYTE)i;
            ppal->palPalEntry[i].peGreen = (BYTE)0;
            ppal->palPalEntry[i].peBlue  = (BYTE)0;
        }

        hpal = CreatePalette(ppal);
        LocalFree(ppal);
    }

    return hpal;
} 

各位可以看到,這個程式碼使用了 Win32 API 中邏輯調色盤結構 LOGPALETTE,這個其實
就是一個色表的結構,而根據 MSDN 裡面記載,要配置色表的記憶體區塊,就是要計算
LOGPALETTE 結構大小加上有多少項 PALETTEENTRY,因此 NPAL 就是 PALETTEENTRY
的個數,最後呼叫 LocalAlloc 配置色表的記憶體區塊,下面迴圈就沒有甚麼,對每個色表
內的項目填值而已,細部用途就不解釋了,迴圈跑完就放入 CreatePalette 用這個設定好的
邏輯調色盤創造調色盤,獲得調色盤的 Handle 後就可以釋放剛剛配置的記憶體了,然後
回傳調色盤的 Handle,這個函式用 C 寫很簡單,但是我發現用 FASM 寫就不好搞了,首先
它有宣告三個區域變數,另外很糟糕的一件事,LOGPALETTE 在 FASM 內提供的引入標頭
竟然沒有,那怎麼辦呢?這個就要考驗你怎麼善用 x86 組合語言的 Register 囉。

在寫組合語言的時候,有一個很重要的觀念要先建立,就是 Register 有分兩大類

第一類是 Volatile 與  第二類是 Nonvolatile

這是許多寫組合語言常常會迷惑的地方,就是 " 目前要用什麼 Register,會不會有問題 "
這個問題的解答就是要弄懂哪些 Register 是 volatile 那些是 nonvolatile,假如是 volatile 就大膽
的用,不用怕,假如是 nonvolatile 也沒甚麼好怕, push 後在用,用完記得 pop 回來,這樣就
很清楚了,所以我的組語版 CreatePhysicalPalette 副函式我決定要偷吃步,用人工最佳化,誰說
一定要放在區域變數,各位還記得 Win32 API 會把回傳值放在 eax 嗎?恩,我們現在用
FASM 寫就有直接操作暫存器的優勢了,在組語裡面,我們手上有哪些籌碼可以讓我們偷吃
步呢? eax, ecx, edx, ebx, esi, edi, ebp 我們手上有這 7 個暫存器可以直接用喔,那我們幹嘛還要
宣告區域變數阿,直接用暫存器在 CPU 內部搬來搬去更有效率,所以下面就給出
我最後寫出來的版本。

CreatePhysicalPalette 的 FASM 原始碼 :

CreatePhysicalPalette:
    push ebx esi edi
    ; LPTR := 0x0040 Combines LMEM_FIXED and LMEM_ZEROINIT
    ; 1032 = sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * NPAL
    invoke LocalAlloc,LPTR,1032
    cmp    eax, 0         ; eax := PLOGPALETTE ppal
    je     ExitCreatePhys ; exit subroutine if memory allocation failed

    mov word[eax],768    ; ppal->palVersion = 0x300
    mov word[eax+2],NPAL ; ppal->palNumEntries
    xor ecx,ecx          ; ecx = 0
    lea edx,dword[eax+4] ; address of ppal->palPalEntry

  CreatePhys:
      mov byte[edx],cl  ; ppal->palPalEntry.peRed
      mov byte[edx+1],0 ; ppal->palPalEntry.peGreen
      mov byte[edx+2],0 ; ppal->palPalEntry.peBlue
      mov byte[edx+3],2 ; ppal->palPalEntry.peFlags = PC_EXPLICIT
      inc ecx
      add edx,4         ; ppal->palPalEntry += 4 move to next that
      cmp ecx,NPAL
      jl  CreatePhys

      mov    edi,eax           ; store ppal into edi
      invoke CreatePalette,edi ; edi := PLOGPALETTE ppal
      mov    ebx,eax           ; ebx <- eax := HPALETTE hpal
      invoke LocalFree,edi     ; release memory
      mov    eax,ebx           ; return hpal

  ExitCreatePhys:
    pop  edi esi ebx
    ret  

我想第一行不用在做多解釋了,以前 FASM 文章出現過很多次了, ebx esi edi 這些是屬於
nonvolatile 的 register 反正先 push 就對了,接下來在組語裡面 invoke 配置記憶體的函式後,
後面組語威力強大的地方就出現了,因為我知道 API 一定會把返回值放在  eax 裡面,因此
直接用 word[eax] 直接就存取 LOGPALETTE 結構第一個欄位 palVersion 直接把 768 寫入
eax 指向的位址,word[eax+2] 直接將 NPAL 寫入結構第二個欄位 palNumEntries,所以跟
C 版完全不同的思路,直接操作 eax 就好了,不用宣告區域變數,直接使用暫存器更快
而且產生的機械碼更短,後面迴圈也不需要宣告變數,把 ecx 抓過來用,因為要存取
調色盤的項目, 調色盤項目本身又是結構,這邊就可以看到結構中的結構在組語中的面貌
就是用 基底位址+偏移位址 又變成另外新的基底位址,也就是程式碼裡面的
lea edx, dword[eax+4] ,  為什麼是 dword 呢?因為位址其實本身就是一個 DWORD 的整數值,
然後接著持續利用暫存器偷懶,直接把新的基底位址放在 edx,指令 lea 是指
load effective address 的縮寫,表示載入有效位址,這樣 edx 就存放了 palPalEntry 的基底位址
所以各位可以看到 CreatePhys 標籤下就開始對每個 相對於 edx 的偏移值把值放進去,假如
ecx 一直小於 NPAL 的話,jl 指令就會跳回 CreatePhys 標籤 就形成了迴圈,每次 edx 都會把
自己累加 4 就相當於推進到下一個顏色項目的結構,整個跑完後,最後一樣繼續偷懶,把
eax 先移動到 edi 中,為甚麼呢?因為 API 回傳值會放在 eax,我們要在 eax 被破壞前趕快
先複製一份到其他的暫存器,這邊我用 edi,其實 ecx 上面跑完迴圈後就沒用了,也可以
抓過來用,這就是寫組語很有趣的地方,變化多端,暫存器當免費變數用,最後,把
edi 傳遞給 CreatePalette,然後把 eax 回傳的 HPALETTE 一樣先複製到 ebx,理由跟上面一樣
因為呼叫 LocalFree 會破壞 eax 的值,把 edi 傳入釋放記憶體,這邊仔細看,組語擅長的
共用技巧再次出現啦,edi 被連用兩次於 CreatePalette 與 LocalFree,從暫存器 push 值絕對比
從記憶體 push 來的更快,而且產生的機械碼更短,最後當然就是把回傳值放入 eax 啦,所以
把剛剛的 ebx 在放入 eax 中就完成了,在 FASM 版本中我走了一條完全不同的思考方法,
完全只用 register 就轉譯了這個函式,這邊告訴我們撰寫組語就要用組語這種語言的腦袋來
思考,盡可能只使用暫存器當變數用,盡可能思考如何只在 CPU 移動資料達成更高的性能
與更簡潔的程式碼,沒有必要盡量不要使用堆疊配置區域變數使用,這是玩組語有趣之處
也是困難的地方,這基本上就是把自己當組譯器用作程式最佳化的問題,這個絕對不是
把 ZoomIn 的 C 原始碼用編譯器產生組語,然後把 MASM 語法改成 FASM 語法那麼容易,
從轉譯 CreatePhysicalPalette 副函式這個例子來看,我在 FASM 的做法已經完全跟原來在
C 版本的原始碼意義完全不同了,有興趣的讀者可以把 ZoomIn 原始的 C 程式碼用 CL 編譯
器產生組語看看,因為受制於得要機械化的將 C 語言轉成組合語言,本篇這種共用有限的
暫存器存放各個階段回傳值的技巧,編譯器就不可能產生這種型式的組語碼,因為編譯器
必須要確保暫存器之間不會衝突導致結果錯誤,一般保守的透過區域變數 ( 堆疊 ) 當媒介
作資料交換,常寫程式的人可能就會碰到因為選擇編譯器最佳化選項,可能導致產生出錯誤
的執行結果 ( 我在編譯 Firefox 原始碼就有碰過,後來不使用任何最佳化編譯程式碼就沒問題 )
可是寫組語使用暫存器的直接控制大權就是落在程式設計者手中,只要你的腦袋夠清醒,寫
出完全只用 register 在 CPU 內移動的高效率程式也是有可能達成的事情,至少盡可能的使用
register 當變數用以提升程式效率。

讀者可能不知道那些暫存器是 volatile 那些是 nonvolatile,這邊特別給個表格讓大家參考
現在 64-bit 時代了,直接看 64-bit 的暫存器表格比較實在,32-bit 你把前面的 r 看成 e 就
對啦,例如 rax 就看成 eax,當然還有很多是 32-bit 世界裡面沒有的東西,這表示甚麼呢
恩, 64-bit 下 CPU 內有更多天生的免費變數可以用啦,不用宣告。

The following table describes how each register is used across function calls:
Register
Status
Use
RAX
Volatile
Return value register
RCX
Volatile
First integer argument
RDX
Volatile
Second integer argument
R8
Volatile
Third integer argument
R9
Volatile
Fourth integer argument
R10:R11
Volatile
Must be preserved as needed by caller; used in syscall/sysret instructions
R12:R15
Nonvolatile
Must be preserved by callee
RDI
Nonvolatile
Must be preserved by callee
RSI
Nonvolatile
Must be preserved by callee
RBX
Nonvolatile
Must be preserved by callee
RBP
Nonvolatile
May be used as a frame pointer; must be preserved by callee
RSP
Nonvolatile
Stack pointer
XMM0
Volatile
First FP argument
XMM1
Volatile
Second FP argument
XMM2
Volatile
Third FP argument
XMM3
Volatile
Fourth FP argument
XMM4:XMM5
Volatile
Must be preserved as needed by caller
XMM6:XMM15
Nonvolatile
Must be preserved as needed by callee.

我想上面這個表格就很清楚囉,64-bit 世界比較特別的是還有通用的 R8~R15 可以用
彈性也比較大,當然這些暫存器 32-bit 模式下是不能用的喔,要在 64-bit 下的長模式
才可以用。


大寫 F 表示 ZoomIn 的 FASM 重寫版,很想玩這支程式的話,就把原始碼貼到 FASMW
自己編譯一下吧,記得找一個 ZOOMIN.ICO 跟原始檔放在同一個目錄,因為程式碼
內有資源區段,隨便找個 ICO 改個檔名。

; Microsoft ZoomIn utility for FASM version.
; This tool magnifies a portion of the screen,
; allowing you to see things at a pixel level.
; Translated by UBIWU

format PE GUI 4.0
entry start

include 'win32a.inc'

macro jif op1,cond,op2,label
{
    cmp op1,op2
    j#cond label
}

macro jeif op1,op2,label
{
    cmp op1,op2
    je label
}

macro jeif_umsg op1,label
{
   jif [umsg],e,op1,label
}

macro ret0 label
{
   xor eax,eax
   jmp label
}

macro valbound op1,min,max
{
    jif op1,ge,min,@F
    mov op1,min
  @@:
    jif op1,le,max,@F
    mov op1,max
  @@:
}

macro memmov op1,op2
{
    mov eax,op2
    mov op1,eax
}

macro splitword op1
{
      mov   eax,op1
      movsx ecx,ax ; ecx := loword(op1)
      shr   eax,16
      movsx edx,ax ; edx := hiword(op1)
}

; CS_BYTEALIGNCLIENT+CS_VREDRAW+CS_HREDRAW
csStyle = 00001003h
; CAPTION+OVERLAPPED+SYSMENU+THICKFRAME+MINIMIZEBOX+VSCROLL
flStyle = 14ee0000h
; EX_LEFT+EX_LTRREADING+EX_RIGHTSCROLLBAR+EX_WINDOWEDGE
flStyleEx = 00000100h

MIN_ZOOM    = 1
MAX_ZOOM    = 32
FASTDELTA   = 8
MM10PERINCH = 254 ; Tenths of a millimeter per inch
NPAL        = 256 ; Number of palette entries.

; Resource ID
IDI_MAINICON = 17

section '.text' code readable executable
  start:
    invoke GetModuleHandle,0
    mov    [ghInst],eax
    mov    [wc.hInstance],eax
    invoke LoadIcon,eax,IDI_MAINICON
    mov    [wc.hIcon],eax
    invoke LoadCursor,0,IDC_ARROW
    mov    [wc.hCursor],eax
    invoke GetStockObject,BLACK_BRUSH
    mov    [wc.hbrBackground],eax
    invoke RegisterClass,wc

    call   CreatePhysicalPalette
    mov    [ghpalPhysical],eax

    invoke GetSystemMetrics,SM_CXSCREEN
    inc    eax
    mov    [gcxScreenMax],eax
    invoke GetSystemMetrics,SM_CYSCREEN
    inc    eax
    mov    [gcyScreenMax],eax

    mov    eax,44
    imul   eax,[gnZoom] ; eax = gnZoom*44
    mov    ecx,eax      ; ecx := _dx
    mov    eax,36
    imul   eax,[gnZoom] ; edx:eax = gnZoom*36
    mov    edx,eax      ; edx := _dy
    invoke SetRect,rc,0,0,ecx,edx
    invoke AdjustWindowRect,rc,flStyle,1

    mov eax,[rc.right]
    sub eax,[rc.left]   ; eax := Window width
    mov ecx,[rc.bottom]
    sub ecx,[rc.top]    ; ecx := Window height
    invoke CreateWindowEx,flStyleEx,szClassName,szAppName,flStyle,\
                          CW_USEDEFAULT,0,eax,ecx,\
                          NULL,NULL,[ghInst],NULL
    test   eax,eax
    jz     end_loop
    mov    [ghwndApp],eax
    invoke ShowWindow,eax,SW_SHOWNORMAL

  msg_loop:
    invoke GetMessage,msg,NULL,0,0
    cmp eax,1
    jb  end_loop
    jne msg_loop
    invoke TranslateMessage,msg
    invoke DispatchMessage,msg
    jmp msg_loop

  end_loop:
    invoke ExitProcess,[msg.wParam]

CreatePhysicalPalette:
    push ebx esi edi
    ; LPTR := 0x0040 Combines LMEM_FIXED and LMEM_ZEROINIT
    ; 1032 = sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * NPAL
    invoke LocalAlloc,LPTR,1032
    cmp    eax, 0         ; eax := PLOGPALETTE ppal
    je     ExitCreatePhys ; exit subroutine if memory allocation failed

    mov word[eax],768    ; ppal->palVersion = 0x300
    mov word[eax+2],NPAL ; ppal->palNumEntries
    xor ecx,ecx          ; ecx = 0
    lea edx,dword[eax+4] ; address of ppal->palPalEntry

  CreatePhys:
      mov byte[edx],cl  ; ppal->palPalEntry.peRed
      mov byte[edx+1],0 ; ppal->palPalEntry.peGreen
      mov byte[edx+2],0 ; ppal->palPalEntry.peBlue
      mov byte[edx+3],2 ; ppal->palPalEntry.peFlags = PC_EXPLICIT
      inc ecx
      add edx,4         ; ppal->palPalEntry += 4 move to next that
      cmp ecx,NPAL
      jl  CreatePhys

      mov    edi,eax           ; store ppal into edi
      invoke CreatePalette,edi ; edi := PLOGPALETTE ppal
      mov    ebx,eax           ; ebx <- eax := HPALETTE hpal
      invoke LocalFree,edi     ; release memory
      mov    eax,ebx           ; return hpal

  ExitCreatePhys:
    pop  edi esi ebx
    ret

proc AppWndProc hwnd,umsg,wparam,lparam
    push ebx esi edi

    jeif_umsg WM_CREATE     ,.wmcreate
    jeif_umsg WM_PAINT      ,.wmpaint
    jeif_umsg WM_SIZE       ,.wmsize
    jeif_umsg WM_LBUTTONDOWN,.wmlbuttondown
    jeif_umsg WM_LBUTTONUP  ,.wmlbuttonup
    jeif_umsg WM_MOUSEMOVE  ,.wmmousemove
   ;jeif_umsg WM_SETFOCUS   ,.wmsetfocus
   ;jeif_umsg WM_COMMAND    ,.wmcommand
    jeif_umsg WM_VSCROLL    ,.wmvscroll
    jeif_umsg WM_KEYDOWN    ,.wmkeydown
    jeif_umsg WM_CLOSE      ,.wmclose
    jeif_umsg WM_DESTROY    ,.wmdestroy

  .defwndproc:
      invoke  DefWindowProc,[hwnd],[umsg],[wparam],[lparam]
      jmp     .finish

  .wmcreate:
      invoke SetScrollRange,[hwnd],SB_VERT,MIN_ZOOM,MAX_ZOOM,FALSE
      invoke SetScrollPos,[hwnd],SB_VERT,[gnZoom],FALSE
      ret0 .finish

  .wmpaint:
      invoke BeginPaint,[hwnd],ps
      stdcall DoTheZoomIn,[ps.hdc]
      invoke EndPaint,[hwnd],ps
      ret0 .finish

  .wmsize:
      stdcall CalcZoomedSize
      ret0 .finish

  .wmlbuttondown:
      ; ecx := loword(lparam)
      ; edx := hiword(lparam)
      splitword [lparam]
      mov [gptZoom.x],ecx
      mov [gptZoom.y],edx

      invoke  ClientToScreen,[hwnd],gptZoom
      stdcall DrawZoomRect
      stdcall DoTheZoomIn,NULL
      invoke  SetCapture,[hwnd]
      mov     [gfTracking],TRUE
      ret0 .finish

   .wmmousemove:
      jif [gfTracking],ne,1,@F
      stdcall DrawZoomRect

      splitword [lparam]  ; ecx:=loword(lparam), edx:=hiword(lparam)
      mov [gptZoom.x],ecx
      mov [gptZoom.y],edx

      invoke  ClientToScreen,[hwnd],gptZoom
      stdcall DrawZoomRect
      stdcall DoTheZoomIn,NULL
     @@:
      ret0 .finish

  .wmlbuttonup:
      jif     [gfTracking],ne,1,@F
      stdcall DrawZoomRect
      invoke  ReleaseCapture
      mov     [gfTracking],FALSE
     @@:
      ret0 .finish

  .wmvscroll:
      mov eax,[wparam]                      ; eax = wparam
      jif ax,e,SB_LINEDOWN     ,.sblinedown ; ax = LOWORD(wparam)
      jif ax,e,SB_LINEUP       ,.sblineup
      jif ax,e,SB_PAGEUP       ,.sbpageup
      jif ax,e,SB_PAGEDOWN     ,.sbpagedown
      jif ax,e,SB_THUMBPOSITION,.sbthumb
      jif ax,e,SB_THUMBTRACK   ,.sbthumb
      jmp @F
    .sblinedown:
        inc [gnZoom]
        jmp @F
    .sblineup:
        dec [gnZoom]
        jmp @F
    .sbpagedown:
        add [gnZoom],2
        jmp @F
    .sbpageup:
        sub [gnZoom],2
        jmp @F
    .sbthumb:
        sar eax,16 ; gnZoom = HIWORD(wparam)
        mov [gnZoom],eax
    @@:
      valbound [gnZoom],MIN_ZOOM,MAX_ZOOM
      invoke   SetScrollPos,[hwnd],SB_VERT,[gnZoom],TRUE
      stdcall  CalcZoomedSize
      stdcall  DoTheZoomIn,NULL

      ret0 .finish

  .wmkeydown:
      jif [wparam],e,VK_UP   ,.keydown
      jif [wparam],e,VK_DOWN ,.keydown
      jif [wparam],e,VK_LEFT ,.keydown
      jif [wparam],e,VK_RIGHT,.keydown
      jmp @F
      .keydown:
        invoke GetKeyState,VK_SHIFT
        and eax,8000h
        invoke GetKeyState,VK_CONTROL
        and ecx,8000h
        stdcall MoveView,[wparam],eax,ecx
    @@:
      ret0 .finish

  .wmclose:
      jif [ghpalPhysical],e,0,@F
      invoke DeleteObject,[ghpalPhysical]

     @@:
      invoke DestroyWindow,[hwnd]
      ret0 .finish

  .wmdestroy:
      invoke PostQuitMessage,0
      xor eax,eax

  .finish:
    pop edi esi ebx
    ret
endp

; Calculates some globals.  This routine needs to be called any
; time that the size of the app or the zoom factor changes.
proc CalcZoomedSize
    push ebx esi edi

    invoke GetClientRect,[ghwndApp],rc

    mov  eax,[rc.right]
    cdq
    mov  ecx,[gnZoom]    ; gnZoom is divisor
    idiv ecx
    inc  eax
    mov  [gcxZoomed],eax ; gcxZoomed = (rc.right/gnZoom) + 1

    mov  eax,[rc.bottom]
    cdq
    idiv ecx
    inc  eax
    mov  [gcyZoomed],eax ; gcyZoomed = (rc.bottom/gnZoom) + 1

    pop edi esi ebx
    ret
endp

; Does the actual paint of the zoomed image.
;
; Arguments:
;   HDC hdc - If not NULL, this hdc will be used to paint with.
;             If NULL, a dc for the apps window will be obtained.
proc DoTheZoomIn hdc
    local hpalOld  : DWORD
    local fRelease : DWORD
    local hdcScreen: DWORD
    local x        : DWORD
    local y        : DWORD
    pushad ; ebx esi edi

    mov [fRelease],FALSE

    jif [hdc],ne,0,@F       ; jmp @@ if hdc != 0
    invoke GetDC,[ghwndApp] ; Get hdc from hwnd
    mov [hdc],eax           ; hdc <- eax
    inc [fRelease]          ; fRelease = 1

   @@:
    jif [ghpalPhysical],e,0,@F ; jmp if ghpalPhysical == 0
    invoke SelectPalette,[hdc],[ghpalPhysical],FALSE
    mov [hpalOld],eax
    invoke RealizePalette,[hdc]

   @@:
    mov     edx,[gcxZoomed]
    sar     edx,1              ; min in edx = gcxZoomed/2
    mov     eax,[gcxScreenMax]
    sub     eax,edx            ; max in eax = gcxScreenMax-(gcxZoomed/2)
    stdcall RangeBound,[gptZoom.x],edx,eax
    mov     [x],eax

    mov ecx,[gcyZoomed]
    sar ecx,1              ; min in ecx = gcyZoomed/2
    mov edi,[gcyScreenMax]
    sub edi,ecx            ; max in edi = gcyScreenMax-(gcyZoomed/2)
    stdcall RangeBound,[gptZoom.y],ecx,edi
    mov [y],eax

    invoke GetDC,NULL
    mov [hdcScreen],eax
    invoke SetStretchBltMode,[hdc],COLORONCOLOR

    mov  eax,[gcxZoomed]
    imul eax,[gnZoom]
    mov  ecx,eax         ; ecx = gnZoom*gcxZoomed
    mov  eax,[gcyZoomed]
    imul eax,[gnZoom]
    mov  edx,eax         ; edx = gnZoom*gcyZoomed

    mov esi,[gcxZoomed]
    sar esi,1   ; esi = gcxZoomed/2
    sub [x],esi ; x   = x-gcxZoomed/2
    sal esi,1   ; esi = gcxZoomed
    mov eax,[x] ; eax = x-gcxZoomed/2

    mov edi,[gcyZoomed]
    sar edi,1   ; edi = gcyZoomed/2
    sub [y],edi ; y   = y-gcyZoomed/2
    sal edi,1   ; edi = gcyZoomed
    mov ebx,[y] ; ebx = y-gcyZoomed/2

    invoke StretchBlt,[hdc],0,0,ecx,edx,\
                      [hdcScreen],eax,ebx,esi,edi,SRCCOPY
    invoke ReleaseDC,0,[hdcScreen]

    jif [hpalOld],e,0,@F
    invoke SelectPalette,[hdc],[hpalOld],FALSE
   @@:
    jif [fRelease],e,0,@F
    invoke ReleaseDC,[ghwndApp],[hdc]
   @@:
    popad ; edi esi ebx
    ret
endp

; This function moves the current view around.
;
; Arguments:
;   INT nDirectionCode - Direction to move.  Must be VK_UP, VK_DOWN,
;                        VK_LEFT or VK_RIGHT.
;   BOOL fFast         - TRUE if the move should jump a larger increment.
;                        If FALSE, the move is just one pixel.
;   BOOL fPeg          - If TRUE, the view will be pegged to the screen
;                        boundary in the specified direction.  This overides
;                        the fFast parameter.
proc MoveView nDirectionCode,fFast,fPeg
    push ebx esi edi

    ; ebx := delta
    mov ebx,1
    jif [fFast],e,FALSE,@F
    mov ebx,FASTDELTA
   @@:
    jeif [nDirectionCode],VK_UP   ,.vkup
    jeif [nDirectionCode],VK_DOWN ,.vkdown
    jeif [nDirectionCode],VK_LEFT ,.vkleft
    jeif [nDirectionCode],VK_RIGHT,.vkright
    jmp .finish

   .vkup:                  ;
    mov eax,[gptZoom.y]
    sub eax,ebx
    mov [gptZoom.y],eax

    cmp [fPeg],0
    je  .updown_rangebound
    mov eax,[gcyZoomed]
    sar eax,1
    mov [gptZoom.y],eax
    jmp .updown_rangebound

   .vkdown:               ;
    mov eax,[gptZoom.y]
    add eax,ebx
    mov [gptZoom.y],eax

    cmp [fPeg],0
    je  .updown_rangebound
    mov eax,[gcyScreenMax]
    mov ecx,[gcyZoomed]
    sar ecx,1
    sub eax,ecx
    mov [gptZoom.y],eax

   .updown_rangebound:
    stdcall RangeBound,[gptZoom.y],0,[gcyScreenMax]
    mov [gptZoom.y],eax
    jmp .finish

   .vkleft:              ;
    mov eax,[gptZoom.x]
    sub eax,ebx
    mov [gptZoom.x],eax

    cmp [fPeg],0
    je  .leftright_rangebound
    mov eax,[gcxZoomed]
    sar eax,1
    mov [gptZoom.x],eax
    je  .leftright_rangebound

   .vkright:            ;
    mov eax,[gptZoom.x]
    add eax,ebx
    mov [gptZoom.x],eax

    cmp [fPeg],0
    je  .leftright_rangebound
    mov eax,[gcxScreenMax]
    mov ecx,[gcxZoomed]
    sar ecx,1
    sub eax,ecx
    mov [gptZoom.x],eax

   .leftright_rangebound:
    stdcall RangeBound,[gptZoom.x],0,[gcxScreenMax]
    mov     [gptZoom.x],eax
    jmp     .finish

   .finish:
    stdcall DoTheZoomIn,NULL
    pop edi esi ebx
    ret
endp

; This function draws the tracking rectangle. The size and shape of
; the rectangle will be proportional to the size and shape of the
; app's client, and will be affected by the zoom factor as well.
proc DrawZoomRect
    ; local x : DWORD
    ; local y : DWORD
    pushad

    mov edx,[gcxZoomed]
    sar edx,1              ; min in edx = gcxZoomed/2
    mov eax,[gcxScreenMax]
    sub eax,edx;           ; max in eax = gcxScreenMax-(gcxZoomed/2)

    stdcall RangeBound,[gptZoom.x],edx,eax
    sub eax,edx       ; eax = x - gcxZoomed/2
    mov [rc.left],eax

    mov ecx,[gcyZoomed]
    sar ecx,1              ; min in ecx = gcyZoomed/2
    mov edi,[gcyScreenMax]
    sub edi,ecx            ; max in edi = gcyScreenMax-(gcyZoomed/2)

    stdcall RangeBound,[gptZoom.y],ecx,edi
    sub eax,ecx       ; eax = y - gcyZoomed/2
    mov [rc.top],eax

    mov eax,[gcxZoomed]
    add eax,[rc.left]
    mov [rc.right],eax

    mov eax,[gcyZoomed]
    add eax,[rc.top]
    mov [rc.bottom],eax

    invoke InflateRect,rc,1,1

    invoke GetDC,NULL
    mov ebx,eax     ; ebx := hdc

    mov eax,[rc.right]
    sub eax,[rc.left]  ; eax = rc.right-rc.left
    invoke PatBlt,ebx,[rc.left],[rc.top],eax,1,DSTINVERT

    mov eax,[rc.top]
    sub eax,[rc.bottom]  ; eax = -([rc.bottom]-[rc.top])
    invoke PatBlt,ebx,[rc.left],[rc.bottom],1,eax,DSTINVERT

    mov eax,[rc.right]
    sub eax,1           ; eax = rc.right - 1
    mov ecx,[rc.bottom]
    sub ecx,[rc.top]    ; ecx = rc.bottom - rc.top
    invoke PatBlt,ebx,eax,[rc.top],1,ecx,DSTINVERT

    mov eax,[rc.bottom]
    sub eax,1           ; eax = rc.bottom - 1
    mov ecx,[rc.left]
    sub ecx,[rc.right]  ; ecx = -(rc.right-rc.left)
    invoke PatBlt,ebx,[rc.right],eax,ecx,1,DSTINVERT

    invoke ReleaseDC,NULL,ebx

    popad
    ret
endp

; eax := return value
proc RangeBound x,min,max
    push ebx esi edi

    mov ebx,[x]  ; ebx := x
    cmp ebx,[min]
    jge @F        ; jmp if x => min
    mov eax,[min] ; eax = min
    jmp .finish

   @@:
    cmp ebx,[max] ; jmp if x <= max
    jle @F
    mov eax,[max] ; eax = max
    jmp .finish

   @@:
    mov eax,ebx   ; eax = x

  .finish:
    pop edi esi ebx
    ret
endp

section '.data' data readable writeable
  szClassName   TCHAR 'FASMZOOMIN',0              ; Class name
  szAppName     TCHAR 'FZoomIn',0 ; Application name

  ghInst        dd ?             ; Instance handle
  ghwndApp      dd ?             ; Main window handle
  gnZoom        dd 4             ; Magnification factor
  ghpalPhysical dd ?             ; Handle to the physical palette
  gcxScreenMax  dd ?             ; Width of the screen
  gcyScreenMax  dd ?             ; Height of the screen
  gcxZoomed     dd ?             ; Client width in zoomed pixels
  gcyZoomed     dd ?             ; Client height in zoomed pixels
  gfTracking    dd 0             ; 1 if tracking is in progress
  gptZoom       POINT 100,100    ; The center of the zoomed area

  ; COLOR_BTNFACE+1 is black not grey
  wc  WNDCLASS csStyle,AppWndProc,0,0,NULL,NULL,NULL,NULL,NULL,szClassName
  msg MSG
  rc  RECT
  ps  PAINTSTRUCT
  hcurOld   dd ?

section '.idata' import data readable writeable
  library kernel,'KERNEL32.DLL',\
          user,'USER32.DLL',\
          gdi,'GDI32.DLL'

  import kernel,\
         LocalAlloc,'LocalAlloc',\
         LocalFree,'LocalFree',\
         GetModuleHandle,'GetModuleHandleA',\
         ExitProcess,'ExitProcess'

  import user,\
         GetDC,'GetDC',\
         ReleaseDC,'ReleaseDC',\
         BeginPaint,'BeginPaint',\
         EndPaint,'EndPaint',\
         RegisterClass,'RegisterClassA',\
         CreateWindowEx,'CreateWindowExA',\
         GetSystemMetrics,'GetSystemMetrics',\
         ClientToScreen,'ClientToScreen',\
         ShowWindow,'ShowWindow',\
         InflateRect,'InflateRect',\
         SetRect,'SetRect',\
         AdjustWindowRect,'AdjustWindowRect',\
         DefWindowProc,'DefWindowProcA',\
         SetWindowLong,'SetWindowLongA',\
         RedrawWindow,'RedrawWindow',\
         GetMessage,'GetMessageA',\
         TranslateMessage,'TranslateMessage',\
         DispatchMessage,'DispatchMessageA',\
         SendMessage,'SendMessageA',\
         LoadCursor,'LoadCursorA',\
         LoadIcon,'LoadIconA',\
         LoadMenu,'LoadMenuA',\
         GetClientRect,'GetClientRect',\
         MoveWindow,'MoveWindow',\
         SetCapture,'SetCapture',\
         ReleaseCapture,'ReleaseCapture',\
         SetFocus,'SetFocus',\
         GetKeyState,'GetKeyState',\
         MessageBox,'MessageBoxA',\
         SetScrollRange,'SetScrollRange',\
         SetScrollPos,'SetScrollPos',\
         DestroyWindow,'DestroyWindow',\
         PostQuitMessage,'PostQuitMessage'

  import gdi,\
         PatBlt,'PatBlt',\
         GetStockObject,'GetStockObject',\
         CreatePalette,'CreatePalette',\
         SelectPalette,'SelectPalette',\
         RealizePalette,'RealizePalette',\
         SetStretchBltMode,'SetStretchBltMode',\
         StretchBlt,'StretchBlt',\
         CreateFont,'CreateFontA',\
         DeleteObject,'DeleteObject'


section '.rsrc' resource data readable

  ; resource directory
  directory RT_ICON,icons,\
            RT_GROUP_ICON,group_icons

  ; resource subdirectories
  resource icons,\
           1,LANG_NEUTRAL,icon_data

  resource group_icons,\
           17,LANG_NEUTRAL,main_icon


  icon main_icon,icon_data,'ZOOMIN.ICO'

沒有留言:

張貼留言