高级四则运算器—结对项目总结(193 &105)

高级四则运算器—结对项目总结

 

为了将感想与项目经验体会分割一下,特在此新开一篇博文。git

界面设计

啥都不说,先上图震慑一下...github

上面的三个界面是咱们本次结对项目的主界面,恩,我也以为挺漂亮的!你问我界面设计花了多久?其实只有6个小时,而后6个小时中有2个小时都是为了一个bug,这个bug以后咱们会提到,也是让我长了一回见识。web

关于整个界面的美化

关于整个界面的美化,由于以前作Java的Swing开发,知道有这种控件的皮肤(Swing里是叫LAF=LookAndFeel),因此在一开始我就敲定了要在C#里也选择一款皮肤为我美化的想法。最后使用了这款开源的控件实现,感受很漂亮,确实也很漂亮,效果很是之棒。下面给出连接,你们之后能够用到http://www.cskin.net/。这个控件的按钮作得很是晶莹剔透,并且它的窗体整个作过必定的美化,并且使用整个界面的美化也很是简单:算法

 public partial class MainForm : CCSkinMain

但实际上我在这个过程当中遇到的问题并非界面美化的问题,主要问题在于相对引用一个dll文件的问题。由于CCSKin.dll须要被项目引用,若是想在另外一台电脑上也能够编译,就必须修改一些参数,在百般挣扎后我仍是在stackoverflow上找到了解决的正解:修改.csproj文件,将原先的绝对引用改成相对引用便可后端

    <Reference Include="CSkin, Version=15.3.10.1, Culture=neutral, processorArchitecture=MSIL">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>lib\CSkin.dll</HintPath>

如今我就至关于引用了.exe同目录下的lib文件夹下的CSkin.dll文件。安全

其实为了保险,也能够将引用的空间的复制本地选项选择为True,这样基本上不会出错。markdown

关于各类控件的选取动机

我选择了:多线程

  • MenuStrip做为菜单栏
  • TabControl做为切换页
  • OpenFileDialog做为浏览文件的弹出窗口
  • FolderBrowserDialog做为浏览文件夹的弹出窗口
  • Checkbox做为是否选中某选项
  • NumericUpDown做为诸如值域以及其余的限制
  • Button做为启动的按键和计算器的界面
  • TextBox做为一些输出参数的显示
  • ProgressBar做为等待提醒

下面我讲一下关于这些控件的妙用吧~ide

关于值域有效的限制

关于值域,因为其自己是有限制的——右边的必须比左边的大至少一个精度单位,并且其精度也是有考量的。因此说我最后采用了这样的作法:
左值域设定最小值为0,最大值为999;右值域最小值为1,最大值为1000。两个的精度都是1,因此只会是整数。
而且我还为左值域里加入了这一条语句:函数

 private void leftRange_ValueChanged(object sender, EventArgs e)
 {
            rightRange.Minimum = leftRange.Value + 1;
 }

固然,咱们同样能够为右值域的NumericUpDown设置同样做用的函数。可是,咱们可否两个一块儿设置呢?好比像这样:

     private void leftRange_ValueChanged(object sender, EventArgs e)
        {
            rightRange.Minimum = leftRange.Value + 1;
        }
        private void rightRange_ValueChanged(object sender, EventArgs e)
        {
            leftRange.Maximum = rightRange.Value - 1;
        }

我实践发现这样作是不行的,这样作就至关于两个循环影响,A影响B,B影响A。最后发现你初始触发改变的那个值根本没有改变,so sad。因此只能写一个触发改变阈值大小的语句便可。

关于导入答案和题目文件

关于导入答案和题目文件的问题,我使用了OpenFileDialog控件来实现,实现的大概功能就是当咱们点击下导入答案文件的按钮时,会出现一个打开文件的Dialog,而后必须选中一个文件才能成功返回。而我在这过程当中对文件的后缀进行了判断,若是是".txt"文件才能够成功导入,不然须要从新再导入一个文件。

     private void ExeButton_Click(object sender, EventArgs e)
        {
            try
            {
                string path = "";
                //实例化一个打开文件窗口
                OpenFileDialog Dialog = new OpenFileDialog();
                DialogResult result = Dialog.ShowDialog();
                //这里的意思就是说当结果打开了正确的文件时
          if ((result == DialogResult.OK) || (result == DialogResult.Yes))
                {
             //获取导入的文件名字(自动带绝对路径)
                    path = Dialog.FileName;
                    if (!path.EndsWith(".txt"))
                        throw new FileNotFoundException("文件后缀不正确,请从新打开!");
                    //在TextBox中显示文件名
                    ExeText.Text = path;
                }
            }
            catch (Exception e1)
            {
                ErrorForm error = new ErrorForm(e1.Message);
                error.ShowDialog();//show Dialog指定只能关闭本模块后才能够关闭其余
            }
        }

