2019年8月15日 星期四

把報表元件丟進垃圾桶,省下錢跟你的生命 Part 1.


列印與Canvas

過去,在Delphi裡面要列印資料到印表機,大家都不知道到底該怎麼做才是對的作法,我也是這樣子過了20多年,從Delphi 7的時代使用FastReport,到後來改用Cristal Report/QuickReport大多數是因應客戶的要求而採用這些報表元件。
這些元件所費不貲,而且許多都在升級時要另外支付升級版費用,最讓人煩的是「每個元件的用法都不一樣、前一手程式人員八仙過海各顯神通的寫法都不一樣」,讓接手的我常常覺得乾脆翻掉重寫一次還比較快。
最近一年的專案更是如此,前幾手的程式人員把Quick Report用到我都認不得了,而且同一家公司的程式碼,不同專案就有不同的寫法,一個Quick Report各自表述。有的專案中,在 Report元件的Hint 裡面用他自己定義的符號來寫該元件對應哪個資料表、哪個欄位;有的專案中,直接把Report的所有資料從主表單當中一筆一筆塞資料,另一個專案則是在報表檔裡面去抓另一個表單的各個欄位來填資料。
OK的,我在學校最常跟同學們說的一個概念,就是「程式不會只有一種寫法,大家可以發揮創意,只要做出來的程式符合規格,都是對的」。
這個概念仍然是對的,我只想補充後續的概念「大家在正確性沒有問題的前提下,都該追求維護的簡便,以及追求更好的效能」。剛剛提到的這一年的客戶的程式,正確性沒有問題,但他們的系統幾乎沒有用到Delphi的物件導向概念,連Object Pascal的規範都很少遵循,居然還能出現在同一個專案中的不同unit檔案裡,宣告同名全域變數這種連學生都不該犯的大錯,這些程式的問題可謂罄竹難書啊⋯⋯
不說了,說了讓自己徒生氣,我們回到主題吧。
Delphi 的類別來操作印表機
要列印資料到印表機,在Turbo Pascal的時代,我們用的是AssignPRN,來連接到印表機,再以WriteWriteln把文字資料寫到印表機去。但這個做法在FireMonkey當中,印出來的文字會像蚊子一樣小,無法閱讀。
而要把圖片列印到的話,則必須透過Canvas的操作。以往對Canvas熟悉的程式人員並不多,能對Canvas操作熟悉的,應該都是已經在開發領域有過一定經驗(吃過一定苦頭)的資深人員了,我自己是在2005年開始,自己製作圖形元件與自訂Delphi程式佈景主題之後,才對Canvas有一點基礎的認識。
列印的時候,不管是哪一款印表機,都是透過Delphi封裝的TPrinter類別來處理的,而且都是使用全域變數Printer來操作,例如要開始列印作業,我們可以這麼寫:
Printer.BeginDoc
要結束列印作業,則寫為:
Printer.EndDoc
而在列印當中,要換頁面的話,則這麼寫:
Printer.NewPage
基礎的操作介紹告一段落了,我們來看細部操作吧,要把文字輸出到印表機上面,我們要透過:
Printer.Canvas.FillText(DestRect, 'Test', WordWrapTrue, 1.0, flags, TTextAlign.Leading, TTextAlign.Center);
DestRect : 要把圖片區塊輸出到印表機 Canvas 的位置與大小
WordWrapTrue: 超過寬度是否要自動換行?
1.0  -> 顯示的透明度
Flag->文字從左到右或從右到左, 建議直接給[]就好
TTextAlign.Leading => 水平對齊設定 (Leading靠左, Tail靠右, Center置中)
TTextAlign.Center => 垂直對齊設定 (Leading靠上, Tail靠下, Center置中)
而要把圖片輸出到印表機上面,則透過這個寫法來達成:
Printer.Canvas.DrawBitmap(要列印的圖片物件, SrcRect, DestRect,1.0, True);
圖片物件的型別是: FMX.Graphics.TBitmap
SrcRect: 要把圖片輸出的範圍區塊
DestRect: 要把圖片區塊輸出到印表機 Canvas 的位置與大小
1.0  => 透明度
True => 是否高速列印 (高速列印一般應該就是品質較差)
透過這些程式碼,就可以把圖片、文字印到印表機去了,如果需要選擇印表機,請使用DelphiTPrinterDialog讓使用者選擇要列印的印表機即可。
第一部分就先講基礎的操作,後面我們再來講複雜的報表要怎麼作吧。
有些夥伴可能會想問,在Windows 10上面想要用Print To PDF這個選項,又想自訂輸出檔名的話要怎麼辦? 讓我賣個關子,下一部一開始就會講這個。

