2019年8月20日 星期二

把報表元件丟進垃圾桶,省下錢跟你的生命(2)


用TFrame 建立漂漂亮亮的報表畫面

從 VCL 時代,TFrame就已經存在了,但自Delphi XE5開始,TFrame才成為Embarcadero官方推薦在行動裝置上搭配TTabControl作為畫面切換與遮罩的選項。TFrame對大多數的Delphi程式人員來說,並不是很熟悉的元件,可是其重要性,在FireMonkey當中是不可忽視的,且聽我細細道來。
在1990年代後期,物件導向程式設計概念蔚為風氣,人必稱物件,言必稱類別,彷彿不用物件導向就落伍了。當然,物件導向的概念很不錯,好好使用、好好設計,程式的效能、可維護性、可讀性都很很好,但凡事也沒有那麼絕對,簡單的迴圈就能解決的事情,也不必搞的那麼複雜。噗,歪樓了,物件導向是好的。
物件導向的概念中,都希望把常用的程式功能、元件封裝成物件,一方面可以讓相關的程式碼放在同一個或同一群程式檔案裡面,二來也省下重新撰寫類似邏輯的時間。這概念也沿用了重複使用(Reuse)的脈絡,省下時間作更好的程式,或者讓自己生活過好一點都好。
但是,視窗的視覺元件,總是只能自己從dpk裡面製作,作一個元件比製作一個程式要複雜的多,而且要撰寫的畫面互動、重繪等程式碼,也是很多程式人員所不熟悉的,所以,視窗的畫面很難套用到快速的物件導向程式設計概念。
例如這個章節的標題後半段『建立漂漂亮亮的報表畫面』,建立一個報表畫面,要用什麼元件?過去很多人用Fast Report, Crystal Report, Quick Report這些報表元件組,但作一個報表就是一個Form,要怎麼把報表的Form跟主畫面的Form合在一起?我們沒辦法!所以在行動裝置的App裡面就沒辦法做了,因為行動裝置App只有一個畫面,之前也無法用Form.ShowModal來顯示其他Form到手機或者平板的螢幕上。
但TFrame的改進,讓我們可以把Form原本的角色用TFrame來取代了。TForm原本就是單一一個畫面的容器(Container),我們可以把各種視覺元件放在TForm上面,構成完整的畫面。如果您使用TForm更深入一點,甚至還可以把TForm註冊到專案當中,讓Delphi在新增一個Form的時候,可以從已經存在的Form繼承下來。節省了很多時間,但是,問題還是存在:TForm沒辦法合併到已經存在的畫面上頭!
所以我們改用TFrame,新增一個TFrame,把它當成TForm一樣來使用,步驟很簡單,我們就來作個範例:建立一個條碼繳款書。

首先建立一個簡單的Multi-Device專案,當然就會自動建立一個表單出來,我們先把它存檔,就叫做FMXPrintSample,接著,我們在專案裡面新增一個TFrame,畫面步驟如下:
1.    在專案上面按滑鼠右鍵,選擇Add New,然後在子選單上面選Other

2.    在跳出來的選單畫面上選FireMonkey Frame

3.    把這個新增的檔案儲存下來,我把它存為Unit_BillFrame.pas 這個檔案
這樣一來,我們就有一個空白的畫面了。我會把這個畫面的寬跟高設定成2100x2970,正好是A4直式列印的大小,沒錯,我就是希望它是A4的大小,用這個Size列印到A4直式上面,解析度夠高、比例也不會變形,多好啊。
我先把它當成TForm來使用,把它拉成報表畫面,然後註冊到元件盤上面:在新製的Frame上面用滑鼠右鍵點選,就會有底下這個畫面出現,在選單上選擇 Add to Palette:

點選以後,會出現要我們填寫元件盤分頁的頁籤名稱,在這個例子裡面,我把它命名為FMXSampleProject:

按下OK以後,元件盤就出現了FMXSampleProject 這個頁籤,裡面只有一個元件,就是TFrameTestBillTemplate

元件註冊好了,我先來說明一下這個TFrame元件的畫面構成吧,從下面的截圖來看,可以看的出這張帳單很類似坊間一般電信、銀行的帳單。沒錯,我是基於電信帳單來作範例的。

