Windowsストアアプリを作ってみました。かたりぃなです。 HoloLensが目標ではあるのですが、Windowsストアアプリの作り方もわからないまま買うのはただのギャンブルなので、まずはかんたんなアプリを作ってみて、慣れてからHololens用に移る算段です。
アプリを作る動機
MagicTheGatheringというカードゲームの遊び方の一つにプレインチェイスというのがあります。 これは次元カードというものを使うのですが、悲しいことに英語版しか発売されていません。 頑張れば読めなくはないですが、わからない単語が出てきたりするとスマホでネットを調べることになり、ゲームが中断されてしまいます。というわけで ゲームが中断されてしまうと興ざめです。
何を作ったの?
できることは次のとおりです。
アプリとしてはとてもシンプルです。
技術的には
- アプリが起動されたら自動的にMTG-WikiのAPIを叩いて、次元カードの一覧を拾ってくる
- 次元カード一覧をリストで表示できる
- ユーザーはカード一覧から詳細を見たいものを選択できる
- 選択されたカードの詳細情報をMTG-WikiのAPIでとってくる
- カードの詳細を表示できる
- 詳細ページを見終わったらリスト画面に戻れる
- リストアップされたカード中から検索できる
- 一度見たMTG-Wikiの情報はアプリのローカルストレージにキャッシュする
です。 とても簡単そうですが、「まずは作ってみる」目標としては丁度いいレベルだと思います。 言語はc++です(HoloLensでDirectX叩きたいので、その準備)。 では開発者アカウントの取得から順にポイントを順に整理していきます。
Microsoft開発者アカウントを取得してVisualStudioに設定する
既にとってあるので省略します。Microsoftの開発者向けサイトから適当に登録します。 登録するためにはMicrosoftアカウントが必要です。クレジットカードを登録して千円くらい支払えば完了です。 この登録でMicrosoftアカウントが開発者アカウントとして登録されるみたいです。
登録後にVisualStudioを起動するとMicrosoftアカウント設定しましょうとか出るので、開発者登録したMicrosoftアカウントを設定するだけです。
Windows Phoneの開発者向け機能のロック解除
まず開発用マシンとWindows PhoneをUSB接続しておきます。今回使用するのはFreetelのKATANA2です。
ドライバインストールが終わってエクスプローラ起動してストレージが見える状態になってればOKでした。 WindowsPhoneが認識されたら「Windows Phone Developer Registration」というツールを起動します。 Windows10SDKに含まれているので、インストール済みならCortanaさんに聞くだけで場所を教えてくれます。 ロック解除が終わると「Windows Phone Developer Registration」はこんな画面になります。
Windows Phone側での設定
これはWindows Phone側の作業です。
開発者向け機能がアンロックされたので、次のぺージを参考にしつつ設定していきます。 Enable your device for development 某Androナントカのときも似たようなことしてた気がします
適当なプロジェクトをビルド・デプロイする
再びPC側での作業です。Visual StudioのテンプレートでUWPの適当なプロジェクトを選択してARM向けビルドするだけです。 Release設定じゃなきゃダメかもなーと思っていましたが、Debugプロジェクトもデプロイできました。
VisualStudioの画面としてはここを設定します。
ちなみに、デプロイ時にデバイスがアクティブである必要があります。(ロック画面や画面が消灯している状態ではダメ) まあ失敗したらエラーメッセージ出るので、その都度直せばいいかと。
デバッガの起動が重い
シンボル情報の読み込みに時間かかりまくります。単純なUWPアプリをデバイスに書き込んでデバッガ起動するまで数分くらいです。 初回だけなので、我慢しましょう。我慢できないときはCtrl+F5でデバッガ接続なしでデプロイです。
ここまで一日かからずにできました。簡単ですね。 やっとプログラミングです。 プログラミングは年明けから始めたので、一か月弱でここまでできたぞと。
プログラミングで躓いたポイント
たくさんあってもう忘れかけていますが、せっかく新鮮な体験をしたので思い出せる限り記録につづりたいと思います。
アプリのマニフェスト
なぜかカメラが起動できないとか、なぜかWebアクセスできないといった時に真っ先に疑ったほうがよいところです。 プロジェクト中のPackage.appmanifestを開いて適切なアクセス権限を設定しましょう。
C++/cxのハット(^)記号
Windowsランタイム側で寿命管理してくれる(マネージド)なポインタのようです。 感覚的にはstd::shared_ptrみたいなもんかと思ってます。インスタンス化するときはnewではなくref newで。std::make_sharedと同じノリですね。 自前のc++クラスをマネージドにしたいとき(データバインディング対象にしたいとか)はref class class_name sealedでクラス定義を書くようです。
文字列処理
Platform::Stringは基本的な機能しか提供していないので、状況によっては自前で編集する必要があります。 とっても面倒ですね。 新しいAPI覚えるの面倒なのでC++の世界に持ってきて解決することにします。 UWP(Platform::String^)の文字列はstd::wstringにそのまま変換できます。
ただ、C++の世界そのままとはいえ文字列がwstringです。 単純な文字列リテラルとの比較をする場合でもwstring同志でやる必要あります。 Lつければこのあたりの面倒みてくれるらしいです。ラクですね。
// テキストボックスの文字列をc++の文字列にする std::wstring str(textbox->Text->Data() ); // C++の世界でやりたいことをやる // たとえば検索 auto pos = str.find(L"Search"); // Lつけるのを忘れずに // uwpに戻す this->textBox->Text = ref new Platform::String(str.c_str() );
create_tasks
モバイル端末もマルチコア時代なので、非同期処理を書きましょう。ユーザースレッドを止めないのが基本です。 毎回スレッド作るのってコスト高い気がしますが、ランタイムがスレッドプールを持っていて必要に応じて割り当ててくれるらしいので、気にせずcreate_taskしましょう。 ちなみにこのライブラリはMicrosoft PPL(並列パターンライブラリ)というらしいです。
非同期の例といえばWebAPIですね。とりあえず適当なURIを叩いてみましょう。
create_task(client->GetAsync(request_uri)).then([](HttpResponseMessage ^ response) { response->EnsureSuccessStatusCode(); return response->Content->ReadAsStringAsync(); }).then([title, this](Platform::String ^ response_string) { });
サンプルコードコピペすれば動くのですが、ここが一番手こずったポイントです。一週間くらい悩みました。 細かく分割して整理します。分けて考えるは基本ですね。
create_taskによってタスク生成する
文法としてはこの部分です
create_task(client->GetAsync(request_uri))
この例ではhttpclientのGetASyncが返す関数オブジェクトを渡すことになりますが、ここには関数ポインタなら何でも渡せます。ラムダでもいいわけです。 作ったタスクは即時実行されるわけではなく、スレッドプールから割り当てられたスレッドを後で実行されます。(UIスレッドを止めない)
ただ、実行が終ったあとで何かしたいですよね?UIの更新だったり、ファイル保存だったり。 このままでは後続の処理に困ります。そこで登場するのがthenです。
.then
新しい言語仕様か?と思ってしまいますが、そんなことはありませんでした。create_taskで生成されたtaskクラスのメソッドthenです。 thenにも関数オブジェクトを渡すことができます。 こいつに渡す関数が先ほど登場した「後から実行したい何か」です。 thenが返すのもtaskなので、こうやって非同期操作を数珠繋ぎにしていくのがスタイルのようです。
ラムダ
c++にもラムダが実装されたので、ここではラムダをthenに渡しています。 昔ながらの非同期操作だとイベントハンドラごとに関数をわけて記述するので全体が見通せなくなりがちでした。 こうやってラムダで記述すれば全体が見通せるのでスッキリしますね
ちなみにMicrosoftのサンプルコードではこういう記述を見かけることがあります。
[](HttpResponseMessage ^ response) -> typename {}
ポインタからの何かを参照しているのかと勘違いしていましたが、どうやらこれはc++の機能の戻り値の型を後置する記法らしいです。
戻り値の型を後置する関数宣言構文 - cpprefjp C++日本語リファレンス
今回は直接関係ないので省略しますが、テンプレートを使うと戻り値の型を調べるのが大変になってくるので、auto宣言しておいてこういう記述を使うと幸せになれそうです。
非同期操作中に変数の寿命が尽きることがある
そもそも設計が間違っているのかもしれませんが、これに悩まされました。 非同期操作の関数にマネージドポインタを渡すと、変数の参照カウントが増えるので非同期操作が終わるまで安心して使えて、解放も自動的に行えるのですが、 taskを数珠繋ぎにしてアダプティブなクラスをかぶせていくようなスタイルだと、途中で変数の寿命が尽きてしまうことがあります。 実際に問題があったコードで絆創膏的な対処しかしていませんが、こういうのです。
readtask.then([this](Streams::IRandomAccessStream ^ stream) { return BitmapDecoder::CreateAsync(stream); }).then([this](BitmapDecoder ^ decoder) -> IAsyncOperation<SoftwareBitmap^>^ { return decoder->GetSoftwareBitmapAsync(); }).then([this](SoftwareBitmap^ bitmap) -> IAsyncOperation<OcrResult^>^ { this->tmp_bmp = bitmap; // ここで保持しておかないと、OCR実行中にbitmapが消失してエラーになってしまう。 // どうしてこうなるか理由は不明。 return this->ocr->RecognizeAsync(bitmap); }).then([this](OcrResult^ result) { // 空白文字を除去する std::wstring str(result->Text->Data() ); std::wstring::size_type pos; do { std::wstring target(L" "); pos = str.find(target); if (pos != std::wstring::npos) { str.erase(pos, target.length()); } } while (pos != std::wstring::npos); // 文字認識結果を取り出す this->textBox->Text = ref new Platform::String(str.c_str() ); });
寿命が尽きるということまでは解っているので、また気が向いたときに追ってみます。
コンパイル時データバインディング
まずデータバインディングとはUIとアプリケーションロジックの分離(コードビハインドというらしい)をスマートに行うものです。 WPFの頃からデータバインディング自体はありましたが、いつの頃からかコンパイル時データバインディングが実装されたようです。 リストビューを作る参考にさせてもらいました。
UWPアプリでコンパイル時バインド(x:Bind)を使ってListViewに値を表示する - Qiita
上記はC#での例なので、C++/cxで同じことをやろうとしたところで少し手こずりました。 ポイントは
- ネイティブ型はデータバインディングのバインディングソースに(というかプロパティとして見せること自体が)できない
- MVVMのVMとして見せるクラスも同様
- プロパティのgetter/setterはUWPランタイムのクラスなら書かなくてもいい
というわけで単純に文字列をlistviewで表示するコードはこうなりました。
まずXAML
<ListView x:Name="listView" ItemsSource="{x:Bind Path=card_list}"> <ListView.ItemTemplate> <DataTemplate x:DataType="local:data_element"> <StackPanel> <TextBlock Text="{x:Bind card_name}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
次にプロパティを宣言するクラス
// viewクラスから見せるプロパティ public: property Windows::Foundation::Collections::IVector< data_element^> ^ card_list;
最後にプロパティをインスタンス化するところ
// OnNavigatedToあたりでリストをインスタンス化する card_list = ref new Platform::Collections::Vector< data_element^>();
あとはcard_listに対してよくある操作(要素の追加/削除/更新)をすれば、自動的にUIに反映されます。
プラットフォーム固有の型を覚えるのが面倒
型情報はC++使いとしてはとても大事ですが、プラットフォーム固有の型とか覚えるの(というかマニュアル調べるの)面倒です。 できるだけautoで済ませましょう。 forはどうするかと考えてしまいますが、コレクションに対する操作なら範囲指定forで充分です。 状況によっては型情報(Platform::Object型を返してくるとか)が必要になりますが、そういうとき以外はautoでいきます。
たとえばアプリのローカルストレージに保存してあるcompositeを読む処理はこうなります。 型がわからなくなりそうですが、このくらいならVisualStudioのIntelisenceも動きますし、マウスカーソルでポイントすると型情報を見せてくれます。autoさん(というかVisualStudio)すごい。
auto localSettings = ApplicationData::Current->LocalSettings; auto composite = safe_cast<ApplicationDataCompositeValue^>(localSettings->Values->Lookup("cards")); if (composite) { // 二回目以降の起動の場合、既にあるカードリストを取り込む for (auto value : composite) { auto cardname = safe_cast<String ^>(value->Value); auto element = ref new data_element(cardname); card_list->Append( element ); } }
まとめ
長くなりましたが、書初めプログラムとしては非常に楽しいものでしたし勉強になりました。 次はDirectX11を叩いてみて、納得がいくところまでできたらHoloLens購入したいと思います。