2019年8月4日 星期日

Delphi 為何仍然是開發工具最佳選擇

前言– Delphi的興衰與奮起

2019,是Delphi問世之後的第26個年頭,IDREA推出了Delphi/C++ Builder/RAD Studio的第20個版本—Delphi XE 10.3.2,但截至筆者撰寫本文的這當口,全世界仍有很高比例的Delphi開發人員持續在使用Delphi 5-Delphi 7,無關工具本身的好壞,而是關於既有專案能否「無痛升級」。
筆者在Delphi開發人員當中屬於異類的少數,並非筆者的能力,而是開發的應用面向不同。傳統的Delphi使用者,大多集中在與資料庫相關的系統,例如ERP、財會、MES等類別,以文件與資訊交換為主要作業。
Delphi甫一推出,就憑著高效能、與多種資料庫(尤其是Oracle)的介接容易、在Windows作業系統中的高相容性,程式語言的完備度,博得了「VB殺手」的薄倖名。
然而從1993-1996年之間,稱譽資訊學界近20年(含AdaPascal)的Pascal程式語言,漸漸被物件導向概念夾擊,先有挾C語言廣泛性優勢的C++出現,到了1997又有號稱全物件導向、可以透過虛擬機跨越所有平台、以沙盒概念提升安全性的JAVA來襲,整個資訊學界言必稱JAVA,語必稱物件,傳統的Pascal失去了學界的關愛眼神,從1998年開始,全台再沒有學校教授Pascal
然而,Borland1994年推出了Delphi,以Pascal為基礎語言,更為Pascal加入了物價導向的概念,成為Object Pascal,整個Delphi的物件導向概念與視覺化元件不斷優化,到了Delphi 5的時候大致完備,當時是1996-1997年間,然而JAVA當時也還沒完備,除了文件得印上幾大本,當年連編譯器都還沒有出現,遑論虛擬機。
到了2000年,Borland 負責DelphiPascal的靈魂人物Anders Hejlsberg被挖角到微軟,把原本沒能在DelphiObject Pascal語言上實現的設計與理想用在微軟新一代的平台跟語言設計上,於是就誕生了.Net FrameworkC#語言。接著Borland策略錯誤,以為可以做出Delphi 8成為跨.Net與機器語言的工具,沒想到微軟四兩撥千斤,2004年以不授權.Net FrameworkBorland為戰技,一下就讓已經要上市的Delphi 8無法推出,Delphi只好回頭繼續改良原本的工具,但也就此流失了關鍵的3年,直到2005年初才又推出Delphi 2005,但效能不如Delphi 7,介面又大幅度改變,所以絕大多數的Delphi 7使用者都不升級,歷經Delphi 20062007,都一蹶不振,於是Borland把開發工具部門獨立出來,成立了CodeGearBorland則專注於軟體開發流程控管的產品,後來越發寂寥,直到現在已經很多年沒聽過Borland了。
Delphi 2009這個版本中,可以算得上是CodeGear發奮圖強的開始,這個版本是第一個全環境支援Unicode的版本,從元件到程式碼,都完全支援Unicode,換句話說,除了Object Pascal語言的關鍵語法,其他的變數、類別命名,都可以使用Unicode字元,如果你願意,可以把變數用中英文混合命名,但打字會麻煩很多,所以筆者雖在2009年就已知道有此一功能,卻從未使用過,因為對維護來說,這是一個很負面的作法!
Delphi 2009算的上是CodeGear努力的成果,但到了2009年,Windows應用程式已經不是市場主流了,取而代之的主流,是手機應用程式。
筆者也是在2009年開始轉向手機應用程式開發,從Objective-C (iOS)JAVA (Android),連後端的控制平台(PHP),沒有一樣不用重新學習。在筆者已經把Objective-C弄到滾瓜爛熟之後,到了2014年,手機應用程式的製作市場又飽和了,此時Delphi已經又推出了Delphi 2010, Delphi XE, Delphi XE2, Delphi XE3這幾個版本,而從Delphi XE2之後,這個工具已經不再只是原本的開發人員所熟悉的Windows應用程式開發工具,而是跨平台的開發工具了,在XE2的年代,Delphi用來開發MacOSiOSWindows 32bit, Windows 64bit這幾種平台的應用程式已經完全沒有問題了,唯一的問題是,原本的開發人員也已經大多轉向了相同設計的C#,而Delphi成為了歷史名詞,不再能夠在一堆免費的開發工具中受人矚目。
之後從XE5 (Android Ready)XE6 (各平台穩定)XE7 (iOS 64bit)XE 10.0, XE10.1, XE 10.2, XE 10.3,除了更為穩定、速度更快,也完成了MacOSX 64 bitLinux程式的編譯功能。
歷史總是有很多有趣的地方,也有很多令人感傷的地方,但大多數的演進都已經介紹過了,我們開始進入主題,從技術面介紹一下新的Delphi

