写给同学的编程建议(续)

接着上一篇讲

方法篇

首先先啰嗦几句。其实编程的学习是一个渐近的过程,不可能一蹴而就,不可能一口就吃个大胖子,就算你一口就吃了个胖子,那也不正常,迟早会出问题。所以大家学习不要太心急,慢慢来,从基础学起,平时多注意积累,每天积累一点新的东西,一段时间下来,你也就成为高手了。我觉得编程就是玩积木,我们平时写的一些小东西,一些零碎的代码,就是那些小积木,我们以后就用这些小积木去拼大图,去拼自己的理想。所以大家平时一定要多注意积累,和学英语一样地去积累,如果太长时间不去写程序,那么你的能力无疑就会退步。我开这么个博客也就是出于这么一个简单的目的,强迫自己每天都要去接收新的东西,也就是要做到博客几乎每天都要更新内容,这样几天可能没什么效果,但是当你坚持下来,时间长了,效果就非常明显。我博客开了有一年多了吧,博客的每天更新就象征我的知识在增长,所以看着文章越来越多,自己也挺欣慰的。

这里就涉及到另一个问题了,如果我们每天都要让自己接受新的东西,那么这些东西从哪里来呢,我们应该到哪里去找这些新的东西呢?书本上的东西肯定都是非常基础的东西,所以不是我们应该去积累的内容,应该集中时间去把它搞好搞透。除了书本,还有网络这一途径,而且我认为网络是最好的一种积累途径。那么我们应该如何在网上找我们需要的东西呢,这是重点。首先,利用好搜索引擎很重要,因为我们需要的绝大部分东西都可以在上面找到。谈到搜索技巧,那又是一个很大的话题了,这里不细说,我只提一点,我们一定要充分利用种搜索商,比如Baidu、Google、Bing、搜狗等,他们的搜索特点都不尽相同,所以当我们用某种搜索无果后,可以去尝试一下其它几种,说不一定会有惊喜。如果实在是没有搜索到,那么我们学校图书馆的数据库资源也是可以利用的。

有时候我实在找不到方向,在网上闲逛的时候也没能找到什么新鲜的好东西,这时候我就会无聊地在网上乱搜,反正没事干嘛。可是每次我乱搜都会搜出什么东西来,比如说我前几天在百度里搜“C# 范型”,然后就找啊找,还找到了不少关于范型的文章。所以当你无聊的时候也不妨去搜搜,搜什么内容不重要,只要是有点相关的就行,然后可以这点一下那点一下,说不一定就点到一个好地方去了。当你真的找到了一个好地方,比如说某个牛人的博客,那么建议你把该网址收藏起来,以后可以常常去那逛逛,看看有没有什么新的东西,我就是这么干的。根据这个说法,当你看完此文章后就应该把我博客收藏了哦,呵呵,开玩笑啦。

技巧篇

我们平时做一些编程作业的时候,多半是写一些有关算法的程序,需要用到非常多的数据,这时对数据的封装就显得非常重要了。一般说来,可以把性质相似的数据封装成一个结构体,比如点的坐标X、Y、Z可以封装成一个结构体(Delphi里面叫记录类型),再比如我们在后方交会里面有三个在键盘上不能直接输入的变量ψ、ω、κ,我们也可以把它封装到一个结构体里面去,方便我们在编写代码的时候输入这三个变量,因为封装后我们只要输入结构体名再按点号,这三个不好输入的符号就会自动地弹出来,如图:

还有一些其它的数据,我们可以选择性的把它设置成全局或函数里面的变量,这得看它的使用范围而定,后面我会用一个例子来说明。

最近学到了一个好的调试技巧。如果你在一个循环里面下断点,观察循环过程中的执行情况,比如说总共有100次循环,但是你并不关心前80次的执行情况,你只关心后面的情况,那么怎么办呢?常规的做法就是在那个位置下好断,运行80次再观察,不过这样做显得太繁琐了。我们下好断点后,可以在断点的那个红点处单击鼠标右键,这时弹出一个菜单,这以通过这里面的功能来设置我们想要的断点效果,如图:

具体怎么设置大家去操作一下就好,非常简单。

实例篇

前面讲了那么多,现在我们通过一个实例来具体讲解。就以我们摄影测量的编程作业为例子,以C#语言,VS2008为开发环境。首先原理大家肯定都知道了,我就不多说,不知道可以去看教材,我这里是严格按照教材的思路来写的。

我们先在VS里面新建一个C#的Windows窗体应用程序,然后在窗体上拖一个ListBox控件,并设置ListBox的布局属性dock为完全填充Fill,也就是占满整个窗体,窗体有多大,ListBox就有多大。设计好后如图所示:

然后我们双击Form1窗体,进入代码编辑,也就是编写窗体创建事件的代码,我们设置一下程序的标题:      this.Text = “单像空间后方交会”;

我们程序采取从文本文件中读取数据的方式,那么需要在程序创建的时候从文本文件中读数据。我们可以把读数据这一功能写成一个函数,再在程序创建的事件中调用该函数,从而实现程序创建时就读取数据这一功能。这里我们先规定文本中的数据格式:点号   控制点坐标X、Y、Z   影像坐标x、y,也就是每行共有六个数据,每两个数据用空格格开。我们先声明几个结构体:

   public struct KZPoint   //地面控制点坐标
    {
        public double x;
        public double y;
        public double z;
    }
    public struct YXPoint //影像点坐标
    {
        public double x;
        public double y;
    }
    public struct IData //三个输入不方便的角度
    {
        public double ψ;
        public double ω;
        public double κ;
    }

再在Form1类里面声明一些必要的全局变量:

        double m, f, x0, y0, Xs, Ys, Zs;//m,f,x0,y0为内方位元素
        int PointNum;                   //控制点的个数
        double LowLimit;                //精度控制
        double a1, a2, a3, b1, b2, b3, c1, c2, c3; //R矩阵各值
        KZPoint[] KZPs;                   //用户输入的已知控制点
        YXPoint[] YXPs;                   //用户输入的已知像点
        IData F3;                         //三个输入不方便的角度
        NNMatrix A, X, L;                 //平差矩阵, X=[dXs,dYs,dZs,dψ,dω,dκ]T; A[2n,6], L[2n,1]
        string splitStr = @"\s{1,}";            //正则表达式的分隔符

好,现在我们来写读取数据的函数

        //===========================从文本文件中读取初始数据,flilename为文本文件名=====================
        public void DataFromText(string filename)
        {
            StreamReader sr = new StreamReader(filename); //声明文件流类,这个我们上篇已讲到
            string text = "";                              //text变量用来临时保存我们读的每一行数据
            int rows = 0;                                  //rows变量表示该文本共有多少非空行 
            while ((text = sr.ReadLine()) != null)
            {
                if (text.Trim() != "")                     //Trim方法用来截取两边的空格,如果为空行,截取后就为空了
                {
                    rows++;                                //如果该行不为空,那么rows就加1
                }
            }
            KZPs = new KZPoint[rows];                      //有多少非空行就有多少个点,声明点坐标数组
            YXPs = new YXPoint[rows];
            PointNum = rows;                               //PointNum为点的个数,声明在全局变量里面
            sr.Close();
            sr = new StreamReader(filename);
            for (int i = 0; i < rows; i++)                 //用for循环来一行一行地读取数据
            {
                text = sr.ReadLine().Trim();
                while (text == "")                         //如果这行为空就一直向下读直到读到不为空的行,也就是忽视空行
                {
                    text = sr.ReadLine().Trim(); 
                }
                Regex reg = new Regex(splitStr);           //C#里面的正则表达式
   //以空格为分隔符把这一行文本分成几个部分(我们这里是六个部分)并保存到数组中 
                string[] list = reg.Split(text);           
                KZPs[i].x = Convert.ToDouble(list[1]);     //实现赋值
                KZPs[i].y = Convert.ToDouble(list[2]);
                KZPs[i].z = Convert.ToDouble(list[3]);
                YXPs[i].x = Convert.ToDouble(list[4]) / 1000;   //注意要将单位mm化成m
                YXPs[i].y = Convert.ToDouble(list[5]) / 1000;
            }
            sr.Close();
            sr.Dispose();                                 //释放文件 
        }