关于自定义生成路径

我在菜单栏中增长了一些自定义的功能,好比可以改变生成菜单栏的问题

这样是为了用户自定义路径的友好性:)。而且在导入后都有各自的Log记录输出框进行记录。

关于计算器的键盘映射

在计算器键盘映射这里,我就是在这里调bug调了2个小时之久,以致于后面差一点放弃键盘映射的功能。
由于是几天前刚接触C#界面开发,只知道C#界面与事件分离带来的清爽,可是殊不知道事件和控件是如何绑定的,一直觉得

private void ExeButton_Click(object sender, EventArgs e)

只要这样写就可让界面记住,恩,只要有个叫ExeButton的玩意,它被单击时就自动调用这个函数。但是后来一想,这不对啊,那我随便写个啥Click那岂不是可能会乱套吗?好比我写两个,这下怎么识别?

private void ExeButton_Click1(object sender, EventArgs e)
private void ExeButton_Click2(object sender, EventArgs e)

我以前键盘映射也是这个问题,经过各类资料都告诉我要设置FormKeypreview属性,让它为True。而后写一个窗体的keyDown事件,就能够创建键盘与按钮之间的映射啦!然而还差一步,这一步就是将事件控件绑定起来。
咱们能够看向这里

咱们须要在闪电符号表明的事件里,为咱们本身写的事件 和 想要绑定的控件对应在一块儿。

其实咱们还能够本身修改.Designer.cs文件,在里面加一句

this.ExeButton.Click += new System.EventHandler(this.ExeButton_Click);

这样就至关于在代码里帮助事件绑定~

关于等候时间

为了有更加人性化的界面,我增长了等待进度条。又因为长条进度条太丑...因此我使用了圆形进度条,效果以下图所示:

这个一开始使用的时候遇到了问题,什么问题呢,就是常遇到的问题——单线程若是要在处理完才释放主线程资源的话,会形成运算期间界面的不响应。因此我使用了多线程。大体了解了下线程的产生与事件的委托,我就开始雄心勃勃地写了:

            Thread Genthread = new Thread(Generate);
            Genthread.Start();

其中Generate函数中已经封装好了产生算式的功能。因而我高兴地在Generate函数的最后加了一句

                GenProgressIndicator.Hide();
                ExeAnsTextBox.Text += "已经生成了" + factcount + "道题目与答案到指定的文件中."+Environment.NewLine;
                ExeAnsTextBox.Show();

就是说圆形滚动条隐藏起来,而后将生成了答案的实际数量打印到log日志里面去,而后把log日志框从新展现出来。
可是这时我却遭遇了一个蛋疼的异常:

“System.InvalidOperationException”类型的未经处理的异常在 System.Windows.Forms.dll 中发生 

其余信息: 线程间操做无效: 从不是建立控件“GenProgressIndicator”的线程访问它。

实际上C#为了保证线程操做的安全性,因而就控制了不能让其余线程修改非该线程建立的UI控件的状态。在搜索了许多答案都感受异常复后,我发现其实只要加一句话就好使:

Control.CheckForIllegalCrossThreadCalls = false;

只要在窗体构造的函数的函数里写上这一句就够了...由于暂时咱们不存在线程同步数据的问题。
固然有更好的解决方法,我也尝试了一下,感受还不错,BackgroundWorker,这个类好像是专门为这种状况设计的同样:D。

算法设计

算法达到的效果

这一次,我重构了算法,是的,你没有看错——由于上一个算法没有扩展性。它没有办法适应新时代——自定义运算符个数的时代的到来,因而无情地被淘汰了。

因而乎,我加入了全部目前可行的优势来作一个算法的表达式,在上面投入了大量的时间来进行算法的优化与性能提高,随机化的提高等。最后作到的效果就以下,上次个人我的项目里范围为3时,只能生成不到1000个式子,如今随机生成能够生成10万数量级的式子数量。即便去掉负数的选项,也能生成3万左右数量的式子,因此数量的生成上是十分有保障的。无图无真相,上图:

需求分析—控制参数

算法的第一步是要有一个明确的参数控制列表,哪些参数会控制哪些函数要很明白

  • 是否含有负数——要求在生成数字、减法过程当中控制

  • 是否含有乘除法——要求在生成操做符过程当中控制

  • 是否含有分数——要求在生成数字、除法过程当中控制

  • 是否含有括号——要求在生成表达式过程当中控制

  • 概括下来实际上要在生成随机数上须要有所控制的有:是否含有分数、是否含有负数

  • 要在生成随机操做符上须要有所控制的有:是否含有乘除法

  • 要在表达式生成上须要有所控制的有:是否含有分数、是否含有负数、是否含有括号