VCL v.s. FireMonkey

Delphi 1.0開始,視覺元件架構就是VCL (Visual Component Library),這個架構從Delphi 1.0一直到目前的Delphi 10.3都是Delphi的重要核心。VCL提供了絕大多數我們能夠在Windows作業系統中看到的視窗元件,並且與時俱進,從Windows 3.1的版本一直到Windows 10的版本,都能直接以原生碼(Native code)的方式執行,無須另外裝.Net Framework,但有些Delphi的元件在動態連結時會需要使用附在Delphi系統中的BPL檔案,這樣的檔案可以橫跨Windows 3.1, Windows 95, Windows 98….. 一直到Windows 10,相信目前沒有任何其他工具可以做到。
Delphi XE2之後,所有跨平台的應用程式,都需要使用FireMonkey這個新的架構來製作,FireMonkey當中,提供了WindowsMacOSXiOSAndroidLinux五大平台的元件,而尤其值得一提的是『單一專案,可編譯五種平台程式,各種程式都是原生機器碼!』
VCL當中,雖然可以滿足所有Windows平台的視覺元件需求,但VCL還是有其功能上的限制,例如:
1.      VCL要製作元件,所有元件都必須透過畫布重繪來顯示、也都必須由開發人員自行控制當中的互動邏輯(VCL元件中不能內含其他元件)
2.      VCL 元件必須透過元件註冊才能分享給其他程式在設計階段使用。
跨平台的功能就完全別提了。
FireMonkey則有幾個好處:
1.      跨平台,而且自動套用各平台的視覺樣式
2.      元件可以內含其他元件,製作元件時可以省下很多時間
3.      畫布與圖片都支援32Bit Alpha圖片,也就是可以製作半透明的畫面
4.      FireMonkey的畫面與座標會自動依照螢幕解析度進行必要的調整,過去在Windows系統中,傳統Delphi程式會在Windows螢幕字形比例大於100%的時候出現畫面錯置的情況,在FireMonkey裡面不會發生。
5.      MDI的程式問題,在FireMonkey不再發生。
理論條列講的差不多了,讓我們舉幾個實例來作深入一點的說明。
以往在VCL的程式中,很難把所有的畫面都集中在同一個表單檔案裡面,但在手機App的概念中,並沒有多視窗的設計,因此FireMonkey也才採取了『所有元件都可以扮演Container角色』的設計,讓程式畫面可以集中在同一個畫面中。
這個設計從XEXE5不斷的被驗證與優化,在XE5之後,FireMonkey提供了TFrame這個元件,讓我們可以把一整個畫面的程式碼跟表單(其實是Frame, 不是Form)獨立為單獨的檔案,這樣一來,所有的畫面雖然保留在同一個Form上面,但不同的畫面呈現可以被儲存在不同的Frame裡面。
很多公司把單一檔案的Size做了限定,希望程式碼可以不要集中在某幾個檔案裡面,這固然可以降低單一檔案的Size,使得Windows 32bitDelphi 7的限制不被挑戰,但有些時候,設計上是無法避免一個Pas檔案上3萬或5萬行的。(筆者在2008-2009服務於美商,製作備份系統的時候,單一主畫面就已經到10萬行,也曾聽聞有到15萬行的)
程式設計應該遵守的準則不少,但也需要看專案的性質跟是否能夠被實現,盡信書不如無書,這句話在程式設計上更應該被重視,也是所有程式人員應該時時刻刻謹記在心的。
透過適當的物件設計,避免獨立、不屬於任何物件的Function與變數,能夠避免一些問題,這雖然是物件導向程式設計的基本,但筆者在替許多專案程式碼進行優化與修改的時候,發現到雖然現在已經是2010年代,即將進入2020年代,但很多程式人員的物件導向概念還停留在1980年代。常見的問題很多,我列出幾個很嚴重的問題,期望大家盡量不要明知故犯:
1.   盡量完全使用物件設計,避免全域變數、全域函式或Procedure,避免多個Unit裡面宣告了相同名稱的變數,卻沒有被發現,在編譯上雖然不會出錯,但執行時卻會發生完全無法預估的錯誤,而且也很難除錯。
2.   在畫面上要使用的資料,務必獨立由該單元檔或表單檔獨立控制,如果其他單元檔案需要處理不同單元檔的資料,一定要透過參數、函式進行傳遞與修改,不要直接抓畫面變數、資料,這會造成未來多個單元檔互相羈絆,無法獨立修改內容,甚至升級,系統寫到這個程度的話,只能說是病入膏肓,除了重新設計,很難有救活的機會,越大的系統難度越高。
3.   承第2點,畫面的設計、資料處理、儲存的方式,最好可以獨立分開設計與處理,不要把這三個主要的部分綁的死死的。例如畫面上的各個欄位可以有獨立的變數名稱,儲存時用內部物件來儲存,要計算的時候透過這個內部物件來處理,需要存檔或讀檔的時候,也透過內部物件來暫存,這樣一來,任何一部分作修改,都不用考量連鎖反應,因為不會有連鎖反應,程式的修改與未來的維護才會簡化人力,也就是簡化成本。這個作法,在設計模式上面,被稱為MVC Pattern.(MVC模式)
4.   處理同一種計算或作業的程式碼,最好可以用同一個Method來處理,未來有任何需要修改的時候,修改一個Method,在系統中所有的地方都一起修改好了。在設計模式上面,這個作法被稱為Factory Pattern.(工廠模式)
5.   在處理可能會被大量呼叫的Method時,務必採用效率最好的演算法與最適合的物件,在這種Method當中使用越多的迴圈,程式的效能就會越低落。
6.   盡量使用Thread以及Task來處理可以同時被處理的作業,這樣可以在多核心的機器上獲得最大的效能。
傳統的VCL有很多難以避免的問題,也不一定可以守的住上述的幾種規範,因此,如果舊系統已經如我所述的『病入膏肓』,重寫在所難免時,請先考量FireMonkey