把数据读进来后我们就可以初始化数据,也就是算初值了,同样我们用一个函数来实现这一功能

        //===========================初始化数据=========================================
        public void InitData()
        {
            double tempx = 0;
            double tempy = 0;
            F3.κ = 0;                          //三个角度初始值都设为0
            F3.ψ = 0;
            F3.ω = 0;
            for (int i = 0; i < PointNum; i++)
            {
                tempx = tempx + KZPs[i].x;
                tempy = tempy + KZPs[i].y;
            }
            Zs = m * f;                        //按照教材上面的方法给的初值
            Xs = tempx / PointNum;             //平均数作为初值 
            Ys = tempy / PointNum;
        }

数据准备好了,我们就开始迭代计算吧,先看核心迭代函数

        public void Calc()
        {
            int times = 0; //迭代的次数
     //在LostBox里面显示每次运算的情况
            listBox1.Items.Add("=======================第" + times.ToString() + "次迭代=========================");
            listBox1.Items.Add("Xs=" + Xs.ToString() + "; Ys=" + Ys.ToString() + "; Zs=" + Zs.ToString());
            listBox1.Items.Add("ψ=" + F3.ψ.ToString() + "; ω=" + F3.ω.ToString() + "; κ=" + F3.κ.ToString());
            listBox1.Items.Add("");
            do
            {
                CalcR();//计算R矩阵
                CalcL();//计算L矩阵
                CalcA();//计算A矩阵
                CalcX();//计算X矩阵
   //算出X矩阵后我们把改正数加到参数里面去,作为下一次迭代的初始值
                Xs = Xs + X.Matrix[0, 0]; Ys = Ys + X.Matrix[1, 0]; Zs = Zs + X.Matrix[2, 0];
                F3.ψ = F3.ψ + X.Matrix[3, 0]; F3.ω = F3.ω + X.Matrix[4, 0]; F3.κ = F3.κ + X.Matrix[5, 0];
   //迭代次数加1
                times = times + 1; //迭代次数加1
   //在LostBox里面显示每次运算的情况
                listBox1.Items.Add("=======================第" + times.ToString() + "次迭代=========================");
                listBox1.Items.Add("Xs="+Xs.ToString()+"; Ys="+Ys.ToString()+"; Zs="+Zs.ToString());
                listBox1.Items.Add("ψ="+F3.ψ.ToString()+"; ω="+F3.ω.ToString()+"; κ="+F3.κ.ToString());
                listBox1.Items.Add("");
            } while ((Math.Abs(X.Matrix[0, 0]) > LowLimit) || (Math.Abs(X.Matrix[1, 0]) > LowLimit) || (Math.Abs(X.Matrix[2, 0]) > LowLimit) || (Math.Abs(X.Matrix[3, 0]) > LowLimit) || (Math.Abs(X.Matrix[4, 0]) > LowLimit) || (Math.Abs(X.Matrix[5, 0]) > LowLimit));
//这个do...while循环实现迭代,其中由while条件来判断什么时候结束迭代,其中LowLimit是我们设置的精度,定义在全局变量中
        }

