绘制文本

    今天我们将学习如何在窗口的客户区“绘制”字符串。我们还将学习关于“设备环境”的概念。

    Windows中的文本是一个GUI(图形用户界面)对象。每一个字符实际上是由许多的像素点组成,这些点在有笔画的地方显示出来,这样就会出现字符。这也是为什么我说“绘制”字符,而不是写字符。通常你都是在你应用程序的客户区“绘制”字符串(尽管你也可以在客户区外“绘制”)。Windows下的“绘制”字符串方法和Dos下的截然不同,在Dos下你可以把屏幕想象成85x25的一个平面,而Windows下由于屏幕上同时有几个应用程序的画面,所以你必须严格遵从规范。Windows通过把每一个应用程序限制在他的客户区来做到这一点。当然客户区的大小是可变的,你随时可以调整。
    提示:客户区是指我们窗体与用户交互的部分。打个比方,比如我们Windows的notepad记事本程序的客户区就是它的编辑框,非客户区则是指它的标题栏和菜单栏及滚动条。
    在你在客户区“绘制”字符串前,你必须从Windows那里得到你客户区的大小,确实你无法像在Dos下那样随心所欲地在屏幕上任何地方“绘制”,绘制前你必须得到Windows的允许,然后Windows会告诉你客户区的大小,字体,颜色和其它GUI对象的属性。你可以用这些来在客户区“绘制”。

    什么是“设备环境”(DC)呢?它其实是由Windows内部维护的一个数据结构。一个“设备环境”和一个特定的设备相连。像打印机和显示器。对于显示器来说,“设备环境”和一个个特定的窗口相连。
    “设备环境”中的有些属性和绘图有关,像:颜色,字体等。你可以随时改动那些缺省值,之所以保存缺省值是为了方便。你可以把“设备环境”想象成是Windows为你准备的一个绘图环境,而你可以随时根据需要改变某些缺省属性。

    当应用程序需要绘制时,你必须得到一个“设备环境”的句柄。通常有几种方法:

    在WM_PAINT消息中使用call BeginPaint。
    在其他消息中使用call GetDC。
    call CreateDC建立自己的DC。

    你必须牢记的是,在处理单个消息后你必须释放“设备环境”句柄。不要在一个消息处理中获得“设备环境”句柄,而在另一个消息处理中再释放它。
    我们在Windows发送WM_PAINT消息时处理绘制客户区,Windows不会保存客户区的内容,它用的方法是“重绘”机制(譬如当客户区刚被另一个应用程序的客户区覆盖),Windows会把WM_PAINT消息放入该应用程序的消息队列。重绘窗口的客户区是各个窗口自己的责任,你要做的是在窗口过程处理WM_PAINT的部分知道绘制什么和如何绘制。

    你必须了解的另一个概念是“无效区域”。Windows把一个最小的需要重绘的正方形区域叫做“无效区域”。当Windows发现了一个“无效区域”后,它就会向该应用程序发送一个WM_PAINT消息,在WM_PAINT的处理过程中,窗口首先得到一个有关绘图的结构体,里面包括无效区的坐标位置等。你可以通过调用BeginPaint让“无效区”有效,如果你不处理WM_PAINT消息,至少要调用缺省的窗口处理函数DefWindowProc,或者调用ValidateRect让“无效区”有效。否则你的应用程序将会收到无穷无尽的WM_PAINT消息。

    下面是响应该消息的步骤:
   
    1、取得“设备环境”句柄
    2、绘制客户区
  3、释放“设备环境”句柄

    假如,我们程序的客户区假设被另一个应用程序的客户区所覆盖,那些此刻另一个应用程序退出,相应我们程序被覆盖客户区需要发生重绘,而被覆盖的这部分叫做“无效区域”,这时候Windows就会检测到我们需要发生重绘的无效区域,然后发送给我们程序的消息队列中一个WM_PAINT消息。此刻我们就需要在我们的窗口过程中处理WM_PAINT消息,即使不处理也要调用缺省的DefWindowProc函数。BeginPaint函数默认使无效区有效。那么BeginPaint返回了一个设备环境的句柄。因为我们Windows下的硬件设备都是以相关的驱动形式来和Windows通信的,那么我们的设备环境也就关联了相应的设备。例如我们的窗体对象关联了相应的显示器设备,那么此时我们要想在窗体上显示文本,我们必须通过设备驱动,因为Windows提供给了我们相应的函数以及相应的数据类型,我们只需要取得设备环境的句柄,然后通过相应的函数来调用,此时我们不需要知道它是如何实现的,我们只需要遵守Windows的规则就可以了。下面我们通过一段代码来深入分析一下。