分析完这些以后,我就开始动手构建算法主体了,此次由于要对重复检测等进行优化,最终我选取了树的结构做为个人表达式的组成结构。

重复性—树的最小表示法

首先要说明的固然是这个很厉害的东西—树的最小表示法。在反复思考史神博客所说,结合网上的一些poj作题的算法解题过程——虽然它们对我最后也没有产生什么影响。可是我总算是理解了树的最小表示法的真正含义

最小表示其实是一种自定义有序的一种表示方法,放在树里的话,其实是对每一个结点来讲,都要对它下面的左右子树进行自定义的有序排序。而后因为自定义序是必定的序,因此只要自定义序是稳定的方法。那么放在一棵树上,无论左右子树如何扭,或者左子树的左右子树如何扭,它们最终都会被有序排序。

下面上我写的代码:

        //根据root递归生成最小表示法得到的字符串
        public string GenerateMinusExp(Node root)
        {
            //若是是叶结点的话,则直接返回该结点的值
            if (root.IsLeaf())
                return root.Value;
            else if (root.Value == "+" || root.Value == "×")
            {
                //对左子树进行递归,获得左子树的最小表示字符串
                string LeftMinus = GenerateMinusExp(root.Left);
                //对右子树进行递归,获得右子树的最小表示字符串
                string RightMinus = GenerateMinusExp(root.Right);
                //对左子树和右子树进行统一排序
                if (string.Compare(LeftMinus, RightMinus) <= 0)
                    return root.Value + LeftMinus + RightMinus;
                else
                    return root.Value + RightMinus + LeftMinus;
            }
            //不然就按照正常次序进行最小字符串表示
            else
                return root.Value + GenerateMinusExp(root.Left) + GenerateMinusExp(root.Right);
        }

有括号—二叉树中序遍历

有括号的状况实际上相对而言比较简单,树能够很好的递归生成(而且这样随机性很强)。我在实际中也是递归来生成树的。实际上递归的逻辑相对也很简单,按以下步骤则可获得一颗随机的树:

1.判断当前符号栈是否有符号,无符号说明必须有两个操做数为子结点。
2.取0到3之间(不包括3)的随机值
3.根据随机值判断是哪种状况:

  • 0-左符号 右符号
  • 1-左数字 右符号
  • 2-左符号 右数字(为何不是4种?由于第四种是被迫出现的,是由于符号栈里的符号都被用光了,而使得两个都是操做数)

4.若是知足某个域的值为操做符,那么就向这个方向递归,最终生成数式。

这样因为生成的是一颗树,咱们再对树进行中序遍历,就能够获得这棵树对应的中缀表达式,也就获得一个有概率出现括号的表达式了。

无括号—中缀表达式转二叉树

因为加入了无括号选项,即咱们容许用户不选择括号。因此在这个里面个人想法是反着的,是先随机生成中缀表达式,而后再由中缀表达式生成一颗二叉树。最后性能这样的作法效率也挺高的。

无括号除法整除——替换因子

整数型的无括号算式,老板忽然要求整除,咋办?

我一开始想:从新生成一个呗。后来发现薪水少了一半—表达式的数量少了不少,生成表达式所用的时间也大幅增加。

后来我想了想,改进了一下算法,改进的步骤以下:
在生成的时候,每遇到除号,就去检测一下前面那个符号是否是除号(若是是第一个符号则不检测,用逻辑短路就短路过了),若是前面那个不是除号,那么就让除号后面这个式子的值变为除号前的数字的某个因子。

这个因子如何找呢,就在leftRangedivider+1之间随机取一个数,使用Fraction类中早已经写好的获取最大公约数的函数找出两个数的最大公约数便可。这样既不须要生成被除数的因子全集而浪费大量时间,也不会由于每次都使用一样的因子而减小多样性。这样能作的缘由是利用了除法的左结合性和优先级,由于除非前面的符号为除号,不然当前算法最早计算的必定是divider/divisor的组合,因此这样能够很大程度上地减小一些除法不整除的状况。

固然咱们说了凡事都有前提,若是出现了连除式怎么办呢?那么下来就要利用咱们有括号的状况下整除的处理方法一块儿处理了。

有括号除法整除—裂解因子

有括号除法的整除,我是和我作我的项目时被自我否认的一个叫裂解的方法结合在一块儿的(确实脑洞够大的)。

