C# GDI+绘图

Graphics

坐标系

主要介绍两种坐标系Page Coordinates 和 World Coordinates.

Page 页面坐标是控件的坐标系,下称老坐标系。一般地,page坐标系是原点在页面(控件)左上角,x轴竖直向下,y轴水平向右的左手坐标系。

设备坐标系是在其上进行绘制的物理设备(如屏幕,纸张)所使用的坐标系。设备坐标的单位只有像素,而页面坐标单位可以设置。在默认情形下,页面坐标和设备坐标相同

World 世界坐标是自己定义的任意的坐标系,也称用户坐标系,建模坐标系,下称新坐标系。建模坐标可以任意设置。

“世界变换”可以将世界坐标系转换为页面坐标系,而“页面变换”可以将页面坐标转换为设备坐标。一般我们用到的是世界变换。

世界变换的矩阵保存在Graphics.Transform 中。是Matrix(System.Drawing.Drawing2D.Matrix)对象。

坐标系的变换

默认的坐标系的变换是坐标变换。设变换的矩阵为P,新基=老基*P,P是从老坐标系到新坐标系的变换阵。老坐标=P\*新坐标。P即是世界变换的矩阵。

结合性

设从依次进行三个变换P1,P2,P3,那么在默认状态下,结合性为prepend。

新基=老基*P1*P2*P3,老坐标=P1*P2*P3*新坐标

从点变换的角度看:变换后的坐标=P1*P2*P3*变换前的坐标。这种结合性非常不理想, 所以微软又提供了一种结合性append。变换后的坐标=P3*P2*P1*变换前的坐标,这种结合性非常理想,只要将世界坐标系里面点的坐标一步一步变到第四象限(从世界坐标系的角度看)里面的页面坐标系的区域即可。

由于世界坐标系一般为右手系,一般在最后一步加上 g.ScaleTransform(1, -1, System.Drawing.Drawing2D.MatrixOrder.Append) 即可变为左手系。

所以对于变换矩阵(初始变换矩阵为I)来讲,prepend相当于右乘(I*P),append相当于左乘(P*I),当然,我们更喜欢左乘。

为啥右乘叫prepend,左乘叫append?因为微软的坐标是行向量!,所以从我们的角度来看,相当于y'=x'P' ,(以’表示转置,以x,y表示(点)变换前后的对应点的坐标(列)向量)这样来看y'=x'*P1'*P2'*P3',这样来看,确实是在后面称,把线性代数中以列向量表示坐标的左乘叫称append也就不足为奇了。

为什么变换的顺序十分重要?因为矩阵乘法没有交换律。(特殊情形下如果矩阵乘法满足交换律,则称这两个矩阵是可交换的。

Matrix

基于微软喜欢行向量,所以二维仿射变换的齐次矩阵也被转置了一下。微软的Matrix定义如下

Matrix=[[m11 m12 0],
        [m21 m22 0],
        [dx dy 1]]

构造函数

public Matrix(float m11,float m12,float m21,float m22,float dx,float dy)
public Matrix(RectangleF,PointF[])

其中第二种方法是求出将一个矩形变换成平行四边形的变换阵。 矩形由RectangleF定义,平行四边形由左上,右上,左下三个点的数组定义。

Maritx的一些方法与Graphics的相应的Transform对应。矩阵的乘相当于变换的复合。

下面对其中主要的几种方法作简要介绍

public void Scale(float scaleX,float scaleY) //对应 m11,m22
public void Shear(float shearX,float shearY)// m21 m12
public void Translate(float offsetX,float offsetY)//dx dy

全局变换和局部变换

全局变换是应用于给定的Graphics对象绘制的每个项目的变换。

与此相反,局部变换是应用于要绘制的特定项目的变换GraphicsPath。 应用时要先创建GraphicsPath对象,再通过Add…方式在上面作图,可以通过Graphics.DrawPath方法将该对象的所有项目进行绘制,通过Graphics.FillPath方法对GraphicsPath上的所有项目进行填充。

矢量数据的显示

下面用append方式讲解

  1. 将要显示的区域扩大一点,因为在PictureBox边缘的项目容易被挡住而显示不完整。
  2. 将扩大后的显示区域平移到第一象限原点附近。
  3. 将第二步的结果缩放到合适位置。
  4. 反射到第四象限的绘图区域(页面坐标系)

这个实现的顺序是2 1 4 3,将就着看

  public float ViewPort2Page(Graphics g, RectangleF viewport, int w, int h)
        {
            g.ResetTransform();
            g.TranslateTransform((-viewport.X + 0.01f * viewport.Width), (-(viewport.Y + viewport.Height) - 0.01f * viewport.Height), MatrixOrder.Append);
            g.ScaleTransform(1, -1, MatrixOrder.Append);
            float sx = 1.02f * viewport.Width / w, sy = 1.02f * viewport.Height / h;
            float dx = 0, dy = 0;
            if (sx != 0 && sy != 0)
                if (sx < sy)
                {
                    sx = sy;
                    g.ScaleTransform(1 / sy, 1 / sy, MatrixOrder.Append);
                    dx = (w - viewport.Width * 1.01f / sx) / 2f;
                }
                else
                {
                    sy = sx;
                    g.ScaleTransform(1 / sx, 1 / sx, MatrixOrder.Append);
                    dy = (h - viewport.Height * 1.01f / sy) / 2f;
                }
            g.TranslateTransform(dx, dy, MatrixOrder.Append);
            return sx;
        }

Q:为啥要返回一个缩放因子s(这里因为sx=sy,将其统一记为s)?

A: 因为将页面坐标系缩放之后,如果再按默认宽度(1 pixel)画图,可能会显示得非常大,将这个默认宽度乘以s即可得到想要的正常的宽度。

杂项

下面称要显示的区域为视口viewport,其由一个RectangleF 来实现。表示世界坐标系中要显示的区域。

参考资料

  1. 微软MSDN官方文档
  2. 课件GDI+(Graphics类)中的坐标