2017年12月11日 星期一

在 Delphi 當中使用 TMapView 顯示地圖之一:程式設定與加上地點標示

在 Delphi Seattle 10 之前,還沒有提供封裝好的地圖元件,因此在那之前的案子,筆者也大多用 TWebBrowser 搭配 JavaScript 土法煉鋼來提供手機上的地圖功能。

在 Delphi Seattle 之後,TMapView總算出現了,但一直沒有實際的機會可以拿來用,最近有個機會,需要在 App 當中提供標示地點、路線的功能,因此正好把多年以前在 iOS 上面製作過的桃園福利地圖App當中的導航跟標示地點的功能拿來複習一下。

在 Delphi 上面要製作具備有 Google Map功能的地圖,請先服用官方說明:http://docwiki.embarcadero.com/CodeExamples/Tokyo/en/FMX.Map_Sample

如果您懶得看,我整理一下懶人包(如何設定你的專案,讓它具備Android平台上能顯示地圖的功能),只有幾點而已:

第一大項:建立 Google Map API 的 Key:

1. 連線到 Google API Console: https://code.google.com/apis/console/?noredirect
2. 在專案列表中,建立一個新的專案
3. 把 App 的 id 跟名字輸入進去
4. 完成,畫面上會出現 Google API Key,這是一長串的字。

第二大項:設定專案的權限:

1. 先把專案切換到 Android 平台

2. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Uses Permissions
3. 確定以下三個權限有勾選起來:
  • Access coarse location
  • Access fine location
  • Access network state 

4. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Entitlement List 確定 Map Service 有勾選起來


5. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Version Info 把 Google API Key 貼到apikey 欄位上,也確認 id 跟在 Google API Console 輸入的一致


這樣就設定完成了。

設定 TMapView

接著就可以從元件盤上面拉一個 TMapView 到 Form 上面了,MapView 要放多大都可以,但放太小的話使用者操作起來會不方便,所以盡量能留個半邊螢幕給它比較恰當。

使用了 TMapView,第一個需要操作的行為,一定是把地圖的中心點拉到我們想要顯示的地方,並且把地圖放大到『適當的大小』。

以上兩個需求,可以透過設定 TMapView 的兩個 Property 來達成,一個是設定大小用的 Zoom,另一個則是設定中心點的 Location。

先介紹 Zoom,它比較單純,是一個 Single 型別的數字,跟 Google Map 的網頁版一樣,數字越大,街道越大,數字越小,街道越小,我自己習慣的大小,是設定在 16.0,這個大小很適合作為導航之用。

只需一行指令:
Self.MapView1.Zoom := 16;

中心點就稍微麻煩一點,因為 Location 屬性的型別是 TMapCoordinate,所以我們得先建立出一個 TMapCoordinate 實體:

var
    mapCenter: TMapCoordinate;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;
    .....
end;

透過這段程式碼,就能夠把地圖的中心點拉到嘉義高鐵站,並且把大小調整為適合導航的大小。

加一個標示點在 TMapView 上

如果您使用過 Google Map網頁版進行開發,這裡就會很熟悉了,在Google Map網頁版上,要加一個標示點,是以 Marker 元件來加的,但只能以 Javascript 或後端工具在網頁上添加。

在 Delphi 程式中,則提供了對應的元件 TMapMarker,要建立 TMapMarker,則需透過 TMapMarkerDescriptor 這個 record,當中包含了 Marker 的座標、文字描述,如果我們想要自定圖示,也可以直接透過 TMapMarkerDescriptor 來指定:
var
    mapCenter: TMapCoordinate;
    MyMarker: TMapMarkerDescriptor;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;

    MyMarker := TMapMarkerDescriptor.Create(mapCenter, '嘉義高鐵站');
    MyMarker.Visible := true;
    MapView1.AddMarker(MyMarker);
    .....
end;
這樣就能在 MapView 上面,把地圖中心點拉到嘉義高鐵站、在上面插個標示了,如果想要自定圖示,只需指定一個 TBitMap 給 MyMarker.Icon 即可。




使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之二 (POST 檔案)

在使用 HTML 進行檔案,已經是很平常的應用了,在手機App裡面,也常常會用到這個作業,例如拍照上傳,或是從相簿選取照片上傳,都是很常見的。

在 HTML 的 Form 裡面,要讓使用者選擇檔案上傳,通常會這樣寫:
<input type="file" name="fileId1" id="fileId1"/>
當我們在 HTML 裡面這麼寫,網頁瀏覽器會自動在畫面上顯示一個按鈕,點選之後,會出現檔案選擇的對話框讓使用者選擇檔案。

