BloggerAds

2016年4月8日 星期五

用遮罩來把 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 需要把方形圖片換成圓形顯示,也可以把方形圖片變成扇形、橢圓形,總之,只要有想要的遮罩,就可以裁切成各種形狀了。


2015年11月27日 星期五

如何建立自己的 Xcode .a 檔案

在使用 Delphi 建立自己的 app 時, 有時候會需要使用到第三方廠商的 source code, 這些 source 如果是用 Xcode objective-c 撰寫, 用來提供 iOS 的某些功能, 直接使用這些驗證過的程式碼, 會比自己照著原始碼重寫一次來的快、來的可靠。


所以,最快的方法就是用 Xcode 把這些原始碼 (通常會是 .m 跟 .h 檔案) 建立一個 .a 檔案, .a 檔案是靜態函式庫, 等同於在 C 語言裡面的 .lib 檔, 連結後就不能動態改變執行檔的內容了。

以下是建立 .a 檔案的簡單步驟說明:

1.     使用 XCode建立一個新的專案

      

2.     選擇 Framework & Library -> Cocoa Touch Static Library, 然後選Next












3.     填寫 library 的基本資料, 任填就行了.
 
4.     選擇一個 folder 來存放專案檔案, 我選擇在 iOS SDK8 目錄下, Xcode會自動建立專案目錄

5.     finder找到要加入的.m .h 檔案, 拖拉進 Xcode 專案中


6.     選擇編譯的專案組態, 必須對Device Simulator分別都進行 Build.



7.     Device Simulator 都編譯好了之後, 找到製作好的 .a 檔案, 快速找尋方法: Products 點選 lib, 然後用滑鼠右鍵點選它, 選擇 Show In Finder, 就會出現一個新的 Finder 視窗, 顯示該檔案的位置。





8.     上面的 Finder 是顯示 Debug-iphones Debug-iphonesimulator兩個目錄裡面的libmyPackage.a 檔案.
9.     iphoneos 目錄裡面的, 是給實際裝置使用的 .a 檔案, iphonesimulator目錄裡面的, 是給模擬器使用的 .a 檔案, 我們可以透過以下的 command line 來把兩個檔案合一, 檔案會變大, 但就不用選擇不同的檔案了, 這個指令的用法是 lipo –create 第一個.a 第二個.a… -output 合併檔案

Mac OSX 裡面, Terminal (終端機)有個很方便的設計, 就是可以從 finder 把檔案直接拖拉到終端機畫面, 可以直接把該檔案的完整路徑拖拉進去:




然後 key in –output, 再拖拉 folder 進去:

10. 輸入檔名, 剛剛指令已經完成輸入到 –output 目錄 , 我們把檔案名輸入一下, 本例中是 libmyPackage.a
11. 按下 Enter, lipo 就會執行, 把兩個 .a 檔案合併成同一個, 完成。

補充: 預設建立的 .a 檔案是 debug 模式的, 裡面會有 debug information, 如果要建立 Release 的版本, 則需要修改一下 Build Configuration:


點選 Edit Scheme, 會出現設定畫面:
下拉 Build Configuration, 選擇 Release 即可.
重複6-12步驟, 這次要選擇 Release-iphoneos Release-iphonesimulator當中的 .a檔案, 建立出來的 .a , 就會是 Release 版本的了.

注意事項: 如果第三方的 source code 裡面有使用到 Delphi IDE 預設匯入的 iOS framework 以外的 framework, 也要記得把他們透過 Delphi IDE 的 Option->Tool->SDK Manager 匯入, 才能正確運作喔.

2015年10月22日 星期四

Indy - TCP 之一 - 以 TIdTCPServer 自製檔案伺服器 (1)

緣起

自 2001年9月, 我的第四本書- Delphi/Kylix Indy 網際網路程式設計出版之後,忙工作,忙教學,不知不覺也過了十幾年。 (好可怕啊~~~ 時光艿苒就是這種感覺嗎?)

在這期間我也曾經發過幾次神經,想要把該書改版一下,當時撰寫的時候,那時空背景是 Delphi 7, Kylix 3, Indy 8.0.25 轉 Indy 9 的時候,現在 Delphi XE7 都舉行了預覽發表會,Kylix 已經緲緲不知所蹤,Indy 都到 10.5 以後的版本了。