這張帳單一共分為三個部分:最上面的寄件者資訊、中間的客戶與帳單資訊,以及最底下的條碼資訊,在本篇文章的結尾,我把完整的範例程式附給大家做練習了,這個專案可以用Delphi XE 10.2 或 Delphi XE 10.3來開啟,用Community edition也沒問題,因為純粹做離線列印功能,沒有用到Enterprise或Architect版本的特殊功能。
這三個部分,都是用TRectangle作為容器,讓三個部分的資訊可以整齊排列的,我們一一分解來做說明。
最上面的寄件者資訊,左邊的恐龍圖案,是使用TImage放上去的,Align設定為 Left,文字的部分則先用一個TText(下圖裡的Text1),Align設定為Client把其餘的畫面先全部填滿,文字大小設定大一點,填寫內容為『恐龍團購』。然後再設定這個 TText的Margin,Top設定多一點(100),讓恐龍團購上面的留白多一點,可以跟恐龍圖案對齊,文字對齊靠左,所以要設定TextSetting.HorzAlign為Leading,這個值就是靠左了。


接著下來的住址(Text2)、電話(Text3)、Email(Text4),再一一放到恐龍團購這個TText裡面(請看Structure裡面,它們都是恐龍團購的子元件),只是記得住址這個 TText的Margin,Top也要設定多一點(90),讓它可以把位置顯示在恐龍團購的下方。
這樣一來,最上方的區塊-寄件者資訊就完成了,其實這跟原本大家習慣的TForm編排畫面並沒有太大差異,頂多就是文字可以放進文字作為子元件這作法大家比較少看到,我只是野人獻曝罷了。
接下來的第二個區塊,則是先用一個TRectangle作為外框,我把它的框線(Stroke)寬度(Thickness)設定為3,所以出現了一個比較粗的外框,內部顏色填空(Fill)則是設定為#FFFFFFFF,也就是白色,如果您是第一次使用Delphi的TAlphaColor的話,請留意一下,這是四組0-FF的數字組合,從左到右分別代表:透明度、R、G、B這四個顏色的屬性,如果您對於網頁設計或者是Photoshop的使用並不陌生,那這個顏色碼應該也不會太少見。
在第二區塊,也就是收件人的資訊裡,我只放了五行文字,但值得注意一下的地方,是這五行文字都有內嵌另一個文字。也就是:『客戶姓名』內嵌了『容易騙』這個文字元件,從下面這個截圖可以清楚看出,Text10有Text_CustomerBillAmount、Text5有Text_CustomerName,這樣的好處,是以後要調整排列的時候,只要拉動一行就好了,不用拉了客戶姓名,又要拉裡面那個文字元件,拉兩次的壞處,是操作上麻煩,對齊上也麻煩。


如果您跟筆者一樣,對畫面構成的整齊有一點龜毛,那這個作法絕對很對您的胃口啊。

第三區塊,是超商條碼的顯示區。如果您已經迫不及待的把範例程式下載、用Delphi打開了,卻看不到條碼???
別緊張,那是因為您還沒安裝條碼字型,在專案目錄裡面,我附上了一個39Code的免費TrueType條碼字型,請先把這個條碼字型複製到您的Windows字型目錄,再回來看畫面,您就可以看到條碼顯示出來了。
在這個範例程式裡面,我用TTabControl在主畫面上頭放了三個頁籤,第一個是MainPage,提供您做測試,可以輸入這五個欄位的資訊,輸入以後,切換到Bill頁籤,就可以看到您修改的文字直接顯示在條碼帳單上面了。


