Atela

导航

windows程序设计 Fifth 笔记(二)

第四章 输出文本

WM_PAINT消息

发生下面几种事件之一时,窗口消息处理程序会接收到一个WM_PAINT消息:

  • 在使用者移动窗口或显示窗口时,窗口中先前被隐藏的区域重新可见。
  • 使用者改变窗口的大小(如果窗口类别样式有着CS_HREDRAW和CS_VREDRAW位旗标的设定)。
  • 程序使用ScrollWindow或ScrollDC函数滚动显示区域的一部分。
  • 程序使用InvalidateRect或InvalidateRgn函数刻意产生WM_PAINT消息。

在某些情况下,显示区域的一部分被临时覆盖,Windows试图保存一个显示区域,并在以后恢复它,但这不一定能成功。在以下情况下,Windows可能发送WM_PAINT消息:

  • Windows擦除覆盖了部分窗口的对话框或消息框。
  • 菜单下拉出来,然后被释放。
  • 显示工具提示消息。

在某些情况下,Windows总是保存它所覆盖的显示区域,然后恢复它。这些情况是:

  • 鼠标光标穿越显示区域。
  • 图标拖过显示区域。

Windows内部为每个窗口保存一个「绘图信息结构」,这个结构包含了包围无效区域的最小矩形的坐标以及其它信息,这个矩形就叫做「无效矩形」,有时也称为「无效区域」。如果在窗口消息处理程序处理WM_PAINT消息之前显示区域中的另一个区域变为无效,则Windows计算出一个包围两个区域的新的无效区域(以及一个新的无效矩形),并将这种变化后的信息放在绘制信息结构中。Windows不会将多个WM_PAINT消息都放在消息队列中。

窗口消息处理程序可以通过呼叫InvalidateRect使显示区域内的矩形无效。如果消息队列中已经包含一个WM_PAINT消息,Windows将计算出新的无效矩形。否则,它将一个新的WM_PAINT消息放入消息队列中。在接收到WM_PAINT消息时,窗口消息处理程序可以取得无效矩形的坐标。通过呼叫GetUpdateRect,可以在任何时候取得这些坐标。

在处理WM_PAINT消息处理期间,窗口消息处理程序在呼叫了BeginPaint之后,整个显示区域即变为有效。程序也可以通过呼叫ValidateRect函数使显示区域内的任意矩形区域变为有效。如果这呼叫具有令整个无效区域变为有效的效果,则目前队列中的任何WM_PAINT消息都将被删除。

 

设备描述表

程序必须在处理单个消息处理期间取得和释放句柄。除了呼叫CreateDC建立的设备内容之外,程序不能在两个消息之间保存其它设备内容句柄。

取得设备内容句柄:方法一

PAINTSTRUCT ps ;
case WM_PAINT:        
  HDC hdc = BeginPaint (hwnd, &ps) ;  
         //使用GDI函数
  EndPaint (hwnd, &ps) ;
  return 0 ;

如果窗口过程不处理,DefWindowProc以下列代码处理

case WM_PAINT:        
  HDC hdc = BeginPaint (hwnd, &ps) ; //只是让无效区域有效 
  EndPaint (hwnd, &ps) ;
  return 0 ;
case WM_PAINT:   //会导致windows一直发送WM_PAINT消息      
  return 0 ;

typedef struct tagPAINTSTRUCT {
    HDC       hdc ;
    BOOL      fErase ;
    RECT      rcPaint ;
    BOOL      fRestore ;
    BOOL      fIncUpdate ;
    BYTE      rgbReserved[32] ;
} PAINTSTRUCT ;

程序只使用前三个字段,其它字段由Windows内部使用。

fErase被标志为FALSE(0),这意味着Windows已经擦除了无效矩形的背景。这最早在BeginPaint函数中发生(如果要在窗口消息处理程序中自己定义一些背景擦除行为,可以自行处理WM_ERASEBKGND消息)。Windows使用WNDCLASS结构的hbrBackground字段指定的画刷来擦除背景。

不过,如果程序通过呼叫Windows函数InvalidateRect使显示区域中的矩形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在呼叫完BeginPaint后PAINTSTRUCT结构的fErase字段将为TRUE(非零)。第二个参数指向RECT的结构体,该结构包含了要添加到更新区域的长方形区域的坐标。如果该参数为NULL,整个客户端区域将会被添加到更新区域。

rcPaint字段定义了无效矩形的边界。它还是一个「剪取」矩形。这意味着Windows将绘图操作限制在剪取矩形内

在处理WM_PAINT消息时,为了在更新的矩形外绘图,可以使用如下呼叫:

InvalidateRect (hwnd, NULL, TRUE) ;

该呼叫在BeginPaint呼叫之前进行,它使整个显示区域变为无效,并擦除背景。但是,如果最后一个参数等于FALSE,则不擦除背景,原有的东西将保留在原处。通常这是Windows程序在无论何时收到WM_PAINT消息而不考虑rcPaint结构的情况下简单地重画整个显示区域最方便的方法。

 

取得设备内容句柄:方法二

hdc = GetDC (hwnd) ;
//使用GDI函数
ReleaseDC (hwnd, hdc) ;

与从BeginPaint传回设备内容句柄不同,GetDC传回的设备内容句柄具有一个剪取矩形,它等于整个显示区域。可以在显示区域的某一部分绘图,而不只是在无效矩形上绘图(如果确实存在无效矩形)。与BeginPaint不同,GetDC不会使任何无效区域变为有效。如果需要使整个显示区域有效,可以呼叫ValidateRect (hwnd, NULL) ;

一般可以呼叫GetDC和ReleaseDC来对键盘消息(如在字处理程序中)和鼠标消息(如在画图程序中)作出反应。此时,程序可以立刻根据使用者的键盘或鼠标输入来更新显示区域,而不需要考虑为了窗口的无效区域而使用WM_PAINT消息。不过,一旦确实收到了WM_PAINT消息,程序就必须要收集足够的信息后才能更新显示。

与GetDC相似的函数是GetWindowDC。GetDC传回用于写入窗口显示区域的设备内容句柄,而GetWindowDC传回写入整个窗口的设备内容句柄。例如,您的程序可以使用从GetWindowDC传回的设备内容句柄在窗口的标题列上写入文字。然而,程序同样也应该处理WM_NCPAINT (「非显示区域绘制」)消息。

 

TextOut (hdc, x, y, psText, iLength) ;

设备内容的属性控制了被显示的字符串的特征。例如,设备内容中有一个属性指定文字颜色,内定颜色为黑色;内定设备内容还定义了白色的背景。在程序向显示器输出文字时,Windows使用这个背景色来填入字符周围的矩形空间(称为「字符框」)。该文字背景色与定义窗口类别时设置的背景并不相同。窗口类别中的背景是一个画刷,Windows用它来擦除显示区域,它不是设备内容结构的一部分。

内定映像方式是MM_TEXT(使用WINGDI.H中定义的标识符)。在MM_TEXT映像方式下,逻辑单位与实际单位相同,都是图素;x的值从左向右递增,y的值从上向下递增。MM_TEXT坐标系与Windows在PAINTSTRUCT结构中定义无效矩形时使用的坐标系相同,这为我们带来了很多方便(但是,其它映像方式并非如此)。

设备内容也定义了一个剪裁区域。对于从GetDC取得的设备内容句柄,内定剪裁区域是整个显示区域;而对于从BeginPaint取得的设备内容句柄,则为无效区域。

设备内容还定义了在您呼叫TextOut显示文字时Windows使用的字体。内定字体为「系统字体」,或用Windows表头文件中的标识符,即SYSTEM_FONT。

从Windows 3.0开始,系统字体成为一种变宽(variable-pitch)字体,这意味着不同的字符具有不同的大小。系统字体是一种「点阵字体」,这意味着字符被定义为图素块

 

GetTextMetrics传回设备内容中目前选取的字体信息,因此它需要设备内容句柄。Windows将文字大小的不同值复制到在WINGDI.H中定义的TEXTMETRIC型态的结构中。TEXTMETRIC结构有20个字段,我们只使用前七个:

typedef struct tagTEXTMETRIC {
    LONG tmHeight ;  //tmHeight=tmAscent + tmDescent
    LONG tmAscent ;  //基线以上纵向高度
    LONG tmDescent ; //基线以下
    LONG tmInternalLeading ;  //重音符号
    LONG tmExternalLeading ; //行间距
    LONG tmAveCharWidth ;    //小写字母加权平均宽度。大写字母的平均宽度=tmAveCharWidth*150%
    LONG tmMaxCharWidth ;    //字体中最宽字符的宽度
           其他结构字段        
} TEXTMETRIC, * PTEXTMETRIC ;

这些字段值的单位取决于选定的设备内容映像方式。在内定设备内容下,映像方式是MM_TEXT,因此值的大小是以图素为单位。

TEXTMETRIC tm ;
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
ReleaseDC (hwnd, hdc) ;

 

Windows启动后,系统字体的大小就不会发生改变,所以在程序执行过程中,程序写作者只需要呼叫一次GetTexMetrics。最好是在窗口消息处理程序中处理WM_CREATE消息时进行此呼叫,WM_CREATE消息是窗口消息处理程序接收的第一个消息。

假设要编写一个Windows程序,在显示区域显示几行文字,这需要先取得字符宽度和高度。

static int cxChar, cyChar ;
case WM_CREATE:
    hdc = GetDC (hwnd) ;
    GetTextMetrics (hdc, &tm) ;
    cxChar = tm.tmAveCharWidth ;
    cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;//对于可变宽度字体,TEXTMETRIC结构中的tmPitchAndFamily字段的低位为1,对于固定宽度字体,该值为0。
    cyChar = tm.tmHeight + tm.tmExternalLeading ;
    ReleaseDC (hwnd, hdc) ;
    return 0 ;

GetSystemMetrics传回Windows中不同视觉组件的大小信息

WM_SIZE消息。传给窗口消息处理程序的lParam参数的低字组中包含显示区域的宽度,高字组中包含显示区域的高度。

#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
static int cxClient, cyClient ;

caseWM_SIZE:
   cxClient = LOWORD (lParam) ;
   cyClient = HIWORD (lParam) ;
   return 0 ;

 

滚动条

在应用程序中包含水平或者垂直的滚动条,程序写作者只需要在CreateWindow的第三个参数中包括窗口样式(WS)标识符WS_VSCROLL(垂直卷动)和/或WS_HSCROLL(水平卷动)即可。于特定的显示驱动程序和显示分辨率,垂直卷动列的宽度和水平卷动列的高度是恒定的。如果需要这些值,可以使用GetSystemMetrics呼叫来取得.

Windows负责处理对滚动条的所有鼠标操作,但是,窗口滚动条没有自动的键盘接口。如果想用光标键来完成卷动功能,则必须提供这方面的程序代码

每个滚动条均有一个相关的「范围」(这是一对整数,分别代表最小值和最大值)和「位置」.

在内定情况下,滚动条的范围是从0(顶部或左部)至100(底部或右部),但将范围改变为更方便于程序的数值也是很容易的

SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;

参数iBar为SB_VERT或者SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画滚动条,则设置bRedraw为TRUE(如果在呼叫SetScrollRange后,呼叫了影响滚动条位置的其它函数,则应该将bRedraw设定为FALSE以避免过多的重画)。

滚动条的位置总是离散的整数值

使用SetScrollPos在滚动条范围内设置新的卷动方块位置:

SetScrollPos (hwnd, iBar, iPos, bRedraw) ;

参数iPos是新位置,它必须在iMin至iMax的范围内。Windows提供了类似的函数(GetScrollRange和GetScrollPos)来取得滚动条的目前范围和位置。

在程序内使用滚动条时,程序写作者与Windows共同负责维护滚动条以及更新卷动方块的位置。下面是Windows对滚动条的处理:

  • 处理所有滚动条鼠标事件
  • 当使用者在滚动条内单击鼠标时,提供一种「反相显示」的闪烁
  • 当使用者在滚动条内拖动卷动方块时,移动卷动方块
  • 为包含滚动条窗口的窗口消息处理程序发送滚动条消息

以下是程序写作者应该完成的工作:

  • 初始化滚动条的范围和位置
  • 处理窗口消息处理程序的滚动条消息
  • 更新滚动条内滚动框的位置
  • 更改显示区域的内容以响应对滚动条的更改

在用鼠标单击滚动条或者拖动卷动方块时,Windows给窗口消息处理程序发送WM_VSCROLL(供上下移动)和WM_HSCROLL(供左右移动)消息。在滚动条上的每个鼠标动作都至少产生两个消息,一条在按下鼠标按钮时产生,一条在释放按钮时产生。

和所有的消息一样,WM_VSCROLL和WM_HSCROLL也带有wParam和lParam消息参数。对于来自作为窗口的一部分而建立的滚动条消息,您可以忽略lParam;它只用于作为子窗口而建立的滚动条(通常在对话框内)。

wParam消息参数被分为一个低字组和一个高字组。wParam的低字组是一个数值,它指出了鼠标对滚动条进行的操作。这个数值被看作一个「通知码」。通知码的值由以SB(代表「scroll bar(滚动条)」)开头的标识符定义。

如果在滚动条的各个部位按住鼠标键,程序就能收到多个滚动条消息。当释放鼠标键后,程序会收到一个带有SB_ENDSCROLL通知码的消息。一般可以忽略这个消息,Windows不会去改变卷动方块的位置,而您可以在程序中呼叫SetScrollPos来改变卷动方块的位置。

当把鼠标的光标放在卷动方块上并按住鼠标键时,您就可以移动卷动方块。这样就产生了带有SB_THUMBTRACK和SB_THUMBPOSITION通知码的滚动条消息。在wParam的低字组是SB_THUMBTRACK时,wParam的高字组是使用者在拖动卷动方块时的目前位置。该位置位于卷动列范围的最小值和最大值之间。在wParam的低字组是SB_THUMBPOSITION时,wParam的高字组是使用者释放鼠标键后卷动方块的最终位置。对于其它的卷动列操作,wParam的高字组应该被忽略。

Windows在您用鼠标拖动卷动方块时移动它,同时您的程序会收到SB_THUMBTRACK消息。然而,如果不通过呼叫SetScrollPos来处理SB_THUMBTRACK或SB_THUMBPOSITION消息,在使用者释放鼠标键后,卷动方块会迅速跳回原来的位置。

程序能够处理SB_THUMBTRACK或SB_THUMBPOSITION消息,但一般不同时处理两者。如果处理SB_THUMBTRACK消息,在使用者拖动卷动方块时您需要移动显示区域的内容。而如果处理SB_THUMBPOSITION消息,则只需在使用者停止拖动卷动方块时移动显示区域的内容。处理SB_THUMBTRACK消息更好一些(但更困难),对于某些型态的数据,您的程序可能很难跟上产生的消息。

在滚动条范围使用32位的值也是有效的,尽管这不常见。然而,wParam的高字组只有16位的大小,它不能适当地指出SB_THUMBTRACK和SB_THUMBPOSITION操作的位置。在这种情况下,需要使用GetScrollInfo函数(在下面描述)来得到信息。

case WM_CREATE:
...    
     SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ; //设定范围,不重画
     SetScrollPos   (hwnd, SB_VERT, iVscrollPos, TRUE) ;//设定位置,重画
     return 0 ;
case WM_SIZE:
     cyClient = HIWORD (lParam) ; //WM_SIZE的lParam高字节新高度,低字节宽度,wParam表明窗口是否最小化、最大化,还是隐藏。 
     return 0 
