system: [SYSTEM]

user: 4. GDI绘制区域的函数 GDI绘制区域的函数有：多边形区域CDC::Polygon，矩形RECT类及相应的填充函数FillRect，椭 图4. 绘制直线段的两种解决方法 图5. 正方向(左)和反方向(右)绘制的折线段(注：图中灰色像素点实际上没有) 图6. 绘制折线段的解决方法 圆区域CDC::Ellipse，矩形区域RGN::CreateRectRgn、多边形区域RGN::CreatePolygonRgn及相应的填充函数FillRgn。下面主要对多边形区域(包括矩形)的填充进行分析。 在计算机图形学中，多边形有两种重要的表示方法：顶点表示和点阵表示。点阵表示是用位于多边形内部的像素的集合来刻画多边形。这种表示方法虽然失去了很多重要的几何信息(如边界、顶点等)，但它是光栅显示系统(如计算机屏幕)显示时所需的表示形式 [ 7 ] 。从多边形顶点表示到点阵表示的转换称为扫面转换多边形，即从多边形的顶点信息出发，求出位于其内部的像素集合，并将其颜色值写入帧缓存中相应的单元。常用的扫描转换多边形的算法有：逐点判断算法、扫描线算法、边缘填充算法和泛滥填充算法。 4.1. Polygon区域填充函数 MSDN中对Polygon的注释：The Polygon function draws a polygon consisting of two or more vertices connected by straight lines. The polygon is outlined by using the current pen and filled by using the current brush and polygon fill mode [ 11 ] 。 CDC::Polygon(const POINT *lpPoints， int cCount)使用当前画笔所画的边框围住多边形，并使用当前的画刷填充多边形 [ 9 ] 。Polygon绘制的多边形边界经过所有指定的顶点像素坐标，但是多边形的几何特性不一致。例如图7(左)所绘制的矩形，长9个像素单位宽7个像素单位，但是用逻辑坐标定义的长为9 − 1 = 8宽为7 − 1 = 6。此外用Polygon绘制相邻的多边形时，公共边界会叠印。例如图7(右)中在绘制完成左矩形后，绘制右矩形，这时左矩形的右边界会被右矩形的左边界覆盖，而导致相同像素位置重复渲染。 //用Polygon绘制的矩形区域(图7左) CPoint ps[ 4 ]; ps[ 0 ].x = 1; ps[ 0 ].y = 1; ps[ 1 ].x = 1; ps[ 1 ].y = 7; ps[ 2 ].x = 9; ps[ 2 ].y = 7; ps[ 3 ].x = 9; ps[ 3 ].y = 1; pDC->Polygon(ps，4); //用Polygon绘制相邻的矩形区域(图7右) ps[ 0 ].x = 9;ps[ 0 ].y = 1; ps[ 1 ].x = 9;ps[ 1 ].y = 7; ps[ 2 ].x = 20; ps[ 2 ].y = 7; ps[ 3 ].x = 20; ps[ 3 ].y = 1; pDC->Polygon(ps，4); 4.2. Ellipse椭圆填充函数 MSDN中Ellipse的注释The Ellipse function draws an ellipse. The center of the ellipse is the center of the specified bounding rectangle [ 12 ] 。用当前画笔画此椭圆边界并使用当前画刷填充它的内部区域 [ 9 ] ，椭圆的中心是外界矩形的中心。 从示例中可以看出用CDC::Ellipse()绘制的椭圆或圆(图8)保留了几何特性(长半轴、短半轴，直径)。Ellipse绘制的椭圆也有一些不足(走样)，例如图8棕色像素(实际不存在)应该是圆的边界，可知圆的边界像素(黑色)不准确。 //长半轴8，短半轴6的椭圆(图8左) pDC->Ellipse(1，1，9，7); //直径10的圆(图8右) pDC->Ellipse(0，0，10，10); 4.3. FillRect、FillSolidRect和FillRgn填充区域的方法(扫描线算法) 扫描线算法是扫描转换多边形(矩形)的常用算法，它通过扫描转换整个绘图窗口内的每一条中心扫描线来实现多边形(矩形)的填充问题。一条扫描线上的填充过程可分为下面三个步骤： 1) 求扫描线与多边形各边的交点； 2) 对所求得的交点按x坐标从小到大排序； 3) 将交点两两配对，并填充每一区段。 MSDN没有给出GDI绘图函数中多边形的填充方法，但是经过大量的实验得知，FillRect、FillSolidRect和FillRgn在扫描转换边界上的交点时，采用如下取整规则 [ 7 ] ： 假设某非水平边与中心扫描线y = e (e为整数)相交，交点的横坐标记作x，则有如下几种情形： 1) x为小数，即交点(x， e)落于扫描线y = e上非像素中心的位置。若交点在多边形的左边界上，则取边界右端的像素((int)x + 1， e)。若交点在多边形的右边界上，取边界左端的像素((int)x， e)。 图7. Polygon绘制的矩形(左)和相邻矩形(右) 图8. 用Ellipse绘制的椭圆(左)和圆(右) 2) 交点(x， e)正好落在整数像素点上(像素中心位置)，若(x， e)在多边形左边界上，看作属于多边形；若(x， e)在多边形右边界上，则它不属于多边形。 3) 在2)中，若落在像素中心上的交点(x， e)是多边形的顶点，将多边形的每条边看作下端闭、上端开的，相当于将每条边的上端点处去掉一个像素。 需要注意的是，在MM_TEXT映射下，原点在左上角，Y轴正方向向下，这时候“下闭上开”中的下和上和计算机屏幕中的下和上相反。 另外，由于水平边和扫描线平行，而且实际上水平边在算法中不起任何作用，所以在算法的预处理阶段将它们去掉。 4.3.1. RECT类及FillRect和Rectangle (LPRECT lprect) RECT在MSDN中的注释：By convention， the right and bottom edges of the rectangle are normally considered exclusive. In other words， the pixel whose coordinates are (right， bottom) lies immediately outside of the rectangle. For example， when RECT is passed to the FillRect function， the rectangle is filled up to， but not including， the right column and bottom row of pixels. This structure is identical to the RECTL structure [ 13 ] 。 FillRect函数填充全部矩形，包括左边和上边边界，但不填充右边和底边边界 [ 9 ] 。 从实验结果以及上述交点取整规则和MSDN中的注释可知，CDC::FillRect()和CDC::FillSolidRect()填充用CRect定义的矩形时，右边界和下边界不显示(图9(a))，这样处理保持了矩形的几何特性(长、宽)不变，同时也解释了用PtInRect (RECT， POINT)判断点是否在矩形内时，右边界和下边界上的点都被判断为不在矩形内的原因。因为PtInRect (RECT， POINT)采用的是像素算法，凡是矩形中显示出来的像素都返回真值。 CPen pen(PS_SOLID， 1， RGB(0，0，0)); CPen *oldpen = pDC->SelectObject(&pen); CBrush brush(RGB(100，100，100)); CRect rect(1，1，9，7); pDC->FillRect(&rect，&brush);//图9(a) 图9. GDI填充CRect (注：图中灰色像素点实际上没有) //pDC->Rectangle(&rect);//图9(c) pDC->SelectObject(oldpen); BOOL yn = 0; CPoint pt; pt.x = 9; pt.y = 7; yn = PtInRect(rect，pt); //yn返回0 在应用程序中，如果不考虑矩形的几何特性，并且需要PtInRect()返回理想的结果，可以在显示时将矩形的右下角顶点的x、y坐标都增加1，这时原矩形右边界和下边界(图9(b)蓝色虚线边框)上的点都被判断为在原矩形内。 如果想要保持矩形的几何特性，又想PtInRect()返回理想的结果，可以改进PtInRect()算法，使右边界和下边界点被判断为在矩形内。 BOOL PtInRectEx(CRect rect， CPoint point) { if(point.x == rect.right && (point.y<=rect.bottom && point.y>=rect.top)) return TRUE; else if(point.y == rect.bottom && (point.x<=rect.right && point.x>=rect.left)) return TRUE; else return rect.PtInRect(point); } 需要注意的是，用CDC::Rectangle(LPCRECT lprect)绘制的矩形(图9(c)所示)，右边和下边的黑色像素边框实际上并不是矩形的边界，用PtInRect判断右边界和下边界上的点都返回零值，而上边界和左边界都返回真值，MSDN中对Rectangle注释：The rectangle that is drawn excludes the bottom and right edges，也可证实这一点 [ 14 ] 。我们也可以理解为这个函数是将世界坐标映射到像素间的屏幕位置，以使物体边界(图9(c)蓝色虚线边框)与像素边界对齐(图9(c)黑色像素边界)，而不是与像素(区域)中心对齐 [ 3 ] 。这样既能显示矩形边界又保持了矩形的几何特性。 4.3.2. CRgn类及CRgn::CreatePolygonRgn， CRgn::CreateRectRgn和CDC::FillRgn 从示例和上述交点取整规则，用CRgn::CreateRectRgn(int x1，int y1，int x2，int y2)创建的矩形和用CRect定义的矩形显示结果一致，都是右边界和下边界不显示(图9(a))。同样用 PtInRegion判断的结果也和用PtInRect判断的结果一样。 //绘制矩形区域(结果和图9(a)相同) rgn.CreateRectRgn(1，1，9，7); pDC->FillRgn(&rgn，&brush); PtInRegion(rgn，9，7); //返回0 //绘制五角星形状的多边形区域(图10(a)) CPoint ps[ 10 ]; ps[ 0 ].x = 10;ps[ 0 ].y = 0; ps[ 1 ].x = 7; ps[ 1 ].y = 8; ps[ 2 ].x = 0; ps[ 2 ].y = 8; ps[ 3 ].x = 6; ps[ 3 ].y = 12; ps[ 4 ].x = 4; ps[ 4 ].y = 20; ps[ 5 ].x = 10;ps[ 5 ].y = 16; ps[ 6 ].x = 16;ps[ 6 ].y = 20; ps[ 7 ].x = 14;ps[ 7 ].y = 12; ps[ 8 ].x = 20;ps[ 8 ].y = 8; ps[ 9 ].x = 13;ps[ 9 ].y = 8; rgn.CreatePolygonRgn(ps，10，WINDING);//反方向绘制的结果一样 pDC->FillRgn(&rgn，&brush); //判断10个顶点是否在多边形内(结果如图10(b) BOOL yn = 1， yn2=1，yn3=1， yn4=1， yn5=1， yn6=1，yn7=1， yn8=1，yn9=1， yn10=1; yn = PtInRegion(rgn，10，0); yn2 = PtInRegion(rgn，7，8); yn3 = PtInRegion(rgn，0，8); yn4 = PtInRegion(rgn，6，12); yn5 = PtInRegion(rgn，4，20); yn6 = PtInRegion(rgn，10，16); yn7 = PtInRegion(rgn，16，20); yn8 = PtInRegion(rgn，14，12); yn9 = PtInRegion(rgn，20，8); yn10 = PtInRegion(rgn，13，8); 图10. GDI填充CRgn MSDN中对Create Rgn的注释:Regions created by the Create Rgn methods (such as CreateRectRgn and CreatePolygonRgn) only include the interior of the shape; the shape’s outline is excluded from the region. This means that any point on a line between two sequential vertices is not included in the region. If you were to call PtInRegion for such a point， it would return zero as the result [ 15 ] 。 根据MSDN的注释所有顶点以及边界上的整数像素点都不在多边形区域内，亦即图10(b)中的值都应该是0，但是事实并非如此。 根据上述交点取整规则和水平线的处理，扫描线y = 0.5与顶点ps[ 0 ]相交，同时顶点ps[ 0 ]又是右边界上的点，不论将ps[ 0 ]算作一个点还是两个点，顶点ps[ 0 ]都判断为不在多边形内；在处理扫描线y = 8.5时，先将两条水平边界删除，根据下闭上开的原则扫描线y = 8.5与顶点ps[ 2 ]和顶点ps[ 8 ]相交，因为ps[ 2 ]是左边界上的点而ps[ 8 ]是右边界上的点，所有ps[ 2 ]判断为在多边形内，ps[ 8 ]判断为不在多边形内；扫描线y = 10.5、y = 12.5和y = 18.5和扫描线y = 8.5类似；扫描线y = 16.5与左边界上点(5，16)，顶点ps[ 5 ]和右边界上点(16，16)相交，其中ps[ 5 ]算作两个点，在填充(5，16)和ps[ 5 ]之间的像素时(5，16)填充画刷颜色，ps[ 5 ]不填充，但是在填充ps[ 5 ]和(16，16)之间的像素时ps[ 5 ]填充，(16，16)不填充，所以这三个点的用PtInRegion判断将以此返回1，1，0；由于下闭上开原则，扫描线y = 20.5不与ps[ 4 ]、ps[ 6 ]相交，所以这两个顶点不在多边形内。 可以看出MSDN的注释与用PtInRegion进行判断时，边界上的点判断不一致。 在应用程序中，如果想要边界上的整数像素点和所以顶点都显示，建议使用Polygon()，在判断边界上的点(包括顶点)是否在多边形上(内)，建议程序员根据上述规则自己编写程序。附录中是笔者自己编写的判断点是否在多边形内的函数BOOL PtInRegionEx (POINT *ps， int num， POINT p)和BOOL PtInRgn (POINT *ps， int num， POINT p)，这两个函数均可以对区域内和边界上的点返回TRUE，对区域外的点返回FALSE。 需要注意的是在用Polygon()显示相邻多边形时，边界上的像素会相互覆盖而重复绘制(图7(右)中间边界)，而用FillRgn()填充的多边形，相邻多边形中的像素不会相互覆盖(图11)。