裂解的思路就是使用一个操做数去裂成两个操做数和一个操做符,这样作的好处就是能够控制结果,经过结果来生成源数。可是这样作较为繁琐。

可能你应该就会想到个人算法了,实际上我是这样的作法。延续上面无括号的分格,若是有括号的状况下,发现某个子树的除法不能经过,由于不是个整数,这下该怎么办呢?

我会先按上面无括号的状况随机构造一个可以整除左子树的随机的操做数,而后根据原有右子树的操做符的个数,以该操做数为起点,裂解生成一颗与以前的右子树操做符个数相同的树。这样作虽然繁琐,可是相比从新 掷色子 更有优点的地方在于其优秀的修补能力——对,就是修补能力。修补得可以使得表达式从非法变成合法表达式,相比起彻底从新生成,这样是使人很是愉悦的。就像是一双打了补丁的衣服,虽然打了补丁,但也是件能够保暖的衣服。

一个裂解的例子以下:
1. 随机数 2
2. 随机符号 +,随机数 9, 产生数 -7,因而有下面这种简单树形结构
    +
  9   -7
3. 随机符号 -,随机数 10,产生数 1,因而再扩展一支
      +
    -   -7
10  1

有括号减法不为负—翻转子树

在树的表达里,有括号的表达式要控制减法不为负数这简直太简单了,是吧?
在有括号的状况下,一颗树只须要翻转两颗子树便可达到结果为非负数的效果。代码示例以下

                 case "-":
                    Fraction LExp = AmendTreeAndCalculate(root.Left,leftRange);
                    Fraction RExp = AmendTreeAndCalculate(root.Right,leftRange);
                    //若是结果为负数、不容许出现负数且是有括号的式子
                    //不容许结果中出现负数的话,就把两颗子树翻转
                    if ((RExp > LExp) & !HasNegative & HasBrack)
                        Transfer(root);
                    return LExp - RExp;

无括号减法不为负—偷天换日

针对没有括号的状况,我想了好久,发现本身只有偷天换日这一条路能够走。简单来讲,就是在无括号式子减法过程当中发现了负数&要求不能够产生负数,则将-替换为+

。。。。。

我知道你看到必定会无语,可是这确实是事实,后来经过热力图发现这个替换的次数仍是蛮多的...主要缘由就是由于咱们没办法经过交换子树的方法来造就减法,由于一旦交换,就可能产生带括号的式子了,并且交换后产生括号的几率还挺大的。

以后我优化了一点来避免这个问题:在生成不带括号的运算表达式时,若是遇到符号为-,就判断下一个符号若是是-+或者没有符号了,就将减数随机为一个letfRange,被减数之间的数,这样能够下降不少的减法被舍弃的状况。

溢出避免—check关键字

因为此次思考再三没有使用上次助教老师所说的double类型的分子与分母定义,由于在它们计算时,要想没有误差地产生整数值,须要用它们的整数部分参与运算,可是没找到相关直接截取整数做为double类型参与运算的方法。后来又以为使用Decimal类型过小题大作,因而使用了提供的check关键字检查了溢出。若是遇到溢出的话,就把生成的式子从新随机生成,毕竟若干个连乘的几率仍是很低的。

long A;
long B;
check{
 long C = A + B;
}
//check的基本用法

界面与算法的对接

界面与计算模块的对接原本应该是很融洽的,可是因为我的比较糟糕的设计—在附加题作的时候也被鸣神吐槽了的—接口很不规范等问题,因而我跟钟焕讨论了一下,决定使用xml做为先后端的传参工具。

计算模块的生成

计算模块的生成还好,不算难,直接新建了项目,类库的问题就能够了。这过程当中只是遇到了引用受保护的类的问题,因此最后只开放了一个public类做为结果而已。

xml的使用

xml的使用其实不难,若是看懂了一些示例的话。最终在十分钟入门教程后,我成功写出了xml文件的创建与写入。在xml文件里有一点比较坑的是,在更改xml属性后必定要注意

XmlDocument.save("文件路径");

不然xml文档是没有办法更改的。

以上就是我本次结对项目里得到的一些经验和总结之谈,但愿你能有所收获。暂时这么多,后面想到什么还会补充。

 鸣谢

 

核桃:sighingnow

他们可能比我作的更好

fzyz999 & hoerwing

kanelim

kibbon

PocetPanacea

SyncShinee

但愿你能在百忙之中也能够抽空看看他们的博客,必定会有所收获:)。

文件附送

文件已经从新生成中...请静候佳音...

相关文章
相关标签/搜索