case WM_VSCROLL:
     switch (LOWORD (wParam)) { //WM_VSCROLL的wParam低字节是通知码
        case SB_LINEUP:
            iVscrollPos -= 1 ;
            break ;
        case SB_LINEDOWN:
            iVscrollPos += 1 ;
            break ;
        case SB_PAGEUP:
            iVscrollPos -= cyClient / cyChar ;
            break ;
        case SB_PAGEDOWN:
            iVscrollPos += cyClient / cyChar ;
            break ;
        case SB_THUMBPOSITION:
            iVscrollPos = HIWORD (wParam) ;
            break ; 
    default :
            break ;
         }
    iVscrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ;
    if (iVscrollPos != GetScrollPos (hwnd, SB_VERT)) {
            SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;
            InvalidateRect (hwnd, NULL, TRUE) ;//客户区失效,并擦出背景
         }

    return 0 ;    
    case WM_PAINT:
            hdc = BeginPaint (hwnd, &ps) ;
            for (i = 0 ; i < NUMLINES ; i++) {
                   y = cyChar * (i - iVscrollPos) ; //负数,在客户区之外,不显示
                   TextOut (hdc, 0, y
                           sysmetrics[i].szLabel,
                           lstrlen (sysmetrics[i].szLabel)) ;
                   TextOut (hdc, 22 * cxCaps, y
                           sysmetrics[i].szDesc,
                           lstrlen (sysmetrics[i].szDesc)) ;
                   SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; //后续传给TextOut的坐标指定为文本串的右上角
                   TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer
                           wsprintf (szBuffer, TEXT ("%5d"),
                                          GetSystemMetrics (sysmetrics[i].iIndex))) ;
                   SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
            }
            EndPaint (hwnd, &ps) ;
            return 0 ;

InvalidateRect的WM_PAINT为入队消息,使用UpadteWindow为不入队消息,指示窗口过程绘制客户区。

Win32 API介绍的两个滚动条函数称作SetScrollInfo和GetScrollInfo。这些函数可以完成以前函数的全部功能,并增加了两个新特性。

1.卷动方块大小与在窗口中显示的文件大小成比例。显示的大小称作「页面大小」

image

2.通过GetScrollInfo函数可以取得真实的32位值表示卷动方块的目前位置。

SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;

si为SCROLLINFO类型
typedef struct tagSCROLLINFO  
    UINT cbSize ;// set to sizeof (SCROLLINFO)
    UINT fMask ;  // values to set or get
    int  nMin ;      // minimum range value  
    int  nMax ;   // maximum range value
    UINT nPage ;  // page size
    int  nPos ;   // current 
    int  nTrackPos ;// current tracking position 
} SCROLLINFO, * PSCROLLINFO ;

fMask字段值: SIF_RANGE, SIF_POS,SIF_PAGE,SIF_TRACKPOS,SIF_DISABLENOSCROLL,SIf_ALL(SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的组合)

在带有SB_THUMBTRACK或SB_THUMBPOSITION通知码的WM_VSCROLL或WM_HSCROLL消息时,通过GetScrollInfo只使用SIF_TRACKPOS旗标,从函数的传回中,SCROLLINFO结构的nTrackPos字段将指出目前的32位的卷动方块位置。

在SetScrollInfo函数中仅使用SIF_DISABLENOSCROLL旗标。如果指定了此旗标,而且新的滚动条参数使滚动条消失,则该滚动条就不能使用了

 

前面程序当滚动条位置是0时,第一行信息显示在显示区域的顶部.但实际上只需把信息最后一行显示在显示区域的底部而不是顶部即可。

当处理WM_CREATE消息时不设置滚动条范围,而是等到接收到WM_SIZE消息后再做此工作:

iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ; 
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;

新滚动条函数的一个好的功能是当使用与滚动条范围一样大的页面时,它已经为您做掉了一大堆杂事。可以像下面的程序代码一样使用SCROLLINFO结构和SetScrollInfo:

si.cbSize   = sizeof (SCROLLINFO) ;
si.cbMask   = SIF_RANGE | SIF_PAGE ;    
si.nMin     = 0 ;
si.nMax     = NUMLINES - 1 ;   
si.nPage    = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;

这样做之后,Windows会把最大的滚动条位置限制为si.nMax - si.nPage +1而不是si.nMax

当滚动条位置为0时显示0~si.nPage-1行, 当位置为si.nMax时,显示第si.nMax - si.nPage +1~si.nMax行

当页面大小与滚动条范围一样大或更大时,Windows通常隐藏滚动条,因为它并不需要。如果不想隐藏滚动条,可在呼叫SetScrollInfo时使用SIF_DISABLENOSCROLL,Windows只是让那个滚动条不能被使用,而不隐藏它。

switch (message){  
   case WM_CREATE:
   //...    
   iMaxWidth = 40 * cxChar + 22 * cxCaps ;
   return 0 ;
case WM_SIZE: 
   cxClient = LOWORD (lParam) ;
   cyClient = HIWORD (lParam) ;
   si.cbSize     = sizeof (si) ;
   si.fMask      = SIF_RANGE | SIF_PAGE ;
   si.nMin       = 0 ;
   si.nMax       = NUMLINES - 1 ;
   si.nPage      = cyClient / cyChar ; 
   SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
   
   si.cbSize     = sizeof (si) ;
   si.fMask      = SIF_RANGE | SIF_PAGE ;
   si.nMin       = 0 ; 
   si.nMax       = 2 + iMaxWidth / cxChar ;
   si.nPage      = cxClient / cxChar ;
   SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; 
   return 0 ;
         
case WM_VSCROLL:    
   si.cbSize     = sizeof (si) ;
   si.fMask      = SIF_ALL ;
   GetScrollInfo (hwnd, SB_VERT, &si) ;
   iVertPos = si.nPos ;
   switch (LOWORD (wParam)) {  
   case   SB_TOP:
           si.nPos       = si.nMin ;
           break ;  
   case   SB_BOTTOM:
           si.nPos       = si.nMax ;
           break ;  
   case SB_LINEUP:
           si.nPos -     = 1 ;
           break ;  
   case   SB_LINEDOWN:
           si.nPos += 1 ;
           break ; 
   case   SB_PAGEUP:
           si.nPos -= si.nPage ;
           break ;
   case   SB_PAGEDOWN: 
           si.nPos += si.nPage ;
           break ;      
   case   SB_THUMBTRACK:
           si.nPos = si.nTrackPos ;
           break ; 
   default:
           break ;       
       
   }
   si.fMask = SIF_POS ;
   SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
   GetScrollInfo (hwnd, SB_VERT, &si) ;
   if (si.nPos != iVertPos) {                  
           ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos),
                                         NULL, NULL) ;
           UpdateWindow (hwnd) ;
   }  
   return 0 ;
case WM_HSCROLL: //...
                 return 0 ;
case WM_PAINT :
   hdc = BeginPaint (hwnd, &ps) ;
   si.cbSize = sizeof (si) ;
   si.fMask  = SIF_POS ;
   GetScrollInfo (hwnd, SB_VERT, &si) ;
   iVertPos = si.nPos ;
   GetScrollInfo (hwnd, SB_HORZ, &si) ;
       
   iHorzPos = si.nPos ;                                    //假设NUMLINES为10行,能显示6行,nMax为9,垂直范围为0-4,假设现在滚动2行,iVertPos=2
   iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ;//以用ScrollWindow只需刷新屏幕最下面两行4-5行(rcPaint.top~rcPaint.buttom,从0行开始)
   iPaintEnd = min (     NUMLINES - 1,                      //更新6~7行内容,iPaintBeg=2+4, iPaintEnd=2+5
                  iVertPos + ps.rcPaint.bottom / cyChar) ;
   for (i = iPaintBeg ; i <= iPaintEnd ; i++){ 
           x = cxChar * (1 - iHorzPos) ; //水平滚动条第一行留空好看点,
           y = cyChar * (i - iVertPos) ;
           TextOut (hdc, x, y,sysmetrics[i].szLabel,lstrlen (sysmetrics[i].szLabel)) ;
           TextOut (hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc,lstrlen (sysmetrics[i].szDesc)) ;
           SetTextAlign (hdc, TA_RIGHT | TA_TOP) 
           TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,
                  wsprintf (szBuffer, TEXT ("%5d"),
                  GetSystemMetrics (sysmetrics[i].iIndex))) ;
           SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
   }
    EndPaint (hwnd, &ps) ;
    return 0 ;
case  WM_DESTROY  
     PostQuitMessage (0) ;
     return 0 ;   
} 
   return DefWindowProc (hwnd, message, wParam, lParam) ;   
}

 