Big5UTF8

在我所看過的,歷史在10年以上的Delphi程式,很少有當時就已經能夠相容於Unicode的程式,即使是我所提到的這些少數的程式碼,只要不是使用Delphi 2009以後的系統升級或處理過的,也只能透過TntWare系列的元件提供Unicode相容的功能。
因此,筆者這十年來被問最多次的問題,就是『要怎麼樣才能無痛把舊版Delphi程式升級到支援Unicode』,然而,這並不是一個單純的問題,我們最少需要考量三個層面:介面、程式資料處理、資料庫或網路通訊。
這三個層面說的輕巧,但是能夠精通的人並不多。而且我說的是『最少』,最嚴重的問題不只是這三個層面,還在於程式中是否使用了『第三方元件』,而這些『第三方元件』是否有支援Unicode的新版本?或者這些『第三方元件』是否還有人在維護?是否擁有這些元件的原始碼?
我們就這些環節一個一個來剖析:
第三方元件通常是最棘手的。很多案例中,使用了大量的第三方元件,可能從網路通訊、視窗外觀客製化、繪圖、加密、資料庫、條碼產生、報表繪製/預覽/列印、到晶片卡讀卡/簽章等功能,除了晶片卡之外,其他我提到的元件,在原廠FireMonkey都可以有原生方式可以解決。
介面:舊版的Delphi製作的介面是傳統的VCL元件,只要該元件還存在,沒有隨著Delphi的版本更迭而被停用,就可以用新版的Delphi開啟該表單檔案,基本上可以無痛升級,但如果有些自行安裝或撰寫的視覺元件是舊版的,就必須要把這些元件全數升級、安裝到新版Delphi,才能正確開啟該表單檔案。
程式資料處理:這部份相對麻煩一點,可以分成兩個部分來處理,第一部分是寫在程式碼裡面的文字訊息,因為檔案本身是ANSI編碼,中文資訊自然就是Big5編碼。此時,我們可以用新版Delphi開啟程式碼,如果該檔案是表單,也可以按F12切換畫面跟程式碼。切換到程式碼之後,在程式碼的任何一個地方點滑鼠右鍵,選擇File Format,將之設定為UTF8,就『大致』完成了。
這只是『大致』而已,因為如果在程式碼當中使用了檔案、網路介面傳遞資料,傳統的Delphi程式人員習慣會把String拿來當成Byte陣列指標,然後用陣列元素來存取當中的資料,如果有這樣的情形,請記得把這些資料的處理改用TRawByteStringTBytes來處理。
Delphi 2009之後,String型別預設就是WideString。而在Delphi 1Delphi 2007當中,String型別預設則是AnsiString,長度是不同的,所以用陣列元素來取資料,會發生全部錯誤的狀況。
資料庫元件:通常傳統的程式中,使用的資料庫連線可能是DBExpressBDE、也可能是ADODBODBC,但到了Delphi XE 10之後,全數資料庫元件都更新為FireDAC元件系列,表單畫面上一定會找不到這些元件,這部份的修改是最費時的。
接著如果要改寫成FireDAC,需要修改的應該只有Connection元件、連線設定、Table/Query/Transaction元件的更換,至於SQL指令則不太需要更改。
還是那句話,這些分析都是輕巧的幾句話,但真的在裡面修改、更換元件,可能會是嘔心瀝血的過程,遇到問題的話,大家可以分享、交流一下經驗。
許多公司仍舊堅持著使用Delphi,可能是因為過去的Code Base很大,無法很快換掉,也可能是因為發現了Delphi原來的優勢、未來的優點。不可諱言的,Delphi的程式人員不像過去那麼多了,但這也是好事,表示濫竽充數的人也少了,我們不用花費太多的成本去抓出這些人來Fire掉。人才很多,帶人帶心,如何找到適合的人才,讓他願意為公司服務、在公司成長的路上一起成長,是每個公司的課題。

