2016年4月9日 星期六

用遮罩來把 TBitmap 裁切成我們需要的形狀

緣起


以往在 VCL Framework 裡面, 我們可以使用 Windows GDI 的相關 API 來處理圖片, 讓原本方方正正的圖片依照我們的需要切成各種形狀。但這個功能在使用FireMonkey的時候, 卻因為Windows GDI無法跨平台而無法使用在其他裝置上面了,但把圖片切成各種形狀的需求還是很常出現,那怎麼辦呢?

在FireMonkey裡面,TBitmap這個 Class 提供了對 Canvas 處理的許多方法可以使用,其中的 CreateFromBitmapAndMask 方法,可以透過一個黑白的圖片當成遮罩來做這個自定形狀的裁切功能。

這個功能在許多網路的文章裡面都有提到過,所以我也不獻醜了。要跟大家分享的是在使用這個方法的時候,需要額外注意的地方。

在Embarcadero 的 docwiki 上面, 有關於 createFromBitmapAndMask 的範例程式,大家可以點連結去看看,但是這個範例程式沒有完整的專案,因此究竟什麼樣的 Mask 可以完美的讓我們把需要的區塊給截下來,還是得試一試。

在範例中,或者我們自己搜尋的網路範例中(山本隆的開發日誌示範的很好,我也從中獲益許多),都可以看到擷取圖片部分區塊的作法,是需要一個黑白圖片作為遮罩的,遮罩當中白色的區塊會被保留,黑色的區塊則會被刪掉。

問題來了

看到這裡,我好高興,感覺什麼都能做的出來了,但是,事情總是沒有想像中這麼順利啊,在實際操作上,我發現了以下的問題:

  • Runtime製作出來的 TBitmap 變數,怎麼都沒辦法弄出圖像啊!
  • 透過這個方法所截出來的圖片,黑色的區塊刪的不乾淨啊!還有矇矇看見原來的圖像,只是沒有很明顯,刪不乾淨怎麼辦啊!

問題一: Runtime製作出來的 TBitmap 怎麼都沒辦法弄出圖像

在 docwiki 的範例程式碼裡面,我看到需要傳入 Bitmap 變數當成參數,所以就想著自己寫個 procedure, 在當中建立 Bitmap, 用完再 Free 掉,相信大多數的人也會有相似的想法,所以我就寫成了這樣一段程式碼:

      LBmp := TBitmap.Create;
      TBmp := TBitmap.Create(127, 127);

      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);
      finally
         LBmp.Free;
         TBmp.Free;
      end;

乍看之下,邏輯沒有問題,但大家可以試試看, 在 finally 之前把 TBmp 或 LBmp 指派給一個 TImage.Bitmap,保證完全透明,什麼都看不見......

要解決這個問題,需要把程式改成以下這樣,只加了兩行:
      LBmp := TBitmap.Create;
      TBmp := TBitmap.Create(127, 127);

      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBmp.Canvas.BeginScene;
         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);
         TBmp.Canvas.EndScene;
      finally
         LBmp.Free;
         TBmp.Free;
      end;
也就是在進行對 Bitmap.Canvas 繪製的時候,一定要先呼叫該 Canvas 的 BeginScene方法,畫完了,要記得呼叫EndScene方法,不然這些DrawBitmap, DrawLine, DrawRect都不會生效。在沒有加入這兩行之前,在進入 finally時,大家可以試著設定一個 Breakpoint, 會發現TBmp.Canvas 居然是 nil......

因此,動態生成的 Bitmap 物件,在建立之後如果要對它的Canvas做編輯,記得一定要先呼叫 Bitmap.Canvas.BeginScene, 編輯結束也要記得呼叫 Bitmap.Canvas.EndScene, 這樣才不會白費工夫。

問題二: 透過遮罩做出來的裁切圖片,想裁切掉的地方弄不乾淨.

這個問題,筆者弄了一個晚上,想著以前用 windows GDI 相關 API的相似方法,找到了一個替代的方法,思路如下:
  1. 用遮罩切掉的周邊有切掉,只是切不乾淨,遺留著一些半透明的點。
  2. 那我用跟遮罩完全相同的一張圖,把遮罩原本白色的區塊用Photoshop 挖成透明,黑色區塊用特定的顏色填上,用這張圖畫上Canvas,原本有半透明點的區塊就會變成特定色,已裁好的區塊則不受影響。
  3. 再把這個特定顏色換成透明色,完成。
換成程式碼,就寫成這樣:
      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBmp.Canvas.BeginScene;
         TBmp.Canvas.Clear(TAlphaColor($00000000));

         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                 self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);

         TBmp.Canvas.EndScene;
         self.replaceBitMapColorWithAnotherColor(TBMP, TAlphaColor($FFFE00FE),
              TAlphaColor($00000000));

         self.Image8.Bitmap := TBmp;
      finally
         LBmp.Free;
         TBmp.Free;
      end;
   end;

在上述的程式碼當中,我還模仿了 Delphi FireMonkey 的 TBitmap.ReplaceOpaqueColor 方法,寫了一個 replaceBitMapColorWithAnothercolor 的方法,可以對參數中的 Bitmap 元件裡的特定顏色進行換色。

procedure TForm2.replaceBitMapColorWithAnotherColor(bitMap: TBitMap;
  originalColor, newColor: TAlphaColor);
var
  I, J: Integer;
  M: TBitmapData;
  C: PAlphaColorRec;
begin
  if bitMap.Map(TMapAccess.ReadWrite, M) then
  try
    for J := 0 to bitMap.Height - 1 do
      for I := 0 to bitMap.Width - 1 do
      begin
        C := @PAlphaColorArray(M.Data)[J * (M.Pitch div 4) + I];
        if C^.Color = originalColor then
           C^.Color := newColor;
      end;
  finally
    bitMap.Unmap(M);
  end;
end;

在本文的範例專案中,大家可以直接試試看效果,這個範例專案中,我提供了一張圖片放在最上方,我們可以用滑鼠游標點擊任何一個地方,以被點到的座標點為中心,會先複製出一張 127x127 大小的圖片。


上面的這張圖片,是尚未使用遮色片做顏色置換的版本,底下是遮罩圖片,上方則是使用遮罩圖片製作出的裁切結果。

以這張圖片,我用6張不同內容的圖片做遮罩,做出裁切的效果,範例專案最左邊的第一張圖片,已經套用了前述的圓形遮罩,並用#FE00FE 這個顏色作為塗色片,最後用換色的程式碼把該顏色換成 TAlphaColor($00000000),這麼一來,效果差強人意。

但圓形的遮罩,邊邊難免會有輕微的混色,所以建議大家在做遮色片的時候,選擇跟您程式外框比較接近的顏色來做遮色片,這樣就算邊邊稍微混了一點顏色,也不容易被發現喔。

這個作法, 可以用來作為 ListView 需要把方形圖片換成圓形顯示,也可以把方形圖片變成扇形、橢圓形,總之,只要有想要的遮罩,就可以裁切成各種形狀了。