%include '..\inc\nasmx.inc'
%include '..\inc\win32\windows.inc'
%include '..\inc\win32\kernel32.inc'
%include '..\inc\win32\user32.inc'

entry    start

[section .bss]
    hInstance:   resd 1
    hWnd:        resd 1
  lpCommandLine: resd 1

[section .data]
    szTitle:    db    "My First Window", 0x0
    szClass:    db    "FirstWindow", 0x0
  szText:    db    "My First Window Text", 0x0

    wc:
    istruc WNDCLASSEX                  ;声明结构体
        at WNDCLASSEX.cbSize,           dd    NULL
        at WNDCLASSEX.style,            dd    NULL
        at WNDCLASSEX.lpfnWndProc,      dd    NULL
        at WNDCLASSEX.cbClsExtra,       dd    NULL
        at WNDCLASSEX.cbWndExtra,       dd    NULL
        at WNDCLASSEX.hInstance,        dd    NULL
        at WNDCLASSEX.hIcon,            dd    NULL
        at WNDCLASSEX.hCursor,          dd    NULL
        at WNDCLASSEX.hbrBackground,    dd    NULL
        at WNDCLASSEX.lpszMenuName,     dd    NULL
        at WNDCLASSEX.lpszClassName,    dd    NULL
        at WNDCLASSEX.hIconSm,          dd    NULL
    iend

    message:
    istruc MSG
        at MSG.hwnd,                    dd    NULL
        at MSG.message,                 dd    NULL
        at MSG.wParam,                  dd    NULL
        at MSG.lParam,                  dd    NULL
        at MSG.time,                    dd    NULL
        at MSG.pt,                      dd    NULL
    iend

  ps:
  istruc PAINTSTRUCT    
  iend

    rect:
  istruc RECT
        at RECT.left,                  dd    NULL
        at RECT.top,                   dd    NULL
        at RECT.right,                 dd    NULL
        at RECT.bottom,                dd    NULL
    iend

[section .code]
proc    start

    invoke   GetModuleHandleA, dword NULL    
    mov      [hInstance], eax          
  
  invoke  GetCommandLineA            
  mov    [lpCommandLine], eax

    invoke   WinMain, dword [hInstance], dword NULL, dword lpCommandLine, dword SW_SHOWNORMAL    
    invoke   ExitProcess, dword NULL      
    ret

endproc