第五章 图形基础

GDI函数分类:

  • 获取和释放设备描述表的函数
  • 获取有关设备描述表信息的函数。GetTextMetrics
  • 绘图函数
  • 设置和获取设备描述表参数的函数. SetTextColor SetTextAlign
  • 使用GDI对象的函数。如创建逻辑画笔和清除等。CreatePen、CreatePenIndirect、ExtCreatePen

GDI图元:

  • 直线和曲线
  • 填充区域
  • 位图
  • 文本

GDI其他方面

  • 映射模式和变换: 允许以任何想使用的单位来绘图
  • 元文件Metafile: 以二进制形式储存的GDI命令集合。Metafile主要用于通过剪贴板传输向量图形
  • 区域:形状任意的复杂区域,通常定义为较简单的绘图区域组合。在GDI内部,绘图区域除了储存为最初用来定义绘图区域的线条组合以外,还以一系列扫描线的形式储存。可以将绘图区域用于绘制轮廓、填入图形和剪裁。
  • 路径:GDI内部储存的直线和曲线的集合。路径可以用于绘图、填入图形和剪裁,还可以转换为绘图区域。
  • 裁剪:绘图可以限制在显示区域的某一部分中。剪裁区域是不是矩形都可以,剪裁通常是通过区域或者路径来定义的。
  • 调色盘:自订调色盘通常限于显示256色的显示器。Windows仅保留这些色彩之中的20种以供系统使用,您可以改变其它236种色彩,以准确显示按位图形式储存的真实图像。
  • 打印

 

设备描述表: 当您想在一个图形输出设备(诸如屏幕或者打印机)上绘图时,您首先必须获得一个设备内容(或者DC)的句柄。将句柄传回给程序时,Windows就给了您使用设备的权限。然后您在GDI函数中将这个句柄作为一个参数,向Windows标识您想在其上进行绘图的设备。

设备内容中包含许多确定GDI函数如何在设备上工作的目前「属性」。

取得设备内容句柄:

  • hdc=BeginPaint(hwnd, &ps); EndPaint(hwn, &ps); //处理WM_PAINT

          PAINTSTRUCT结构的hdc字段是返回的设备描述表句柄,rcPaint字段为RECT结构,表示无效范围。BeginPaint获得的设备描述表句柄只能在这个区域内绘图。BeginPaint调用使该区域有效 

  • hdc=GetDC(hwnd); ReleaseDC(hwnd,hdc);

          返回的hdc可以在整个客户区绘图,不使任何可能的无效区域有效

  • hdc=GetWindowDC(hwnd); ReleaseDC(hwnd,hdc); //获取整个窗口的设备描述表句柄

         必须捕获WM_NCPAINT(非客户区绘制)消息,Windows使用该消息在窗口的非客户区上绘图

  • hdc = CreateDC (pszDriver, pszDevice, pszOutput, pData) ;  DeleteDC (hdc) ; //通用函数

          hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;  //获取整个屏幕的hdc

  • 如果只是想获取设备描述表的信息而不进行任何绘画,可以使用CreateIC

          hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;

  • 使用位图时获取一个内存设备描述表

         hdcMem = CreateCompatibleDC (hdc) ;  DeleteDC (hdcMem) ;

  • 通过取得metafile设备描述表来建立metafile

         hdcMeta = CreateMetaFile (pszFilename) ;  hmf = CloseMetaFile (hdcMeta) ;

         在metafile设备描述表有效期间,任何用hdcMeta所做的GDI调用都变成metafile的一部分而不会显示。在CloseMetaFile之后,设备描述表句柄变为无效,函数传回一个指向metafile(hmf)的句柄。

 

获取设备描述表信息:

iValue = GetDeviceCaps (hdc, iIndex) ;  //iIndex取值为WINGDI.H表头文件中定义的29个标识符之一

HORZRES VERTRES等价于SM_CXSCREEN SM_CYSCREEN在GetSystemMetrics得到像素尺寸

HORZSIZE和VERTSIZE返回以mm为单位的物理屏幕尺寸

LOGPIXELSX和LOGPIXELSY返回水平和垂直方向的DPI (dots per inch)

ASPECTX、ASPECTY和ASPECTXY是每一个像素的相对宽度高度和对角线大小

 

设备大小: 如果想在设备画1英寸长度怎么办? 设备默认使用像素值

分辨率(resolution):每度量单位(一般为英寸)内的像素数。

如果设备的水平分辨率与垂直分辨率相等,就称设备具有「正方形图素」。

在传统的排版中,字体的字母大小由磅表示。1磅(point)大约1/72英寸,在计算机排版中1磅正好为1/72英寸。

理论上。字体磅值=tmHeight-tmInternalLeading 不包括重音符号。TEXTMETRIC结构

Windows系统字体-不考虑是大字体还是小字体,也不考虑所选择的视频图素大小-固定假设为10磅字体和12磅行距。这听起来很奇怪,如果字体都是10磅,为什么还把它们称为大字体和小字体呢?选择小字体或大字体时,实际上是选择了一个假定的视讯显示分辨率,单位是每英寸的点数.

小字体,它依据的显示分辨率为每英寸96点。它是10磅字体。10 point. 小字体= 10/72inch * 96 dot/inch=13dot

这即是tmHeight减去tmInternalLeading的值。行距是12磅,12/72inch * 96dot/inch=16dot。这即是tmHeight的值。

大字体依据每英寸120点的分辨率。它是10磅字体,大字体=10/72inch * 120dot/inch=16dot,即是tmHeight减tmInternalLeading的值。12磅行距等于20dot,即是tmHeight的值。

水平大小(HORZSIZE) mm=25.4 * 水平分辨率(HORZRES)/逻辑像素X(LOGPIXELSX, DPI,每英寸点数)

垂直大小(VERTSIZE) mm=25.4 * 垂直分辨率(VERTRES)/逻辑像素Y(LOGPIXELSX, DPI)

 

色彩:

  • 全色「Full-Color」视频显示器的分辨率是每个图素24位-8位红色、8位绿色以及8位蓝色。
  • 高色「High-Color」显示分辨率是每个图素16位-5位红色、6位绿色以及5位蓝色。绿色多一位是因为人眼对绿色更敏感一些。
  • 显示256种颜色的显示卡每个图素需要8位。然而,这些8位的值一般由定义实际颜色的调色盘组织的。
  • 显示16种颜色的显示卡每个图素需要4位。这16种颜色一般固定分为暗的或浅的红、绿、蓝、青、品红、黄、两种灰色、黑和白

返回色彩平面color plane数目

iPlanes = GetDeviceCaps (hdc, PLANES) ;

返回每个像素的色彩位数

iBitsPixel = GetDeviceCaps (hdc, BITSPIXEL) ;

大多数彩色图形显示设备使用多个色彩平面或每图素有多个色彩位的设计,但是不能同时一齐使用这两种方式;换句话说,这两个调用必有一个传回1。现在一般不用户色彩平面为所以1

能表示的色彩数计算公式

iColors = 1 << (iPlanes * iBitsPixel) ; //2^次方

 

iColors = GetDeviceCaps (hdc, NUMCOLORS) ;

256色视频适配器使用色彩调色板, NUMCOLORS为参数时,函数返回windows保留的色彩数,值为20。剩余的236种颜色可以由Windows程序用调色盘管理器设定。

对于High-Color和True-Color显示分辨率,带有NUMCOLORS参数的GetDeviceCaps通常传回-1,这样就无法得到需要的信息,因此应该使用前面所示的带有PLANES和BITSPIXEL值的iColors公式。

 

大多数GDI函数调用中,使用COLORREF值(只是一个32位的无正负号长整数)来表示一种色彩。COLORREF值按照红、绿和蓝色的亮度指定了一种颜色,通常叫做「RGB色彩」

image

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r) | \
                   ((WORD)((BYTE)(g)) << 8)) | \  
                   (((DWORD)(BYTE)(b)) << 16)))

注意三个参数的顺序是红、绿和蓝。因此,值:

RGB (255, 255, 0)  //0x0000FFFF

当所有三个参数设定为0时,色彩为黑色;当所有参数设定为255时,色彩为白色。

