高級四則運算器—結對項目總結(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

但願你能在百忙之中也能夠抽空看看他們的博客,必定會有所收穫:)。

文件附送

文件已經從新生成中...請靜候佳音...

相關文章
相關標籤/搜索