WPFでGoogle mapsを使う
はじめに
とある案件で、位置情報を利用するために、Google mapsを使うことになりました。
本来ならWebブラウザで、JavaScriptを使って、利用するのが正道なのですが、位置情報を利用するのは一部分で、他の部分は、DBのメンテンスをするだけの治具ツールなので、自分が最も慣れた開発環境ということで、WPFで作ることにしました。
はじめは、誰かが公開しているサンプルを流用すれば良いとタカを括っていたのですが、ネットの情報はどれも古く茨の道でした・・・
WebBrowserコントロールでは、表示すら不可能
まず、標準のWebBrowserコントロールを使って試してみたのですが、最新のGoogle mapsは未対応なブラウザとして、表示すら出来ませんでした・・・
WPFで最後まで改善されなかった弱点(ちょっと脇道)
WPFが2006年に誕生したときからWPFには、HTMLと親和性が低いという弱点が存在していました。
標準で、WebBrowserコントロールというHTMLを表示することのできる部品は用意されていましたが、御世辞にも出来の良いものではなく、制限の多い、とても利用し辛いものでした。
後年は修正されましたが、WebBrowserコントロールの上に、WPFのコントロールを重ねられないなど暗に使うなと言っているに等しいものを何故リリースしたのか理解に苦しみます。
誕生当時は、HTMLとJavaScriptの仕様は貧弱で、Flashが全盛期であり仕方のない面もあったのですが、その後HTMLが大きく躍進してもWPFのHTMLに対する姿勢は変わることはありませんでした。
マイクロソフト自身の評価でもWPFは失敗作ということのようですが、XAMLが非常に良い仕様だっただけに惜しまれます。
例えば、WPFのもうひとつの弱点であるPDF表示をWebBrowserコントロールで補完できたらどんなに良かったか・・・
Chromium Embedded Framework(CEF)
仕方がないので、CEF(Chromium Embedded Framework)という外部コントロールを使って、表示をする方法を試すことにしました。
少し前に試した時は、Google ChromiumをWPFへ組み込むのは、自前でコンパイルが必要で複雑な作業が必要でした。
今回改めて調べたところNugetを使って、簡単に組み込めるようです。
WPFが、MSからも見放されてしまってから久しいですが、もっと早くこの環境があったと思うと残念です。
(もしかしたら私が気付いてなかっただけかも知れませんが・・・)
Nuget組み込み
CefSharp.WpfをNugetからインストールします。
GHKENさんの記事がとても参考になりました。
WPFでCefSharp(Chromiumの.NET向け実装)を使う - 1
初期化コードと終了コード
ChromiumWebBrowserコントロールで、固定のURLのサイトを表示するだけなら簡単ですが、制御しようと思うと少々面倒です。
まず、初期化コードと終了コードを実装します。
CefCustomSchemeは、カスタムスキーマーによるページ読み込みを実装する仕組みです。
ResourceSchemeHandlerFactoryは、WPFの組み込みリーソースのHTMLファイルを読み込ませる実装となります。
resource:/
から始まるURIが指定されると実行されます。
/// <summary>
/// App.xaml の相互作用ロジック
/// </summary>
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
var cefSettings = new CefSettings();
var customScheme = new CefCustomScheme();
customScheme.SchemeName = ResourceSchemeHandlerFactory.SchemeName;
customScheme.SchemeHandlerFactory = new ResourceSchemeHandlerFactory();
cefSettings.RegisterScheme(customScheme);
Cef.Initialize(cefSettings);
}
protected override void OnExit(ExitEventArgs e)
{
base.OnExit(e);
Cef.Shutdown();
}
}
public class ResourceSchemeHandlerFactory : ISchemeHandlerFactory
{
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
{
return new ResourceSchemeHandler();
}
public static string SchemeName { get { return "resource"; } }
}
public class ResourceSchemeHandler : ResourceHandler
{
private Assembly ass;
private String resourcePath;
private String file;
private string url;
public override bool ProcessRequestAsync(IRequest request, ICallback callback)
{
this.url = request.Url;
Uri u = new Uri(request.Url);
this.file = u.Authority + u.AbsolutePath;
this.ass = Assembly.GetExecutingAssembly();
this.resourcePath = ass.GetName().Name + "." + this.file.Replace("/", "");
if (ass.GetManifestResourceInfo(resourcePath) != null)
{
callback.Continue();
return true;
}
return false;
}
public override Stream GetResponse(IResponse response, out long responseLength, out string redirectUrl)
{
switch (Path.GetExtension(this.resourcePath))
{
case ".html":
response.MimeType = "text/html";
break;
case ".js":
response.MimeType = "text/javascript";
break;
case ".png":
response.MimeType = "image/png";
break;
case ".appcache":
case ".manifest":
response.MimeType = "text/cache-manifest";
break;
default:
response.MimeType = "application/octet-stream";
break;
}
var ret = this.ass.GetManifestResourceStream(this.resourcePath);
responseLength = ret.Length;
redirectUrl = string.Empty;
return ret;
}
}
表示処理
ChromiumWebBrowserコントロールをXamlへ配置してください。(サンプルコードは抜粋です)
<UserControl xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf">
<cefSharp:ChromiumWebBrowser x:Name="browser" />
</UserControl>
google map apiのkeyが無いと制御はできませんので、各自取得してください。
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div id="map"></div>
<script>
var _map;
var _marker;
var _lat_lng;
function initMap() {
var latlng = new google.maps.LatLng(35.699173, 139.4441873);
var opts = {
zoom: 14,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
_map = new google.maps.Map(document.getElementById("map"), opts);
_map.addListener('click', function (e) {
getClickLatLng(e.latLng, map);
});
}
function getClickLatLng(lat_lng, map) {
_lat_lng = lat_lng;
if (_marker != undefined) {
_marker.setMap(null);
}
// マーカーを設置
_marker = new google.maps.Marker({
position: lat_lng,
map: _map
});
callbackObj.setMarker(_lat_lng.lat(), _lat_lng.lng());
}
function setMarker(lat, lng) {
if (_marker != undefined) {
_marker.setMap(null);
}
_lat_lng = new google.maps.LatLng(lat, lng);
_marker = new google.maps.Marker({
position: _lat_lng,
map: _map
});
}
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=[API KEY]&callback=initMap">
</script>
</body>
</html>
google_map.htmlは、埋め込みリソースへ設定してください。
this.browser.Address = "resource:google_map.html";
HTTP リファラー制限にマハる
APIの制限として、HTTP リファラーを設定する必要があります。
不正使用されないように、ドメイン等で縛るのですが、アプリへの組み込まれるファイルに当然ながらドメインなどはありません。
resource:/*
を登録すれば、回避できるかと試して見たのですが、駄目でした。
アプリに組み込まれたHTMLファイルを利用することは、Googleも想定していて、__file_url__/*
をリファラーへ設定すれば回避できます。
Get API Key and Signature
最後に
WPFで開発をする機会は少なくなってしまいましたが、CEFがここまで簡単に使えるとなると弱点をHTMLで補完しながら延命できそうな気がしました。