但是,三個條碼的內容我沒有改,這個我想留給大家做練習。
在MainPage的畫面裡,我透過四個TEdit欄位的OnChangeTracking事件來修改對應文字的變化,只要您輸入了任何一個字,Bill頁籤裡面的文字就會自動跟著改變。這個事件跟VCL裡面的事件不太一樣,並不做任何過濾或判別,如果要判別,請使用OnKeyDown或OnKeyPressed事件,就可以過濾內容了。
在條碼顯示上面,我們必須要注意,真正要掃瞄出來的條碼內容,前後都有加了一個*符號,例如要讓條碼掃描槍掃出0810016AE,我們得在設定了條碼字型的TText元件的Text屬性放進*0810016AE* 這串字,也就是前後都加上一個 * 符號。
用條碼字型的好處,就是什麼程式也不用寫,把要顯示條碼的TText元件Font.Family設定為Free 3 of 9即可。當然,禍福相倚,有方便的地方,就會有不方便的地方,不方便的地方就是我們得在安裝程式中,把這個字型檔案複製到Windows的字型資料夾裡面去。如果是在iOS或Android,則放在程式的Document目錄裡面即可。
這份報表的複雜度並不高,我們先Demo如何製作單頁的報表,下一篇裡面再來弄一個估價單、多頁報表,先預告一下就好,不囉嗦,繼續把列印跟生成PDF的作法介紹完。

把漂亮的報表畫面印到印表機跟PDF檔案裡面

做好了漂亮的報表,也已經把它註冊到元件盤上面了,也就是說在Form裡面我們可以把它拖拉進來了,在範例中,我們要把它放到Bill頁籤裡面來。
我並不是直接把這個Frame直接拉進來就好,因為我還想在上頭顯示一個『列印』、『另存PDF』的按鈕,並且讓使用者能夠在視窗改變大小的時候,可以用拖拉的功能檢視整份報表。
所以,在這個頁籤裡面,我先放上了一個TRectangle,Align設定為Top,用來放置『列印』、『另存PDF』這兩顆按鈕。接著我放了一個TFramedScrollBox上去,這個元件超好用,放在它裡頭的元件如果超過TFramedScrollBox自己的大小,拖拉Bar就會自己出現,如果沒超過,拖拉Bar就會自動隱藏。
接著,我也擔心報表Frame的大小太大,可能有縮放的需求,所以在TFramedScrollBox裡面,我又放了一個ScaledLayout上去,用以讓ScaledLayout裡面的元件可以依比例縮放,這樣一調整之後,Bill頁籤的元件結構就變成了下圖左方的Structure這樣,而畫面就成了下圖右方的長相:


從上圖的畫面中,可以看到我在畫面上還放了一個PrintDialog,待會就用它提供給使用者選擇要列印的印表機。

列印到印表機

列印到印表機,跟另存PDF這兩個按鈕,我做了一個共用的function,叫做printBillSinglePage,程式碼如下:
procedure TForm2.printBillSinglePage(bShowPrintDialog: Boolean);
var
   SrcRect, DestRect: TRectF;
   sshot: FMX.Graphics.TBitmap;
begin
   if bShowPrintDialog then begin
      self.PrintDialog1.Execute;
   end;

   Cursor := crHourGlass;
   Printer.Orientation := TPrinterOrientation.poPortrait;
   sshot := nil;
   try
      // 把帳單畫面作截圖, 不管畫面內容是否能夠完全顯示在螢幕上,
      // 內容都會完整被做成截圖.
      sshot := TFrameTestBill1.MakeScreenshot;
      SrcRect := TRectF.Create(0, 0, sshot.Width, sshot.Height);

      // 印表機初始化
      Printer.BeginDoc;
      DestRect := TRectF.Create(0, 0, Printer.PageWidth,  
          Printer.PageHeight);

      // 把截圖輸出到印表機去
      Printer.Canvas.DrawBitmap(sshot, SrcRect, DestRect, 1.0, True);
   finally
      Printer.EndDoc;
      if Assigned(sshot) then
         sshot.Free;
      Cursor := crDefault;
   end;
end;

列印按鈕的點擊事件就很簡單:
procedure TForm2.btnPrintBillSinglePageClick(Sender: TObject);
begin
   self.printBillSinglePage(True);
end;
這個作法是FireMonkey內建的印表機操作,所以可以適用不同平台的印表機喔,操作也很方便,跟本文的第一部分很像吧?

另存為PDF

