• AutoCAD.Net/C#.Net QQ群:193522571 GDI+中的坐标系


    GDI+中的坐标系

    1 什么是坐标系

    坐标系就是确定一组数据位置的标尺。按按照维数分为2维平面坐标系和3维空间坐标系。其实2维坐标系也是z=0的3维坐标系的特例。

    坐标系有三要素,一是原点,二是方向,三是单位大小。如果两个坐标系这三点完全一样,那么这两个坐标系就完全相同。关于坐标系和点的关系,我们可以这么理解:点本身是固定的,但在不同坐标系下的表示是不同的。那么为什么要定义那么多的坐标系呢,答案是为了描述方便。比如描述一个圆,如果把坐标系原点放到圆心,那么对圆的描述就是 x2+y2=r2。而如果原点不在圆心,那么圆描述就成了:(x-x0)2+(y-y0)2=r2

    2 坐标系变换与矩阵运算

    既然可以找到描述形状方便的坐标系,那么问题也来了。比如要同时描述两个形状,如两个圆,而且这两个圆是有相对位置的,比如是自行车的两个轮子。

    尽管两个圆各自在自己的坐标系里都能很方便的描述,但是要建立两者之间的关系时,却遇到了麻烦。因为要计算两个圆的位置关系,必须把两个圆放到同一个坐标系下描述才行。所以就引出了坐标系变换的概念。在此例中,可以把第二个圆也放到第一个坐标系下描述,方法就是把第二个坐标系放到第一个坐标系中合适的位置(两个坐标系的关系),然后根据两个坐标系的关系,推算出第二个圆在第一个坐标系中的描述。

    这种方式对于CAD中的任务分隔特别重要,比如做汽车设计的公司,可以把不同的部件分配给不同的人来做。设计人员接到任务后,自由选择合适的坐标系来描述负责的部件。等所有部件设计完成以后,再把所有的部件转换的整车坐标系上。坐标系间的转换(2维和3维)是非常有规律的,有数学基础的人可以自己推导公式,没有数学基础的也没有关系,各种图形库都已经把坐标系变换公式做成了函数API供程序调用。比如OpenGL提供了三维坐标系间的各种变换API,GDI+则提供了2维坐标的变换API。需要了解的是,坐标系间的变换,一般是通过矩阵运算完成的,感兴趣的读者可以参考任何讲解OpenGL坐标变换算法的书籍,重要的是矩阵运算可以通过硬件流水线完成,这就是图形显示中的显卡硬件加速的一部分。当然矩阵运算不光应用于坐标系转换,还广泛运用于其他计算领域,因此有人提出了用GPU代替CPU来进行大规模科学计算的方案。

    3 GDI+中的三种坐标系

    作为Windows中图形显示的关键部件,GDI+代表了Windows下2维图形API。三维则是D3D的领域了。图形API要提供的函数大概是两类,一是绘图函数,二是坐标系转换函数。GDI+提供了很多绘图函数,如DrawRectangle,DrawEclipse,DrawString等等。所有这些函数中都需要位置或大小参数,对于这些参数含义的理解是很重要的。

    3.1 调用者自定义坐标系(world)

    一是参数的单位是什么?位置参数的坐标系是什么?答案很有意思:不确定。因为这些东西有调用者自由确定。那么GDI+怎么根据这些不确定的参数绘制图形呢?答案是调用者要提供自己定义的坐标系和PAGE坐标系的关系。

    3.2 Page坐标系

    Page坐标系附属在某一个窗口或控件上,是一个固定的坐标系,原点位于窗口的左上角,x轴方向向右,y轴方向向下。单位为cm,inch或pixel,根据实际情况设定。GDI+提供了Page坐标系和World坐标系间的转换API。含义是把world坐标系放到Page坐标系合适的位置。回到前面讲过的汽车分部件设计的例子,此处Page坐标系就是最后的整车坐标系,GID+提供的就是把各个部件(GDI+绘制函数绘制的图形)连同其坐标系一起放到整车(Page)坐标系里。

    这是很合理的方式。在利用GDI+作图时也要按照这种思路来做。具体说来,先把整个图形分解成各个小的图形,在画某一个小的图形时不要考虑它最终在Page坐标系的位置,只要按照你自己设想的坐标系来调用GDI+的绘图函数就可以了。

    当所有的图形都绘制完毕后,在把这些小的图形统统放到Page坐标系里。具体就是,调用绘制小图形的代码之前调用GDI+的xxxTransform()系列函数把小图形的建模坐标系放置到Page坐标系里,在绘制小图形的代码之后,调用ResetTransform()。

    讲到这里,也许大家会有疑问了,GDI+最后是如何把Page坐标系的图形绘制到屏幕上的呢,这就是显示器的Device坐标系。

    3.3Device坐标系

    对于Page坐标系和Device坐标系的转换,应用程序员不需要了解了,GDI+已经把这部分隐藏了。

    4 GDI+中坐标系的转换实例

    4.1题目

    利用GDI+绘制如下图形:

    4.2 分析

    仔细看上面的图形,不难发现,此图形有6部分组成:头,左臂,右臂,身体,左腿,右腿。分别把各个部分分给6个设计师去建模,然后把各个模型连同其建模坐标系一起放到到Page坐标系中。如下图:

    4.3代码

            private PointF pHead;

            private PointF pBody;

            private PointF pLeftArm;

            private PointF pRightArm;

            private PointF pLeftLeg;

            private PointF pRightLeg;

     

            private SizeF sHead = new SizeF(30, 30);    //头大小30cm

            private SizeF sBody = new SizeF(50, 70);    //身体大小

            private SizeF sArm = new SizeF(10, 60);     //胳膊大小

            private SizeF sLeg = new SizeF(20, 70);     //腿大小

     

               

            public void DrawHead(PaintEventArgs e)

            {

                e.Graphics.DrawEllipse(Pens.Red, -sHead.Width / 2.0f, -sHead.Height / 2.0f, sHead.Width, sHead.Height);

            }

            public void DrawBody(PaintEventArgs e)

            {

                e.Graphics.DrawRectangle(Pens.Black, 0, 0, sBody.Width, sBody.Height);

            }

            public void DrawLeftArm(PaintEventArgs e)

            {

                e.Graphics.DrawRectangle(Pens.Black, 0, 0, sArm.Width, sArm.Height);

            }

            public void DrawRightArm(PaintEventArgs e)

            {

                e.Graphics.DrawRectangle(Pens.Black, 0, 0, sArm.Height, sArm.Width);

            }

            public void DrawLeftLeg(PaintEventArgs e)

            {

                e.Graphics.DrawRectangle(Pens.Black, 0, 0, sLeg.Width, sLeg.Height);

            }

            public void DrawRightLeg(PaintEventArgs e)

            {

                e.Graphics.DrawRectangle(Pens.Black, 0, 0, sLeg.Height, sLeg.Width);

            }

            private void Form1_Paint(object sender, PaintEventArgs e)

            {

                pHead = new PointF(this.Width / 2.0f, 100f);           //放置头坐标系

                e.Graphics.TranslateTransform(pHead.X, pHead.Y);

                DrawHead(e);                                     //调用负责头建模的代码

                e.Graphics.ResetTransform();                         //重置矩阵

     

                pBody = new PointF(pHead.X - sBody.Width/2.0f, pHead.Y+sHead.Height/2.0f);

                e.Graphics.TranslateTransform(pBody.X, pBody.Y);

                DrawBody(e);

                e.Graphics.ResetTransform();

     

                pLeftArm = pBody;

                e.Graphics.TranslateTransform(pLeftArm.X, pLeftArm.Y);

                e.Graphics.RotateTransform(45);

                DrawLeftArm(e);

                e.Graphics.ResetTransform();

     

                pRightArm = new PointF(pBody.X + sBody.Width, pBody.Y);

                e.Graphics.TranslateTransform(pRightArm.X, pRightArm.Y);

                e.Graphics.RotateTransform(45);

                DrawRightArm(e);

                e.Graphics.ResetTransform();

     

                pLeftLeg = new PointF(pBody.X, pBody.Y + sBody.Height);

                e.Graphics.TranslateTransform(pLeftLeg.X, pLeftLeg.Y);

                e.Graphics.RotateTransform(45);

                DrawLeftLeg(e);

                e.Graphics.ResetTransform();

     

                pRightLeg = new PointF(pBody.X + sBody.Width, pBody.Y + sBody.Height);

                e.Graphics.TranslateTransform(pRightLeg.X, pRightLeg.Y);

                e.Graphics.RotateTransform(45);

                DrawRightLeg(e);

                e.Graphics.ResetTransform();

            }

        }

    5 MSDN中的几个不妥

    由于GDI存在的时间很长,而GDI+诞生后为了兼容,一部分函数采用了与GDI相同的名称,甚至参数,参数的含义在MSDN中也按照原来的解释,导致了一些容易让读者误解的地方。

    5.1绘图函数中关于左上角upper-left corner的描述

    我们知道,GDI+的绘图函数使用的坐标系是由建模人员随意定义的,以函数
    publicvoid DrawRectangle(
          Pen pen,
    float x,
    float y,
    float width,
    float height
    )

    为例,MSDN中对x,y解释如下:

    x

    Type: System. Single

    The x-coordinate of the upper-left corner of the rectangle to draw.

    y

    Type: System. Single

    The y-coordinate of the upper-left corner of the rectangle to draw.

    然而此处的x,y真的是矩形左上角坐标吗?换句话说,左上是从什么角度看的。如下图:

    在A1坐标系中,DrawRectange()函数中的x,y确实代表了矩形的左上角。A2和A3是A1经过旋转以后得到的坐标系,此时x,y在建模者看来显然是右下角和左下角。而对于更常见的笛卡尔右手坐标系B来说,(x,y)也不是左上角。左上角的概念仅在Page坐标系中成立。关于(x,y)的正确解释应该是:

    矩形四个角中,x,y值都最小的那个角的坐标。

    5.2 MatrixOrder问题

    5.2.1不带MatrixOrder参数的变换函数顺序执行的理解

    前面提到过其实对坐标系的转换就是矩阵的乘法运算。这里面涉及到了变换的顺序和矩阵乘法的顺序的问题。讲解之前一定要确立坐标系变换的视角:把建模坐标系放置到Page坐标系中,从Page坐标系一步步变化成最终坐标系。

    可以通过两步完成:

    (1)平移至虚线位置;

    TranslateTransform(0, dy);

    (2)旋转一定角度。

    RotateTransform(45);

    如果我们把(1)(2)顺序反过来,那么最终的结果如下图:

    所以说先平移、旋转的顺序很重要。当前的操作是在前一步完成之后的新坐标系中进行的。对应于矩阵算法相当于左乘。

    5.2.2 带MatrixOrder参数的函数的理解

    publicvoid TranslateTransform(
    float dx,
    float dy,
    MatrixOrder order
    )

    在MSDN中对于MatrixOrder解释为:

      

    其实际效果是,Prepend相当于无此参数的版本,也就是说此次调用是在前面操作结果的基础上操作的;相当于矩阵左乘。

    Append相当于此次操作先于前面的操作起作用,也就是先进行当前的操作,在此基础上进行此次调用前面的操作。相当于矩阵右乘。在OpenGL中都是采用Append模式的。

    如此看来,上述英文解释正好相反了。这个不能说是错,可能是从不同的视角来看问题,我们的视角是:从Page坐标系一步步变化成最终坐标系。

    5.3 Graphics.Transform属性

    这个属性比较特殊。MSDN的解释为:

    获取或设置此Graphics的几何世界变换的副本。

    获取的是Graphics变换矩阵的副本,而不是变换矩阵本身,也就是每次获取时,新建一个和变换矩阵成员值相同的矩阵对象,并返回。

    问题是设置。设置的不是副本,而是Graphics变换矩阵本身。

    内部实现伪代码

    class Graphics

    {

    private Matrix transform;

    public Matrix Transform

    {

    get { return transform.Clone(); }

    set { transform=value; }

    }

    }

    如果要通过矩阵乘法进行坐标转换,那么代码如下:

    e.Graphics.Transform = Graphics.Transform.Multiply(newMatrix(....))

    6 变换函数不能解决的问题

    GDI+提供了平移、旋转、缩放等函数供我们调用,来把Page坐标系一步步改造成最终的建模坐标系在Page中的表现形态。对于绝大多数情况,这些函数足够了,但是看如下:

    无论怎么平移和旋转,Page坐标系也无法编程上面红色显示的坐标系。这是因为Page坐标系属于左手坐标系,而红色所示是右手坐标系。在数学上,大多数情况使用的都是右手坐标系,模型也是建立在右手坐标系上,那么如何把Page转化为右手坐标系呢。也就是把y轴反向。答案是通过直接操纵Graphics的变换矩阵。

     

    e.Graphics.Transform = e.Graphics.Transform.Multiply(newMatrix(1, 0, 0, -1, 0, 0));//y反向

    上述代码就实现了y轴反向的目的。

    如果你对变换矩阵很了解,直接操作矩阵左右乘法,与调用相关的GDI+变换函数功效完全相同,可以实现任意变换。

    (后注:Y轴反方向可以通过g.ScaleTransform(1, -1)完成)

  • 相关阅读:
    网络编程学习笔记:Socket编程
    C# url 路径转换 相对路径 转换为 绝对路径
    利用pycharm运行scrapy以及scrapy的配置
    基于scrapy的分布式爬虫抓取新浪微博个人信息和微博内容存入MySQL
    LeetCode 1. Two Sum
    【转载】C#异常Retry通用类
    【转载】WebDriver(C#)之十点使用心得
    c#值类型和引用类型
    percona5.7 源码安装
    android UI进阶之用ViewPager实现欢迎引导页面[转]
  • 原文地址:https://www.cnblogs.com/swtool/p/14783653.html
Copyright © 2020-2023  润新知