這年頭,實習、培訓、找外包,方法多的是,如果不用Delphi的原因只是因為怕人不好找,那麼,這家公司的文化也應該會讓很多資深的人敬而遠之了,許多資深的人才並不怕苦,怕的是苦完了還要被精神剝削,那麼,誰也不願意留下來了……

2019年4月28日 星期日

使用 Delphi 的指令進行自動建置,並排除執行時可能遇到的問題

前言、CI簡單介紹

在專業的軟體公司中,需求分析、系統分析、系統設計、Coding、版本控制、Build 版本、測試,是每一天不斷在進行的過程。從 Delphi 還在 Borland 旗下的時候,就不斷在這些流程當中尋求優化。

2005年到2007年之間,Borland出色的版本控制系統 - Star Team 是我當時服務的公司非常倚重的核心版本控制軟體,只可惜 Star Team 的價格很高,無法普及,隨著時間的流逝,版本控制系統一度流行走過了 SVN,現在則是全球流行用 Git 來做版本控制。

目前最流行的免費 Git 版本控制系統提供商,絕對是 GitHub 與 GitLab,這兩個提供者之間的差別,坊間有許多的比較資訊,透過 Command Line,我們可以很快速的把專案透過 GitHub/GitLab 同步,讓許多開發人員同時一起對一個相同的專案進行程式碼的修改與新增。

GitLab/GitHub 要如何跟 Delphi 搭配做到 CI (Continue Integration),Embarcadero在過去兩年內,也和很多家 CI 的軟體進行搭配,簡單的 Google 一下,絕對可以找的到相關文章,我在這裡就不獻醜了。

這一篇文章要跟大家分享的,是我們自己土炮製作出自動/手動建置系統時,透過 Delphi編譯指令時,一定會遇到的問題,截至 2019/4/28 下午4:41,我還沒有回報問題給 Embarcadero,但即使回報了,也不見得所有開發人員都會更新到最新版的 Delphi,所以,如何在現狀中找到求生方法,就是這一篇要跟大家分享的主題了。

如何透過指令來建置 Delphi 專案

Delphi 的開發人員,對於在 Delphi 的 IDE 當中要如何編譯、建置專案,應該都不用我多說,無論是 F9, Ctrl + F9, 或是 Shift + F9, 都可以簡單的把 EXE 檔案生出來。