另存為PDF檔案的功能,就只限於Windows 10使用了,因為Windows 10裡面終於內建了Print to PDF這個PDF的文件產生印表機,看名稱很像是從XPS印表機進化而來的,不囉嗦,直接來看該按鈕的點擊事件(btnSaveSingleBillAsPDFClick)程式碼,在這段程式碼裡面,我先判斷是否為Windows 10,接著從所有印表機裡面抓出名稱裡面有 PDF 字樣的印表機來用:
procedure TForm2.btnSaveSingleBillAsPDFClick(Sender: TObject);
var
   printerCount, idx: Integer;
   bFoundPDFPrinter: Boolean;
   selectedPrinter: TPrinterDevice;
begin
   if TOSVersion.Name = 'Windows 10' then begin
      // 先找出系統中到底有多少個安裝好的印表機.
      printerCount := Printer.Count;

      selectedPrinter := nil;
      if printerCount > 0 then begin
         bFoundPDFPrinter := False;
         idx := 0;
         while (idx < printerCount) and (not bFoundPDFPrinter) do
         begin
            // 我要找名字裡面有 PDF 的印表機
            bFoundPDFPrinter := Pos('PDF',
                  Printer.Printers[idx].Title) > 0;

            if not bFoundPDFPrinter then begin
               Inc(idx);
            end
            else begin
               // 找到了, 把它指派給 selectedPrinter 變數
               selectedPrinter := Printer.Printers[idx];
            end;
         end;

         if bFoundPDFPrinter then begin
            // 把找到的 PDF 印表機設定成選擇的印表機.
            Printer.ActivePrinter := selectedPrinter;

            // 把印表機的 DPI 設定成 1200x1200, 檔案不會太大的
            Printer.ActivePrinter.SelectDPI(1200, 1200);

            // 直接呼叫列印的 function, 在
            // Printer.BeginDoc 的時候會問使用者要存的檔名.
            self.printBillSinglePage(False);

            // 跑完了, 顯示個訊息告訴使用者存好了.
            // 但我還不知道列印失敗的情況要怎麼判斷
            TDialogService.MessageDialog('PDF 檔案儲存完畢',
                TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK],
                TMsgDlgBtn.mbOK, 0, nil);
         end;
      end;
   end
end;

這樣很簡單吧? 算簡單啦,但看完以後,大家會問說,那能不能直接指定要儲存的PDF檔案檔名跟路徑? 當然可以,請把檔名(完整路徑跟檔名)直接指派給Printer.Port 這個屬性就可以了,或許您會嚇到,『Port???』沒錯,就是Port,我也找了很久才找到的,很難以置信,但它就是Work, so?


另存為 PDF 的這段程式碼裡面,有一段TDialogService.MessageDialog的程式呼叫,是跨平台的ShowMessage,如果您看了不習慣,也可以使用ShowMessage取代它,效果是一樣的。

第二部分就到這裡,看完了本文,您應該學會了TFrame的製作,以及怎麼把它放到程式畫面中、怎麼把它印出來了。但這還只是單一一頁,如果您很習慣操作動態建立元件,那麼就可以先試試看動態建立多頁報表來試試看,第三部分的文章剛剛已經有預告過,會弄一個估價單、多頁報表的動態產生,這樣就可以完整的用FireMonkey來做自定的報表了,您的報表元件還在嗎?還沒丟進垃圾桶嗎? 是時候考慮一下了。

