顯示具有 Windows 標籤的文章。 顯示所有文章
顯示具有 Windows 標籤的文章。 顯示所有文章

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月6日 星期二

如何用 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年8月29日 星期五

FireMonkey v.s. VCL 之一 - 動態產生視覺元件的差異

在撰寫程式的過程中,對於畫面的製作,會隨著程式經驗與類型的積累,產生幾種演進:
所有畫面都事先在DesignTime,也就是設計階段,把畫面都先處理好
能在設計階段製作的,先在設計階段處理,不行的部分,在RunTime即時產生
所有畫面都在RunTime 產生

演進的狀況不外乎上述三種,但未必每個程式人員都有機會遭遇到全部三種狀況,也很難說那一種做法是功力最高的,因為所有的程式製作方法,都應該視當時的條件來決定製作的方式,這些條件包含了:
開發時程 - 有時候很多專案會被要求在極短的時間之內完成,沒時間,就沒機會使用全RunTime的做法完成。

對FootPrint (整個編譯結果的檔案大小)的要求- 如果遇到只能在非常局限的磁碟空間中安裝,完全RunTime 的做法或許是達成要求的唯一方法。

便於團隊對程式碼的長久維護- 此時就要考慮整個團隊的程式設計習慣,挑選適合的做法。

且不管最後選了哪一種,在RunTime建立畫面的需求都是可能會存在的,所以我們在此對完成這個需求的方法做些介紹。,也就是動態產生元件的方法,並就VCL跟 FireMonkey 兩種架構的差異進行比較。

我們以最簡單的,在 Form 上面建立 Button 的程序進行比較

VCL版:
procedure TForm1.btnAddButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    btn.Parent := self.Panel1;

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Caption := 'btn' + IntToStr(idx);

    countByRow := (self.Panel1.Width div btn.Width);
    btn.Left := idx mod (countByRow) * btn.Width + 10;
    btn.Top := idx div (countByRow) * btn.Height + 10;
end;
上面這段程式可以在 VCL 建立一個 Button, 但在 FireMonkey 當中, 建立的方法就不一樣了:

FireMonkey版:
procedure TForm2.btnAddNewButtonClick(Sender: TObject);
var
    btn: TButton;
    countByRow, idx: integer;
begin
    btn := TButton.Create(self);
    self.scrollBox.AddObject(btn);

    self.ButtonList.Add(btn);
    idx := self.ButtonList.IndexOf(btn);
    btn.Text := 'btn' + IntToStr(idx);

    countByRow := Trunc(self.scrollBox.Width / btn.Width);
    btn.Position.X := idx mod (countByRow) * btn.Width + 10;
    btn.Position.Y := idx div (countByRow) * btn.Height + 10;
end;
在 VCL 當中,動態建立了一個新的按鈕以後,我們只需指定該按鈕的 Parent, 就可以把建立的按鈕放到 Parent 這個元件上面,當然,也需要先指定新按鈕的位置 (Left, Top) 與大小 (Width, Height).



而 FireMonkey 要做到相同的效果,則是看我們要把建立出來的按鈕放在哪個元件上面,就呼叫該元件的AddObject 方法,例如我們要把新建立的按鈕顯示在表單上面,就直接呼叫 scrollBox.AddObject(newButton);



帶一句題外話:從上面的兩個範例 procedure 裡面, 可以看出 FireMonkey 的座標與元件大小都可以有小數點, 但 VCL 不行。

要動態移除該元件的時候,也非常不同,VCL只需要呼叫該元件的 Free 方法即可:
procedure TForm1.btnRemoveBtnClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            btn.Free;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
FireMonkey 則不一樣:
procedure TForm2.btnRemoveClick(Sender: TObject);
var
    btn: TButton;
begin
    if self.ButtonList.Count > 0 then begin
        btn := self.ButtonList.Last;

        if Assigned(btn) then begin
            self.ButtonList.Remove(btn);
            self.scrollBox.RemoveObject(btn);
            btn.DisposeOf;
        end;
    end else begin
        ShowMessage('No Button');
    end;
end;
在 VCL 裡面,呼叫了 Free 方法以後,obj 所佔用的記憶體就會在 Windows 系統裡面被釋放出來,但在 FireMonkey 當中,由於要適應絕大多數系統上的特性,所以 Free 方法只會把該元件的 Reference Count 減一,在 Android 跟 iOS, Mac OS 裡面,一個元件要被系統釋放掉的話,必須要 Reference Count 等於 0, 才會真正把元件刪除掉。

所以 RemoveObject 被呼叫以後,obj 元件會在畫面上看不見了,但它還沒有真的被刪除,在 FireMonkey 當中,我們可以呼叫 obj 元件的 DisposeOf 方法,這個方法在Windows 平台上,會有等同於呼叫了 Free 方法的作用。

但在 iOS, Andorid, Mac OSX 上面, 則會強制把 Reference Count 遞減為 0, 真正強制的把佔用的記憶體釋放出來。

結語
許多已經很習慣於傳統 Delphi 或 傳統 Windows 應用程式設計的 Programmer, 會很習慣於把所有的畫面都放在同一個 Form 裡面。

