5.6 矩形、区域和裁剪

        摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P161

        Windows 还有其他几个使用 RECT(矩形)结构和区域的绘图函数。一个区域指的是屏幕上的一块空间,它由矩形、多边形和椭圆组合而成

5.6.1  处理矩形

        下面三个绘图函数需呀一个指向矩形结构的指针:

FillRect    (hdc, &rect, hBrush);
FrameRect   (hdc, &rect, hBrush);
InvertRect  (hdc, &rect);
在这三个函数中,参数 rect 是一个类型为 RECT 的结构,它有 4 个字段:left、top、right 和 bottom。 在这个结构中,坐标是逻辑坐标

        FillRect 函数使用指定的画刷填充矩形(达到但不包括右下坐标)。这个函数不需要事先把画刷选入设备环境。

        FrameRect 使用画刷绘制一个矩形框,但是它并不填充矩形。使用画刷来绘制边框似乎有点奇怪,因为到目前为止,你所看到的绘制边框的函数(例如 Rectangle)都是由当前画笔绘制的。FrameRect 函数允许你绘制一个不一定是纯色的矩形框。矩形的边框是 1 个逻辑单位宽。如果逻辑单位大于设备单位,边框的宽度将是 2 个或者更多的像素。

        InvertRect 函数翻转矩形内所有的像素,将 1 变为 0,0 变为 1。即这个函数将白色区域变为黑色区域,黑色区域变为白色区域,绿色区域变为洋红色区域。

        Windows 还包含 9 个可用于轻松便捷地操纵 RECT 结构的函数。例如,通常使用下面的代码将 RECT 结构的 4 个字段设置为特定值:

rect.left     = xLeft;
rect.top      = yTop;
rect.right    = xRight;
rect.bottom   = yBottom;
然而,通过 SetRect 函数的调用,只用一行代码即可实现相同的结果:

SetRect (&rect, xLeft, yTop, xRight, yBottom):

        如果想做下列事情之一,可以方便的使用其他 8 个函数。

  • 将矩形沿 x 轴和 y 轴移动几个单位:
    OffsetRect (&rect, x, y);
  • 增大或减小矩形的尺寸:
    InflateRect (&rect, x, y);
  • 把矩形结构的各字段设置为0:
    SetRectEmpty (&rect);
  • 将一个矩形结构复制到另一个矩形结构:
    CopyRect (&DestRect, &SrcRect);
  • 获取两个矩形的交集:
    IntersectRect (&DestRect, &SrcRect1, &SrcRect2);
    
  • 获取两个矩形的并集:
    UnionRect (&DestRect, &SrcRect1, &SrcRect2);
  • 判断矩形是否为空:
    bEmpty = IsRectEmpty (&rect);
    
  • 判断点是否在矩形内部:
    bInRect = PtInRect(&rect, point);
    

        大多数情况下,还有一些简单的代码可以实现与这些函数相同的功能。例如,复制结构时,可以通过逐个字段的结构复制操作,来代替调用 CopyRect 函数,如下面的语句:

DestRect = SrcRect;

5.6.2  随机矩形

        在任何一个图形系统中,总存在这样一个有趣的程序,即简单地使用随机的尺寸和颜色不停地绘制一系列的图像,例如,随机大小和颜色的矩形。在 Windows 中可以创建这样的一个程序,但是这并不像想象的那样容易。 我希望你能够意识到,不能在处理 WM_PAINT 消息中简单地使用 while(TRUE) 循环。当然,这样做会奏效,但是这样做的结果是,程序将停止对其他消息的处理,而且程序不能退出或者最小化

        一种可接受的方式是设置一个向你的窗口函数发送 WM_TIMER 消息的 Windows 计时器。(我将在第 8 章介绍计时器。)对于每个 WM_TIMER 消息,可以调用 GetDC 函数获取设备环境,然后绘制一个随机矩形,接着调用 ReleaseDC 函数释放设备环境。但是那样做又会使程序失去一些趣味性,因为程序不能很快地绘制随机矩形。必须等待每个 WM_TIMER 消息,那样会依赖于系统时钟的精度

        在 Windows 中有很多的“空闲时间”,在这期间所有的消息队列都是空的,Windows 就在等待键盘或者鼠标的输入。那么能否在空闲期间从某种程度上获取控制并绘制随机矩形,而一旦有消息加载到程序的消息队列,就释放控制呢?这正是 PeekMessage 函数的“用武之地”。下面是 PeekMessage 函数调用的一个例子:

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

        函数的前 4 个参数(一个是指向 MSG 结构的指针,一个是窗口句柄,另外两个值表示信息范围)与 GetMessage 函数相同。设置第二、三、四个参数为 NULL 或者 0,表示我们想使用 PeekMessage 函数返回程序中所有窗口的所有消息。如果要删除消息队列中的消息,可以把 PeekMessage 函数的最后一个参数设置为 PM_REMOVE。如果不想删除,就设置为 PM_NOREMOVE。这就是 PeekMessage 名字的意思,它是“偷看”而不是“获得”。它允许一个程序检查程序队列中的下一个消息,而不是真实地获得并删除它看到的消息。

        GetMessage 函数并不把控制权交还给程序,除非它从程序的消息队列中获得了消息。但是 PeekMessage 函数却总是立即返回,不管消息是否出现。当一个消息在程序的消息队列中时,PeekMessage 函数的返回值是TRUE(非 0),而消息则像正常情况一样处理。当队列中没有消息时,PeekMessage 函数返回FALSE(0)

        这允许我们替换正常的消息循环,正常的消息循环如下所示:

while (GetMessage (&msg, NULL, 0, 0))
{
   TranslateMessage (&msg);
   DispatchMessage (&msg);
}
return msg.wParam;
替换后的消息循环如下:

while (TRUE) 
{
    if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
    {
        if (msg.message == WM_QUIT)
            break;
        TranslateMessage (&msg);
        DispatchMesage (&msg);
    } 
    else 
    {
        [other program lines to do some work]
    }
}
return msg.wParam;

注意:在这里,必须明确检查 WM_QUIT 消息,在一个正常的消息循环中,不需要这样做,因为当获取一个 WM_QUIT 消息时,GetMessage 函数的返回值是 FLASE(0)。但是 PeekMessage 函数的返回值是表示队列中是否有消息,因此检查 WM_QUIT 是必要的。


        如果 PeekMessage 函数返回 TRUE,那么消息会正常执行。如果返回 FLASE,那么程序可以在返回给 Windows 控制之前做些事情(如显示另一个随机矩形)。

        (尽管 Windows 文档中指出不能使用 PeekMessage 函数从消息队列中删除 WM_PAINT 消息,但是这并没有什么问题。毕竟,GetMessage 函数其实也不能从队列中删除 WM_PAINT 消息。使客户区的无效区域变成有效是从队列中删除 WM_PAINT 消息的唯一办法,可以使用 ValidateRect、ValidateRgn 或者成对的 BeginPaint 和 EndPaint 函数来完成。如果使用 PeekMessage 函数从消息队列获取 WM_PAINT 消息后,按照正常的方式对它进行处理,就不会又任何问题。但使用下面的代码来清除消息队列中的所有消息是不允许的

while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE));
这条语句表示从你的消息队列中删除 WM_PAINT 消息之外的所有消息。如果 WM_PAINT 在队列中,你将永远陷于 while 循环无法终止。)

        PeekMessage 函数在早期版本的 Windows 中比在 Windows 98 中重要得多。这是因为 16 位版本的 Windows 使用非抢占式多任务系统。Windows 自带的 Terminal 程序使用 PeekMessage 函数循环检查从通信端口接收到的数据。打印机管理程序也使用这项技术来打印,其他的 Windows 打印程序通常也使用一个 PeekMessage 函数的循环。在抢占式多任务的 Windows 98 中,应用程序可以建立多个线程。

5.6.3  建立和绘制区域

         一个区域是对显示器一块空间的描述,这个空间可以是矩形、多边形和椭圆的组合可以使用区域进行绘图或者裁剪。将区域选入设备环境,就可以使用这个区域来裁剪(也就是说,将绘制动作限制在客户区的一个特定部分)。同画笔和画刷一样,区域也就是 GDI 对象,应当通过调用 DeleteObject 函数来删除所有建立的区域。

        当建立一个区域时,Windows 返回一个类型为 HRGN 的区域句柄。最简单的区域类型是一个矩形区域。可以用下面的两种办法建立一个矩形区域:

hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom);
或者

hRgn = CreateRectRgnIndirect (&rect);
也可以使用下面的函数建立椭圆区域:

hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom);
或者

hRgn = CreateEllipticRgnIndirect (&rect);
创建圆角矩形区域可以通过 CreateRoundRectRgn 函数实现。

        创建一个多边形区域的函数和 Polygon 函数类似:

hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode);
参数 point 是一个类型为 POINT 结构的数组, iCount 是点的个数,iPolyFillMode 或者是 ALTERNATE,或者是 WINDING。你也可以调用 CreatePolygonRgn 函数创建多个多边形区域。

        那么你会问,区域有什么特别之处吗?下面的函数显示了区域的作用:

iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine);
这个函数将两个源区域(hSrcRgn1 和 hSrcRgn2)结合起来,并产生目标区域句柄(hDestRgn) 来表示那个组合区域。 这三个区域句柄都必须有效,但是 函数调用后 hDestRgn 先前描述的区域都被销毁了。(当使用这个函数时,可能要让 hDestRgn 在初始时表示一个很小的矩形区域。)

        参数 iCombine 描述 hSrcRgn1 区域和 hSrcRgn2 区域结合的方式:

iCombine 值新的区域
 RGN_AND 两个源区域的公共部分
 RGN_OR 两个源区域的全部
 RGN_XOR 两个源区域的全部,但除去公共部分
 RGN_DIFF hSrcRgn1 不在 hSrcRgn2 中的部分
 RGN_COPY hSrcRgn1 的全部(忽略 hSrcRgn2)

        iRgnType 值是从 CombineRgn 返回的下列值之一:NULLREGION,指的是一个空的区域;SIMPLEREGION,指的是一个简单的矩形、椭圆或者多边形;COMPLEXREGION,指的是矩形、椭圆或多边形的组合;ERROR,指的是有错误发生。

        一旦有了一个区域的句柄,就可以使用下面 4 个绘图函数:

FillRgn   (hdc, hRgn, hBrush);
FrameRgn  (hdc, hRgn, hBrush, xFrame, yFrame);
InvertRgn (hdc, hRgn);
PaintRgn  (hdc, hRgn);
FillRgn、FrameRgn 和 InvertRgn 函数类似于 FillRect、FrameRect 和 InvertRect 函数。FrameRgn 的参数 xFrame 和 yFrame 是表示在区域周围的、要绘制的边框的逻辑宽度和高度。PaintRgn 函数使用当前被选入设备环境的画刷来填充区域。所有的这些函数都假定使用的是逻辑坐标。

        用完一个区域后,可以用于删除其他 GDI 对象相同的函数来删除它:

DeleteObject (hRgn);

5.6.4  矩形与区域的裁剪

        区域在裁剪中也扮演着重要角色。InvalidRect 函数使显示的矩形区域无效,并产生一个 WM_PAINT 消息。例如,可以使用 InvalidateRect 函数来擦除客户区的内容,并产生一个 WM_PAINT 消息:

InvalidateRect (hwnd, NULL, TRUE);
可以通过调用 GetUpdateRect 函数获取无效矩形的坐标,并且使用 ValidateRect 使客户区的矩形有效。当接收到一个 WM_PAINT 消息时,PAINTSTRUCT 结构中的无效矩形的坐标是可以利用的。这个结构是通过 BeginPaint 函数填充的。这个无效矩形也定义了一个“裁剪区域”。 不能在裁剪区域之外绘图

        Windows 有两个类似 InvalidateRect 和 ValidateRect 的函数,用于处理区域而不是矩形;

InvalidateRgn (hwnd, hRgn, bErase);

ValidateRgn (hwnd, hRgn);
当接收一条由无效区域产生的 WM_PAINT 消息时,裁剪区域在形状不一定是矩形。

        可以通过将一个区域选入到设备环境来创建你自己的裁剪区域,将区域选入设备环境可以使用

SelectObject (hdc, hRgn);

SelectClipRgn (hdc, hRgn);
裁剪区域被假定使用的是设备坐标

        GDI 为裁剪区域做了一个副本,因此当把区域对象选入到设备环境后,可以删除它。Windows 还包括几个操纵这个裁剪区域的函数,例如 ExcludeClipRect 函数用来从裁剪区域中去除一个矩形;IntersectClipRect 函数用来建立一个新的裁剪区域,这个新的裁剪区域是先前的裁剪区域和某个矩形的交集;OffsetClipRgn 函数用来把一个裁剪区域移动到客户区的另外一部分。

5.6.5  CLOVER 程序

        CLOVER 程序由四个椭圆形成一个区域,然后把这个区域选入设备环境,接着从窗口区中心发散绘制一系列直线。这些直线仅出现剪裁区域内。

        如果使用传统的方法绘制这个图形,必须依据椭圆的圆周角公式计算出每条线段的端点。但是通过使用一个复杂的裁剪区域,就可以直接绘制直线,而让 Windows 去确定这些端点。

/*------------------------------------------------------------
    CLOVER.C  --  Clover Drawing Program Using Regions
                    (c) Charles Petzold, 1998
------------------------------------------------------------*/
#include <windows.h>
#include <math.h>