但曾和幾家台灣地區較為知名的出版社接洽出版,得到的回音不外乎:『目前還有人在寫 Delphi 喔?』『現在出寫程式的書沒有人要買了,如果你寫 PhotoShop 怎麼修照片的書,我們就出』,而我對於 PhotoShop 的使用,只停留在剪貼、放大縮小、模糊、去背的基本使用,所以還是不獻醜了。

從 2001年到現在2014年,13個年頭過去了,對於Indy相關的應用不少,但真正用到Indy強大功能的不多。這幾年從沒間斷在程式寫作上的浸淫,Delphi 除了 2009-2011之間,我把重心稍微轉到 Objective-C 的階段之外,也從來沒放下過,但看到對Indy真的運用的很棒,居然只有 Embarcadero.

別說 Embarcadero 是原廠,所以他們就自然會對Indy 熟悉,這是牽強的說法,因為Indy從 WinShoes 開始,都是OpenSource 的軟體元件,只要你肯花點時間,一定能很快熟悉它,甚至寫些元件發佈給 Indy 用,君不見,TIdDNSServer 就是筆者所寫?

扯遠了,拉回來。

Embarcadero 這幾年在 Delphi XE3 之後新增的 Server 端元件,不僅是傳統的 DataSnap server 支援了 TCP/IP與 REST 兩種模式,用的就是 Indy 元件來寫的,大家可以花點時間進去 C:\Program Files (x86)\Embarcadero\Studio\14.0\source 這個目錄看看, 裡頭真的花了很多心思,用 Indy 用的非常精闢。

舜何人? 予何人? 有為者亦若是!

筆者可能算是比較老派的,所以老愛掉書包,我們也別落人後,今天就來個範例,讓大家寫寫 Indy 小程式,做做傳遞檔案的功能吧。

可能您會說:我用 FTP Server 就好了,我用 Apache 加 PHP 用 Upload File 來做就好了等等。

我的回應是:都很好,能達成功能,不傷身體就好,但多學一種方法也不錯,最近不是有個廣告,比別人多會一招,勝算就多一成?

就來多學這一招半式吧。

使用時機

什麼時候會用到這東西? Indy 會飛嗎?

別再亂鬧了,哪天你的專案需要傳檔案,你就等著哭吧!

如果哪天專案要把資料回傳到 Server, 而這些資料又是檔案,架設 FTP 是OK的,到時候你還是得寫個 FTP Client 來上傳, 而且設定也還是得花時間寫的。

不多說,來看一個簡單的 TCP Server 是怎麼運作的吧,FTP Server 跟 HTTP Server 都不例外喔。

TCP Server 的運作,就是從啟動開始,先建立一些等待的 Queue, 也就是上圖裡面的等待隊序,像 TIdTCPServer 預設的 ListenQueue 數量就是 15 個, 當然可以隨我們的需要來修改這個數字。

在等待的過程當中,一旦有使用者連線進來了,就會由其中一個線程來處理這個使用者的需求,等這個使用者的需求處理完畢,該連線就會由 Client 或 Server 端來切斷。

命令有哪些?

這個問題很好,既然現在我們正在製作一個新的 Server, 也就沒有傳統命令的包袱,我們可以自己來決定,以下我們假設要讓特定的 Client 端傳送檔案到 Server 來,所以可以來定義幾個簡單的命令,你也可以把這些個命令當成通訊協定,因為本來這就是一個自定的通訊協定。

我們定義有以下幾個簡單的命令:
filename
filereplace
fileresume
sendfile

命令的順序要正確,不然就從 server 端強制斷線,我們定義順序如下:
Client                                                          Server
 "sendfile Open-Sesume" -------------------->

             <----------------------------- "200, OK, Please send file name, size, data"

"filename 要存放在 server 端的檔案路徑"--->

             <----------------------------- "200, ok, ready for get file: 檔案路徑"

"filesize 檔案大小" ------------------------------>

"檔案內容" ---------------------------------------->

             <----------------------------- "200, ok, file accepted"

Server 端只存放 filesize 後面所帶的數字描述的資料量,多的全放棄,接著,Server 斷線。

講到這裡,概念說完了,就是 Client 跟 Server 之間把字串送來送去,如此而已,接著,我們要來看怎麼用 Indy 達成這個要求了,請接著看Part2.

2015年5月11日 星期一

Delphi XE7, XE8 在 Windows XP 上面佈建時要注意的事項

緣起

之前有個專案, 目標平台是 Windows XP, 這個專案的 Scope 是要把原本記載在 XP 上面的 Access MDB 檔案資料, 透過 Someway 回傳到 Web server 上, 好讓管理人員能夠 Centralize 管理各個不同節點的資料.