但是,在手機作業系統中,為了提供檔案系統的Sandbox 功能,並無法從網頁直接提供相簿照片選擇或拍照上傳的功能,所以大多數類似的功能,還是得透過 App 提供才行。

在這個範例中,為了讓使用者能直覺使用檔案選擇的功能,所以也同時介紹了如何從手機相簿選擇與拍照上傳的功能。

如何讓Delphi程式提供相簿選照片的功能

在Delphi環境中,要提供使用者從相簿選相片,或是啟動相機拍照,都要透過 TAction 來完成,只是從相簿選照片跟啟動拍照功能,是以兩個不同的 TAction 來完成的。雖說是不同的 TAction 處理不同的程序,但取得照片的時候,回傳給程式的結果都是用TBitMap 來儲存相片。

換句話說,取得了 TBitMap 之後,我們想要對TBitMap做任何影像處理、格式處理,都可以由我們的程式碼來操作。

從 Delphi XE6 之後,這些直接使用行動裝置功能的程式碼,都已經被載入到TAction裡面了,我們只需要在使用上了解如何把TAction跟按鈕事件綁在一起即可。

首先,我們需要在 Form 上面放上一個 TActionList 元件,然後雙擊這個元件,就可以顯示出 ActionList 的編輯畫面:
在這畫面中,我們要新增Media Library 的兩個 Standard Action,就是上圖的TakePhotoFromLibraryAction (從相簿取得照片),以及TakePhotoFromCameraAction (從相機拍攝照片)。


建立兩個 Action 之後,我們就需要為這兩個 Action 設定 onDidFinish 跟 onDidCancel 的事件,前者是用來處理確定拍攝/挑選照片的事件,後者則是用來處理取消拍攝/挑選照片的事件。

在這個範例中,onDidFinish 的時候,會把回傳來的圖片資料直接放在畫面上的 TImage 元件顯示,所以它的程式碼很單純:

procedure TForm2.TakePhotoFromCameraAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

procedure TForm2.TakePhotoFromLibraryAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

就是直接把 Image Assign 給 Image1.Bitmap 屬性即可。

最後,點選按鈕上傳,完整的程式碼如下:

procedure TForm2.btnUploadClick(Sender: TObject);
var
   newItem: TRESTRequestParameter;
   allPass: boolean;
   imgPath, houseNum, floorIdx: string;
   nameStr: String;
   obj: TJSONObject;
