点在多边形内部算法比较
----by wangsh
判断点在多边形内外是计算机图形学的最基本算法,在计算机图形处理、模式识别、 CAD 、科学计算可视化以及 GIS 中有着广泛的应用。判断点在多边形内外的算法有主要有定向射线法、角度法、数学公式计算法和网格索引法等方法。角度法要使用复杂的三角运算,计算量大;在工程上应用最多的是定向射线法,这种方法简单、可靠,但其难以处理对边界点及边界与射线共线等特殊情况的处理。
常见的算法是射线法。
该算法顾名思义,即从给定点引一条射线,计算出该点与多边形交点的个数。若与多边形各边交点为偶数个,则在多边形外,否则就在多边形内。算法需要考虑一些边界条件:射线若正好通过多边形的顶点,射线与多边形的边重合等。若射线穿过多边形的顶点时,若共享顶点的两边在射线的同一侧,则交点计数为 2 ,否则为 1 。具体计数时,当一条边的两个端点 y 值都大于 y 0 ,即边处于上方时,计数加 1 ,否则不加。当射线与多边形边重合时,可以判断其重合边的前一节点和后一节点,若为与射线同一侧,计数为 0 ,否则为 1 。通过以上的补充原则,我们可以正确的判断点位于多边形内的测试。
另外一种在图形学中常见算法为夹角和判断法
假设某平面上有点 P 0 和多边形 P 1 P 2 P 3 P 4 P 5 。将点 P 0 分别与 Pi 相连,构成向量 V i = P i – P 0 。假设角 ∠ P i P 0 P i+1 = аi 如果 ∑аi = 0 ,则点 P 0 在多边形之外,如果 ∑аi = 2 π ,则点 P 0 在多边形之内。可通过下列公式计算
аi = arcos((V i ·V i+1 )/(|V i ||V i+1 |))
这个公式计算量比较大。它涉及三个点积、两个开平方和一个反三角函数计算。另外,还必须判断角度的方向,一般是通过计算两个向量的叉积的符号来判断。实际上,用反正切函数求 аi ,计算更简单。令 S i = V i xV i+1 ,C i = V i ·V i+1 则 tg( аi ) = S i / C i , 所以 аi = arctg ( S i / C i )且 аi 的符号即代表角度的方向。
还有针对凸多边形的叉积判断法,假设判断点为 P 0 。多边形顶点按顺序排列为 P 1 ,P 2 , ··· ,P n 。令 V i =P i – P 0 , i =1 ,2 , ··· n , V n+1 = V 1 。 那么, P 0 在多边形内的充分必要条件是叉积 V i+1 x V i (i =1 ,2 , ··· n ) 的符号都相同。
下面比较了 GIS 相关开源软件的判断点在多边形内的算法研究,其中 GEOS 采用的是扫描线算法、 CGAL 、 Grass 、 Ogre 中的方法、 ACM Graphics Gems Repositor 中的方法、某 Blog 中算法、还有按照数学公式向量计算的等多个版本。
首先是 CGAL 的代码:
代码位置为 CGAL-3.5/include/CGAL/Polygon_2/Polygon_2_algorithms_impl.h 中:
template < class ForwardIterator, class Point, class Traits >
Bounded_side bounded_side_2(ForwardIterator first,
ForwardIterator last,
const Point & point,
const Traits & traits)
... {
ForwardIterator current = first;
if (current == last) return ON_UNBOUNDED_SIDE;
ForwardIterator next = current; ++ next;
if (next == last) return ON_UNBOUNDED_SIDE;
typename Traits::Compare_x_2 compare_x_2 = traits.compare_x_2_object();
typename Traits::Compare_y_2 compare_y_2 = traits.compare_y_2_object();
typename Traits::Orientation_2 orientation_2 = traits.orientation_2_object();
bool IsInside = false ;
Comparison_result cur_y_comp_res = compare_y_2(*current, point);
do // check if the segment (current,next) intersects
// the ray { (t,point.y()) | t >= point.x() }
... {
Comparison_result next_y_comp_res = compare_y_2(*next, point);
switch (cur_y_comp_res) ... {
case SMALLER:
switch (next_y_comp_res) ... {
case SMALLER:
break ;
case EQUAL:
switch (compare_x_2(point, *next)) ... {
case SMALLER: IsInside = !IsInside; break ;
case EQUAL: return ON_BOUNDARY;
case LARGER: break ;
}
break ;
case LARGER:
switch (i_polygon::which_side_in_slab(point, *current, *next,
orientation_2, compare_x_2)) ... {
case -1: IsInside = !IsInside; break ;
case 0: return ON_BOUNDARY;
}
break ;
}
break ;
case EQUAL:
switch (next_y_comp_res) ... {
case SMALLER:
switch (compare_x_2(point, *current)) ... {
case SMALLER: IsInside = !IsInside; break ;
case EQUAL: return ON_BOUNDARY;
case LARGER: break ;
}
break ;
case EQUAL:
switch (compare_x_2(point, *current)) ... {
case SMALLER:
if (compare_x_2(point, *next) != SMALLER)
return ON_BOUNDARY;
break ;
case EQUAL: return ON_BOUNDARY;
case LARGER:
if (compare_x_2(point, *next) != LARGER)
return ON_BOUNDARY;
break ;
}
break ;
case LARGER:
if (compare_x_2(point, *current) == EQUAL) ... {
return ON_BOUNDARY;
}
break ;
}
break ;
case LARGER:
switch (next_y_comp_res) ... {
case SMALLER:
switch (i_polygon::which_side_in_slab(point, *next, *current,
orientation_2, compare_x_2)) ... {
case -1: IsInside = !IsInside; break ;
case 0: return ON_BOUNDARY;
}
break ;
case EQUAL:
if (compare_x_2(point, *next) == EQUAL) ... {
return ON_BOUNDARY;
}
break ;
case LARGER:
break ;
}
break ;
}
current = next;
cur_y_comp_res = next_y_comp_res;
++next;
if (next == last) next = first;
}
while (current != first);
return IsInside ? ON_BOUNDED_SIDE : ON_UNBOUNDED_SIDE;
}
上面是 CGAL 中的相关代码,值得注意的是两点:
首先,边界条件处理。每条线段都认为是前闭后开,这样就避免了当射线通过顶点,重复统计的问题, 另外,如果射线和直线重合,直接忽略(其实这时点可能在多边形边界上,直接 return )。
其次,程序中判断射线是否和多边形各边是否相交的方 法:用了 N 个巨大的 switch 语句,仅通过比较边的顶点和射线端点的坐标大小关系,判断射线是否与边是否相交。这样代码虽然写起来比较麻烦,但是比之采 用了一般的求两条直线交点的方法(二元二次方程组),避免了比较慢的一次乘法和更加慢的一次除法(这还是在这条射线是水平放置或垂直放置的时候)。另外还 有一些小优化,比如说多边形边由于首尾相接,上次计算的顶点关系( next_y_comp_res )可以再次利用。
这里采用 n 个巨大的 switch 嵌套,估计会对算法效率有影响。
下面是 GRASS 相关代码:
代码位置: grass-6.3.0/lib/vector/Vlib poly.c
int Vect_point_in_area_outer_ring ( double X, double Y, struct Map_info *Map, int area)
... {
static int first = 1;
int n_intersects, inter;
int i, line;
static struct line_pnts *Points;
struct Plus_head *Plus;
P_LINE *Line;
P_AREA *Area;
G_debug ( 3 , "Vect_point_in_area_outer_ring(): x = %f y = %f area = %d", X, Y, area );
if (first == 1) ... {
Points = Vect_new_line_struct();
first = 0;
}
Plus = &(Map -> plus);
Area = Plus -> Area[area];
/**/ /* First it must be in box */
if ( X < Area-> W || X > Area-> E || Y > Area-> N || Y < Area-> S ) return 0;
n_intersects = 0;
for (i = 0; i < Area-> n_lines; i++) ... {
line = abs(Area-> lines[i]);
G_debug ( 3 , " line[%d] = %d", i, line );
Line = Plus-> Line[line];
/**/ /* dont check lines that obviously do not intersect with test ray */
if ((Line ->N < Y) || (Line ->S > Y) || (Line ->E < X)) continue ;
Vect_read_line (Map, Points, NULL, line );
inter = segments_x_ray ( X, Y, Points);
G_debug ( 3 , " inter = %d", inter );
if ( inter == -1 ) return 2;
n_intersects += inter;
G_debug ( 3 , " n_intersects = %d", n_intersects );
}
if (n_intersects % 2)
return 1;
else
return 0;
}
int segments_x_ray ( double X, double Y, struct line_pnts *Points)
... {
double x1, x2, y1, y2;
double x_inter;
int n_intersects;
int n;
G_debug ( 3, "segments_x_ray(): x = %f y = %f n_points = %d", X, Y, Points-> n_points );
/**/ /* Follow the ray from X,Y along positive x and find number of intersections.
* Coordinates exactly on ray are considered to be slightly above. */
n_intersects = 0;
for ( n = 0; n < Points-> n_points-1; n++ ) ... {
x1 = Points -> x[n];
y1 = Points -> y[n];
x2 = Points -> x[n+1];
y2 = Points -> y[n+1];
G_debug ( 3 , "X = %f Y = %f x1 = %f y1 = %f x2 = %f y2 = %f", X, Y, x1, y1, x2, y2 );
/**/ /* I know, it should be possible to do that with less conditions, but it should be
* enough readable also! */
/**/ /* segment left from X -> no intersection */
if ( x1 < X && x2 < X ) continue ;
/**/ /* point on vertex */
if ( (x1 == X && y1 == Y) || (x2 == X && y2 == Y) ) return -1;
/**/ /* on vertical boundary */
if ( (x1 == x2 && x1 == X) && ( (y1 <= Y && y2 >= Y) || (y1 >= Y && y2 <= Y) ) ) return -1;
/**/ /* on horizontal boundary */
if ( (y1 == y2 && y1 == Y) && ( (x1 <= X && x2 >= X) || (x1 >= X && x2 <= X) ) ) return -1;
/**/ /* segment on ray (X is not important) */
if ( y1 == Y && y2 == Y ) continue ;
/**/ /* segment above (X is not important) */
if ( y1 > Y && y2 > Y ) continue ;
/**/ /* segment below (X is not important) */
if ( y1 < Y && y2 < Y ) continue ;
/**/ /* one end on Y second above (X is not important) */
if ( (y1 == Y && y2 > Y) || (y2 == Y && y1 > Y) ) continue ;
/**/ /* For following cases we know that at least one of x1 and x2 is >= X */
/**/ /* one end of segment on Y second below Y */
if ( y1 == Y && y2 < Y) ... {
if ( x1 >= X) /**/ /* x of the end on the ray is >= X */
n_intersects++;
continue ;
}
if ( y2 == Y && y1 < Y ) ... {
if ( x2 >= X)
n_intersects++;
continue ;
}
/**/ /* one end of segment above Y second below Y */
if ( (y1 < Y && y2 > Y) || (y1 > Y && y2 < Y) ) ... {
if ( x1 >= X && x2 >= X ) ... {
n_intersects++;
continue ;
}
/**/ /* now either x1 < X && x2 > X or x1 > X && x2 < X -> calculate intersection */
x_inter = dig_x_intersect ( x1, x2, y1, y2, Y);
G_debug ( 3, "x_inter = %f", x_inter );
if ( x_inter == X )
return 1;
else if (x_inter > X)
n_intersects ++;
continue ; /**/ /* would not be necessary, just to check, see below */
}
/**/ /* should not be reached (one condition is not necessary, but it is may be better readable
* and it is a check) */
G_warning ( " segments_x_ray() conditions failed:" );
G_warning ( "X = %f Y = %f x1 = %f y1 = %f x2 = %f y2 = %f", X, Y, x1, y1, x2, y2 );
}
return n_intersects;
}
通过分析 Segments_x_ray 中的代码,可以看出算法核心思想是选用射线法判断点的位置。不过这次采用无数 if 组成的大分支结构,看上去没有 CGAL 中的 switch 清晰.但是主函数 Vect_point_in_area_outer_ring 中的两处优化值得一提,首先就是判断点是否在多边形的外接矩形内,这一个不起眼的筛选在很多时候能极大的提高效率(这就是跨立实验,用来排除大量大概率发生情况,这个非常关键),因为判断点在不在矩形内这个操作的开销非常小,而一般来说点不在多边形内的几率比较高。其次就是在选取多边形边界线段的时候,只选择有可能和射线相交的线段,这样对于复杂多边形,性能会进一步提高。
下面是 GEOS 的扫描算法:
代码位置: geos31/source/algorithm/CGAlgorithms.cpp
/*public static*/
bool
CGAlgorithms ::isPointInRing (const Coordinate & p ,
const Coordinate ::ConstVect & ring )
{
double xInt ; // x intersection of segment with ray
int crossings = 0; // number of segment/ray crossings
double x1 ; // translated coordinates
double y1 ;
double x2 ;
double y2 ;
/*
* For each segment l = (i-1, i), see if it crosses ray from
* test point in positive x direction.
*/
for (size_t i =1, nPts =ring .size (); i <nPts ; ++i )
{
const Coordinate *p1 =ring [i ];
const Coordinate *p2 =ring [i -1];
x1 = p1 ->x - p .x ;
y1 = p1 ->y - p .y ;
x2 = p2 ->x - p .x ;
y2 = p2 ->y - p .y ;
if (((y1 > 0) && (y2 <= 0)) ||
((y2 > 0) && (y1 <= 0)))
{
/*
* segment straddles x axis, so compute intersection.
*/
xInt = RobustDeterminant ::signOfDet2x2 (x1 , y1 , x2 , y2 )
/ (y2 - y1 );
/*
* crosses ray if strictly positive intersection.
*/
if (0.0 < xInt ) crossings ++;
}
}
/*
* p is inside if number of crossings is odd.
*/
if ((crossings % 2) == 1) return true ;
return false ;
}
还有一个调用接口为:
代码位置: geos31/source/algorithm/RobustDeterminant.cpp
int RobustDeterminant ::signOfDet2x2 (double x1 ,double y1 ,double x2 ,double y2 )
{
// returns -1 if the determinant is negative,
// returns 1 if the determinant is positive,
// retunrs 0 if the determinant is null.
int sign =1;
double swap ;
double k ;
long count =0;
/*
* testing null entries
*/
if ((x1 ==0.0) || (y2 ==0.0)) {
if ((y1 ==0.0) || (x2 ==0.0)) {
return 0;
} else if (y1 >0) {
if (x2 >0) {
return -sign ;
} else {
return sign ;
}
} else {
if (x2 >0) {
return sign ;
} else {
return -sign ;
}
}
}
if ((y1 ==0.0) || (x2 ==0.0)) {
if (y2 >0) {
if (x1 >0) {
return sign ;
} else {
return -sign ;
}
} else {
if (x1 >0) {
return -sign ;
} else {
return sign ;
}
}
}
/*
* making y coordinates positive and permuting the entries
* so that y2 is the biggest one
*/
if (0.0<y1 ) {
if (0.0<y2 ) {
if (y1 <=y2 ) {
;
} else {
sign =-sign ;
swap =x1 ;
x1 =x2 ;
x2 =swap ;
swap =y1 ;
y1 =y2 ;
y2 =swap ;
}
} else {
if (y1 <=-y2 ) {
sign =-sign ;
x2 =-x2 ;
y2 =-y2 ;
} else {
swap =x1 ;
x1 =-x2 ;
x2 =swap ;
swap =y1 ;
y1 =-y2 ;
y2 =swap ;
}
}
} else {
if (0.0<y2 ) {
if (-y1 <=y2 ) {
sign =-sign ;
x1 =-x1 ;
y1 =-y1 ;
} else {
swap =-x1 ;
x1 =x2 ;
x2 =swap ;
swap =-y1 ;
y1 =y2 ;
y2 =swap ;
}
} else {
if (y1 >=y2 ) {
x1 =-x1 ;
y1 =-y1 ;
x2 =-x2 ;
y2 =-y2 ;
} else {
sign =-sign ;
swap =-x1 ;
x1 =-x2 ;
x2 =swap ;
swap =-y1 ;
y1 =-y2 ;
y2 =swap ;
}
}
}
/*
* making x coordinates positive
*/
/*
* if |x2|<|x1| one can conclude
*/
if (0.0<x1 ) {
if (0.0<x2 ) {
if (x1 <= x2 ) {
;
} else {
return sign ;
}
} else {
return sign ;
}
} else {
if (0.0<x2 ) {
return -sign ;
} else {
if (x1 >= x2 ) {
sign =-sign ;
x1 =-x1 ;
x2 =-x2 ;
} else {
return -sign ;
}
}
}
/*
* all entries strictly positive x1 <= x2 and y1 <= y2
*/
while (true ) {
count =count +1;
k =std ::floor (x2 /x1 );
x2 =x2 -k *x1 ;
y2 =y2 -k *y1 ;
/*
* testing if R (new U2) is in U1 rectangle
*/
if (y2 <0.0) {
return -sign ;
}
if (y2 >y1 ) {
return sign ;
}
/*
* finding R'
*/
if (x1 >x2 +x2 ) {
if (y1 <y2 +y2 ) {
return sign ;
}
} else {
if (y1 >y2 +y2 ) {
return -sign ;
} else {
x2 =x1 -x2 ;
y2 =y1 -y2 ;
sign =-sign ;
}
}
if (y2 ==0.0) {
if (x2 ==0.0) {
return 0;
} else {
return -sign ;
}
}
if (x2 ==0.0) {
return sign ;
}
/*
* exchange 1 and 2 role.
*/
k =std ::floor (x1 /x2 );
x1 =x1 -k *x2 ;
y1 =y1 -k *y2 ;
/*
* testing if R (new U1) is in U2 rectangle
*/
if (y1 <0.0) {
return sign ;
}
if (y1 >y2 ) {
return -sign ;
}
/*
* finding R'
*/
if (x2 >x1 +x1 ) {
if (y2 <y1 +y1 ) {
return -sign ;
}
} else {
if (y2 >y1 +y1 ) {
return sign ;
} else {
x1 =x2 -x1 ;
y1 =y2 -y1 ;
sign =-sign ;
}
}
if (y1 ==0.0) {
if (x1 ==0.0) {
return 0;
} else {
return sign ;
}
}
if (x1 ==0.0) {
return -sign ;
}
}
}
利用数学公式的代码,摘自某 Blog :
int insidepolygon( int vcount,POINT Polygon[],POINT q)
... {
int c = 0,i,n;
LINESEG l1,l2;
bool bintersect_a,bonline1,bonline2,bonline3;
double r1,r2;
l1.s=q;
l1.e=q;
l1.e.x= double (INF);
n =vcount;
for (i =0;i<vcount;i ++ )
... {
l2.s = Polygon[i];
l2.e = Polygon[(i+1)%n];
if (online(l2,q)) return 1; // 如果点在边上,返回 1
if ((bintersect_a=intersect_A(l1,l2))|| // 相交且不在端点
(
(bonline1=online(l1,Polygon[(i+1)%n])) && // 第二个端点在射线上
(
(!(bonline2=online(l1,Polygon[(i+2)%n]))) && /**/ /* 前一个端点和后一个端点在射线两侧 */
((r1 =multiply(Polygon[i],Polygon[(i+1)%n],l1.s) *multiply(Polygon[(i+1)%n],Polygon[(i+2)%n],l1.s)) > 0) ||
(bonline3=online(l1,Polygon[(i+2)%n])) && /**/ /* 下一条边是水平线,前一个端点和后一个端点在射线两侧 */
((r2 =multiply(Polygon[i],Polygon[(i+2)%n],l1.s) * multiply(Polygon[(i+2)%n],
Polygon[(i+3)%n],l1.s)) > 0)
)))
c++;
}
if (c % 2 == 1)
return 0;
else
return 2;
}
// ( 线段 u 和 v 相交 )&&( 交点不是双方的端点 ) 时返回 true
bool intersect_A(LINESEG u,LINESEG v)
... {
return ((intersect(u,v))&&
( !online(u,v.s))&&
(!online(u,v.e))&&
(!online(v,u.e))&&
(!online(v,u.s)));
}
// 如果线段 u 和 v 相交 ( 包括相交在端点处 ) 时,返回 true
bool intersect(LINESEG u,LINESEG v)
... {
return ((max(u.s.x,u.e.x)>= min(v.s.x,v.e.x))&& // 排斥实验
(max(v.s.x,v.e.x)>= min(u.s.x,u.e.x)) &&
(max(u.s.y,u.e.y)>=min(v.s.y,v.e.y))&&
(max(v.s.y,v.e.y)>=min(u.s.y,u.e.y))&&
(multiply(v.s,u.e,u.s)*multiply(u.e,v.e,u.s)>= 0)&& // 跨立实验
(multiply(u.s,v.e,v.s)*multiply(v.e,u.e,v.s)>= 0 ));
}
/**/ /* 判断点 p 是否在线段 l 上,条件: (p 在线段 l 所在的直线 上 )&& ( 点 p 在以线段 l 为对角线的矩形内 ) */
bool online(LINESEG l,POINT p)
... {
return ((multiply(l.e,p,l.s)==0)
&&( ( (p.x -l.s.x)*(p.x-l.e.x)<= 0 )&& ( (p.y-l.s.y) *(p.y-l.e.y)<= 0 ) ) );
}
double multiply(POINT sp,POINT ep,POINT op)
... {
return ((sp.x - op.x)*(ep.y-op.y)-(ep.x-op.x)*(sp.y-op.y));
}
从数学上来说,这段代码比上面两段代码都好看,采用向量积的形式判断三点关系,用这个运算为基础,推出 online 和 intersect 操作,它们组合成 point in area 的代码.虽然这里也用了射线法,但是其中无数的乘法运算和未作任何筛选就进行的判断,使这段程序的效率几乎是三段程序中最低的。
还有 ogre1.6 代码:
代码位置:
ogre/OgreMain/src/OgrePolygon.cpp
//-----------------------------------------------------------------------
bool Polygon::isPointInside(const Vector3& point) const
{
// sum the angles
Real anglesum = 0;
size_t n = getVertexCount();
for (size_t i = 0; i < n; i++)
{
const Vector3& p1 = getVertex(i);
const Vector3& p2 = getVertex((i + 1) % n);
Vector3 v1 = p1 - point;
Vector3 v2 = p2 - point;
Real len1 = v1.length();
Real len2 = v2.length();
if (Math::RealEqual(len1 * len2, 0.0f, 1e-4f))
{
// We are on a vertex so consider this inside
return true;
}
else
{
Real costheta = v1.dotProduct(v2) / (len1 * len2);
anglesum += acos(costheta);
}
}
// result should be 2*PI if point is inside poly
return Math::RealEqual(anglesum, Math::TWO_PI, 1e-4f);
}
该方法采用的是计算该点与多边形边的角之和是否等于 360 度,难道做过测试该算法效率优于射线法?
下面是 Graphics Gems 的方法这里仅仅列出方法链接,供读者去学习参考;
http://webdocs.cs.ualberta.ca/~graphics/books/GraphicsGems/gemsiv/ptpoly_haines/
和 http://webdocs.cs.ualberta.ca/~graphics/books/GraphicsGems/gemsiv/ptpoly_weiler/
其中,前者的链接是 Graphics Gems 第四卷 Eric Haines 的 Point in Polygon Strategies 的源码,后者链接是 Graphics Gems 第四卷中 Kevin Weiler 的 An Incremental Angle Point in Polygon Test 的源码。相比而言,前者的代码更全面一些,做了几个算法的比较,值得大家学习,揣摩。
另外在 cheungmine 的 Blog 中用 C 语言实现了点在多边形内算法,但是源码中对于点正好落在多边形边上的极端情形,有可能得出 2 种不同的结果没有做进一步的处理(源码见参考资料 11 )。
通过这些算法代码的学习,可以看出这个最基本的算法其中蕴含了众多的思想,考核了一个合格程序员的理性思维。大家不妨自己实现一个判断点在多边形内算法,进行对比学习。
参考资料
1. GRASS GIS GRASS GIS (Geographic Resources Analysis Support System http://grass.itc.it/
2. CGAL, Computational Geometry Algorithms Library. http://www.cgal.org
3. GEOS Geometry Engine - Open Source http://trac.osgeo.org/geos/
4. Point in Polygon http://blog.csdn.net/Zricepig/archive/2007/03/14/1529393.aspx
5. Point in Polygon Strategies http://erich.realtimerendering.com/ptinpoly/
6. How Reliable Are Practical Point in Polygon Strategies? http://wwwisg.cs.uni-magdeburg.de/ag/pointInPolygonReliability/
7. ftp://ftp-graphics.stanford.edu/pub/Graphics/GraphicsGems/
8. http://webdocs.cs.ualberta.ca/~graphics/books/GraphicsGems/gemsiv/ptpoly_haines/
9. Graphics Gems Repositor http://tog.acm.org/resources/GraphicsGems/
10. http://www.ogre3d.org/ http://ogre.cvs.sourceforge.net/ogre/
11. http://blog.csdn.net/cheungmine/archive/2007/09/24/1798095.aspx
转载请注明:http://blog.csdn.net/wsh6759/article/details/5490951