好,核心函数里面我们只有那么几个计算矩阵的函数功能没有实现了,我们再逐一实现就好,照着书上的公式抄上去就行了,非常简单

    //===========================计算R矩阵==========================================
        public void CalcR()//用到的是教材32页公式3-9
        {
            a1 = Math.Cos(F3.ψ) * Math.Cos(F3.κ) - Math.Sin(F3.ψ) * Math.Sin(F3.ω) * Math.Sin(F3.κ);
            a2 = -Math.Cos(F3.ψ) * Math.Sin(F3.κ) - Math.Sin(F3.ψ) * Math.Sin(F3.ω) * Math.Cos(F3.κ);
            a3 = -Math.Sin(F3.ψ) * Math.Cos(F3.ω);
            b1 = Math.Cos(F3.ω) * Math.Sin(F3.κ);
            b2 = Math.Cos(F3.ω) * Math.Cos(F3.κ);
            b3 = -Math.Sin(F3.ω);
            c1 = Math.Sin(F3.ψ) * Math.Cos(F3.κ) + Math.Cos(F3.ψ) * Math.Sin(F3.ω) * Math.Sin(F3.κ);
            c2 = -Math.Sin(F3.ψ) * Math.Sin(F3.κ) + Math.Cos(F3.ψ) * Math.Sin(F3.ω) * Math.Cos(F3.κ);
            c3 = Math.Cos(F3.ψ) * Math.Cos(F3.ω);
        }
        //===========================计算L矩阵==========================================
        public void CalcL()//用到的是教材62页公式5-4
        {
            L = new NNMatrix(2 * PointNum, 1);
            double JSx = 0;
            double JSy = 0;
            for (int i = 0; i < PointNum; i++)
            {
  //算近似值用到教材34页公式3-15,其中加上了x0,y0不为0的情况
                JSx = x0 / 1000 - f * ((a1 * (KZPs[i].x - Xs) + b1 * (KZPs[i].y - Ys) + c1 * (KZPs[i].z - Zs)) / (a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs)));
                JSy = y0 / 1000 - f * ((a2 * (KZPs[i].x - Xs) + b2 * (KZPs[i].y - Ys) + c2 * (KZPs[i].z - Zs)) / (a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs)));
  //给L矩阵赋值
                L.Matrix[2 * i, 0] = YXPs[i].x - JSx;
                L.Matrix[2 * i + 1, 0] = YXPs[i].y - JSy;
            }
        }
        //===========================计算A矩阵==========================================
        public void CalcA()//用到教材63页公式5-8及66页公式5-9b
        {
            double a11, a12, a13, a21, a22, a23, a14, a15, a16, a24, a25, a26, Z1;
            A = new NNMatrix(2 * PointNum, 6);
            for (int i = 0; i < PointNum; i++)
            {
                Z1 = a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs);
                a11 = (a1 * f + a3 * YXPs[i].x) / Z1;
                a12 = (b1 * f + b3 * YXPs[i].x) / Z1;
                a13 = (c1 * f + c3 * YXPs[i].x) / Z1;
                a21 = (a2 * f + a3 * YXPs[i].y) / Z1;
                a22 = (b2 * f + b3 * YXPs[i].y) / Z1;
                a23 = (c2 * f + c3 * YXPs[i].y) / Z1;
                a14 = YXPs[i].y * Math.Sin(F3.ω) - ((YXPs[i].x * (YXPs[i].x * Math.Cos(F3.κ) - YXPs[i].y * Math.Sin(F3.κ))) / f + f * Math.Cos(F3.κ)) * Math.Cos(F3.ω);
                a15 = -f * Math.Sin(F3.κ) - YXPs[i].x * (YXPs[i].x * Math.Sin(F3.κ) + YXPs[i].y * Math.Cos(F3.κ)) / f;
                a16 = YXPs[i].y;
                a24 = -YXPs[i].x * Math.Sin(F3.ω) - ((YXPs[i].x * (YXPs[i].x * Math.Cos(F3.κ) - YXPs[i].y * Math.Sin(F3.κ))) / f - f * Math.Sin(F3.κ)) * Math.Cos(F3.ω);
                a25 = -f * Math.Cos(F3.κ) - YXPs[i].y * (YXPs[i].x * Math.Sin(F3.κ) + YXPs[i].y * Math.Cos(F3.κ)) / f;
                a26 = -YXPs[i].x;
  //给A矩阵赋值
                A.Matrix[2 * i, 0] = a11;
                A.Matrix[2 * i, 1] = a12;
                A.Matrix[2 * i, 2] = a13;
                A.Matrix[2 * i, 3] = a14;
                A.Matrix[2 * i, 4] = a15;
                A.Matrix[2 * i, 5] = a16;
                A.Matrix[2 * i + 1, 0] = a21;
                A.Matrix[2 * i + 1, 1] = a22;
                A.Matrix[2 * i + 1, 2] = a23;
                A.Matrix[2 * i + 1, 3] = a24;
                A.Matrix[2 * i + 1, 4] = a25;
                A.Matrix[2 * i + 1, 5] = a26;
            }
        }
        //===========================计算X矩阵==========================================
        public void CalcX()//用到了教材63页公式5-6,主要为矩阵运算,矩阵运算代码及用法见附录中的链接
        {
            NNMatrix AT = new NNMatrix(6, 2 * PointNum);//声明矩阵A的转置为6行,两倍点个数列
            NNMatrix ATA = new NNMatrix(6, 6);          //ATA为A的转置乘以A
            NNMatrix ATA1 = new NNMatrix(6, 6);         //ATA1为ATA的逆 
            NNMatrix ATA1AT = new NNMatrix(6, 2 * PointNum);  //ATA1AT为ATA1乘以A的转置
            X = new NNMatrix(6, 1);
            AT = NNMatrix.Transpos(A);  //求AT
            ATA = AT * A;
            ATA1 = NNMatrix.Invers(ATA); //求ATA的逆
            ATA1AT = ATA1 * AT;
            X = ATA1AT * L;
        }