好死不死, 我的 Windows 開發系統早從 2010 年起就已經全面 VM 化, 且從那時候就一直保留在 Windows 7 上面, 因為真的又快又穩定.

直到 2014 年, 我的開發系統已經是 Windows 7 + Delphi XE7, 所以對於 Windows XP 已經有種日薄西山的感覺.

在開發這個系統的過程當中, 並沒有特別注意什麼, 就很順利的用 FireDAC 連接遠端的 MySQL server,  Client 端用 idHTTPServer 作為 WebServer, 中央的 Web Server 在用戶從網頁進行資料同步索取的時候, 發一個 jquery 向 idHTTPServer 指定要索取的資料的日期、並且給 session string, 以及資料的 offset number.

IdHTTPServer 在接到 Request 的時候, 透過 TIdCommander, 建立一個 Thread, 把 server 端傳來的資料 select 出來, 然後一筆一筆寫成單一一個 Insert 指令, 用 FireDAC 連接 MySQL 資料庫, 直接執行 Insert 指令, 把資料全寫進去.

在 Windows 7, Windows 8 上面, 執行的超快樂的, 去年的實習生對於這種架構, 完全就是看傻了眼, 壓根沒想過能這樣搞, 但跑的超順的, 他也 Happy 的把 PHP 程式搞定了.


直到去幫業主做 Deployment 的時候, 這下是我傻眼了, 四個營業點, 用的系統都是 Windows XP, 其中三個更是 Windows XP HOME.......

看倌們應該有些已經跟我一樣, 心裡咒罵連連了吧.
Windows XP 也就算了, Windows XP "HOME", 這就是說, 系統一開始就沒打算讓外面的機器或網路能夠連進去 (當然網路芳鄰可能是唯一例外吧, 是叫我們這些寫 TCP/IP Protocol 去跳樓嗎?)

和業主進行反映之後, 業主先跟設備商詢問是否有升級為 Windows Vista/Windows 7/Windows 8的可能性?

設備商的反應是: 沒辦法, 因為他們的 VB6 程式, 在控制實體設備的時候, 用的是 Windows Message 來傳遞同步資訊跟指令的, 他們不會, 也沒有打算升級到 Windows XP 之後的技術.

這一點, 筆者在 2008 年有吃到苦頭, 在 2 個月當中, 把原本用來做 IPC (Inter-Process Communication) 的 Windows Message 改成 Named Pipe, 這是在 Vista 把 Windows 的 Session 改為真實多使用者 Session 之後的重大變革.

在 Windows XP 當中, 雖然可以有多使用者的假象, 但實際上, 所有登入、登出的使用者都還是共用同一個 Session, 也就是 Session 0, 因此驅動程式、病毒、駭客, 都很熟悉透過 Windows Message 來傳遞訊息.

回憶 2007 年的時候, Vista 剛推出,Nvidia, AMD 哀鴻遍野, 因為這兩家視訊卡大廠的驅動程式跟工具程式也是完全透過 Windows Message 作為傳遞介面跟工具程式之間訊息的管道, 直到 2008 年中, 整整過了半年多, 新版能支援 Vista 的驅動程式跟工具程式才 release...

WIC 元件


這是多說的了, 回頭說, 在 Delphi XE7 的 FireMonkey, 要 Deploy 到 Windows XP 的時候, 可能會遇到直接 Crash 的狀況, 我自己也在此卡關許久, 後來發現, 原來是需要自己在Windows XP 上面, 安裝 WIC 元件, 這個元件的取得路徑如下:

http://www.microsoft.com/zh-CN/download/confirmation.aspx?id=32

只有簡體中文版的網頁, 但下載回來, 安裝的都是 enu, 也就是英文版, 反正沒有什麼複雜的操作要進行, 不斷的按下一步就行了.

這個情形會發生在一執行的時候, 而且沒有什麼訊息提示, 就跟一般我們遇到 Memory Access Violation 的時候差不多, 只會在 XP 上面說要回報給 Microsoft, 但 Microsoft 已經停止了對 Windows 的支援, 所以回傳也沒什麼幫助.

我是後來想了很久, 覺得不對, 在透過 PAServer 從 Windows 8 VM 去連 Windows XP VM做遠端偵錯的時候, 才從 Delphi Debug message 裡面找到了這個錯誤訊息, 不然真的不知道為什麼會 Crash.....