但到了 DOS 視窗裡面,要怎麼面對 Delphi 的 DProj 與 DPR 檔案,恐怕連資深的開發人員也要稍微想上一會兒,不囉嗦,我們直接開講:

開啟一個 DOS 命令提示字元視窗 -> 快速鍵 (Win + R, Cmd, Enter)
開啟 DOS 視窗之後,我先把路徑切到專案所在的目錄 (D:\XE10.3-Rio\jsonDefTest),如上圖所示。

從 Delphi 2007 開始,Delphi 不做自己的組建系統,改以使用 MSBuild 來建置 Delphi 的專案,專案檔仍是使用從 Delphi 2005 開始就沿用的 dproj 檔案格式。一般來說,如果沒有特殊的需求,直接鍵入 "MSBuild 專案名.dproj",就能把專案建置出來了,我們這就來試試看:
居然出現有錯,說是找不到 msbuild 這個指令?!!!

是的,Delphi 的編譯還需要不少環境變數的設定,所以在安裝有 Delphi 的系統中,都會在 Delphi 的目錄裡加入一個名為 rsvars.bat 的批次檔案,這個路徑已經在安裝 Delphi 的時候自動被安裝程式加到了我們的登入設定中。

所以要執行 MSBuild 之前,請先執行 rsvars 這個指令,就可以成功了:






在 Embarcadero 的說明網站中,也有介紹許多 MSBuild 對於 Delphi 專案的參數,大家可以先看一下這些參數,了解一下 MSBuild 的操作方法,最少我們會需要知道如何建立 Debug/Release 這兩種不同設定組態的執行檔,以下的指令就是建置 Release 組態的 EXE檔案,相當於我們在 IDE 裡面按下 Shift+F9.

MSBuild "D:\XE10.3-Rio\jsonDefTest\jsonDefTest.dproj" /t:Build /p:Config="Release"


這樣的建置,會把專案裡面用到的所有檔案都重新編譯,不會用到之前就已經建置好的 dcu 檔案 (當然 RTL 除外啦),這作法也是大多數的開發朋友們最常用來建立 EXE 檔案的作法。

Pre-build, Post-build Event

在 Delphi 使用了 MSBuild 之後,也把 MSBuild 的好處一起帶進到 Delphi 的編譯環境中,例如建置前、建置後要執行哪些作業,就可以在專案裡面設定好,設定的畫面如下:


這個功能,只有在比較複雜的專案中,才能發覺它的重要性,舉幾個目前我遇到使用這個功能的專案需要它的原因:
  • 專案常常需要變更 Output Dir,因為開發環境跟安裝環境的路徑中,有些檔案可能不一致,所以編譯完成後,要把產出的檔案複製到固定目錄,讓安裝程式找到的都是最新版本的檔案。
  • 多個專案需要使用到共用的資源檔案,為了避免這些資源檔案有版本不一致的機會,在編譯 EXE 檔案前要先編譯資源檔 (*.RES),編譯好了之後,再把編譯出來的 RES檔案複製到各個專案目錄中。

介紹到這裡,相信大家都已經可以回頭在自己的系統中試試看怎麼用指令方式建置自己的系統了吧?如果要整合 GitLab/GitHub,相信也並不複雜,真的有問題,可以詢問 Embarcadero 的代理商,說不定我就會出現在貴單位,協助建置自動建置系統了。

問題一:版號如何維持最新?要用什麼方法更換版號與Debug/Release組態?

這是一個好問題,但如果你是比較積極的 Delphi 開發人員,應該不用問這個問題,可能你已經找到解決方法了。

這個方法就是:在 MSBuild 執行之前,先把 dproj 檔案裡面的 FileVersion 跟 ProductVersion 更換為我們想要的內容,我已經把這些邏輯寫成以下的程式碼,開放給所有需要的開發人員,可以免費商用,但請在產品的說明文件中提及作者與原始網址(就是本篇文章的部落格網址),程式碼在本篇文章的最後。

問題二:多個Pre-build/Post-build指令在 dproj 當中沒有問題,但為何用 MSBuild 指令建置的時候,會回報錯誤?這問題要怎麼解決?  

這問題就是本篇文章最開始的時候提到的,問題是因為在 dproj 存檔的時候,是以XML作為檔案格式,而多行指令存檔的時候,會以&作為換行的記號。

但問題來了,&符號是 XML 裡面用來標註特殊符號的保留字,但存檔的時候,XML會自行對它做 Encode,所以 & 符號就成了 sLineBreak + &&