好了,到目前为止我们所以的函数模块都完成了,现在我们来在程序创建的时候调用他们,我们按Shift+F7来到窗体设计视图,双击窗体,写如下代码

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Text = "单像空间后方交会";
            DataFromText("data.txt"); //读取数据
     //初始化参数
            x0 = 0;
            y0 = 0;
            m = 50000;
            f = 0.0281359;
            LowLimit = 0.000029;
     //初始化数据
            InitData();
     //进行迭代计算并显示结果
            Calc();
        }

OK,到这里这个程序就算是完成了。我们将数据都填好到data.txt这个文件中,然后把data.txt拷到和我们程序一个目录下,然后我们F5调试运行,效果如图:

拓展篇

虽然我们这个程序最基本的功能都已经实现了,但还是有一些地方可以改进。比如说我们在程序创建时候在代码里面设置x0,y0,m,f以及精度LowLimit的值,这样就不太好,因为这样使得使用我们程序的人不能自己去定义这些参数,所以还是应该把这些数据放到文本文件中去,由用户自己输入,我们再调用函数去读这个数据。也就是我们需要DataFromText这个函数里面做些修改,使其能达到我们的目的,这个大家自己去写。

注意观察代码就会发现,我们程序只是从特定的文件data.txt中读取数据,那么要是我们还有其它数据呢,怎么办,难到要我们去把它名字都改成data.txt再来运行程序么,所以这个地方需要我们再改进。我们其实可以达到这么一个效果,我们把数据文件拖到程序窗体中去,然后就可以进行对这个文件进行运算。其实实现这个功能一点都不难,几行代码就搞定了。跟着我一步一步来做吧。

我们来到窗体设计视图,选中窗体,在属性那一栏中切换到事件(点一下那个闪电状的图标),我们找到DragEnter这一事件,然后双击进去写代码:

       private void Form1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))  e.Effect = DragDropEffects.Link;
            else e.Effect = DragDropEffects.None;   
        }

同理我们找到DragDrop事件,双击进去写上如下代码:

     private void Form1_DragDrop(object sender, DragEventArgs e)
            {
                //MessageBox.Show(((System.Array)e.Data.GetData(DataFormats.FileDrop)).GetValue(0).ToString());
                listBox1.Items.Clear();
                DataFromText(((System.Array)e.Data.GetData(DataFormats.FileDrop)).GetValue(0).ToString());
                InitData();
                Calc();
            }

好了,该功能就这么简单地被我们实现了,不妨运行一下,然后拖个文件试试。咦,好像可以哦,是不是很爽?

最后我们还可以设置程序的图标,在Form的Icon属性里面,选择一个Ico格式的图标文件就好。再设置一下程序,使其运行的时候窗体居中,也就是位于屏幕正中,在Form的StartPosition属性中选择CenterScreen就好。还可以有其它的设置,比如说可以设置程序最小化显示在系统托盘等等,大家自己去想吧。