GetRValue、GetGValue和GetBValue宏从COLORREF值中抽取出原色值。

在16色或256色显示卡上,Windows可以使用「抖动」来模拟设备能够显示的颜色之外的色彩。抖动利用了由多种色彩的图素组成的图素图案。可以呼叫GetNearestColor来决定与某一色彩最接近的纯色:

crPureColor = GetNearestColor (hdc, crColor) ;

 

设备描述表属性

11

通常,在调用GetDC或BeginPaint时,Windows用默认值建立一个新的设备内容,您对属性所做的一切改变在设备内容用ReleaseDC或EndPaint呼叫释放时,都会丢失。

但是您还可能想要在释放设备内容之后,仍然保存程序中对设备内容属性所做的改变,以便在下一次呼叫GetDC和BeginPaint时它们仍然能够起作用。为此,可在登录窗口类别时,将CS_OWNDC旗标纳入窗口类别的一部分:

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC ;

现在,依据这个窗口类别所建立的每个窗口都将拥有自己的设备内容,它一直存在,直到窗口被删除。如果使用了CS_OWNDC风格,就只需初始化设备内容一次,可以在处理WM_CREATE消息处理期间完成这一操作。CS_OWNDC风格只影响GetDC和BeginPaint获得的设备内容,不影响其它函数(如GetWindowDC)获得的设备内容。

某些情况下,您可能想改变某些设备内容属性,用改变后的属性进行绘图,然后恢复原来的设备内容。以通过如下调用来保存设备内容的状态:

idSaved = SaveDC (hdc) ;

现在,可以改变一些属性,在想要回到呼叫SaveDC前存在的设备内容时,呼叫:

RestoreDC (hdc, idSaved) ;

当您呼叫SaveDC时,不需要保存传回值:SaveDC (hdc) ;

然后,您可以更改某些属性并再次呼叫SaveDC。要将设备内容恢复到一个已经保存的状态,呼叫:

RestoreDC (hdc, -1) ;

 

写图素

SetPixel (hdc, x, y, crColor) ; //在指定的x和y坐标以特定的颜色设定图素。如果在函数中指定的颜色视讯显示器不支持,则函数将图素设定为最接近的纯色并从函数传回该值

crColor = GetPixel (hdc, x, y) ;//传回指定坐标处的图素颜色

 

画线函数

  • LineTo 画直线。
  • Polyline和PolylineTo 画一系列相连的直线。
  • PolyPolyline 画多组相连的线。
  • Arc 画椭圆线。
  • PolyBezier和PolyBezierTo 画贝塞尔曲线。

另外,Windows NT还支持3种画线函数:

  • ArcTo和AngleArc 画椭圆线。
  • PolyDraw 画一系列相连的线以及贝塞尔曲线。

既画线也填入所画图形的封闭区域的函数,这些函数是:

  • Rectangle 画矩形。
  • Ellipse 画椭圆。
  • RoundRect 画带圆角的矩形。
  • Pie 画椭圆的一部分,使其看起来像一个扇形。
  • Chord 画椭圆的一部分,以呈弓形。

设备内容的五个属性影响着用这些函数所画线的外观:目前画笔的位置(仅用于LineTo、PolylineTo、PolyBezierTo和ArcTo )、画笔、背景方式、背景色和绘图模式。

MoveTo(hdc,x,y): 16位版本, 通过两个16位数拼成32位无符号长整数返回先前的当前位置。32位中,坐标是32位的,不能拼成64位的整型

MoveToEx(hdc,xBeg,yBeg,NULL): 最后一个参数是指向POINT结构的指针。从该函数传回后,POINT结构的x和y字段指出了先前的目前位置。如果您不需要这种信息(通常如此),可设定为NULL。

 

如果您需要目前位置,就可以通过以下呼叫获得:

GetCurrentPositionEx (hdc, &pt) ;//pt是POINT结构的

画一个矩形边框:

POINT apt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; 
for (i = 1 ; i < 5 ; i++)
        LineTo (hdc, apt[i].x, apt[i].y) ;
//可用Polyline等价。既不使用也不改变目前位置
Polyline (hdc, apt, 5) ;
//PolylineTo有些不同,这个函数使用目前位置作为开始点,并将目前位置设定为最后一根线的终点。下面程序等价
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ;        
PolylineTo (hdc, apt + 1, 4) ;

 

边界框(bounding box)函数

Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
Ellipse (hdc, xLeft, yTop, xRight, yBottom) ;
RoundRect (hdc, xLeft, yTop, xRight, yBottom,    //圆角矩形
           xCornerEllipse, yCornerEllipse) ; 

xCornerEllipse和yCornerEllipse的值越大,角就越明显。如果xCornerEllipse等于xLeft与xRight的差,且yCornerEllipse等于yTop与yBottom的差,那么RoundRect函数将画出一个椭圆。

image

