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也沒問題,因為純粹做離線列印功能,沒有用到EnterpriseArchitect版本的特殊功能。
這三個部分,都是用TRectangle作為容器,讓三個部分的資訊可以整齊排列的,我們一一分解來做說明。
最上面的寄件者資訊,左邊的恐龍圖案,是使用TImage放上去的,Align設定為 Left,文字的部分則先用一個TText(下圖裡的Text1),Align設定為Client把其餘的畫面先全部填滿,文字大小設定大一點,填寫內容為『恐龍團購』。然後再設定這個 TTextMarginTop設定多一點(100),讓恐龍團購上面的留白多一點,可以跟恐龍圖案對齊,文字對齊靠左,所以要設定TextSetting.HorzAlignLeading,這個值就是靠左了。


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


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

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


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

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

做好了漂亮的報表,也已經把它註冊到元件盤上面了,也就是說在Form裡面我們可以把它拖拉進來了,在範例中,我們要把它放到Bill頁籤裡面來。
我並不是直接把這個Frame直接拉進來就好,因為我還想在上頭顯示一個『列印』、『另存PDF』的按鈕,並且讓使用者能夠在視窗改變大小的時候,可以用拖拉的功能檢視整份報表。
所以,在這個頁籤裡面,我先放上了一個TRectangleAlign設定為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來做自定的報表了,您的報表元件還在嗎?還沒丟進垃圾桶嗎? 是時候考慮一下了。

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