這麼一來, MSBuild 執行的時候,遇到 & 符號就沒辦法處理(因為它應該是換行符號啊.......)。所以,我們得在執行MSBuild之前,把這個符號先換成一個 & 這樣一來,MSBuild 才能正確處理它。

MSBuild 使用方法很重要,使用時會遇到的兩個問題也為大家提供了解決方法,解決這兩個問題的程式碼如下,公開給大家免費使用,如果這個小工具有幫助,記得使用的時候發個 Email 給我,告訴我一聲,如果有遇到問題,也歡迎跟我聯繫一下:


program changeProjVer;

////////////////////////////////////////////////////////////////////////////////
/// Created by Dennies Chang dennies@ms4.hinet, dennies226@gmail.com
///
///   If you need to use this utility, please refer the original URL:
///   https://firemonkeylessons.blogspot.com/2019/04/delphiBuildCommandAndTools.html
///
///   And do not remve these lines.
///   The code is opened for all Delphi programmers, you can use it as
///   commercial/non-commercial usage, what you have to do, is to have a notice
///   for the original author.
///
///   And send an Email to dennies@ms4.hinet.net to me, thanks.

{$APPTYPE CONSOLE}
{$R *.res}

uses
   System.SysUtils, IdGlobal, Classes;

var
   currentFile, tmpStr, completeStr, tmpMajor, tmpMinor, tmpRelease,
       tmpBuild, configName: String;
   lineIdx: Integer;
   src: TStringList;
   bDebug : boolean;
begin
   try
      { TODO -oUser -cConsole Main : Insert code here }
      if ParamCount < 2 then begin
         writeln('Usage: changeProjVer.exe dprojFileFullPath versionNo [Debug|Release]');
         writeln('versionNo should be contain 3 dots, e.g.,: 107.1.108.321');
         writeln;
         Readln;
      end
      else begin
         currentFile := ParamStr(1);
         tmpBuild := ParamStr(2);

         bDebug := False;
         if ParamCount >= 3 then begin
            configName := ParamStr(3);
            bDebug := configName.ToLower = 'debug';
         end;

         tmpMajor := Trim(Fetch(tmpBuild, '.'));
         tmpMinor := Trim(Fetch(tmpBuild, '.'));
         tmpRelease := Trim(Fetch(tmpBuild, '.'));
         tmpBuild := Trim(Fetch(tmpBuild, '.'));

         if FileExists(currentFile) then begin
            src := TStringList.Create;
            try
               src.LoadFromFile(currentFile, TEncoding.UTF8);

               for lineIdx := 0 to src.Count - 1 do begin
                  completeStr := src.Strings[lineIdx];
                  tmpStr := '';

                  if Pos('<VerInfo_MajorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MajorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MajorVer>' + tmpMajor +
                         '</VerInfo_MajorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_MinorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MinorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MinorVer>' + tmpMinor +
                         '</VerInfo_MinorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Release>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Release>');
                     tmpStr := #9 + #9 + '<VerInfo_Release>' + tmpRelease +
                         '</VerInfo_Release>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Build>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Build>');
                     tmpStr := #9 + #9 + '<VerInfo_Build>' + tmpBuild +
                         '</VerInfo_Build>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('FileVersion=', completeStr) > 0 then begin
                     // FileVersion
                     completeStr := src.Strings[lineIdx];
                     tmpStr := '';
                     while Pos('FileVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'FileVersion=');
                        tmpStr := tmpStr + 'FileVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  // 這兩個會出現在同一行, 不要加 else
                  if Pos('ProductVersion=', completeStr) > 0 then begin
                     completeStr := tmpStr;
                     tmpStr := '';
                     // ProductVersion
                     while Pos('ProductVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'ProductVersion=');
                        tmpStr := tmpStr + 'ProductVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  if (tmpStr = '') and (tmpStr <> completeStr) then
                     tmpStr := completeStr;

                  src.Strings[lineIdx] := tmpStr;
               end;

               src.Text := StringReplace(src.Text, sLineBreak + '&amp;&amp;', '&amp;', [rfReplaceAll]);
               src.SaveToFile(currentFile, TEncoding.UTF8);
            finally
               src.Free;
            end;
         end;
      end;
   except
      on E: Exception do
         writeln(E.ClassName, ': ', E.Message);
   end;

end.