我懷疑過的點挺多的, 例如 MDAC 版本不對, MySQL dll 版本不對, 什麼都試過了, 但是在業主的營業點沒得測試, 所以這次用完全乾淨的 Windows XP Pro 來做這個驗證, 得到這個經驗, 或許遇到這情形的先進不多, 但他山之石, 可以攻錯, 所以也把這個體驗留在這個園地, 讓大家有需要的時候可以互相分享一下.

2015年1月5日 星期一

Get HTML/JSON from TWebBrowser FireMonkey (Delphi XE7)

Starting


FireMonkey starts from Delphi XE2, this new framework provides many powerful components and runtime library, and the most important point is, most of the components can be used cross-platform.

XE2 -> iOS fundmental features ready.
XE3-XE5 -> Android features were appended.
XE6 -> Almost features were ready, but minor performance issue remained.
XE7 -> Everything is ready, we still need some completion for couple components.

With mobile apps development, we need to connect the web server from time to time. Indy components play as a proper role for Delphi and C++ Builder, but the SSL feature is not perfect yet.

Hence, we still need TWebBrowser for user to interact with website, if the feedback or redirect path contains HTTPS URL, e.g., Facebook login, Google+ login.

However, there is no interfaces provided by TWebBrowser for retrieving the content of the page.

A->B, B->C, C->D

It's said in some movies, "Genius can get A to D without intermediate path, other cannot", I am not genius, so I have to find out A->B, B->C, C->D to achieve A->D.

The following is my thought:
A->D, A is the TWebBrowser, D is retrieveing content.

TWebBrowser cannot get content, but it can run Javascript without return value.
Javascript can get the content, but there is no way to send the content back to Delphi.
TWebBrowser can get the URL, which the browser wish to navigate.
Javascript can redirect the web browser to another specific URL.

So, the workaround is:
1. waiting for the target URL we want to get the content, e.g. the Google+ login result page.

http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (login page)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (login success page, the user information will be sent back with JSON format.)

2. if the login success page is done, the onDidFinishLoad event handler will handle the JSON Page:
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;

3. Javascript redirect the browser to http://1.1.1.1/ (you can modify it to another http url), and append the JSON data (innerText in above codes) to the URL.

4. Let onShouldStartLoadURL of Browser to get "http://1.1.1.1/" url, then we got the content in Delphi!
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;
I had search some work around with the following key words in Google, Stackoverflow, and it's pity that there is no any solution yet:
Delphi, FireMonkey, WebBrowser, get HTML, get JSON, get Content.

So, this work around might be the only way to get content from TWebBrowser, if you need the sample project, please download from here.

Best Regard, and Enjoy your Delphi.
Dennies Chang.

如何用 FireMonkey 的 TWebBrowser 取回 JSON 資料

緣起

從 Delphi XE2 開始, FireMonkey就包含了跨平台的TWebBrowser元件(Windows版本跟行動版本是分開的, 我記得Windows版本的 TWebBrowser 元件, 是 VCL 版本的 ActiveX),但從 XE2 開始到 XE6, 每一版的 TWebBrowser 都少了點東西.

XE2到XE4都沒有Android版本的WebBrowser, 到 XE5 總算行動平台的 WebBrowser 都具備了, 但在網頁當中的 Form 輸入文字, 卻有選擇了文字無法傳送到文字框的缺失, 這個問題在 XE6 被解決掉了,XE6 的 WebBrowser 又有了效能不夠好的問題。

到了 XE7,終於大多數問題都被解決掉了,也加入了強制執行 Javascript 的功能,可謂十全八美 (按:iOS執行 Javascript之後,可以把執行後的字串當成回傳值讓 UIWebBrowserDelegate 處理,Delphi XE7 目前還不行)。

另外,也還無法直接 access WebBrowser 裡面的內容,在以往我們使用 VCL 版本的 TWebBrowser 時,我們至少還可以透過 ActiveX 介面取得 innerHTML 或者 innerText,然而目前在行動裝置上,FireMonkey 並沒有可以讓我們可以取得內容的途徑。

由A到B 由B到C 由C到D的轉折

俗話說「山不轉路轉」,TWebBrowser沒有可以 access 內容的途徑,但是Javascript有。Javascript 不能把字串直接傳給 Delphi 程式碼,但是可以把字串當成Javascript 的變數。TWebBrowser 不能直接取得內容,但可以取得網址。