begin
   allPass := True;

   if self.EditName.Text = '' then begin
      allPass := False;
      ShowMessage('請填寫住戶名字');
   end
   else if (Length(self.EditCardNo.Text) < 5) or
       (Length(self.EditCardNo1.Text) < 5) then begin
      allPass := False;
      ShowMessage('請填寫悠遊卡卡號10碼');
   end
   else if self.Image1.Bitmap.IsEmpty then begin
      allPass := False;
      ShowMessage('請提供悠遊卡感應卡號照片');
   end;

   if allPass then begin
      self.RectWaiting.Visible := True;
      self.AniIndicator1.Enabled := True;

      imgPath := System.IOUtils.TPath.Combine
          (System.IOUtils.TPath.GetDocumentsPath, 'tmp123.png');
      self.Image1.Bitmap.SaveToFile(imgPath);

      self.RESTClient1.BaseURL :=
          'http://testURL/acceptNewCard.php';

      self.RESTRequest1.Params.Clear;
      RESTRequest1.Method := rmPOST;

      nameStr := self.EditName.Text;

      self.RESTRequest1.AddParameter('cardNum', self.EditCardNo.Text + ':' +
          self.EditCardNo1.Text);
      nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('name', nameStr,
          TRESTRequestParameterKind.pkGETorPOST,
          [TRESTRequestParameterOption.poDoNotEncode]);
      // self.RESTRequest1.AddParameter('name', nameStr, TRESTRequestParameterKind.pkGETorPOST, [TRESTRequestParameterOption.poDoNotEncode]);

      case self.ComboBoxHouseNum.ItemIndex of
         0: begin
               houseNum := '1';
            end;
         1: begin
               houseNum := '374';
            end;
      end;

      houseNum := TIdURI.ParamsEncode(houseNum, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('houseNum', houseNum);

      floorIdx := IntToStr(self.ComboBoxFloor.ItemIndex + 1);
      self.RESTRequest1.AddParameter('floorIdx', floorIdx);
      self.RESTRequest1.AddFile('picFilename', imgPath);
      self.RESTRequest1.Execute;

      obj := TJSONObject.ParseJSONValue(self.RESTRequest1.Response.JSONText)
          as TJSONObject;
      ShowMessage(obj.GetValue<String>('result'));

      self.RectWaiting.Visible := False;
      self.AniIndicator1.Enabled := False;
   end;
end;

上面的程式碼裡面,處理檔案的重點只有一行:
self.RESTRequest1.AddFile('picFilename', imgPath);

在更前面一點的程式碼中,imgPath 是由TImage.Bitmap即時存下來的照片,這個範例只適用於 Delphi 10.2 (Tokyo) 以後的版本,因為在 Berlin 當中,對於檔案的 MIME Type 還沒有完全支援,所以在 Berlin 或 Seattle 當中要使用這個方法上傳檔案的朋友,請升級吧,我也沒辦法直接提供 TRESTClient 在新版的程式碼給大家。


2017年6月8日 星期四

使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之一 (POST 字串參數)

在 Delphi XE 推出以前的年代,Delphi的發展方向是筆直朝向資料庫連結Windows 應用程式這個目標不斷前進的,從Delphi 1開始,到Delphi 7,Delphi奠定了VB Killer的外號,主要依靠的就是與資料庫的連接功能超越其他開發工具,而且超越的距離不只一個世代。

在 .NET開始發展,Delphi 8, Delphi .NET 不斷延遲的時候,與資料庫連接功能的方便性,仍然讓許多ERP廠商、軟硬體廠商持續愛用 Delphi.

直到 Web 開發與 App 開發超越了 Windows 應用程式的需求,VC, VB, Delphi 也開始隨著這波潮流,漸漸不再像 1990年代那麼廣受愛戴了。

在1990到 2010年之間,Delphi的網路連線功能,主要是藉由第三方元件來提供的,其中知名度最高,全球使用人數也最多的,應該就是 Indy 這套元件了。

這套元件在 2000 年前,叫做 WinShose,從第八版之後才改名為Indy,全球投入這套元件開發的開發人員,前後超過40人,從最基礎的 TCP/IP 功能到各種協定的Client與Server 端元件,筆者從中得益非常多,也開發了當中的DNS Server元件,對通訊協定的深入了解,Indy團隊可說是我不可或缺的師長。

隨著Delphi 工具走入了多平台開發的領域,Indy的侷限性也在這兩三年凸顯了出來,主要是在各個作業系統上面對於SSL與加密功能的支援無法緊密結合到作業系統內建的功能所致。

由於這個局限性,Delphi XE6開始,REST Client系列元件漸漸開始成為 Delphi 團隊的重點開發項目之一,所以我們從 Delphi XE6, Delphi XE7 之後的版本,可以發現到,使用 TRESTClient, TRESTRequest, TRESTResponse 系列組合的應用程式越來越多了,原廠也不斷鼓勵大家使用這套元件來提供 REST API 的連線功能。

REST API 的基礎是 HTTP 協定,大多以 HTTP 的 POST 方法把 JSON 編碼形式的參數傳遞到 Server,而 Server 再以 JSON 形式的參數回傳。

有時作法也會稍有變化,例如以 POST 方法把 Web-Form 編碼形式的參數傳遞給 Server,Server 再以 JSON 形式把資料回傳。

形式不一而足,但相同的是 HTTP 協定,最常用的也是以 POST 方法把參數傳給 Server 端。

今天要跟大家分享的主題,則是如何『使用 TRESTClient 與 TRESTRequest 作為 HTTP Client』。

前面已經提到過,在沒有 TRESTClient 整組元件以前,我們通常用的是 Indy 系列的元件來提供網路傳輸的功能,而現在有了 TRESTClient 整組元件,我們在行動平台上面就可以不需要另外配置函式庫,也能夠直接使用 https 與 server 連線了,在勒索病毒氾濫的今天,使用 https 會讓使用者比較安心。

POST作業說明

在 HTTP 的 POST 作業當中,參數跟 GET 作業一樣,Client端需要以 name=value&name2=value2 這種形式進行字串連接,再傳送到 Server 端去。Get 跟 POST的差異,在於 Get 方法是把所有參數當做 URL 的一部分,發送 HTTP GET 指令的時候,參數連同 URL  一起傳送。

而 POST 作業則是發送完 POST 指令後,把所有的參數與資料隨之傳送。依照 HTTP 型定的規範,GET 作業的 URL 是無法加密的,而且長度也有限制。因此,當需要傳遞的資料比較多,或者有機敏性,透過 HTTPS 傳送,就是最直接,也最方便,更是目前最通用的資料保護方法。

透過 POST 傳遞的參數,除了字串以外,還常常包含了檔案傳遞。我們很常看到在網頁上面以按鈕提供使用者選擇要上傳的檔案,也常看見提供以拖拉的方式把檔案上傳到遠端系統,尤其網頁郵件系統最常見到這種作法。

過去以 TIdHTTP 元件的 POST 方法發送參數時,呼叫方式如下:
var
    httpClient : TIdHTTP;
    url, params, httpResultStr : string;
begin
    url := 'http://mytestURL.com/test.php';
    params := 'name=我的名字&test=測試';

    httpClient := TIdHTTP.Create(self);
    try
         httpResultStr := httpClient.Post(url, params);
         showMessage(httpResultStr);
    finally
        httpClient.Free;
    end;
end;
  
這樣就可以把 params 字串的眾參數傳到 server 去了。理論上是這樣沒錯,但事情並沒有這麼簡單,在 HTTP 協定當中要傳參數給 Server,如果這些字串包含了特殊字元,則必須要先經過編碼,而編碼,是我們一生都需要與之對抗的繁複程序。

在 HTTP GET 方法當中,所有的參數除了要以 name=value 對每一個參數做描述,以及需要用 & 來連接各組參數,所有的 value 都需要以 url encode 來擺脫 URL 保留字元的糾纏。name 是否需要編碼呢?筆者建議,name 就乖乖的用英文吧,可以省下很多問題,以及處理這些可避免的問題所需要的時間!

那麼同樣的功能,以 TRESTClient 跟 TRESTRequest 要怎麼達成呢? 也很容易,作法如下:
1. 在 form 裡面放上 TRESTClient 跟 TRESTRequest 元件各一。
2. 把要傳遞的參數加到 TRESTRequest 實體的 params 屬性裡面去,這個屬性的型別是 TArray,所以可以存放多組參數。
3. 設定 TRESTClient 要傳送參數的URL,注意,URL 是設定在TRESTClient 哦!
4. 設定 TRESTRequest 要使用的傳輸方法,要設定為POST(因為我們正在介紹的是POST方法,請按照您的需求調整)
5. 呼叫 TRESTRequest 實體的 execute 方法,就可以把資料送去 server 了。

寫成 Delphi 的程式碼,會像以下這樣:
self.RESTClient1.BaseURL :=
          'http://我的網址/acceptNewCard.php';
self.RESTRequest1.Params.Clear;
self.RESTRequest1.Method := rmPOST;

self.RESTRequest1.AddParameter('test', self.EditCardNo.Text);
self.RESTRequest1.AddParameter('name', self.EditName.Text);

是不是很容易呢?的確很容易,裡頭的問題我們等下再深入探討,先來看 server 端要怎麼接收這些個參數,我們用 PHP 當範例,需要用 C#,JSP的讀者朋友們請自行轉譯喔⋯⋯

PHP Server 端接收 POST 參數的方法
從 1994 年開始,筆者就陸續撰文說明 HTTP POST 方法如何接參數,包含了CGI 用C,perl等語言實作,也包含 ISAPI 以 Delphi 實作,近幾年比較流行的是 PHP,JSP,C#,但 PHP 程式碼讀起來比較簡潔易懂,所以我就選擇 PHP 來做範例了。

在 PHP 裡面,透過GET 跟 POST 方法傳遞的參數,會被分別存放在 $_GET 跟 $_POST 這兩個陣列變數裡面,如果要偷懶,不想區分 GET 或 POST 方法,也可以從 $_REQUEST 這個變數試著讀取,當中有些安全性考量,最好勤勞一點,把它們區分開來。

以剛剛的例子來看,我們傳了一個名為 name,以及一個名為 test 的字串,用的是 POST 方法,所以我們得用以下兩個變數來存取這兩個字串:
  • $_POST['name']  這個變數可以取得 Client 端發送出來的 name  
  • $_POST['test']  這個變數可以取得 Client 端發送出來的 test
所以在 server 端,我們可以這樣寫,來抓到這兩個資料:
$name = $_POST["name"];
$test = $_POST["test"];

這樣寫會不會出問題呢? 答案是不會!如果使用者不輸入中文的話!

中文資料的編碼處理 

Delphi的開發人員絕大多數都是英美語系的人,我推測因此對於亞洲語系的文字顯示與傳輸比較沒有辦法完整的測試,但對於我們以中文為母語的人來說,從電腦誕生的那個年代,中文的顯示在每個作業系統、每種通訊協定的設計都比英文協定來的困難。

以上面的例子來看,如果我們直接拿這個例子來測試,筆者寫的範例程式,執行傳輸資料時,Server 所抓到的文字並不是正確的中文字,如下圖所示:

可以看得出來,傳到 server 的時候,server 是讀不到資訊的。這是怎麼回事呢?筆者屬於不認輸的好奇寶寶,使出了渾身解數,終於解決了這個問題。

寫過 Web 程式的讀者們一定可以立刻推測出來,這絕對是文字編碼出問題了,然而,是什麼地方出問題?可能出問題的點我列出來跟大家分享:
  • HTTP Client 的 charset 設定錯了
  • HTTP Request 裡面的文字編碼出問題
檢查的方向也是從這兩個關鍵點出發,第一點的檢測很容易,從Object Inspector檢查一下 RESTRequest1的設定:

AcceptCharset 確定是 UTF-8,沒錯,所以設定不是問題。

接著,就要從 Client 端發出去的資料下手了。有讀者或許會問『你怎麼不懷疑Server端程式寫錯了?』這個問題很好,之所以排除了這個問題,是因為同一個 Server 端的 PHP 程式,我用了 Postman 做過比對測試,回傳的結果是正確的,因此判定是 Client 端程式的問題。

接著筆者從 TRESTRequest.AddParameter 的各種多載形式來嘗試,AddParameter 這個方法有以下幾種多載的形式:
  • procedure AddParameter(const AName, AValue: string); overload;
  • procedure AddParameter(const AName: string; AJsonObject: TJSONObject; AFreeJson: boolean = true); overload;
  • procedure AddParameter(const AName, AValue: string; const AKind: TRESTRequestParameterKind); overload;
三種形式我都測試過,從 AddParameter 的執行中 trace 進去看各個可能性,由於 TRESTRequest 的參數中,Get 跟 Post 的加入方法是混用的,在程式碼裡面編碼又會有點不同。

在 REST.Client.pas 裡面,我曾經懷疑過編碼錯誤,所以也在執行階段對各個變數都進行觀察,最後,找到了原因與解法,至於過程,就不多說了,花了我兩天咧。

原因:編碼錯誤

用HTTP傳遞中文的時候,務必用UTF-8編碼,但一定要記得,中文字在作業系統中,都是UCS32編碼,這個現象在Windows裡面如此,在Android裡面如此,在iOS跟Mac我不確定,但處理方法是一樣的。

直接以 AddParameter('name', '中文測試'); 把參數加進 TRESTRequest 的時候,REST.Client.pas 的程式碼是把 '中文測試' 這個字串直接抓 Ord 的資料來做編碼的,然而,這個作法,是錯的!!!!!!!!

在 HTTP 傳遞 UTF-8 資料的時候,我們要傳遞的是 UTF-8 文字的二進位資料,但直接把 '中文測試' 這個字串直接拿來轉成二進位? 當時編碼並不是 UTF-8 啊,當然怎麼編碼送到 server 都是錯的!!!!

解法:AddParameter之前先做 UTF-8 轉換

這個解法,筆者第一天就已經想到,只是很想像以前改 Indy 程式一樣,直接改好 REST.Client.pas 之後,回饋給原廠使用,所以花了不少時間找方法,最後發現這個方法不用動到 REST.Client.pas,又能正確處理,就直接這麼跟大家分享了,寫成 Delphi 程式碼如下:
var
     nameStr : String;
begin
   ...
   nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
   self.RESTRequest1.AddParameter('name', nameStr,     
             TRESTRequestParameterKind.pkGETorPOST,    
             [TRESTRequestParameterOption.poDoNotEncode]);
   ...
end;
在把字串透過 AddParameter 加入參數陣列之前,我先把字串做個 UTF-8 轉換,在這裡用的是 TIdURI 的類別方法 ParamsEncode,這個方法只有兩個參數,第一個參數是字串內容,第二個參數則是要文字編碼的種類,在這裡我選擇了 UTF8,寫法就是上面範例程式的第一行。

接著,在呼叫 AddParameter 的時候,我使用了多載形式當中的第三種,要求 AddParameter 處理資料的時候不要再動我的編碼,因為我已經處理好了。

這麼修改過之後,在各個作業系統當中,執行結果都是正確的,上面兩個圖以 Windows 作業系統為例,現在我們拿 Android 截圖來做為例子:


讀者可以看到右邊的截圖裡面,server 回傳的資料已經是正確的中文字了。

最後,我把 PHP 程式碼也附上來給大家參考:
<?php
date_default_timezone_set("Asia/Taipei");
header('Content-Type: charset=utf-8');

include("public/DBClassPDO.php");
$objDBPDO = new DBClassPDO();

$cardNum = $_POST["cardNum"];
$floorIdx = $_POST["floorIdx"];
$name = $_POST["name"];
$houseNum = $_POST["houseNum"];

$params = array();
$params['name'] = $name;
$params['cardno'] = $cardNum;
$params['houseNum'] = $houseNum;
$params['floorIdx'] = $floorIdx;
$params['picFilename'] = $fileSaveName;
$params['created'] = 0;

$result["resultCode"] = "0";
$result["result"] = "成功";
$result["sqlcmd"] = $name;

$jsonStr = json_encode($result);
echo $jsonStr;
?>
這是最基本的傳遞字串,下次再給大家示範怎麼傳檔案,透過 TRESTRequest來做也是很簡單的,TRESTRequest 跟 TRESTClient 的確是取代 TIdHTTP 的好工具。

2017年5月21日 星期日

在 Android 的 Edit 中, 如何按下 Enter 即隱藏虛擬鍵盤

在 Windows 的應用程式中,我們常常為了讓使用者能夠快速輸入,在Edit元件中的onKeyUp或者 onKeyDown 事件中主動偵測使用者輸入的字元是否有換行符號 (Enter),當使用者按下了Enter,程式碼就主動把游標 Focus 帶到下個欄位,但在行動裝置中,又多了一個課題:『如果是多個欄位,就帶到下個欄位。但如果是單一欄位,或是最後一個欄位,就隱藏虛擬鍵盤』

這個課題筆者在 2014九月的筆記『Virtual Keyboard 的顯示與隱藏』裡面已經有介紹過一次,只是當時是聚焦在iOS系統上,因為當時筆者開發的平台大都聚焦在iOS,對Android系統的關切並不多。

最近在一些專案中,也把觸角伸到了Android平台,但筆者仍堅持『不到最後關頭,絕不輕言JAVA』的原則,認識筆者的朋友們也都知道,筆者會寫,但非不得已,絕不寫JAVA的原則。

加上Delphi從XE6之後,對Android的支援也很深入了,筆者就更能堅定這個立場。

言歸正傳,在TEdit元件中要在使用者按下虛擬鍵盤的Enter鍵時,隱藏虛擬鍵盤的話,要怎麼做呢?

顯然用2014九月那個作法(把focus轉移到別的元件), 對Android平台沒用,不然筆者也不用多寫這篇筆記了。

是的,熟習Android的使用者都知道,按下Android的實體Back鍵,就能隱藏虛擬鍵盤。所以這個作法也很簡單,就是『在onKeyUp事件中,把Key 改成 137 即可』

onKeyUp 事件中,有兩個 call by Reference 的參數,一個是 Key, 另一個是 KeyChar, 都讓我們可以在處理完鍵盤事件後,重新賦予新的值給系統。在Delphi Seattle, Berlin, Tokyo (前面的版本我已經移除了,無從驗證),這三個比較新的版本中,都定義有 vkHardwareBack這個值,其數值就是 137。

只需在 onKeyUp 事件中把 137 指派給 Key 這個變數即可隱藏鍵盤了,但通常我們程式碼可能會用於多種平台上,所以我的範例程式碼會加上 {$IFDEF Android} 這樣的判別式,以利多平台的程式維護:

procedure TFormMain.Edit_changeDeviceNameKeyUp(Sender: TObject; var Key: Word;
    var KeyChar: Char; Shift: TShiftState);
begin
   if Key = vkReturn then begin
      self.btn1.SetFocus;

{$IFDEF ANDROID}
      Key := vkHardwareBack
{$ENDIF}
   end;
end;
這樣應該很容易理解,對嗎?

2017年5月18日 星期四

在 iOS 10 之後, 使用手機相機與相簿應注意事項

iOS 的 SDK 每一年至少都會有一次大改版,從 2009 到 2016 年,版號已經到了第 10 版了,很輕易的就追上了 Mac OSX. 每一次的大改版都會有不少新的功能或新的規範,在 iOS 7的改版算的上是幅度最大的,把 iOS 1.0 到 iOS 6.0 以實物視覺與立體視覺的基礎來了個髮夾彎,轉向去跟 Android, Windows 一起走平面化極簡風。(不知道賈柏斯天上有靈會怎麼反應......)
  • iOS 8的改版中,要求使用地理資訊的App需要在 info.plist 當中自己寫出提示的文字
  • iOS 10 的改版中,更進一步要求使用相機、相簿的App也要自己提示。
如果沒有在 info.plist 裡面定義提示文字,App執行到該功能的時候,會直接發生閃退,這對於使用者來說很不方便,所以App開發的人員也只好為了使用者對 App 進行改版。

如果是使用 Xcode 來開發,就需要編輯 info.plist, 加入以下圖片中兩個灰色背景的設定,一個是使用相機,另一個是使用相簿。



這跟是否使用第三方的 Framework 無關喔,即使您使用了 Cocos2D, 或是 CocoaPad, 這些宣告也是跑不掉的。

如果使用的是 Delphi,則需要從專案設定選項 (Project Option)來設定,設定視窗如下圖所示:


叫出這個視窗的方法有兩個,一個是從Delphi的視窗中點選 Project 選項,選擇裡面的 Option。另一個則是用滑鼠右鍵點選視窗右邊的專案名稱,選擇 Menu 裡面的 Option 選項,都可以顯示出這個視窗。

設定好這兩個選項之後,在 Delphi 裡面呼叫 TTakePhotoFromCameraAction 或
TTakePhotoFromLibraryAction的時候,就不會閃退了.

要留意一點,透過 Delphi 取用相機或相簿的時候,OnDidFinishTaking 所取得的照片是 TBitMap 元件,我們可以先對圖片做一些簡單的處理,例如可以把圖片的 解析度弄小一點,或者改變圖片的大小,甚至是儲存成不同格式 (PNG或JPG)。

對 iOS 模擬器處理時,由於模擬器通常沒有相機,所以不用特別設定模擬器的相機使用描述。Delphi 的設定會依照不同的裝置平台做個別的設定,而且還分成 Debug/Release,所以我自己通常會直接編輯 All Configuration。

從 iOS 5S 之後,也已經沒有 32 bit 的新裝置了,所以如果您的 App 不打算提供給 32 bit 裝置使用的話,iOS Device - 32 bit platform 也可以不用理他了。



2017年5月12日 星期五

Delphi 快速使用 AES 加密的方法

從 1994 年開始,筆者就開始接觸加密與網路安全的世界,從魯立忠老師的指導當中獲益良多,後來在元智就讀研究所的時候,也以此為研究主題。

在當時,電子商務是顯學,Visa跟 Master Card還特別為了網路交易製作了厚厚三大本的商務通訊協定,命名為SET (Secure Electronic Transaction,安全電子交易),從客戶端、商店端、銀行端定義了綿綿密密的交易規範。

然而,網際網路的世界跟 Visa Master Card所熟悉的專用網路世界差的遠了,不是大狗們(Big dogs)說了算,很快的 SSL 128 被吹捧成『最安全的交易保護機制』,每年透過這『最安全的交易保護機制』成交的金額越攀越高。

破解網路而得逞的網路詐欺,始終維持在一個很低的比例,反而從商家端流出的詐欺資料年年創新高,SET也很快的成為一個歷史名詞。

但是,SET所本的一些加密基礎,並沒有就此被埋沒。X.509電子憑證、RC4, RC5, DES, 3-DES, RSA, SHA-1, SHA256, SHA-2, 還有我們這次要介紹的AES,也不斷的推陳出新,在世界上蓬勃發展。這些聽了令人打呵欠的主題跟名詞,在很多地方都會被用上,只是用了不同的面貌呈現給使用者而已。

像是在自然人憑證、健保卡裡面,都有個人電子憑證(X.509),每年五月我們都可以用這兩種憑證進行網路報稅。

或是像電子發票,當中就需要用到 AES 加密,依據財政部的『紙本電子發票二維條碼內容規範』第五頁所述:


左方二維條碼裡面,就需要用到 AES 對發票字軌10碼及隨機碼4碼以字串方式合併後使用AES加密,並採用Base64編碼轉換。

但是,在Delphi裡面好像沒有可以直接使用AES加密的單元可以使用。筆者在碩士論文的程式撰寫時,使用的是OpenSSL 0.4的函式庫,當時還叫做SSLeay呢。但是,這作法只能在Windows 平台上面順順的用,有沒有什麼方法可以讓我們在不同的作業系統下都能順利使用 AES 呢?

經過約莫兩三個小時上窮碧落下黃泉的搜尋,找到了一個在 SourceForge 上面的加密範例程式,更棒的是,它是用 Delphi + FireMonkey 寫的,不使用 DLL,而是使用純粹的Pascal 寫的 (感謝 Eldos 的 OpenSource, 但直接到 Eldos 網站的連結目前已經找不到了)。

換句話說,這是一個跨平台都可以正常運作的 Delphi 程式,不用依靠載入的 DLL 或 Lib,用這個範例來製作電子發票的驗證加密字串,就能夠很方便的達成了。

Source Forge 的範例程式可以從這個連結下載,下載之後,請看到裡面的範例專案『FlyUtilsAESCBC.dproj』,這個範例程式中,支援用字串作為AES加密金鑰(Key)對文字進行加解密,執行起來的畫面也很清楚,筆者做了一點點修改,修改後的執行畫面如下圖所示:
 

左圖是未執行加密作業前的畫面,右圖則是執行了加密作業之後的畫面。原始的範例程式中,只支援完整的字串作為 AES 金鑰,但我們常會用到二進位資訊來做金鑰,這種情形下,金鑰通常也會經過Base64編碼過。

所以筆者稍微改寫了一個function,新增了一個按鈕,就是畫面最底下的『AES加密with Byte Key』這個按鈕的event handler。

如果點選按鈕是 AES加密,則Key裡面的字串不會被做任何處理,直接會被當成AES金鑰,點選的如果是最底下的『AES加密with Byte Key』,則Key裡面的字串會先被做Base64 解碼,變成二進位資訊,AES金鑰就是這些二進位資訊了。

procedure TFormMain.Button1Click(Sender: TObject);
var
  KeyBit: TKeyBit;
  APaddingMode: TPaddingMode;
begin
   KeyBit := TKeyBit.kb256;
   APaddingMode := TPaddingMode.pmZeroPadding;
   Memo2.text := AESEncryptStrToBase64_Base64Key(Memo1.Text, Edit1.text, TEncoding.UTF8, KeyBit, '', APaddingMode, CheckBoxCBC.IsChecked,
      rlCRLF, rlCRLF, Process);
end;

這兒的 AESEncryptStrToBase64_Base64Key 是筆者照著原本 Eldos 的程式做了一點小手腳,方便大家把電子發票平台取得的金鑰直接貼上來就能用:
function AESEncryptStrToBase64_Base64Key(Value, Key: string; StrEncoding: TEncoding = nil;
  KeyBit: TKeyBit = kb128;
  InitVectorStr: string = '';
  APaddingMode: TPaddingMode = TPaddingMode.pmPKCS5or7RandomPadding; CBCMode: Boolean = True;
  ValueCRLFMode: TCRLFMode = rlCRLF;
  KeyCRLFMode: TCRLFMode = rlCRLF;
  OnProcessProc: TOnProcessProc = nil; ProcessProc: TProcessProc = nil): string;
var
   tStrm : TBytesStream;
   keyBytes: TBytes;
   IVBytes: TBytes;
   IdDecoderMIME1 : TIdDecoderMIME;
begin
   tStrm := TBytesStream.create;
   IdDecoderMIME1 := TIdDecoderMIME.Create(nil);
   try
      IdDecoderMIME1.DecodeBegin(tStrm);
      IdDecoderMIME1.Decode(Key);
      IdDecoderMIME1.DecodeEnd;

      keyBytes := tStrm.Bytes;
   finally
       tStrm.Free;
   end;

   tStrm := TBytesStream.create;
   try
      IdDecoderMIME1.DecodeBegin(tStrm);
      IdDecoderMIME1.Decode(InitVectorStr);
      IdDecoderMIME1.DecodeEnd;

      IVBytes := tStrm.Bytes;
   finally
       tStrm.Free;
   end;
   IdDecoderMIME1.Free;

  Result := EncodeBase64Bytes(AESEncryptStr_BytesKey(Value, keyBytes, IVBytes, TEncoding.UTF8, KeyBit, APaddingMode, CBCMode, ValueCRLFMode,
    KeyCRLFMode, OnProcessProc, ProcessProc));
end;

單獨使用這個 function 的話,ProcessProc 參數可以給 nil, 這是用來讓大家看到有進度列可以顯示處理進度用的,通常這些處理是在背景進行,沒有介面的時候直接給個 nil, 就可以不用管進度條了。

大家可以看到,上面的程式碼裡面,筆者使用了Indy的 Base64解碼元件『IdDecoderMIME』,因為一來它是原本 Delphi 安裝就內建的,使用上比較方便,二來筆者也熟悉這套元件,所以不另外找其他元件了。

AES 裡面除了 Key 之外,還可以指定 IV (initialization vector), 如果使用上需要使用二進位的 IV, 您可以把二進位的 IV 先做好 Base64 編碼,這個 function 也會將它先做 Base64 解碼之後,作為 IV 進行處理的。

寫到這裡,說明的差不多了,範例程式專案我也準備好了,有興趣的讀者請自行取用吧。範例在此



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