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

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 即可。




2017年6月9日 星期五

使用 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月22日 星期一

在 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;
這樣應該很容易理解,對嗎?