所以,筆者兜兜轉轉,拼湊出了這麼一套方法:
1. 先確定進入了我們需要的內容網址,筆者以自己寫的 Google+ 登入網頁作為範例。
http://www.moveinpocket.com/demo/GoogleLogin_API/index2.php (登入)
https://www.googleapis.com/oauth2/v1/userinfo?access_token= (登入成功頁, 會以 JSON 格式回傳使用者的資料)
2. 檢查是否進入了登入成功頁,如果進入了,就讓 WebBrowser 執行Javascript,把 innerText 抓出來當字串變數。
if (Pos('https://www.googleapis.com/oauth2/v1/userinfo?access_token=',
        self.WebBrowser1.URL) = 1) then begin

            js := 'var markup = document.documentElement.innerText;' + #13 + #10
                + 'var newURL = "http://1.1.1.1/" + markup;' + #13 + #10 +
                'window.location = newURL;';
            self.WebBrowser1.EvaluateJavaScript(js);
    end;
3. 用 Javascript 的 redirect 方法:windows.location,轉到一個不存在的網址,把innerText 當成該網址的參數,不用名字,直接接在網址後面就行了。
4. 透過 TWebBrowser 的 ShouldStartLoadURL, 就可以抓到這串字了。
if (Pos('http://1.1.1.1/', URL) = 1) then begin
        self.WebBrowser1.Height := 1;
        jsonStr := URL;
        Fetch(jsonStr, 'http://1.1.1.1/');

        jsonStr := TIdURI.URLDecode(jsonStr, IndyTextEncoding_UTF8);

        ShowMessage('got it');
        self.Memo1.Lines.Text := jsonStr;
        self.TabControl1.ActiveTab := self.TabItemResult;
    end;

也太費功夫了吧⋯⋯ 不過沒辦法,這方法還真的能抓到TWebBrowser的內容喔。

使用時機

大家可能會問我:為什麼不用IdHttp,或者 TRESTClient來抓資料,這麼費神幹嘛,你自己不是Indy的愛用者嗎?

是的,筆者愛用Indy是事實,也還寫了Indy的書沒有錯,但有時候還是會有需要用TWebBrowser的時候,像是如果需要用 Google+ 當作登入頁,試問除了TWebBrowser,還有什麼方式可以做到?

這時候千萬別回我「用IdHTTP」喔,在行動裝置上的IdHTTP,對https網址是無法載入的,不信可以試試看。所以認命點,這是你在全球唯一能找到的解決方法了,問問我怎麼這麼有自信?因為我才剛在寫這篇文章之前做過功課,從 stackoverflow,到 edn,到Marco cantu的 Blog,expert-exchange,KTOP我都找過了,到2015年一月,目前也只有這個解法了,請相信!

以下,附上範例程式碼,太長的內容我不保證一定成功喔,但JSON資料還OK啦。
本篇文章範例程式專案

2014年9月3日 星期三

Virtual Keyboard 的顯示與隱藏

緣起

在 iOS app 裡面, 很多時機仍舊需要使用者輸入文字, 因此 iDevice (iPhone, iPad, iPod Touch) 上面的虛擬鍵盤就很重要。

然而,在 Delphi 上面,我們要構成一個完整的 app 畫面,需要許多的設計,包含美術設計與介面設計,美術設計會構築美觀的畫面,而介面設計跟程式設計的人則賦予這個畫面完整的功能。

如果您很習慣於使用 XCode 來製作 iOS app, 一定很常使用 UIImageView 或者 TapDetectingImageView 來取代一般的 UIButton, 因為 UIButton 常常在圖片的調整上面不精準,會讓整個畫面跟原來美術設計的構想差距很大。

所以,這個觀念在轉向到 Delphi 設計 app 的時候,也就會有一樣的擔憂,進而也使用 Delphi 的 TImage 來構築畫面,在 TImage 上面建立 onClick 的事件處理常式,把 TImage 當成 TButton 來用,畢竟這樣使用,在畫面的呈現上,要比先把一個 TButton 放到畫面上,再把圖片拉進到 TButton 裡面去要更為精準,也更為直接。

然而,在這種作法之下,居然產生了很大的副作用,所以有了這篇分享文章的誕生。

Virtual Keyboard 的顯示

圖一: 主要討論介面,模擬登入
我們接下來在整篇文章當中都會以圖一作為討論的介面,因為筆者已經把所有重點都濃縮在這個畫面當中了。