在傳統的 Windows 或桌機型的應用程式環境當中,這是沒有問題的,但在行動裝置或者嵌入式裝置的應用程式當中,這是行不通的,因為在行動裝置當中,記憶體極其有限,有時只要使用超過了 40MB 的記憶體,應用程式就會被強制停止,也就是俗稱的閃退。

大家或許會覺得,40MB 很少,或者自己的應用程式不會用這麼多,但請大家記得一個數字,一張 2048x1536 的圖片,就可能佔用了約 10-15MB 的記憶體。

為什麼我提到 2048x1536 這個數字?

因為它正是 iPad Retina 螢幕的實際解析度,包含 iPad 3, 4, 5 (iPad Air), 以及 iPad Mini 2, 都是這個解析度,我們一旦有這個解析度大小的應用程式,只要兩張全螢幕的圖片,就已經處在危險邊緣。

如果還像以往的寫法,把所有的圖片、畫面都放在同一個 Form 裡面的話,只要換個兩頁,應用程式就會閃退,這種應用程式連上架的機會都沒有,所以,動態產生、動態刪除元件或畫面的需求將會越來越大,大家也一定要實際學會怎麼處理這種情形,不然前路絕對是坎坷不平的。

動態產生範例 (VCL & FireMonkey)

Delphi 用 for..in 迴圈釋放物件的注意事項 (XE5, XE6)

從 Delphi 2009 開始, 新增了不少原本 Object Pascal 沒有支援的語法, 包含了我還蠻常用的 for-each 迴圈. (在 Perl, obj-c 我都還蠻常用這個功能的)

為了搭配動態產生元件, 用 for-each 迴圈來處理釋放動態新增的物件, 在 Delphi XE5 分外重要,因為在 XE5 當中,我常用 TVertScrollBox 這個元件來處理需要動態新增、高度不確定的 Contents.

舉個大家最常見的例子來說,即時通訊軟體的對話內容,內容會有多少?誰也說不準,可能跟朋友A的對話內容有一千則,但跟不熟的路人甲則只有三則。

這時候,要用什麼元件來顯示這些對話內容呢? 從 XE2 開始,大多數的範例程式都是用 TListBox 或者 TListView 來載入這些資料,TListBox 跟 TListView 也很好,但這兩個元件的內容呈現比較制式,用來顯示通訊錄裡面的聯絡人非常適合,但要用來顯示對話內容的話,就不很合適了。

請回憶一下,現在大家都常用的通訊軟體,Line, What's app, iMessage 等等,都是以左邊顯示對方傳來的訊息,右邊顯示我方傳過去的訊息,底下會用個氣泡圖片來襯底,我們看一下 iMessage 的畫面:


要在 Delphi XE5, XE6 裡面製作完全相同的效果的話, 要怎麼做?

透過 TListView, 很抱歉, 我一時半刻創意不足, 想不出來, TListBox 倒是可以。

因為TListBox 的 Items 是 TListItem,建立TListItem 出來以後,可以用 AddObject 這個方法來把圖片 (TImage), 文字 (TLabel) 加到 TListItem 裡,所以也可以製作出像上圖的畫面,但如果要在上圖的畫面上顯示圖片呢?現在即時通訊軟體都流行貼圖,這種功能要怎麼在 TListBox 做出來呢?

抱歉,我又創意不足,一時技窮了。

這時候,大概也只有 TVertScrollBox 可以用來製作這樣的畫面效果了。

TVertScrollBox 跟 THorzScrollBox 都是從 TScrollBox 繼承而來的,從名稱來看,TVertScrollBox 是垂直方向的捲動視窗,而 THorzScrollBox 則是水平方向的捲動視窗,所以要製作上圖的畫面的話,就可以用 TVertScrollBox 來製作。

讓我寫一段簡單的 procedure 來示範怎麼把一個 TStrings 裡面的文字放進 TVertScrollBox 裡面去:

procedure addTextToScrollBox(info : TStringList);
var
   oneLine : string;
   currentY : Single;
   idx : integer;
   newLabel : TLabel;
begin
    currentY := 0.0;

    for idx := 0 to info.Count -1 do begin
         oneLine := info.Strings[idx];
         newLabel := TLabel.Create(self);
         newLabel.Position.X := 10;
         newLabel.Position.Y := currentY;
         newLabel.Text := oneLine;

         vScrollBox.AddObject(newLabel);

         currentY := currentY + 10.0 + newLabel.Height;  
    end;
end;

以上這段程式就可以把傳進去的 TStrings 的內容一行行顯示在名為 vScrollBox 的 TVertScrollBox 裡面去了,但這範例會有個小困擾,就是在對話內容要釋放,或者刷新的時候,需要把所有的資料全部清除,在 XE6 底下,可以用以下這個 procedure 來達成:

procedure clearScrollBox;
var
    obj : TFMXObject;
begin
    for obj in vScrollBox.Children do begin
         vScrollBox.removeObject(obj);
         obj.DisposeOf;
    end;
end;