Arc(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
Chord(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
Pie(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
image

image

image

 

贝塞尔样条:PolyBezier(hdc, apt,iCount); PolyBezierTo(hdc, apt,iCount);

 

画笔:

画笔决定线的色彩、宽度和画笔样式,画笔样式可以是实线、点划线或者虚线,内定设备内容中画笔为BLACK_PEN。不管映像方式是什么,这种画笔都画出一个图素宽的黑色实线来。BLACK_PEN是Windows提供的三种现有画笔之一,其它两种是WHITE_PEN和NULL_PEN,NULL_PEN什么都不画。您也可以自己自订画笔。

HPEN hPen = SelectObject (hdc, GetStockObject (WHITE_PEN)) ;
SelectObject的传回值是调用之前设备内容中的画笔句柄

通过CreatePen或CreatePenIndirect建立一个「逻辑画笔」。

逻辑画笔是一种「GDI对象」,它是您可以建立的六种GDI对象之一,其它五种是画刷、位图、区域、字体和调色盘。除了调色盘之外,这些对象都是通过SelectObject选进设备内容的。

在使用画笔等GDI对象时,应该遵守以下三条规则:

  • 最后要删除自己建立的所有GDI对象。
  • 当GDI对象正在一个有效的设备内容中使用时,不要删除它。
  • 不要删除现有对象。
hPen = CreatePen (iPenStyle, iWidth, crColor) ;

iPenStyle参数确定画笔是实线、点线还是虚线,该参数可以是WINGDI.H表头文件中定义的以下标识符

image

对于PS_SOLID、PS_NULL和PS_INSIDEFRAME画笔样式,iWidth参数是画笔的宽度。iWidth值为0则意味着画笔宽度为一个图素。现有画笔是一个图素宽。如果指定的是点划线或者虚线式画笔样式,同时又指定一个大于1的实际宽度,那么Windows将使用实线画笔来代替。

CreatePen的crColor参数是一个COLORREF值,它指定画笔的颜色。对于除了PS_INSIDEFRAME之外的画笔样式,如果将画笔选入设备内容中,Windows会将颜色转换为设备所能表示的最相近的纯色。PS_INSIDEFRAME是唯一一种可以使用混色的画笔样式,并且只有在宽度大于1的情况下才如此。

在与定义一个填入区域的函数一起使用时,PS_INSIDEFRAME画笔样式还有另外一个奇特之处:对于除了PS_INSIDEFRAME以外的所有画笔样式来说,如果用来画边界框的画笔宽度大于1个图素,那么画笔将居中对齐在边界框在线,这样边界框线的一部分将位于边界框之外;而对于PS_INSIDEFRAME画笔样式来说,整条边界框线都画在边界框之内。

您也可以通过建立一个型态为LOGPEN(「逻辑画笔」)的结构,并呼叫CreatePenIndirect来建立画笔。

LOGPEN logpen ;
LOGPEN有三个成员,lopnStyle(UINT)画笔线型,lopnWidth(POINT的x,忽略y)按逻辑单位度量的画笔宽度,lopnColor(COLORREF)画笔颜色
hPen = CreatePenIndirect (&logpen) ;

CreatePen和CreatePenIndirect函数不需要设备内容句柄作为参数。这些函数建立与设备内容没有联系的逻辑画笔。直到呼叫SelectObject之后,画笔才与设备内容发生联系。因此,可以对不同的设备(如屏幕和打印机)使用相同的逻辑画笔。

HPEN hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
DeleteObject (SelectObject (hdc, hPen)) ;

如果有一个画笔的句柄,就可以通过呼叫GetObject取得LOGPEN结构各个成员的值:

GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;
        

如果需要目前选进设备内容的画笔句柄,可以呼叫:

hPen = GetCurrentObject (hdc, OBJ_PEN) ;

 

填充空隙:背景模式

使用点式画笔和虚线画笔会产生一个有趣的问题:点和虚线之间的空隙会怎样呢?您所需要的是什么?

空隙的着色取决于设备内容的两个属性-背景模式和背景颜色。内定背景模式为OPAQUE,在这种方式下,Windows使用背景色来填入空隙,内定的背景色为白色。这与许多程序在窗口类别中用WHITE_BRUSH来擦除窗口背景的做法是一致的。

您可以通过如下呼叫来改变Windows用来填入空隙的背景色:

SetBkColor (hdc, crColor) ;
        

与画笔色彩所使用的crColor参数一样,Windows将这里的背景色转换为纯色。可以通过用GetBkColor来取得设备内容中定义的目前背景色。

通过将背景模式转换为TRANSPARENT,可以阻止Windows填入空隙:

SetBkMode (hdc, TRANSPARENT) ;
        

此后,Windows将忽略背景色,并且不填入空隙,可以通过呼叫GetBkMode来取得目前背景模式(TRANSPARENT或者OPAQUE)。

 

绘图方式:

当Windows使用画笔来画线时,它实际上执行画笔图素与目标位置处原来图素之间的某种位布尔运算。图素间的位布尔运算叫做「光栅运算」,简称为「ROP」。由于画一条直线只涉及两种图素(画笔和目标),因此这种布尔运算又称为「二元光栅运算」,简记为「ROP2」。Windows定义了16种ROP2代码,表示Windows组合画笔图素和目标图素的方式。在内定设备内容中,绘图方式定义为R2_COPYPEN,这意味着Windows只是将画笔图素复制到目标图素。

R2_BLACK始终为黑色。

R2_NOTMERGEPAN: 只有当画笔与目标都为黑色时,画出的结果才是白色,其他情况都是黑色。

R2_NOT翻转目标色彩、R2_NOP不操作

 

image

可以通过以下呼叫在设备内容中设定新的绘图模式和获取绘图模式:

SetROP2 (hdc, iDrawMode) ;
iDrawMode = GetROP2 (hdc) ;

 

绘制填入区域:

Rectangle Ellipse RoundRect

Chord Pie

Polygon PolyPolygon

 

Windows用设备内容中选择的目前画笔来画图形的边界框,边界框还使用目前背景方式、背景色彩和绘图方式,这跟Windows画线时一样。关于直线的一切也适用于这些图形的边界框。

Windows定义六种现有画刷:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH (也叫HOLLOW_BRUSH)。

SelectObject (hdc, GetStockobject (NULL_BRUSH) ;

 

多边形:

Polygon (hdc, apt, iCount) ; 如果该数组中的最后一个点与第一个点不同,则Windows将会再加一条线,将最后一个点与第一个点连起来(在Polyline函数中,Windows不会这么做)。

PolyPolygon (hdc, apt, aiCounts, iPolyCount) ;

该函数绘制多个多边形。最后一个参数给出了所画的多边形的个数。对于每个多边形,aiCounts数组给出了多边形的端点数。apt数组具有全部多边形的所有点。除传回值以外,PolyPolygon在功能上与下面的代码相同:

for (i = 0, iAccum = 0 ; i < iPolyCount ; i++) {
    Polygon (hdc, apt + iAccum, aiCounts[i]) ;
    iAccum += aiCounts[i] ;  
}
多边形填入方式,您可以用SetPolyFillMode函数来设定:

SetPolyFillMode (hdc, iMode) ;

ALTERNATE方式: 从一个无穷大的封闭区域内部的点画线(射线),只有假想的线穿过了奇数条边界线时,才填入封闭区域。

WINDING方式: 从那个无穷大的区域画线。如果假想的线穿过了奇数条边界线,区域就被填入,这和ALTERNATE方式一样。如果假想的线穿过了偶数条边界线,则区域可能被填入也可能不被填入。如果一个方向(相对于假想线)的边界线数与另一个方向的边界线数不相等,就填入区域。

 

用画刷填充内部:

画刷是一个8×8的位图,它水平和垂直地重复使用来填入内部区域。

用色彩抖动来显示多于可从显示器上得到的色彩。实际上是将8*8的位图画刷用于色彩。
在单色系统上,对于纯黑色,8×8位图中的所有位均为0。第一种灰色有一位为1,第二种灰色有两位为1,以此类推,直到8×8位图中所有位均为1,这就是白色。在16色或256色显示系统上,抖动也是位图。

创建GDI对象-逻辑画刷:
hBrush = CreateSolidBrush (crColor) ; 函数中的Solid并不是指画刷为纯色。在将画刷选入设备内容中时,Windows建立一个抖动色的位图,并为画刷使用该位图。

建立影线画刷的函数为:

hBrush = CreateHatchBrush (iHatchStyle, crColor) ; //iHatchStyle参数描述影线标记的外观

image

crColor参数是影线的色彩,在将画刷选进设备内容时,Windows将这种色彩转换为与之最相近的纯色。

影线之间的区域根据设备内容中定义的背景方式和背景色来着色。如果背景方式为OPAQUE,则用背景色(它也被转换为纯色)来填入线之间的空间。在这种情况下,影线和填入色都不能是抖动色。如果背景方式为TRANSPARENT,则Windows只画出影线,不填入它们之间的区域。

可以使用CreatePatternBrush和CreateDIBPatternBrushPt建立自己的位图画刷。

建立逻辑画刷的第五个函数包含其它四个函数:

hBrush = CreateBrushIndirect (&logbrush) ;

logbrush是一个型态为LOGBRUSH(「逻辑画刷」)的结构,该结构的三个字段如表5-4所示,lbStyle字段的值确定了Windows如何解释其它两个字段的值:

image

SelectObject (hdc, hBrush) ;

DeleteObject (hBrush) ;

GetObject (hBrush, sizeof (LOGBRUSH), (LPVOID) &logbrush) ;

 

GDI映射方式:

有四种设备内容属性-窗口原点、视端口原点、窗口范围和视端口范围-与映像方式密切相关。

在TextOut中,以及在几乎所有GDI函数中,这些坐标值使用的都是一种「逻辑单位」。Windows必须将逻辑单位转换为「设备单位」,即图素。这种转换是由映像方式、窗口和视端口的原点以及窗口和视口的范围所控制的。映像方式还指示着x轴和y轴的方向(orientation)

image

SetMapMode (hdc, iMapMode) ;

iMapMode = GetMapMode (hdc) ;

定映像方式为MM_TEXT。在这种映像方式下,逻辑单位与实际单位相同,这样我们可以直接以像素为单位进行操作。

映射方式不影响消息!!

Windows对所有消息(如WM_MOVE、WM_SIZE和WM_MOUSEMOVE),对所有非GDI函数,甚至对一些GDI函数,永远使用设备坐标。

由于映像方式是一种设备内容属性,所以,只有对需要设备内容句柄作参数的GDI函数,映像方式才会起作用。

GetSystemMetrics不是GDI函数,所以它总是以设备单位(即图素)为量度来传回大小的。

GetTextMetrics呼叫中传回的TEXTMETRIC结构的值是使用逻辑单位的。

 

设备坐标系

1.屏幕坐标: CreateDC. 屏幕坐标用在WM_MOVE消息(对于非子窗口)以及下列Windows函数中:CreateWindow和MoveWindow(都是对于非子窗口)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect以及WindowFromPoint(这不是全部函数的列表)。

2.整窗口坐标: GetWindowDC

3.客户区坐标: GetDC, BeginPaint

ClientToScreen, ScreenToClient

不管采用什么映射方式,窗口设备坐标总是x向右增,y向下增。映射方式只改变逻辑坐标系

 

视口和窗口:

映射方式用于定义从「窗口」(逻辑坐标)到「视口」(设备坐标)的映射。

「视口」是依据设备坐标(像素)的。通常,视口和客户区相同,但是,如果您已经用GetWindowDC或CreateDC取得了一个设备内容,则视端口也可以是指整窗口坐标或者屏幕坐标。

「窗口」是依据逻辑坐标的,逻辑坐标可以是图素、毫米、英寸或者您想要的任何其它单位。您在GDI绘图函数中指定逻辑窗口坐标。

Windows都用下面两个公式来将窗口(逻辑)坐标转化为视口(设备)坐标:

image

(xWindow,yWindow)是待转换的逻辑点,(xViewport,yViewport)是转换后的设备坐标点。

(xWinOrg,yWinOrg)是逻辑坐标的窗口原点;(xViewOrg,yViewOrg)是设备坐标的视口原点。内定的设备内容中,这两个点均被设定为(0,0)

(xWinExt,yWinExt)是逻辑坐标的窗口范围;(xViewExt,yViewExt)是设备坐标的窗口范围。在多数映像方式中,范围是映像方式所隐含的,不能够改变。每个范围自身没有什么意义,但是视端口范围与窗口范围的比例是逻辑单位转换为设备单位的换算因子。

下面的函数将设备点转换为逻辑点:

DPtoLP (hdc, pPoints, iNumber) ;

这个函数对于将GetClientRect(它总是使用设备单位)取得的显示区域大小转换为逻辑坐标很有用:

GetClientRect (hwnd, &rect) ;
        
DPtoLP (hdc, (PPOINT) &rect, 2) ;
        

下面的函数将逻辑点转换为设备点:

LPtoDP (hdc, pPoints, iNumber) ;

 

处理MM_TEXT:

对于MM_TEXT映像方式,内定的原点和范围如下所示:

窗口原点:(0, 0) 可以改变

视埠原点:(0, 0) 可以改变

窗口范围:(1, 1) 不可改变

视埠范围:(1, 1) 不可改变

端口范围与窗口范围的比例为1,所以不用在逻辑坐标与设备坐标之间进行缩放.

xViewport = xWindow - xWinOrg + xViewOrg

yViewport = yWindow - yWinOrg + yViewOrg

Windows提供了函数SetViewportOrgEx和SetWindowOrgEx,用来改变视口和窗口的原点

如果将视口原点改变为(xViewOrg,yViewOrg),则逻辑点(0.0)就会映像为设备点(xViewOrg,yViewOrg)。如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)将会映像为设备点(0,0),即左上角。不管对窗口和视端口原点作什么改变,设备点(0,0)始终是显示区域的左上角。

如果想将逻辑点(0,0)定义为显示区域的中心(cxClient/2,cyClient/2),可进行如下调用:

SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

与下面等价,逻辑点(-cxClient / 2,-cyClient / 2)映像为设备点(0,0),:

SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

可以使用下面两个函数取得目前视端口和窗口的原点:

GetViewportOrgEx (hdc, &pt) ;
GetWindowOrgEx (hdc, &pt) ;

一个应用:垂直滚动条的目前位置来调整显示输出的y坐标

case WM_PAINT:
    hdc = BeginPaint (hwnd, &ps) ;
    for (i = 0 ; i < NUMLINES ; i++) {
            y = cyChar * (i - iVscrollPos) ;
            // 显示文字 
    }
    EndPaint (hwnd, &ps) ;
    return 0 ;

等价:

case WM_PAINT:        
    hdc = BeginPaint (hwnd, &ps) ; 
    SetWindowOrgEx (hdc, 0, cyChar * iVscrollPos) ;  
    for (i = 0 ; i < NUMLINES ; i++) {
            y = cyChar * i ;
            // 显示文字
    }
    EndPaint (hwnd, &ps) ;
    return 0 ;

 

度量映射方式: 注意客户区内y坐标为负

坐标转换,x轴y轴往哪个方向增减,取决于转换公式的换算因子视口范围/窗口范围的正负

从低精度到高精度

image

内定窗口及视端口的原点和范围如下所示:

窗口原点:(0, 0) 可以改变

视扣原点:(0, 0) 可以改变

窗口范围:(?, ?) 不可改变

视扣范围:(?, ?) 不可改变

对于MM_LOENGLISH,windows计算的范围如下

xViewExt/xWinExt = 0.01英寸中的水平像素数

-yViewExt/yWinExt = 0.01英寸中的垂直像素数

 

窗口和视口的范围是怎么确定的呢?

windows98下,假设在控制面板的显示里选择了96DPI的字体,GetDeviceCaps对于LOGPIXELSX和LOGPIXELSY索引都将传回值96

image

Windows NT使用不同的方法设定视端口和窗口的范围(与早期16位版本的Windows一致的方法)。视端口范围依据屏幕的图素尺寸。可以使用HORZRES和VERTRES索引从GetDeviceCaps取得这种信息。窗口范围依据假定的显示大小,它是您使用HORZSIZE和VERTSIZE索引时由GetDeviceCaps传回的。我在前面提到过,这些值一般是320和240毫米。如果您将显示器的图素尺寸设定为1024×768,则表5-8就是Windows NT报告的视端口和窗口范围的值。

image

窗口范围表示包含显示器全部宽度和高度的逻辑单位数值

 

应用:将逻辑(0,0)映射到客户区中央

1.SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

2.

pt.x = cxClient ;
pt.y = cyClient ;
DptoLP (hdc, &pt, 1) ;
SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2, NULL) ;

 

自作主张的映射方式: MM_ISOTROPIC和MM_ANISOTROPIC。

只有这两种映像方式可以让改变视口和窗口范围,即改变Windows用来转换逻辑和设备坐标的换算因子。

MM_ISOTROPIC: 使用相同的轴,x轴上的逻辑单位与y轴上的逻辑单位的实际尺寸相等。对建立纵横比与显示比无关的图像是有帮助的。

MM_ANISOTROPIC: 可以控制逻辑单位的实际尺寸。如果愿意,您可以根据客户区的大小来调整逻辑单位的实际尺寸,从而使所画的图像总是包含在客户区内,并相应地放大或缩小。

 

MM_ISOTROPIC:各向同性

Windows在调整视口和窗口范围时,必须让逻辑窗口适应实际窗口,这就有可能导致显示区域的一段落到了逻辑窗口的外面。必须在呼叫SetViewportExtEx之前呼叫SetWindowExtEx,以便最有效地使用显示区域中的空间。

假设您想要一个「传统的」单象限虚拟坐标系,其中(0,0)在显示区域的左下角,宽度和高度的范围都是从0到32,767,并且希望x和y轴的单位具有同样的实际尺寸。以下就是所需的程序:

SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 32767, 32767, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; 
SetViewportOrgEx (hdc, 0, cyClient, NULL) ;

Windows将根据显示设备的纵横比来调整范围,以便两条轴上的逻辑单位表示相同的实际尺寸。

如果显示区域的宽度大于高度(以实际尺寸为准),Windows将调整x的范围,以便逻辑窗口比显示区域视端口窄。这样,逻辑窗口将放置在显示区域的左边:

image

果显示区域的高度大于宽度(以实际尺寸为准),那么Windows将调整y的范围。这样,逻辑窗口将放置在显示区域的下边:

image

如果您希望逻辑窗口总是放在显示区域的左上部,那么将前面给出的程序代码改为:

SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 32767, 32767, NULL) ; //逻辑坐标在客户区之内,cxClient,cyClient>32767
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; 
SetWindowOrgEx (hdc, 0, 32767, NULL) ;//逻辑(0,0)落在设备点(0,32767),正负根据转换公式,范围的正负

如果显示区域的高大于宽,则坐标系将安排为:

image

在MM_ISOTROPIC映像方式下,可以使逻辑单位大于图素。例如,假设您想要一种映像方式,使点(0,0)显示在屏幕的左上角,y的值向下增长(和MM_TEXT相似),但是逻辑坐标单位为1/16英寸。以下是一种方法:

SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 16, 16, NULL) ;
SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX),
                       GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;