一般來說,只要是有需要使用者登入的介面,大多會有帳號、密碼的輸入框,就像上圖當中的 Account 跟 Password 欄位,當我們觸碰輸入框 (在本例是 TEdit) 的時候,虛擬鍵盤就會自動出現,如圖二所示。
圖二:虛擬鍵盤出現了
虛擬鍵盤旁邊有時候會有些 Done, Prev, Next 的功能按鈕,但介面上的設計就是要讓使用者自由自在的使用,所以我們在設計介面的時候,沒辦法規定使用者一定只能照著特定的操作步驟來用 app.

以圖二當中的登入畫面來看,如果密碼欄位旁邊有個『登入』按鈕,大多數的使用者一定會直接點選它,並期盼虛擬鍵盤會自己消失,然後完成登入的動作,切換到下一頁,幾乎不會有使用者先把虛擬鍵盤關掉,然後才去點選『登入』按鈕的,這個程序在 iPad 就更是如此了。

因此,在圖一一開始製作的時候,筆者就設定了三個按鈕在上面,分別是最上方的『By Button』,這是一個 TButton 元件,然後筆者再把 TImage 跟 TLabel 放進裡面去,如果還不知道這個步驟要如何達成,請參考筆者之前這篇文章

第二列則有兩個按鈕,都是 TImage 當中直接放了一個 TLabel 來製作的,因為有實作出 onClick 事件,所以點選它的時候,也可以跟點選 TButton 有幾乎完全相同的效果,當然,筆者一開始也是這樣以為的,只是,遇到了虛擬鍵盤的時候,就多了很多需要注意的地方,以下,且聽我為您娓娓道來。

使用 TButton

如果我們的『登入』按鈕是一個TButton, 那麼,只需實作它的 onClick 事件處理常式,其他什麼都不用管,點擊它,虛擬鍵盤就會消失,並且執行按鈕的 onClick 事件處理常式,如圖三所示。
圖三: 點選了By Button 按鈕,虛擬鍵盤消失,顯示 ShowMessage 的內容.



使用 TImage

接著,我們用 TImage 來試試看,同樣只實作它的 onClick 事件處理常式,然後點擊這個 TImage 看看,這是圖三當中下面那列按鈕的右方按鈕。
圖四:點擊了 TImage, 虛擬鍵盤卻沒有消失
這時候,TImage 的 onClick 事件處理常式程式碼如下:
procedure TForm1.Image2Click(Sender: TObject);
begin
   ShowMessage('IMage Clicked');
end;

單純的 ShowMessage, 沒辦法隱藏虛擬鍵盤, 如果這個動作還會把整個畫面釋放掉的話,引起的問題就會更多,甚至會造成 Access Violation, 原因是從 iOS 向上層層回傳的 SingleTap 事件沒有元件可以處理,所以造成了 Access Violation.

要解決這個問題,可以無副作用解決的方法有二:
1. 用 TButton 來包覆 TImage, 這樣就不會有上面的問題。
2. 在 TImage 的 onClick 事件處理常式當中,首先就把 Focus 轉移到其他可以取得 Focus 的元件上,例如途中的 CheckBox, 改過後的程式如下:
procedure TForm1.Image3Click(Sender: TObject);
begin
   self.CheckBox1.SetFocus;
   ShowMessage('Image clicked with transfer focus');
end;

而執行後的程式畫面則如圖五:
圖五: 把 Focus 移轉後的畫面

當然,鐵齒如我,也嘗試過了很多方法,例如把 TVirtualKeyboard 從系統中硬鏟出來,然後呼叫它的 HideVirtualKeyboard 方法:

首先宣告
FService: IFMXVirtualKeyboardService;

procedure TForm1.FormCreate(Sender: TObject);
begin
    if TPlatformServices.Current.SupportsPlatformService
        (IFMXVirtualKeyboardService, IInterface(FService)) then begin
    end;
end;

procedure TForm1.Image3Click(Sender: TObject);
begin
   FService.HideVirtualKeyboard;
end;

這樣在畫面上,看起來的確是會把虛擬鍵盤弄不見,但如果沒有把 Focus 先移到別的元件上頭,切換到下一個畫面以後,就會發現不管點什麼,還是會出現 Access Violation, 所以別鐵齒。

要強制虛擬鍵盤出現,就直接在該 Edit/Memo 元件上面設定 Focus, 如前面的程式碼範例中的 self.CheckBox1.SetFocus;

要強制虛擬鍵盤出現,就把 Focus 從編輯文字的元件上移除即可。

本篇文章的範例程式,可以在這裡下載。