在行動平台中,請記得除了透過 vScrollBox.removeObject(obj); 把 obj 從 vScrollBox 的可視範圍移除以外,還要呼叫 obj.DisposeOf; 這樣 obj 才會實際消失,不然的話,雖然obj 當時是看不見的,但卻也還存在記憶體當中沒有沒事放掉。

視覺效果上,我們會看到 vScrollBox 內容空無一物,但捲動軸卻不會消失,這就會讓使用者覺得很納悶了,所以記得,除了RemoveObj之外,還要呼叫 DisposeOf 把物件是放掉。

如果是 XE5 的話, TVertScrollBox 的 Children 就不只存放我們透過 addObject 放進去的物件,我實測的結果,會連同捲動軸也列名在 Children 裡面,所以得先判斷其 Class 是否是我們要釋放的,因此要加入 Delphi 的 RTTI 功能,改為:

procedure clearScrollBox;
var
    obj : TFMXObject;
begin
    for obj in vScrollBox.Children do begin
         if (obj is TLabel) then begin
             vScrollBox.removeObject(obj);
             obj.DisposeOf;
         end;
    end;
end;

不然也可能會把捲動軸弄消失,但這一點在 XE6 以後的版本應該不用再擔心了,真是太好了。

2014年6月30日 星期一

FireMonkey 桌面應用程式最小化

問題 - 視窗縮小時不會整個應用程式縮小

在製作 Delphi 或 VB 程式時, 常常會使用多個 Form, 在每個 Form 的畫面當中, 會賦予不同的功能, 但每個 Form 都會被當成獨立的畫面, 也就是 MainForm.

我們對 MainForm 的預期, 就是在按下右上方的縮小鍵時, 整個應用程式縮小到工作列上, 按了工作列上的按鈕時, 整個程式畫面還原回來原來的狀態.

但從過去 VCL 的時期到現在的 FireMonkey, 都並沒有這麼容易, 多個 Form 的時候, 只要不是按下 MainForm 的縮小按鈕, 視窗都會縮在桌面左下角。

視窗沒有縮到工作列上, 而是成為縮小視窗
以往這種 Form 的特性, 是為了製作 MDI Application, 也就是一個大的 Form 當中有多個文件會被開啟, 就像 Word, Powerpoint 那樣, 可以開啟多個文件, 讓每個文件有獨立的小視窗用來檢視文件。

以往在 VCL 架構的解決方法

在 VCL 架構的應用程式中, 解決這個問題的方法, 是在任何一個 Form 裡面建立一個 onMessage 的事件處理常式, 並在 FormCreate 事件中把它指派給 Application.OnMessage 事件:

procedure TForm1.HandleMessage(var Msg: TMsg; var Handled: Boolean); 
begin
   if (Msg.Message=WM_SYSCOMMAND) and (Msg.wParam=SC_MINIMIZE) then
     Application.Minimize;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
   Application.onMessage := self.HandleMessage;
end;

當我們發現 SC_MiniMize 事件發生, 就直接執行 Application.Minimize, 整個應用程式就會縮到工作列上了, VCL 上面如此, FireMonkey 卻不然...... 時至今日, 網路上的各個論壇還在尋找著跟 VCL 架構當中的 Application.onMessage 對應的事件...... 然而, 我也找不到, 但是在 FireMonkey 當中, 有很多玩法跟 VCL 不相同, 花了一陣子搜尋 XE5 裡面的 RTL 原始碼, 找到了這個玩法, 有效, 簡易, 跟大家分享。

範例-製作兩個 Form 互相切換

先建立一個 FireMonkey 應用程式, 包含兩個 Form, Form1 跟 Form2, Form1 是 預設的MainForm, Form2 則是一般的 Form.

Form1, 放一個 button 切換到 Form2
Form2, 放一個 button 切換到 Form1













Form1 上面的 Button1Click 內容如下
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;

   self.Hide;
end;

Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;

   self.Hide;
end;

這是最常見的寫法, 然而切換到 Form2 的時候, 按下視窗上的縮小按鈕, 就會跟本文最上方的圖片所顯示的一樣:

這時候, 只要在 Form1 的 Button1Click 加上一行:
Application.MainForm := form2;

就可以把 Application.MainForm 從 form1 切換成 form2, 此時再按 form2 的縮小按鈕, 就不會再出現縮小的視窗了耶, 超神奇, 兩個 click 事件處理常式如下
修改後, Form1 上面的 Button1Click 內容如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
   form2.Show;
   Application.MainForm := form2;
   self.Hide;
end;

修改後, Form2 上面的 Button1Click 內容如下
procedure TForm2.Button1Click(Sender: TObject);
begin
   form1.Show;
   Application.MainForm := form1;
   self.Hide;
end;

結語

FireMonkey 上面許多用法都很神奇, 由於所有元件都有 Container 的特性, 所以可以直接收納其他元件, Application.MainForm 也不是唯讀屬性, 所以可以在切換 form 的時候順帶把 Application.MainForm 做重新指派, 雖然很難想到這個用法, 不過它確確實實的有效, 程式碼也簡潔, 所以推薦給大家使用.

範例程式連結