本文章的範例專案可以從這裡下載

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,來連接到印表機,再以Write、Writeln把文字資料寫到印表機去。但這個做法在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 => 是否高速列印 (高速列印一般應該就是品質較差)
透過這些程式碼,就可以把圖片、文字印到印表機去了,如果需要選擇印表機,請使用Delphi的TPrinterDialog讓使用者選擇要列印的印表機即可。
第一部分就先講基礎的操作,後面我們再來講複雜的報表要怎麼作吧。
有些夥伴可能會想問,在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年(含Ada與Pascal)的Pascal程式語言,漸漸被物件導向概念夾擊,先有挾C語言廣泛性優勢的C++出現,到了1997又有號稱全物件導向、可以透過虛擬機跨越所有平台、以沙盒概念提升安全性的JAVA來襲,整個資訊學界言必稱JAVA,語必稱物件,傳統的Pascal失去了學界的關愛眼神,從1998年開始,全台再沒有學校教授Pascal。
然而,Borland在1994年推出了Delphi,以Pascal為基礎語言,更為Pascal加入了物價導向的概念,成為Object Pascal,整個Delphi的物件導向概念與視覺化元件不斷優化,到了Delphi 5的時候大致完備,當時是1996-1997年間,然而JAVA當時也還沒完備,除了文件得印上幾大本,當年連編譯器都還沒有出現,遑論虛擬機。
到了2000年,Borland 負責Delphi與Pascal的靈魂人物Anders Hejlsberg被挖角到微軟,把原本沒能在Delphi跟Object Pascal語言上實現的設計與理想用在微軟新一代的平台跟語言設計上,於是就誕生了.Net Framework跟C#語言。接著Borland策略錯誤,以為可以做出Delphi 8成為跨.Net與機器語言的工具,沒想到微軟四兩撥千斤,2004年以不授權.Net Framework給Borland為戰技,一下就讓已經要上市的Delphi 8無法推出,Delphi只好回頭繼續改良原本的工具,但也就此流失了關鍵的3年,直到2005年初才又推出Delphi 2005,但效能不如Delphi 7,介面又大幅度改變,所以絕大多數的Delphi 7使用者都不升級,歷經Delphi 2006、2007,都一蹶不振,於是Borland把開發工具部門獨立出來,成立了CodeGear,Borland則專注於軟體開發流程控管的產品,後來越發寂寥,直到現在已經很多年沒聽過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用來開發MacOS、iOS、Windows 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 bit跟Linux程式的編譯功能。
歷史總是有很多有趣的地方,也有很多令人感傷的地方,但大多數的演進都已經介紹過了,我們開始進入主題,從技術面介紹一下新的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當中,提供了Windows、MacOSX、iOS、Android與Linux五大平台的元件,而尤其值得一提的是『單一專案,可編譯五種平台程式,各種程式都是原生機器碼!』
在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角色』的設計,讓程式畫面可以集中在同一個畫面中。
這個設計從XE到XE5不斷的被驗證與優化,在XE5之後,FireMonkey提供了TFrame這個元件,讓我們可以把一整個畫面的程式碼跟表單(其實是Frame, 不是Form)獨立為單獨的檔案,這樣一來,所有的畫面雖然保留在同一個Form上面,但不同的畫面呈現可以被儲存在不同的Frame裡面。
很多公司把單一檔案的Size做了限定,希望程式碼可以不要集中在某幾個檔案裡面,這固然可以降低單一檔案的Size,使得Windows 32bit與Delphi 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。

Big5與UTF8

在我所看過的,歷史在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陣列指標,然後用陣列元素來存取當中的資料,如果有這樣的情形,請記得把這些資料的處理改用TRawByteString或TBytes來處理。
從Delphi 2009之後,String型別預設就是WideString。而在Delphi 1到Delphi 2007當中,String型別預設則是AnsiString,長度是不同的,所以用陣列元素來取資料,會發生全部錯誤的狀況。
資料庫元件:通常傳統的程式中,使用的資料庫連線可能是DBExpress、BDE、也可能是ADODB或ODBC,但到了Delphi XE 10之後,全數資料庫元件都更新為FireDAC元件系列,表單畫面上一定會找不到這些元件,這部份的修改是最費時的。
接著如果要改寫成FireDAC,需要修改的應該只有Connection元件、連線設定、Table/Query/Transaction元件的更換,至於SQL指令則不太需要更改。
還是那句話,這些分析都是輕巧的幾句話,但真的在裡面修改、更換元件,可能會是嘔心瀝血的過程,遇到問題的話,大家可以分享、交流一下經驗。
許多公司仍舊堅持著使用Delphi,可能是因為過去的Code Base很大,無法很快換掉,也可能是因為發現了Delphi原來的優勢、未來的優點。不可諱言的,Delphi的程式人員不像過去那麼多了,但這也是好事,表示濫竽充數的人也少了,我們不用花費太多的成本去抓出這些人來Fire掉。人才很多,帶人帶心,如何找到適合的人才,讓他願意為公司服務、在公司成長的路上一起成長,是每個公司的課題。

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