SetWindowExtEx函数的参数指出了每一英寸中逻辑单位数。SetViewportExtEx函数的参数指出了每一英寸中实际单位数(图素)。

然而,这种方法与Windows NT中的度量映像方式不一致。这些映射方式使用显示器的图素大小和公制大小。要与度量映像方式保持一致,可以这样做:

SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 160 * GetDeviceCaps (hdc, HORZSIZE) / 254, //毫米除以25.4转英寸,乘以16转为以1/16英寸为单位
                     160 * GetDeviceCaps (hdc, VERTSIZE) / 254, NULL) ;
SetViewportExtEx (hdc, GetDeviceCaps (hdc, HORZRES)
             GetDeviceCaps (hdc, VERTRES), NULL) ;

 

MM_ISOTROPIC:各向异性,根据需要放缩图像

MM_ANISOTROPIC不需要维持正确的纵横比。

一种使用MM_ANISOTROPIC的方法是将x和y轴的单位固定,但其值不相等。例如,如果有一个只显示文字的程序,您可能想根据单个字符的高度和宽度设定一种粗刻度的坐标:

SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 1, 1, NULL) ; 
SetViewportExtEx (hdc, cxChar, cyChar, NULL) ;//字符宽度和高度
TextOut (hdc, 3, 2, TEXT ("Hello"), 5) ;