#define TWO_PI (2.0 * 3.14159)

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT ("Clover");
    HWND         hwnd;
    MSG          msg;
    WNDCLASS     wndclass;

    wndclass.style          = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc    = WndProc;
    wndclass.cbClsExtra     = 0;
    wndclass.cbWndExtra     = 0;
    wndclass.hInstance      = hInstance;
    wndclass.hIcon          = LoadIcon (NULL, IDI_APPLICATION);
    wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW);
    wndclass.hbrBackground  = (HBRUSH) GetStockObject (WHITE_BRUSH);
    wndclass.lpszMenuName   = NULL;
    wndclass.lpszClassName  = szAppName;

    if (!RegisterClass (&wndclass))
    {
        MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                    szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow (szAppName, TEXT("Draw a Clover"),
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL);
    ShowWindow (hwnd, iCmdShow);
    UpdateWindow (hwnd);

    while (GetMessage (&msg, NULL, 0, 0))
    {
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HRGN hRgnClip;
    static int  cxClient, cyClient;
    double      fAngle, fRadius;
    HCURSOR     hCursor;
    HDC         hdc;
    HRGN        hRgnTemp[6];
    int         i;
    PAINTSTRUCT ps;

    switch (message)
    {
    case WM_SIZE:
        cxClient = LOWORD(lParam);
        cyClient = HIWORD(lParam);

        hCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
        ShowCursor(TRUE);

        if (hRgnClip)
            DeleteObject(hRgnClip);

        hRgnTemp[0] = CreateEllipticRgn(0, cyClient / 3,
                                        cxClient / 2, 2 * cyClient / 3);

        hRgnTemp[1] = CreateEllipticRgn(cxClient / 2, cyClient / 3,
                                        cxClient, 2 * cyClient / 3);

        hRgnTemp[2] = CreateEllipticRgn(cxClient / 3, 0,
                                        2 * cxClient / 3, cyClient / 2);

        hRgnTemp[3] = CreateEllipticRgn(cxClient / 3, cyClient / 2,
                                        2 * cxClient / 3, cyClient);

        hRgnTemp[4] = CreateRectRgn(0, 0, 1, 1);
        hRgnTemp[5] = CreateRectRgn(0, 0, 1, 1);
        hRgnClip    = CreateRectRgn(0, 0, 1, 1);

        CombineRgn(hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR);
        CombineRgn(hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR);
        CombineRgn(hRgnClip,    hRgnTemp[4], hRgnTemp[5], RGN_XOR);

        for (i = 0; i < 6; ++ i)
            DeleteObject(hRgnTemp[i]);

        SetCursor(hCursor);
        ShowCursor(FALSE);
        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

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

        fRadius = hypot(cxClient / 2.0, cyClient / 2.0);

        for (fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)
        {
            MoveToEx(hdc, 0, 0, NULL);
            LineTo(hdc, (int) ( fRadius * cos (fAngle) + 0.5),
                        (int) (-fRadius * sin (fAngle) + 0.5));
        }

        EndPaint(hwnd, &ps);
        return 0;
    case WM_DESTROY:
        DeleteObject(hRgnClip);
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

        因为区域总是使用设备坐标,所以 CLOVER 程序不得不在每次收到 WM_SIZE 消息时 重新创建区域。几年前,运行 Windows 的机器要花费几秒钟来重绘这个图形。今天,快速的机器几乎在瞬间就能完成绘制。

        CLOVER 先创建 4 个椭圆区域,它们被存储在 hRgnTemp 数组的前 4 个元素中。接着,程序创建三个 “空”区域:

hRgnTemp[4] = CreateRectRgn(0, 0, 1, 1);
hRgnTemp[5] = CreateRectRgn(0, 0, 1, 1);
hRgnClip    = CreateRectRgn(0, 0, 1, 1);
在客户区左边和右边的两个区域先合并:

CombineRgn(hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR);
同样地,在客户区顶部的两个椭圆区域也合并了:

CombineRgn(hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR);
最后两个合并后的区域再合并成 hRgnClip:

 CombineRgn(hRgnClip,    hRgnTemp[4], hRgnTemp[5], RGN_XOR);
RGN_XOR 标识符表示要从结果区域中排除重叠的区域。最后,6 个临时的区域被删除:

for (i = 0; i < 6; ++ i)
     DeleteObject(hRgnTemp[i]);

        相对结果而言,WM_PAINT 消息处理很简单。视口原点设置在客户区的中心(这样使画直线更容易),在处理 WM_SIZE 消息时创建的区域被选入设备环境作为裁剪区域:

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

        现在,剩下要做的就是画直线了,一共画 360 条,每一度画一条。每条线的长度是变量 fRadius,它表示的是从中心到客户区角落的距离:

fRadius = hypot(cxClient / 2.0, cyClient / 2.0);

for (fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)
{
     MoveToEx(hdc, 0, 0, NULL);
     LineTo(hdc, (int) ( fRadius * cos (fAngle) + 0.5),
                 (int) (-fRadius * sin (fAngle) + 0.5));
}
在处理 WM_DESTROY 消息期间,裁剪区域被删除:

DeleteObject(hRgnClip);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值