proc    WinMain
hinst    argd        ; Current instance handle
hpinst   argd        ; Previous instance handle
cmdln    argd        ; Command line arguments
dwshow   argd        ; Display style

    mov     [wc + WNDCLASSEX.cbSize], dword WNDCLASSEX_size        
  mov    [wc + WNDCLASSEX.style], dword CS_HREDRAW | CS_VREDRAW    
  invoke  LoadIconA, dword NULL, dword IDI_APPLICATION
    mov     edx, eax
    mov     eax, dword argv(hinst)
    mov     ebx, dword szClass
    mov     ecx, dword WndProc
    mov     [wc + WNDCLASSEX.hInstance], eax
  mov    [wc + WNDCLASSEX.hbrBackground], dword COLOR_WINDOW + 1
    mov     [wc + WNDCLASSEX.lpszClassName], ebx
    mov     [wc + WNDCLASSEX.lpfnWndProc], ecx
    mov     [wc + WNDCLASSEX.hIcon], edx
    mov     [wc + WNDCLASSEX.hIconSm], edx

    invoke   RegisterClassExA, dword wc          

  
    invoke   CreateWindowExA, dword NULL, dword szClass, dword szTitle, dword WS_OVERLAPPEDWINDOW + WS_VISIBLE, dword CW_USEDEFAULT, dword CW_USEDEFAULT, dword CW_USEDEFAULT, dword CW_USEDEFAULT, dword NULL, dword NULL, dword [wc + WNDCLASSEX.hInstance], dword NULL
    mov      [hWnd], eax

    invoke   ShowWindow, dword hWnd, dword argv(dwshow)      
    invoke   UpdateWindow, dword hWnd              

    .WHILE:                            
        invoke   GetMessageA, dword message, dword NULL, dword NULL, dword NULL
        cmp      eax, dword 0
        je       .ENDW
        invoke   TranslateMessage, dword message
        invoke   DispatchMessageA, dword message
        jmp      .WHILE
    .ENDW:

    mov      eax, dword [message + MSG.wParam]
    ret

endproc

proc    WndProc
hwnd    argd        ; Window handle
umsg    argd        ; Window message
wparam  argd        ; wParam
lparam  argd        ; lParam

  locals                ;locals/local/endlocals,声明局部变量,使用var()宏获得变量地址
    local hdc, Dword          
  endlocals

  if argv(umsg), ==, dword WM_DESTROY                 
    invoke  PostQuitMessage, dword NULL

  elsif argv(umsg), ==, dword WM_PAINT
    invoke  BeginPaint, dword argv(hwnd), dword ps      ;重绘指定的窗口
    mov    dword var(hdc), eax
    invoke  GetClientRect, dword argv(hwnd), dword rect    ;获取客户区的坐标,放到RECT结构体中
    invoke  DrawTextA, dword var(hdc), dword szText, dword -1, dword rect, dword DT_SINGLELINE | DT_CENTER | DT_VCENTER  ;在指定窗口写入格式化文本。
    invoke  EndPaint, dword argv(hwnd), dword ps      ;指定窗口的绘制过程结束,这个函数在每次调用BeginPaint函数之后被请求。

  else 
    invoke  DefWindowProcA, dword argv(hwnd), dword argv(umsg), dword argv(wparam), dword argv(lparam)
    
  endif
  
  ret

endproc

    今天的代码跟我们前面写的窗口显示代码,只有几点不同的地方。
    一、数据段中增加了两个结构体的声明:PAINTSTRUCT和RECT
  ps:
  istruc PAINTSTRUCT    
  iend

    rect:
  istruc RECT
        at RECT.left,                  dd    NULL
        at RECT.top,                   dd    NULL
        at RECT.right,                 dd    NULL
        at RECT.bottom,                dd    NULL
    iend

  二、窗口过程中声明了局部变量hdc:

  locals                ;locals/local/endlocals,声明局部变量,使用var()宏获得变量地址
    local hdc, Dword          
  endlocals
  
  上面这两点中声明的变量,都是由处理WM_PAINT消息的GDI函数调用。locals/local/endlocals是nasmx提供给我们在函数中声明局部变量的宏,locals是局部变量区的开头,endlocals是局部变量区的结尾,中间用local声明,“local parm, 数据类型(byte,word,dword等)”。使用var(parm)宏来调用。
  hdc用来存放调用BeginPaint返回的“设备环境”句柄。ps是一个PAINTSTRUCT数据类型的变量。它由Windows传递给BeginPaint,在结束绘制后再原封不动的传递给EndPaint。它的函数原型如下:

typedef struct tagPAINTSTRUCT { 
  HDC hdc; 
  BOOL fErase; 
  RECT rcPaint;
  BOOL fRestore;
  BOOL fIncUpdate;
  BYTE rgbReserved[32]; 
  } PAINTSTRUCT, *PPAINTSTRUCT; 
  PAINTSTRUCT 结构体包含了用于绘制窗口客户区的信息。
  hdc是用于绘制的句柄,fErase如果为非零值则擦除背景,否则不擦除背景,rcPaint 通过制定左上角和右下角的坐标确定一个要绘制的矩形范围,该矩形单位相对于客户区左上角,后面三个参数都是系统预留的,编程一般用不到。
  
  rect是一个RECT结构体类型的参数,它的定义如下:
STRUC RECT
.left RESD 1
.top RESD 1
.right RESD 1
.bottom RESD 1
ENDSTRUC
  left和top是客户区正方形左上角的坐标。right和buttom是正方形右下角的坐标。

  三、在窗口函数的消息判断中增加了对WM_PAINT消息的处理

  elsif argv(umsg), ==, dword WM_PAINT
    invoke  BeginPaint, dword argv(hwnd), dword ps      ;重绘指定的窗口
    mov    dword var(hdc), eax
    invoke  GetClientRect, dword argv(hwnd), dword rect    ;获取客户区的坐标,放到RECT结构体中
    invoke  DrawTextA, dword var(hdc), dword szText, dword -1, dword rect, dword DT_SINGLELINE | DT_CENTER | DT_VCENTER  ;在指定窗口写入格式化文本。
    invoke  EndPaint, dword argv(hwnd), dword ps      ;指定窗口的绘制过程结束,这个函数在每次调用BeginPaint函数之后被请求。

  大家注意到了吗?对hdc的引入是使用var(hdc)宏实现的。

  在上面已经讲过,Windows只要检测到我们有需要重绘的无效区域,就会发送WM_PAINT消息,那么我们必须处理WM_PAINT或通过缺省的DefWindowProc函数来处理。因为BeginPaint函数接受一个窗口句柄和未初始化的PAINTSTRUCT型参数后,会自动的将我们的无效区域设置为有效,所以此时我们一般调用BeginPaint来设置我们的无效区域有效,而且BeginPaint函数返回我们相应的窗体对象的设备环境的句柄,我们此时获得相应设备环境句柄后,再调用GetClientRect获得我们应用程序窗体客户区的大小。大小放在rect结构体中。然后把它传给DrawTextA。DrawText的原型如下:

int DrawText(HDC hdc, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT uFormat);

  hdc:“设备环境”的句柄。
    lpString:要显示的文本串,该文本串要么以NULL结尾,要么在nCount中指出它的长度。
  nCount:要输出的文本的长度。若以NULL结尾,该参数必须是-1。
  lpRect:指向要输出文本串的正方形区域的指针,该正方形必须是一个裁剪区,也就是说超过该区域的字符将不能显示。
  uFormat:指定如何显示。我们可以用“|”把以下标志“或”到一块。
    DT_SINGLELINE:是否单行显示。
    DT_CENTER:是否水平居中。
    DT_VCENTER:是否垂直剧中。

  四、在.data段中声明了窗口显示变量:

szText:    db    "My First Window Text", 0x0

  结束绘制后,必须调用EndPaint释放“设备环境”的句柄。好了,现在我们把“绘制”文本串的要点总结如下:
  1、必须在开始和结束处分别调用BeginPaint和EndPaint;
  2、在BeginPaint和EndPaint之间调用所有的绘制函数;
  3、如果在其它的消息处理中重新绘制客户区,你可以有两种选择;
    (1)用GetDC和ReleaseDC代替BeginPaint和EndPaint;
    (2)调用InvalidateRect或UpdateWindow让客户区无效,这将迫使Windows把WM_PAINT放入应用程序消息队列,从而使得客户区重绘。