当您第一次设定MM_ANISOTROPIC映像方式时,它总是继承前面所设定的映像方式的范围,这会很方便。可以认为MM_ANISOTROPIC不「锁定」范围;也就是说,它允许您任意改变窗口范围。例如,假设您想用MM_LOENGLISH映像方式,因为希望逻辑单位为0.01英寸,但您不希望y轴的值向上增加,喜欢如MM_TEXT那样的方向,即y轴的值向下增加,可以使用如下的代码:

SIZE size ;
SetMapMode (hdc, MM_LOENGLISH) 
SetMapMode (hdc, MM_ANISOTROPIC) ; //让范围可以自由改变
GetViewportExtEx (hdc, &size) ; 
SetViewportExtEx (hdc, size.cx, -size.cy, NULL) ;//将y范围取反

 

矩形、区域和剪裁

FillRect (hdc, &rect, hBrush) ; 用指定画刷来填入矩形(直到但不包含right和bottom坐标),该函数不需要先将画刷选进设备内容。

FrameRect (hdc, &rect, hBrush) ; 使用画刷画矩形框,但是不填入矩形。FrameRect允许使用者画一个不一定为纯色的矩形框。该边界框为一个逻辑单位元宽。

InvertRect (hdc, &rect) ; 将矩形中所有图素翻转,1转换成0,0转换为1,该函数将白色区域转变成黑色,黑色区域转变为白色,绿色区域转变成洋红色。

 

Windows还提供了9个函数,使您可以更容易、更清楚地操作RECT结构

SetRect (&rect, xLeft, yTop, xRight, yBottom) ;
OffsetRect (&rect, x, y) ; //将矩形沿x轴和y轴移动几个单元
InflateRect (&rect, x, y) ;//增减矩形的尺寸
SetRectEmpty (&rect) ;//矩形各字段设定为0
CopyRect (&DestRect, &SrcRect) ;//将矩形复制给另一个矩形,等价于DestRect = SrcRect ; 
IntersectRect (&DestRect, &SrcRect1, &SrcRect2) ;//取得两个矩形的交集
UnionRect (&DestRect, &SrcRect1, &SrcRect2) ;//取得两个矩形的并集
bEmpty = IsRectEmpty (&rect) ; //确定矩形是否为空
bInRect = PtInRect (&rect, point) ;//确定点是否在矩形内

 

替换GetMessage循环:

while (TRUE) {  
    if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT)
            break ;
        TranslateMessage (&msg) ;
        DispatchMessage (&msg) ;   
    }else    
    {
        // 完成某些工作的其它行程序
    } 
}   
return msg.wParam ;

参数:一个指向MSG结构的指针、一个窗口句柄、两个值指示消息范围

如果要将消息从消息队列中删除,则将PeekMessage的最后一个参数设定为PM_REMOVE。如果您不希望删除消息,那么您可以将这个参数设定为PM_NOREMOVE。

它使得程序可以检查程序的队列中的下一个消息,而不实际删除它。

WM_QUIT消息必须显示检查,PeekMessage的返回值为消息队列中有无消息,而非GetMessage遇到WM_QUIT返回0。

从队列中删除WM_PAINT消息的唯一方法是令窗口显示区域的失效区域变得有效,这可以用ValidateRect和ValidateRgn或者BeginPaint和EndPaint对来完成。

 

while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ; 

这行叙述从消息队列中删除WM_PAINT之外的所有消息。如果队列中有一个WM_PAINT消息,程序就会永远地陷在while循环中。

 

建立和绘制区域

区域是对显示器上一个范围的描述,这个范围是矩形、多边形和椭圆的组合。区域可以用于绘制和剪裁,通过将区域选进设备内容,就可以用区域来进行剪裁(就是说,将可以绘图的范围限制为显示区域的一部分)。与画笔、画刷和位图一样,剪裁区域是GDI对象,您应该呼叫DeleteObject来删除您所建立的剪裁区域。

HRGN hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom) ;
HRGN hRgn = CreateRectRgnIndirect (&rect) ;
HRGN hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom) ;
HRGN hRgn = CreateEllipticRgnIndirect (&rect) ;
HRGN hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode) ;//多边形区域,iPolyFillMode可为ALTERNATE,WINDING

 

iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine) ;

这一函数将两个区域(hSrcRgn1和hSrcRgn2)组合起来并用句柄hDestRgn指向组合成的区域。这三个区域句柄都必须是有效的,但是hDestRgn原来所指向的区域被破坏掉了(当您使用这个函数时,您可能要让hDestRgn在初始时指向一个小的矩形区域)。

image

从CombineRgn传回的iRgnType值是下列之一:NULLREGION,表示得到一个空剪裁区域;SIMPLEREGION,表示得到一个简单的矩形、椭圆或者多边形;COMPLEXREGION,表示多个矩形、椭圆或多边形的组合;ERROR,表示出错了。

 

区域的句柄可以用于四个绘图函数

FillRgn (hdc, hRgn, hBrush) ; //FillRect
FrameRgn (hdc, hRgn, hBrush, xFrame, yFrame) ; //FrameRect  
InvertRgn (hdc, hRgn) ;  //InvertRect
PaintRgn (hdc, hRgn) ;//设备描述表中当前画刷填充

 

在您用完一个区域后,可以像删除其它GDI对象那样删除它:

DeleteObject (hRgn) ;

矩形与区域的剪裁

InvalidateRect函数使显示的一个矩形区域失效,并产生一个WM_PAINT消息。

InvalidateRect (hwnd, NULL, TRUE) ;

GetUpdateRect来取得失效矩形的坐标,并且可以使用ValidateRect函数使显示区域的矩形有效。当您接收到一个WM_PAINT消息时,无效矩形的坐标可以从PAINTSTRUCT结构中得到,该结构是用BeginPaint函数填入的。这个无效矩形还定义了一个「剪裁区域」,您不能在剪裁区域外绘图。

Windows有两个作用于剪裁区域而不是矩形的函数,它们类似于InvalidateRect和ValidateRect:

InvalidateRgn (hwnd, hRgn, bErase) ;
ValidateRgn (hwnd, hRgn) ;
SelectObject (hdc, hRgn) ;
SelectClipRgn (hdc, hRgn) ;

任选其一,建立自己的剪裁区域,这个剪裁区域使用设备坐标

GDI为剪裁区域建立一份副本,所以在将它选进设备内容之后,使用者可以删除它。

Windows还提供了几个对剪裁区域进行操作的函数:

ExcludeClipRect用于将一个矩形从剪裁区域里排除掉

IntersectClipRect用于建立一个新的剪裁区域,它是前一个剪裁区域与一个矩形的集

OffsetClipRgn用于将剪裁区域移动到显示区域的另一部分。

posted on 2011-04-28 13:22  Atela  阅读(4115)  评论(0编辑  收藏  举报