Xamarin Android Player と Hyper-V を共存させてエミュレータ環境を改善する

Xamarin Android Player | Xamarin
http://developer.xamarin.com/guides/android/getting_started/installation/android-player/
Xamarin Android Player のインストール方法 ( Google Apps & Google Play Services 含む ) – Yuta Watanabe’s Blog
http://yutawatanabe.hatenablog.com/entry/xamarin-android-player-preview-install
Xamarin Android Player 小ネタ – Xamarin 日本語情報
http://ytabuchi.hatenablog.com/entry/2014/10/09/164202

インストール等は、上記の情報を参照して頂くとして、自前の環境で Hyper-V と Xamarin Android Player を同時に動かしています。Xamarin Android Player 自体は、VirtualBox 上で動いているものを OpenGL で描画部分だけ取り出している(っぽい)ので、結論的には Hyper-V と VirtualBox を共存させればよい、ということみたいですね。

ちなみに、Xamarin Andorid Player 自体 ver.0.0.24 というアルファ版っぽいバージョンなので、このあたりは今後改善されるでしょう。OpenGL 経由で表示するので、Hyper-V の仮想環境内やリモートデスクトップでは動きません。OpenGL 対応のリモートツールがあれば動くのかも。

若干パフォーマンスが落ちるらしい

Hyper-V を有効にした状態だと、下のような警告がでます。最初、なかなか先に進まないのでエラーなのかと思ったのですが「警告」です。

image_thumb[1]

Client Hyper-V is enabled on this system; this can result in decreased performance.  For maximum performance, please disable Hyper-V and reboot.

パフォーマンスは落ちるけど、動くよ、ってことなので、そのまま進めます。Windows Phone 8.1 を開発していなかったり、Windows 10 Technical Perview などを試さない場合は、Hyper-V を切っておくとよいでしょう。コントロールパネルで「Windows の機能」で検索して、Hyper-V のチェックを外しておけば ok です。

image_thumb[2]

私の環境では、Windows TP と Windows Phone エミュレータと同時に動いています。

VirtualBox のネットワークを確認する

私が嵌ったのは、ネットワークの問題でした。最初にインストールした状態では、下のようなダイアログがでて起動しなかったんですよね。

image

なにやら、OpenGL のサーバーに繋げているときに、ネットワークが通らない…って話です。VirtualBox を導入したのは初めてなもので、Hyper-V と Xamarin Android Player は共存できないのか?とあきらめました。あれこれ弄ると、下のようなメッセージにもなります。

image_thumb[4]

どうやら、VisualBox 上で動く OpenGL サーバーと Player が通信していることが分かったので、VirtualBox のネットワークを確認してみます。

image_thumb[5]

一発でうまくいく場合は、VirutalBox Host-Only Ethernet Adapter しかないと思います。私の場合は、このホスティングのインストールが失敗したらしく、#3 のようにいくつかのアダプターがつくられています。コントロールパネルで「ネットワーク接続」を開いて IP を調べてみると、こんな風に違いがあります。左がつながらない元のネットワークで、右が「#3」のつながっているほうです。

image_thumb[8] image_thumb[9]

私の場合、以前 VMWare を入れていたので、ローカルネットワーク構築時の IP がややこしくなっているのかなと想像しています。うまく OpenGL サーバーにつながらないときは、この部分を見直してみてください。(10.* なのに、サブネットマスクが 255.255.255.0 ってのもアレですが)。

外部接続は アダプター2 で接続している

Player に OpenGL をホストしている方は「アダプター1」を使っていて、外部ネットワークへ接続する方(Google Play などで必要)は「アダプター2」を使っているようです。私の場合、初期値である割り当てが「NAT」ではうまくつながらなかったので、ブリッジアダプターを使って接続しました。

image_thumb[10]

一度接続した後は、NAT に戻しても接続できるようになっているので、VirtualBox のネットワークの問題ですよね、おそらく。うまく繋がらないときは、このあたりを変更してみましょう。私の環境が QEMU とかを含めてややこしいネットワーク環境になっているため、ってのもありますが。

最初は Xamarin Studio で起動させる

Player への接続ですが、最初は Visual Studio から起動できませんでした。選択肢に Player の KitKat が現れません。これも環境によるのかもしれませんが、

  1. Xamarin Android Player を起動させる。
  2. Xamarin Studio で認識させる。
  3. Visual Studio で Target Android Device に現れる。

という流れで確実に表示できます。

スピード的には HAXM で Android 仮想デバイスを高速化 と同じぐらいのスピードで動いています。Hyper-V を切って、VirtualBox Only で動かせば、結構なスピードがかもしれません。AEDSearch の Azure サービス部分が妙に重たくて Mac のほうを使っていたりしたのですが、これで開発のスピードアップができそう。かなり早く動いてます(なんか、地図のところがエミュレーターでうまく動かないんですよね…)。

カテゴリー: Xamarin | Xamarin Android Player と Hyper-V を共存させてエミュレータ環境を改善する はコメントを受け付けていません

[WinRT] Youtube のサムネールを GridView に表示させる

VS魂100連発アプリでは Youtube のリストを拾ってきて表示させています。Youtube の API を使うためには、Google アカウントが必要で、それぞれの APP KEY が必要になってきます。
データアクセスは Google.Apis.YouTube.v3 で、起動時の URL を取得するところは MyToolkit.Extended を使っています。MyToolkit を使わなくても URL のパラメータでできるハズなんですが、なんとなく。

Google.Apis.YouTube.v3 の APP KEY を取得する

Google Developer Console で「YouTube Data API v3」を ON にします。

「APIs & auth」→「Credentials」で、Public API access キーを作成します。検索するだけなので、ここで作られる API KEY を使います。もし、この API KEY が漏れて悪用されているようであれば、もう一度作成してアプリに埋め込めば ok です。
まあ、ストアアプリの場合、実行ファイルを覗けてしまうので API KEY を暗号化とかしない限りばれてしまうわけですが。そのあたりは適当に。

NuGetを利用する

Youtube へのアクセスは、Google.Apis.YouTube.v3 で検索します。

MyToolkit も Nuget にあります。

なんか、いっぱい DLL をインストールされますが、気にしないことにします。まあ、適当なツールアプリなので。

動画リストを取得する

100連発のリストは、動画リストから取ってきているのですが、これたちょうど VisualStudioJapan のチャンネルID にあたります。このチャンネル ID をどうやって拾うのか?は疑問なのですが、あれこれ実験しているうちに取れたものです。動画を検索したときに、どうやら一致しているっぽい値を入れました。URL には出てこないので、別途検索キーにするといいでしょう。この場合は「VisualStudioJapan」の検索にマッチさせたものを取ってくると良いでしょう。

async Task<VideoList> getYoutube()
{
    // タイトル一覧を取得する
    var youtube = new YouTubeService(new Google.Apis.Services.BaseClientService.Initializer()
    {
        ApiKey = "", // Google API key
        ApplicationName = "VS100Watcher"
    });

    var videos = new VideoList();

    var req = youtube.Search.List("snippet");
    req.ChannelId = "UCy2j4l0auN8DJaQ6itwVWFA"; // "VisualStudioJapan" のチャンネルID
    var res = new Google.Apis.YouTube.v3.Data.SearchListResponse(); 
    do
    {
        req.PageToken = res.NextPageToken;
        res = await req.ExecuteAsync();
        // Youtube のサムネールをリスト化する
        foreach (var it in res.Items)
        {
            var vi = new Video() { Result = it };
            videos.Add(vi);
        }
    } while (res.NextPageToken != null && res.NextPageToken != "");

    // 番号でソートする
    var lst = new VideoList();
    foreach ( var it in videos.OrderBy(x => {
        if ( x.Title.StartsWith("VS100") ) {
            return x.Title.Replace( " ", "" ).Replace("-", "");
        } else {
            return x.Title;
        }}))
    {
        lst.Add(it);
        Debug.WriteLine(it.Title);
    }
    return lst;
}

Youtube クラスの使い方に若干癖がある(ビデオや他のデータと混在しているため)のですが、それはインテリセンス等で確認してください。
Java のコードサンプルを参考にするとよいでしょう。.NET のもあるのですが、数が少なくなってます。

YouTube Data API: Java コード サンプル – YouTube ? Google Developers
https://developers.google.com/youtube/v3/code_samples/java?hl=ja

リファレンスがあるのですが、個人用のアプリであればそこまで使わないような気がします。

検索結果は複数のページに分かれています。応答データの NextPageToken プロパティを見て次のページを取得していきます。最後の番号でソートしているのは、タイトルフォーマットがいくつか違っているので揃えるためです。

動画のURLを取得する

GridView のリストをタップしたときには、高画質動画を表示しています。このためだけに MyToolkit を使っているという贅沢な仕様ですが、まあ手早く作れるのでよいでしょう。以前作ったときのコードをそのまま使っています。

private async void itemGridView_Tapped(object sender, TappedRoutedEventArgs e)
{
    var item = (sender as GridView).SelectedItem ;
    if (item == null ) return;

    vm.Current = item as Video;
    MainPage.SelectItemIndex = (sender as GridView).SelectedIndex;

    var uri = new Uri( vm.Current.Url );
    if (vm.GoIE == true)
    {
        await Launcher.LaunchUriAsync(uri);
    }
    else
    {
        try
        {
            var url = await YouTube.GetVideoUriAsync(vm.Current.VidoId, YouTubeQuality.Quality1080P);
            this.media.AutoPlay = true;
            this.media.Source = url.Uri;
            this.media.Play();
        }
        catch { }
    }
}

今回は、常時接続を前提としているので動画ファイルは保存していませんが、ストリームを使ってローカルファイルに保存もできるでしょう。よく見る画像は、ビデオフォルダに保存しておくと、手軽に見られると思います。また、検索用のキーワードを付ければ汎用的な Youtube の閲覧ツールにもなるでしょう。

サンプル

moonmile/WatchVs100
https://github.com/moonmile/WatchVs100

カテゴリー: C#, WinRT | [WinRT] Youtube のサムネールを GridView に表示させる はコメントを受け付けていません

[WinRT] MediaElement の操作メニューは AreTransportControlsEnabled で出す

既に出オチ…というか、これだけなのですが、Windows 8.1 から(というか、.NET Framework 4.5.1 からは)MediaElement に AreTransportControlsEnabled プロパティというのがあって、簡単に操作ボタンを出せます。あの「Play」とか「Stop」とかのボタンを自作しないで済むんですね。インジケータも出るので、AreTransportControlsEnabled=”True” と書いておけば一発です。

いつも忘れるのですが、Windows 8 のほうには無くて、

MediaElement プロパティ (System.Windows.Controls)
http://msdn.microsoft.com/ja-jp/library/system.windows.controls.mediaelement_properties(v=vs.110).aspx

Windows 8.1 のほうにはあるのです。

MediaElement.AreTransportControlsEnabled property – Windows app development
http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.controls.mediaelement.aretransportcontrolsenabled

なので、普段 Developer Network で検索してもなかなか出てこなくて、うまくやると Dev Center のほうで検索されるという。いや、Dev Center のほうを使えばいいのですが、英語が主なので、何となく日本語版の Developer Network が google で検索されるというオチです。

普段は小さ目に貼り付けておいて、フル画面表示にすることもできます。

20141009_05

VS魂100連発ではスタート画面のピン留めから起動されたときには、自動的にフル画面になるようにしてあって MainPage クラスにこんな仕込みをしてあります。

public async void LoadState( string videoid )
{
    if (videoid != "")
    {
        this.media.IsFullWindow = true;
        var url = await YouTube.GetVideoUriAsync(videoid, YouTubeQuality.Quality1080P);
        this.media.AutoPlay = true;
        this.media.Source = url.Uri;
        this.media.Play();
    }
}

そして、app.xaml.cs の OnLaunched メソッドのときにページの状態をみて LoadState メソッドを呼び出します。100連発のアプリは一枚しかないので、Page を見る必要はないのですが念のため。

	if (rootFrame.Content == null)
	{
	    // ナビゲーションの履歴スタックが復元されていない場合、最初のページに移動します。
	    // このとき、必要な情報をナビゲーション パラメーターとして渡して、新しいページを
	    // 作成します
	    if (!rootFrame.Navigate(typeof(MainPage), e.Arguments))
	    {
	        throw new Exception("Failed to create initial page");
	    }
	}
	else
	{
	    // 既存のページにパラメータを渡す。
	    var page = rootFrame.Content as MainPage;
	    if ( page != null ) {
	        page.LoadState(e.Arguments);
	    }
	}
	// 現在のウィンドウがアクティブであることを確認します
	Window.Current.Activate();
}

実は、プロトコルからアクセスしたときのために OnActivated イベントがあるのですが、これは Launch の時(スタート画面などから起動されたとき)は呼び出されないため、OnLaunched の中に書きます。それ以外のときは、args.Kind で呼び出された状態を識別して分岐させます。

ActivationKind enumeration – Windows app development
http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.activation.activationkind(v=win.10).aspx

FileOpenPicker や ContactPicker を作ると、独自のピッカーが作れるという訳です…まだ作ったことはありませんが。

カテゴリー: C#, WinRT | [WinRT] MediaElement の操作メニューは AreTransportControlsEnabled で出す はコメントを受け付けていません

[WinRT] ストアアプリで動画のサムネールを取得する

怒涛の GridView シリーズの続きです。Kindle Launcher ネタはひと区切りついたので MP4 Launcher で使っているテクニックの紹介です。とある理由で、MP4 ファイルがたくさんあると仮定して、Surface RT やら PC でそれを観賞しようとすると、真っ先に動くのはストアアプリの「ビデオ」アプリなんですが、このアプリ、いちいちフォルダを開いて動画ファイルを探しに行かないといけないし、そもそも前の状態を覚えておいてくれないので、シリーズものを連続で観賞するには非常に不便です。いきおい、従来の Windows Media Player を動かすという手もあるのですが、Surface のようなタブレットの場合、音量の調節とか「標準ビデオ」のほうがやりやすいんですよね。

以前、シリーズ毎のストアアプリを作ってみたものの、シリーズ毎に自作しなくてはいけなくて途中で面倒になってしまいました。で、Kindle Launcher と同じようにスタート画面にピン留めすれば良いのでは?と思って勢いで作ったのが MP4 Launcher です。
Kindle Launcher の場合は、漫画の表紙をタイルに表示しています。表紙そのものが png あるいは jpeg でダウンロードできるので比較的簡単です。ビデオファイルの場合はどうするのか、と悩むところなのですが、実は WinRT には動画ファイルのサムネールを取得できる機能があります。

ファイルピッカーを使うと、mp4 のような動画ファイルはサムネールが表示されます。動画ファイルだけでなく
ピクチャファイルなどのサムネールも取れたりするのですが、ここでは動画を対象にします。この機能をそのまま、自作アプリに持ってこれるといいですよね。

自作アプリ内の GridView に表示させるとこんな感じになります。それぞれのセルにサムネールを表示させています。サムネール自体は、自動で作成されるらしく動画の位置を指定することはできません(と思います)。なので、時には同じ画像ばっかりが並ぶことがあるのですが、大抵の場合は違う画像が並びます。

フォルダを開くピッカーを使う

まずは、動画の入っているフォルダを指定します。ファイル指定でもいいのですが、大抵はシリーズものがひとつのフォルダにまとめてある(と思われる)ので、ユーザーにフォルダを指定して貰います。

private async void OnButtonItemSearch(object sender, RoutedEventArgs e)
{
    var picker = new FolderPicker();
    picker.CommitButtonText = "フォルダを指定する";
    picker.ViewMode = PickerViewMode.Thumbnail;
    picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
    picker.FileTypeFilter.Add(".mp4");

    var folder = await picker.PickSingleFolderAsync();

PickerViewMode には、リスト形式(List)とサムネール形式(Thumbnail)があります。ここでは Thumbnail を指定しておきます。このあたりはファイルを指定するときと同じです。

フォルダ内の動画のサムネールを取得する

ピッカーで取れたフォルダを自前のリストに保存します。リストは ViewModel スタイルにして、GridView にバインドできるようにしておきます。

public class VideoList : ObservableCollection<Video> { }
public class Video : BindableBase
{
    /// <summary>
    /// タイトル
    /// </summary>
    private string _Title;
    public string Title
    {
        get { return _Title; }
        set { this.SetProperty(ref this._Title, value); }
    }
    /// <summary>
    /// ファイルパス
    /// </summary>
    private string _Path;
    public string Path
    {
        get { return _Path; }
        set { this.SetProperty(ref this._Path, value); }
    }
    /// <summary>
    /// サムネール自体
    /// </summary>
    private Windows.Storage.FileProperties.StorageItemThumbnail _Thum;
    public Windows.Storage.FileProperties.StorageItemThumbnail Thum
    {
        get { return _Thum; }
        set { this.SetProperty(ref this._Thum, value); }
    }
    /// <summary>
    /// サムネールのBitmap
    /// </summary>
    private ImageSource _IconImage;
    public ImageSource IconImage
    {
        get { return _IconImage; }
        set { this.SetProperty(ref this._IconImage, value); }
    }
    /// <summary>
    /// サムネールの保存パス名
    /// </summary>
    private string _IconUrl;
    public string IconUrl
    {
        get { return _IconUrl; }
        set { this.SetProperty(ref this._IconUrl, value); }
    }
}

ちょっと、ややこしいですが、サムネール自体(Thum)とサムネールのビットマップ(IconImage)を別に用意しておきます。ビットマップのほうは Image コントロールにバインドするものです。サムネール自体を保存しているのは、スタート画面にピン留めするためにアプリケーションデータにファイル保存をするためです。

XAML 自体は、こんな風になります。Image コントロールに Source=”{Binding IconImage}” でバインドですね。これと Video クラスの IconImage プロパティが結びつきます。

<GridView
    Grid.Column=&quot;1&quot; Grid.Row=&quot;1&quot;
    x:Name=&quot;itemGridView&quot;
    TabIndex=&quot;1&quot;
    Padding=&quot;10&quot;
    SelectionMode=&quot;{Binding SelectMode}&quot;
    ItemsSource=&quot;{Binding Items}&quot;
    IsSwipeEnabled=&quot;false&quot;
    Tapped=&quot;itemGridView_Tapped&quot;>
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid HorizontalAlignment=&quot;Left&quot; Width=&quot;250&quot; Height=&quot;250&quot;>
                <Border Background=&quot;{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}&quot;>
                    <Image Source=&quot;{Binding IconImage}&quot; Stretch=&quot;UniformToFill&quot; />
                </Border>
                <StackPanel VerticalAlignment=&quot;Bottom&quot; Orientation=&quot;Vertical&quot;
                    Background=&quot;{StaticResource ListViewItemOverlayBackgroundThemeBrush}&quot;>
                    <TextBlock Text=&quot;{Binding Title}&quot;
                        Foreground=&quot;{StaticResource ListViewItemOverlayForegroundThemeBrush}&quot;
                        Height=&quot;30&quot; Margin=&quot;3&quot;
                        TextWrapping=&quot;NoWrap&quot;/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

フォルダのピッカーで拾えるのは、StorageFolder オブジェクトなので、この中のふぁいるからサムネールを取ってきます。

async void OpenFolder(StorageFolder folder)
{
    if (folder == null) return;
    vm.FolderPath = folder.Path;
    vm.FolderName = folder.Name;
    // 拡張子 *.mp4 のファイルを探す
    var lst = await folder.GetFilesAsync();
    var items = lst.Where(x =>
    {
        switch (System.IO.Path.GetExtension(x.Name))
        {
            case &quot;.mp4&quot;:
                return true;
            default:
                return false;
        }
    })
        .Select(async x =>
        {
            var thum = await x.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.VideosView);
            var bmp = new BitmapImage();
            bmp.SetSource(thum);
            return new Video()
            {
                Title = x.DisplayName,
                Path = x.Path,
                Thum = thum,
                IconUrl = x.Name + &quot;.png&quot;,
                IconImage = bmp,
            };
        }
    );
    vm.Items.Clear();
    foreach (var it in items)
    {
        vm.Items.Add(await it);
    }
}
  1. GetFilesAsync クラスでフォルダ内のファイルを探索
  2. LINQ の Where メソッドで拡張子 .mp4 を拾い出す(switch になっているのは、.mpeg なども調べられるように修正した名残です)。
  3. Select メソッドで Video オブジェクトを作成する

サムネール自体は、GetThumbnailAsync メソッドで作成できます。これを Image コントロールに渡せる BitmapImage に直すためには、そのまま SetSource メソッドを呼び出せば ok です。BitmapImage オブジェクトは GC で不要になったら解放されます。サムネールを自前の GridView に表示している間、保持しておけばよいわけです。具体的には、ViewModel にあたる VideoList オブジェクトがキープされる間、保持されています。

これで先ほどの自前のサムネール表示ができます。ここではサムネール自体を加工せずに表示させていますが、ちょっと手間をかければ加工することも可能です。BitmapImage オブジェクトはそのままでは加工できないので、WritableBitmap に変換するか、DirectX を使います。C# から直接 DirectX を使うことはできないので Win2D(NuGet で取得できます)を使うとよいでしょう。Win2D での加工は今度やってみましょう。

サムネールをアプリケーションデータに保存する

Kindle Launcher のように、動画のサムネール画像を使ってスタート画面にピン留めできるようにしておきましょう。タイルに表示させる画像はリソースかアプリケーションデータ内と決まっているので、アプリデータにユニークな名前になるように保存します。動画ファイルのファイル名をそのまま使ってもよいのですが、セカンダリタイルの TileID として使いたいので、名前をユニークになるように変換します。TileID には「(」などの特殊な記号が使えないので、この方法を使っています。

private string toMD5(string s)
{
    //文字列をbyte型配列に変換する
    byte[] data = System.Text.Encoding.UTF8.GetBytes(s);

    //MD5CryptoServiceProviderオブジェクトを作成
    var md5 = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Md5);
    BinaryStringEncoding encoding = BinaryStringEncoding.Utf8;
    var buff = CryptographicBuffer.ConvertStringToBinary(s, encoding);
    var hash = md5.CreateHash();
    hash.Append(buff);
    var dest = hash.GetValueAndReset();
    string signature = CryptographicBuffer.EncodeToHexString(dest);
    return signature;
}

private async System.Threading.Tasks.Task<bool> CreateSecondaryTileAsync(Video item)
{
    string tileId = &quot;MP4-&quot; + toMD5(item.Title);
    var tile = new Windows.UI.StartScreen.SecondaryTile()
    {
        TileId = tileId,
        DisplayName = item.Title,
        Arguments = toMD5(item.Title),
        RoamingEnabled = true,
    };
    tile.VisualElements.ForegroundText = Windows.UI.StartScreen.ForegroundText.Light;
    tile.VisualElements.ShowNameOnSquare150x150Logo = true;

    // アプリローカルに保存
    var stt = item.Thum.GetInputStreamAt(0);
    byte[] data = new byte[item.Thum.Size];
    var st = stt.AsStreamForRead();
    st.Read(data, 0, data.Length);
    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
    var path = toMD5(item.Title) + &quot;.jpg&quot;;
    try
    {
        var file = await folder.CreateFileAsync(path);
        using (var sw = await file.OpenStreamForWriteAsync())
        {
            sw.Write(data, 0, data.Length);
            sw.Flush();
        }
    }
    catch
    {
        // 同名のファイルがある場合は、そのまま使う
    }
    // テキストファイルにmp4のパスを保存
    var path2 = toMD5(item.Title) + &quot;.txt&quot;;
    try
    {
        var file = await folder.CreateFileAsync(path2);
        using (var sw = await file.OpenStreamForWriteAsync())
        {
            var data2 = System.Text.Encoding.UTF8.GetBytes(item.Path);
            sw.Write(data2, 0, data2.Length);
            sw.Flush();
        }
    }
    catch
    {
        // 同名のファイルがある場合は、そのまま使う

    }
    tile.VisualElements.Square150x150Logo = new Uri(&quot;ms-appdata:///local/&quot; + path);
    tile.VisualElements.Square30x30Logo = new Uri(&quot;ms-appdata:///local/&quot; + path);

    return await tile.RequestCreateForSelectionAsync(GetElementRect(this.btnMakeTile));
}

public static Rect GetElementRect(FrameworkElement element)
{
    GeneralTransform buttonTransform = element.TransformToVisual(null);
    Point point = buttonTransform.TransformPoint(new Point());
    return new Rect(point, new Size(element.ActualWidth, element.ActualHeight));
}

サムネールを取得したときの Thum プロパティの値を使います。StorageFile クラスの GetInputStreamAt メソッドを使って先頭位置からのストリームを取得します。byte[] を使っていますが、System.IO.MemoryStream を使ってもよいでしょう。このあたりは定番の処理になります。

たまたまサムネールの位置が同じ場合には同じ画像が並んでしまうのと、タイトルで表示するときに文字が読み辛くなるという難点はありますが、ひとまずスタート画面に画像付きのタイルができます。自前のリストにも画像付きのリストがでいるので、結構見栄えがよくなるのではないでしょうか。

サムネール保存時に遅延が発生する

ファイルピッカーもそうなのですが、サムネールを表示するときに遅延が発生します。await/async を使ってバインドを使っているせいなのですが、ちょっと面白い/困った現象が出ます。ViewModel でバインドをしているので、サムネールの画像が取得できたときに画面に表示されます。この操作自体がパラレルで行われるためか、次のようにたくさん動画ファイルのあるフォルダを指定したときに問題が出ます。

  1. 動画ファイルがたくさんあるフォルダを指定する。
  2. サムネールが順々に表示される。
  3. 表示の途中で、別のフォルダを選択する。
  4. 別のフォルダを指定したが、前のフォルダのサムネールがいくつか表示される。

4 のように、まだサムネールを取得しきっていないセルが GridView に表示されてしまいます。これはサムネールの遅延処理のための対策が、先にコードに不十分と思われるので、何か対処が必要ですよね。まあ、このあたりが ViewModel と遅延処理(あるいは重たい処理)の弊害ってところなんです。この話はまた別途。

 

カテゴリー: C#, WinRT | [WinRT] ストアアプリで動画のサムネールを取得する はコメントを受け付けていません

[WinRT] デスクトップアプリをスタート画面にタイルで表示する方法

デスクトップ アプリのスタート画面のタイルをカスタマイズする方法 (Windows ランタイム アプリ) (Windows)
http://msdn.microsoft.com/ja-jp/library/windows/apps/xaml/dn449733.aspx

基本は上記をよく読めばいいのですが、何故か最終的に 150x150Logo.png 等のタイルのファイルを何処に置くのか?が書いてないので、補足&メモ的に残しておきます(ステップ1から5までが対象になりますね)

*.visualelementsmanifest.xml ファイルを用意する

<Application xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;>
    <VisualElements
        BackgroundColor=&quot;#FF0000&quot;
        ShowNameOnSquare150x150Logo=&quot;on&quot;
        ForegroundText=&quot;light&quot;>
</Application>

あるいは

<Application xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;>
    <VisualElements
        BackgroundColor=&quot;#FF0000&quot;
        ShowNameOnSquare150x150Logo=&quot;on&quot;
        ForegroundText=&quot;light&quot;
        Square150x150Logo=&quot;Assets150x150Logo.png&quot;
        Square70x70Logo=&quot;Assets70x70Logo.png&quot;/>
</Application>

という形で、ファイルを作成する。*.exe と同じ位置に用意するので、下の図のようにアプリケーション名 + visualelementsmanifest.xml で書いておきます。

image

Square150x150Logo と Square70x70Logo を指定しないときは、自動的にアプリのアイコンが使われます。背景色 BackgroundColor で揃えてもいいのですが、普通のストアアプリと同じように Logo のファイルも用意しておきます。
Assets フォルダを作っていますが、App.ico などと同じ場所に作ってもよいでしょう。その場合は、Assets150x150Logo.png を 150x150Logo.png のように書き換えます。

画像ファイルを「常にコピーする」に変更する。

image

ファイルのビルドアクションは「コンテンツ」、出力ディレクトリに「常にコピー」あるいは「新しい場合はコピーする」にします。ビルドしたときに、Assets フォルダに2つの画像ファイルが作られるようにしておけば ok です。

image

プログラムファイルにショートカットを作る

  1. Win+R キーを押して、shell:programs を起動する。
  2. *.exe のショートカットを作成する。

ショートカットは普通の *.lnk ファイルになります。

image

スタート画面のアプリメニューからピン留めする

スタートメニューのプログラム以下にショートカットを置くと、アプリ一覧にアイコンが出るのでスタート画面にピン留めします。

image

「Kindle 本棚」のアイコンがそれで、ストアアプリと同じように作れます。アイコン自体は、

Windows Store App logo Maker – 高橋 忍のブログ – Site Home – MSDN Blogs
http://blogs.msdn.com/b/shintak/archive/2013/01/01/10418997.aspx

を使うと便利です。必要なのは 150 と 70 のアイコンなので、それを Assets フォルダにコピーすれば ok です。

インストーラでスタートメニューに登録する

Visual Studioのセットアッププロジェクトが復活
http://www.infoq.com/jp/news/2014/05/vs2013_installer_project
Microsoft Visual Studio Installer Projects 拡張機能
http://visualstudiogallery.msdn.microsoft.com/9abe329c-9bba-44a1-be59-0fbf6151054d

標準の Visual Studio 2013 にはインストーラが付いていないのですが、Gallery からダウンロードできます。作り方は従来の Setup と同じなので、これを使ってスタートメニューに登録もできます。Kindle Launcher の購入一覧の取得ツールはこれを使っています。

image

アプリケーションフォルダに「コンテンツファイル」を追加すると Assets 以下のファイルもインストールの対象になります。

タイルを更新する裏ワザ

先の MSDN のヘルプにある、「ステップ 7: 重要!ショートカット ファイルを更新する」を利用します。裏ワザというか、リンクファイル(*.lnk)をスタート画面に再読み込みさせる仕組みです。画像ファイルや *.visualelementsmanifest.xml だけを更新してもスタート画面の画像は変化しません。説明にある通り、powsershell を使って (ls “$env:programdatamicrosoftwindowsstart menuprogramscontoso.lnk”).lastwritetime = get-date で *.lnk ファイルを更新します。いわゆる、touch コマンドと同じです。

なので、デスクトップアプリで定期的に画像ファイルを更新して、*.lnk を更新すればスタート画面のタイルをライブタイル化できるということです。定期的に *.visualelementsmanifest.xml の中身を書き換えるツールを作ればよいので、これはそれほど難しくないと思います。おそらくストアアプリの制限よりも早く回せるでしょう。まあ、デスクトップ側にスケジューラ用のアプリが自前で必要にはなりますが。

カテゴリー: WinRT | [WinRT] デスクトップアプリをスタート画面にタイルで表示する方法 はコメントを受け付けていません

Windows 10 Technical Preview を Hyper-V で試す

タブレットでWindows 10 TPに突撃してわかった3つの注意事項 – Surface 2 & Pro 3 非公式マニュアル + WP(使い方・活用法)
http://surface.viva-m-tablet.info/entry/2014/10/03/150520

等、タブレットPC 直接入れている方も多々居られるのですが、安全に(苦笑)Hyper-V に入れます。ひとまず、メモリ 8GB + HDD 128 GB を割り当てて Windows 10 TP を入れました。

英語版/中国語版/ポルトガル語版があるので「英語版」を選んでいます。インストール時に日本語キーボードがつながっていると、日本語IMEを自動で有効にしてくれるようです(ひょっとすると Hyper-V の機能かもしれません)。インストール自体は英語で行われますが、基本は Windows 8 のインストールと同じなので迷うことはないでしょう。

日本語キーボードに直す

日本語IMEは有効になるのですが、キーボードは英語キーボードのままです。英語Windows7で日本語キーボードを使う方法 | Moonmile Solutions Blog を参考にして、日本語キーボードに直しておくとよいでしょう。

スタート画面を有効にする

Windows 10 では「スタートメニューが復活します」と言われているのですが、そこそこ Windows 8 に慣れてしまうと「スタート画面」のほうが便利です。

image

Win キーを押しても、スタートメニューしか開かないので、タスクバーを右クリックして「properties」を開き「start menu」タブから「Use the Start..」のチェックを外します。この項目は Window 8 にはなく Windows 10 特有のものです。

image

一旦、サインアウトするとことで、スタート画面が有効になります。

image

日本版のストアアプリをインストールする

英語版のストアアプリはそのままインストールできるのですが、何故かに日本語版のストアアプリは「Get Widnows 8.1 to run this app.」になっていてインストールできません。

image

ですが、ストアのマイアカウントからマイアプリを選択するとインストールが可能です。なんででしょうね…不思議ですが、これでお試しができます。

image

ストアプリは2つのモードで動く

事前に噂があった通り Windowed モード(ウィンドウモード)とフルスクリーンで動きます。

image image

ウィンドウモードは、こんな風に大きさを自由に変えられます。この Frozen Free Fall というゲームはタブレットの横置きと縦置きに対応しているので、横幅を変えると画面が変わります。このあたりはアプリ/ゲームがどの程度対応しているかによると思います。

フルスクリーンにする場合は、システムメニューから「Full Screen」を選択します。まだ、ショートカットキーが付けられていませんが、何かつくでしょう。

image

フルスクリーンにすると、従来の Windows 8 と同じように全画面表示になります。何故か、タスクバーの表示とかチャームとかも表示できない不完全なフルスクリーンモードなのですが…これは開発途中だからでしょう。

image

普段は、いわゆる「最大化」モードでストアアプリが起動します。

マルチモニタでのストアアプリの動き

Hyper-V でマルチモニタを使うようにしてみました。

image

一見、ストアプリのウィンドウモードも自由に動いて結構いい感じのような感じがしますが、問題があります。

  1. ストアアプリをフルスクリーンで開いた状態にしておく。
  2. 別のモニタで、デスクトップのウィンドウあるいはストアプリのウィンドウを操作する。
  3. 1のストアプリがウィンドウモードになってしまう。

全画面のストアアプリがウィンドウモードに戻ってしまうのは難点ですね。これも開発途中だから、なのかもしれません。ちなみに、私の MP4 Launcher などは FilePicker を使ってフォルダを選択するのですが、これが別画面になって開いてしまいます。本来ならば同じ画面に開くものなのに(理由は分かりますが…ピッカーはコントラクトなので別アプリなんですよね)。そんな訳で、デスクトップに特化して、ストアアプリ(タブレットモード)がおざなりになってしまっています。内実は想像できるのですが(リリース自体が1年以上遅れてるのですから)、ちょっと不安です。

ちなみに IE はデスクトップモードしかなくストア版がありません。このあたりも相当変です。タブレットPC に Windows 10 TP を入れる勇者の方はご注意ください。

Task View が奇怪な動きをする

鳴り物入り?っぽいタスクビュー(マルチデスクトップ)ですが、これもかなり奇怪な動きをします。

タスクバーにある四角い箱をクリックすると、複数のデスクトップを開くことができるのですが…いま、どこのデスクトップにいるのか分かりません。さらに言うと、別のデスクトップで開いたウィンドウを別のデスクトップに動かす手段は…あるのかな?ちょっとわかりません。

image

追記:Thx です。

 

とのこと、ドラッグ等はいずれサードパーティ製で作られそうですね

デスクトップごとに背景を切り替えたいところなのですが、複数の Task View の全てが切り替わります。さらに言うと、デスクトップ毎にタスクバーのアイコンが切り替わりません。このあたりの動きは Ubuntu などの Linux 系のウィンドウシステムを見習ってほしいところです。

と言いますか、個人的にはこの Task View 機能はいりません。マルチモニタの場合には、作業用モニタと観賞用モニタを分けたり切り替えたりするので、作業領域を変えたりしないんですよね。おそらく、ノート PC のようなモニタが1枚しかない状態を想定しているのでしょうが、この手のマルチデスクトップはフリーウェアでいくつか出ているので OS の機能として必要なのかは疑問です。まあ、フリーウェアの機能を次バージョンで取り入れてしまうのが Microsoft の手法なので、それの一環なのかもしれませんが。

ちなみに Task View にストアプリを開いて切り替えようとすると、OS ごと固まります。Preview なのかもしれませんが、企業向けの評価版でもあるので、これによって「評価が下がる」ことも覚悟しないといけませんね。ちょっと、この状態では Windows 10 は人に勧められる状態ではありまえん。素直に来年まで待って、顧客バージョンを以って人に勧めるほうがよさそうです。

ストアアプリは ApplicationFrameWindow 内にある

まあ、そんな不具合込みでも色々弄りたいもので、ストアアプリがウィンドウ化したのですから、なんらかのウィンドウハンドルを持っているハズですよね。

20141003_03

Visual Studio を入れて Spy で見ると「ApplicationFrameWindow」というウィンドウの下につくられています。Windows 8.1 の時は、Windows.UI.Core.CoreWindow がトップウィンドウだったので、これをさらにくるむ形になります。おそらく、最大化/最小化/フルスクリーンは、このウィンドウに対して SendMessage を送ればよいのではないでしょうか。

低スペックPCではどうだろうか?

実はおもしろいことに、Windows 10 の要求するスペックは Windows 8 と同等です。Windows 8 の要求するスペックがそもそも低いので、結構古いノート PC でも動くということですね。Twitter を見ると、ハンドヘルドな Windows 7 マシンに Windows 10 TP を入れている方もいらっしゃいます。私の手元にあるのは、Windows CE なもんだからちょっと無理(苦笑)。古めのノート PC に Windows 10 TP を入れてみましょう。ただし、要求する CPU スペックは低いのですが、HDD のアクセスの推奨値は結構高いです。これに引っ張られて、起動はできるけど実用に耐えないという感じになるかもしれないので注意が必要です。うちの 32bit Windows 8.1 PC は Firefox を立ち上げていくつか動かすだけでメモリ不足になるので(Windows Media Center が悪さをしている可能性も高いのですが)、32bit だとちょっと難しいかもしれませんが。

カテゴリー: 開発 | Windows 10 Technical Preview を Hyper-V で試す はコメントを受け付けていません

HttpListener と TcpListener の違いと Firewall と netsh の設定

PC でネットワーク通信をするときは IIS を使って HTTP プロトコルを使うのが定番なのですが、途中に IIS みたいな Web サーバーが入ってしまうのが難点です。いや、ASP.NET MVC や Web API レベルで C/S 作っている分にはいいけど、昔からの TCP/IP を使わないと実現できない(実現しやすい)というパターンを想定して、HttpListener と TcpListener を改めて使ってみます。

HttpListener を使う

HTTPプロトコルを 8188 ポートで受けて、Win キーを押すというサンプルコードです。Win キーを押すのはおまけで、まあ、こんな感じで独自の HTTP サーバーが作れるという意味ですね。

private void button1_Click(object sender, EventArgs e)
{
    string url = &quot;http://+:8188/&quot;;
    sv = new HttpListener();
    sv.Prefixes.Add(url);
    this.listBox1.Items.Add(&quot;start: &quot; + url);
    task = new Task(
        () =>
        {
            this.sv.Start();
            while (loop)
            {
                var cont = this.sv.GetContext();
                Debug.WriteLine(&quot;url:&quot; + cont.Request.Url);
                this.listBox1.Invoke(new AddListItem(
                    () =>
                    {
                        this.listBox1.Items.Add(cont.Request.Url);
                    }));

                var cmd = cont.Request.Url.Query;
                cmd = cmd.Substring(1);
                var dic = System.Web.HttpUtility.ParseQueryString(cmd);
                var val = dic[&quot;key&quot;];
                switch (val)
                {
                    case &quot;HttpKey&quot;:
                        this.Invoke(new Click(() =>
                        {
                            SendKeys.Send(&quot;^{ESC}&quot;);
                        }));
                        break;
                }
                using (var sw = new StreamWriter(cont.Response.OutputStream))
                {
                    sw.WriteLine(&quot;OK&quot;);
                }
                cont.Response.Close();
            }
            this.sv.Stop();
        });
    task.Start();
}

動かすときは、管理者モード&ローカル環境で動作確認するのが手っ取り早いです。というのも、別のPCから送信する場合は、Firewall と netsh の設定が必要になって結構ややこしいのです。
が、結構ややこしいところは避けて通れないので、後で解説します。

クライアントは、ブラウザに http://localhost:8188/sendkey?key=HttpKey な感じで呼び出せば ok です。
SendKeys.Send を送るときは一旦 Invoke しないと呼び出せないようです。new Click はデリゲート型です。

TcpListener を使う

同じものを TCP/IP のリスナー TcpListener を使って実装するとこんな感じです。今度は 8189 ポートで待ちます。

private void button2_Click(object sender, EventArgs e)
{
    this.tcp = new TcpListener(IPAddress.Any, 8189);
    this.listBox1.Items.Add(&quot;start: &quot; +&quot;tcp&quot; );
    tcp.Start();

    task = new Task(() => {
        while (loop)
        {
            var sock = tcp.AcceptSocket();
            byte[] data = new byte[1024];
            sock.Receive(data);
            var url = System.Text.Encoding.ASCII.GetString(data);
            this.listBox1.Invoke(new AddListItem(
                () =>
                {
                    this.listBox1.Items.Add(url);
                }));
            // 実際はもうちょっとパースしないと駄目
            var cmd = url.Substring(url.LastIndexOf(&quot;/&quot;) + 1);
            cmd = cmd.Substring(1);
            var dic = System.Web.HttpUtility.ParseQueryString(cmd);
            var val = dic[&quot;key&quot;];
            switch (val)
            {
                case &quot;WinKey&quot;:
                    //SendKeys.Send(&quot;^{ESC}&quot;);
                    break;
            }
            var res = &quot;OK&quot;;
            data = System.Text.Encoding.ASCII.GetBytes(res);
            sock.Send(data);
            sock.Close();
        }
    });
    task.Start();
}

同じことができるならば、HttpListener でも TcpListener でもどちらでも良いような気がするのですが、実は違います。もちろん、TcpListener のほうでバイナリデータを渡せるという利点も捨てがたいのですが、いまとなっては HTTP プロトコルで XML 形式で渡したほうが手軽でしょう。まあ、ストリーミングなんかは TCP/IP にしないと駄目なんですが、Win キーを押させるぐらいの単発動作ならば、どちらでやっても構いません。

が、いざ、別の PC から送信しようとすると、Firewall という壁があってそれぞれの設定が異なります。

Surface RT から PC にキー送信する

こんな風に、Surface RT のストアアプリから、別の PC のストアアプリを操作しよう、というのを考えました。PC のほうのストアアプリにリスナーを追加することはできないので、デスクトップにリスナー用のアプリをかませます。PC 内でのデスクトップアプリとストアアプリとの連携をどうするのか?は懸念事項ですが(たぶん、ショートカットキーの送信で済ませる予定)、タブレットから扱えると便利かなと。
タブレット自体は、iPhone でも Android でも良いわけですが、ひとまず Surface RT のストアアプリを想定します。配布が楽ですからね。

ここで、問題になるのが真ん中にはさまっている Firewall です。無効にすれば簡単(笑)なのですが、まあ配布するとなるとそうはいかないし、この際だからきっちりと Firewall の設定も済ませよう、という魂胆です。

HttpListener の場合は、Firewall と netsh の両方の設定が必要

HttpListener の場合は、HTTP プロトコルが通るので netsh http を使って HTTP プロトコルのアドレスを開けると同時に、Firewall を開けないといけません。なんとなく両方とも開けないと駄目なことが分かるのですが、実は HttpListener は Http.sys を通しているので設定が曲者です。
HTTP.SYS は、HTTP プロトコルを扱う特別なカーネルプロセスで、これが HTTP プロトコルを特別扱いしているための、Firewall の設定が一筋縄ではいきません。

まずは、netsh http を使って http://+:8188/ の呼び出しが有効になるようにします。呼び出し元を絞ることができるのですが、ここでは everyone を指定してすべてのユーザーを有効にしておきます。

netsh http add urlacl url=http://+:8188/ user=everyone

コントロールパネルでファイアウォールを開いて「詳細設定」をクリックします。

受信の規則で目的のルールをクリックするか新規に作成して、次のプログラムを「system」に書き換えます。

これを通常の *.exe ファイルにしておくと HttpListener を使ったときに Firewall が通りません。

C# HttpListener and Windows Firewall – Stack Overflow
http://stackoverflow.com/questions/17863294/c-sharp-httplistener-and-windows-firewall

理由は簡単で、HttpListener を使っているときは HTTP.SYS が一括して HTTP プロトコルを管理するので、HTTP.SYS のほうの Firewall を設定しないと駄目なんですよね。
で、せっかくプログラム単位でポートを絞っていたのが、HTTP.SYS 単位になってしまうので穴が大きくなってしまいます。仕方がないので「プログラムおよびポート」のほうで、許可するポートを絞っておきましょう。

ここでは、TCP の 8188 のポートだけを通すようにします。

netsh http のほうですが、本来は、http://+:8188/sendkey/ のように特定のアドレスを通すようにします。こうするほうがセキュリティが高いのですが、HttpListener を使うと 8188 ポートを占有してしまうので、他のアプリでこのポートで待つことは当然できなくなります。なので、あまりアドレスを書いても意味がないので、http://+:8188/ な感じでポートだけ指定しています。この firewall + netsh の書き方では、どのプログラムも通るようになるので、別のプログラムを使って先に同じポートを使うことができます。排他的ですが共有できるということですね。逆に言えば、他のプログラムに使われてしまうリスクがあるので、セキュリティが低いとも言えます。

TcpListener の場合は Firewall だけを設定する

TcpListener でリスナーを作るときは Firewall だけを設定します。

指定プログラムだけ通すので、安全といえば安全ですよね。HTTP.SYS の場合も、こんな風にできればいいのですが、ちょっと無理みたいです。
ただし、動かしてみたいときは、TcpListener の呼び出しにちょっと時間がかかります。たぶん、プログラムの起動時とファイアウォールの設定を再読み込みすることになるので、スタートだけ遅いって感じなんでしょう。

HttpListener と同じようにポートで絞ることもできるのですが、プログラム単位で絞ってあるので必要はないでしょう。このプログラム自体をすり替えれば別の動きもできるのですが、まあ、それはバージョンアップ時も再設定がいらないということです。

スクリプトを使って netsh http を設定する

windows – C# HttpListener without using netsh to register a URI – Stack Overflow
http://stackoverflow.com/questions/2583347/c-sharp-httplistener-without-using-netsh-to-register-a-uri/2782880

netsh http add urlacl url=http://+:8188/ user=everyone

削除するときは

netsh http delete urlacl url=http://+:8188/

スクリプトを使って firewall を設定する

HTTP.SYS を使う時は program=system を設定する。

netsh advfirewall firewall add rule name="SendKeySv HTTP" dir=in action=allow
netsh advfirewall firewall set rule name="SendKeySv HTTP" new ^
	program=system ^
	profile=private ^
	protocol=tcp localport=8188

TcpListener を使う場合は、program=<プログラム名>にしておく。

netsh advfirewall firewall add rule name="SendKeySv TCP" dir=in action=allow
netsh advfirewall firewall set rule name="SendKeySv TCP" new ^
	program=<プログラム名> ^
	profile=private

コマンド自体は、netsh http add urlacl などと打つとヘルプが出るので調べられる。

設定自体を見るときは、show コマンドを使えばよい。ファイルに落とす dump コマンドがあるけども、内部実装はされていません。これはヘルプにも記述がある。

> netsh http show urlacl
    予約済み URL            : http://+:8089/
        ユーザー: Everyone
            リッスン: Yes
            委任: No
            SDDL: D:(A;;GX;;;WD)

 

D:temp>netsh advfirewall firewall show rule name=&quot;SendKeySv HTTP&quot;

規則名:                               SendKeySv HTTP
----------------------------------------------------------------------
有効:                                 はい
方向:                                 入力
プロファイル:                         プライベート
グループ:
ローカル IP:                          任意
リモート IP:                          任意
プロトコル:                           TCP
ローカル ポート:                      8188
リモート ポート:                      任意
エッジ トラバーサル:                  いいえ
操作:                                 許可
OK

プログラムから runas を使って管理モードで動かす

インストーラを使うときは、管理者モードのプロンプトが必要になるので runas を使う。

private void button4_Click(object sender, EventArgs e)
{
    string args = "http add urlacl url=http://+:8188/ user=everyone";
    ProcessStartInfo psi = new ProcessStartInfo("netsh", args)
    {
        Verb = "runas",
        CreateNoWindow = true,
        WindowStyle = ProcessWindowStyle.Normal,
        UseShellExecute = true
    };
    Process.Start(psi).WaitForExit();
}

あえて、ProcessWindowStyle.Normal を使っているけど Hidden にするとウィンドウは表示されない。UAC(ユーザアカウント制御)を有効にしておくと(大抵のユーザーは「有効」のまま)、管理者モードで動かしますか?のダイアログが表示されるので「はい」を押してもらう。

ひとまず、これで外部の PC から HttpListener に送信するプログラムが作れる&動作します。

カテゴリー: C# | 1件のコメント

[WinRT] ストアアプリの WebView の中身を取得する方法

絶版マンガ図書館
http://zeppan.com/

というのがあって、絶版になった本を集めて公開しています。いろいろ経緯は絶版マンガ図書館を読んで貰うとして、この本には広告が挟み込んであってそれが収入代わりになっています。絶版本なので、古い漫画が多いのですが、漫画歴の長い私としては懐かしい本やらファンな本があって、何かと手伝いたい感じだったので、勢いで作ってみました。いや、単に Kindle Launcher と同じ形式にしたできた、ってだけなんですけどね。

絶版マンガ図書館 Launcher
http://apps.microsoft.com/windows/ja-jp/app/launcher/69c16e83-005c-49a3-ac83-6414b08d89fd

Windows 8.1 以降でどれくらいの人が読むのか?は謎なのですが、まあ良しとしましょう。

確か、Jコミの頃から API とかは無かったんですよね。なので、清く HTML ページからサルベージして情報を抜き出すようにしています。レイアウトが変わったり、id や class が変わると呼び出せなくなってしまうのですが、まあ、そのときはそのときに追随するとして。

■ストアアプリで WebView を使う

データを取得するときには、HttpClient を使うか、WebView を使って間接的に使うかします。今回の場合、書式が HTML 形式なので、WebView を使うことにしました。WinRT の場合、手軽な HTML パーサがないのが難点ですよね。処理が XML にまとまっているのはいいのですが、こんな風な時はちょっと手間です。

ただし、試してみると、WebView はコード上で new をしても使えることがわかりました。つまりは画面に出さなくても使えるんですよね。まあ、非表示にして使ってもいいのですが、今回のコードでは中で new しています。

private void OnButtonItemSearch(object sender, RoutedEventArgs e)
{
    string text = vm.Title;
    if (text == &quot;&quot;) return;
    string url = string.Format(&quot;http://www.zeppan.com/title/?query={0}&submit=&quot;,
        System.Net.WebUtility.UrlEncode(text));
    var web = new WebView();
    web.NavigationCompleted += async (_,__ ) => {
        string[] para = { &quot;$('#title').html();&quot; };
        string html = await web.InvokeScriptAsync(&quot;eval&quot;, para);
        Debug.WriteLine(html);

        html = html.Replace(&quot;.jpg&quot;>&quot;, &quot;.jpg&quot;/>&quot;).Replace(&quot;<br>&quot;, &quot;<br/>&quot;);
        html = &quot;<root>&quot; + html + &quot;</root>&quot;;
        var books = GetBooks(html);

        this.vm.Books = books;
    };
    web.Navigate(new Uri(url));
}

問題は、WevView の中身を取り出す方法です。実は、Microsoft のサンプルにこの方法が乗っています。Javascript をインジェクションして eval 実行させるという技ですね。なるほど。サンプルには document.title のような簡単な例があったのですが、コード見て分かるように jQuery が動きます。と言いますか、サイトが jQuery を使っていると、こんな風に手軽に HTML コードが取り出せるよ、という例です。

ただ、ここで難点なのは、HTML 文字列を取り出した後にパースする手段がないということです。

Deathspike/HtmlAgilityPack-PCL
https://github.com/Deathspike/HtmlAgilityPack-PCL

を使うとパースできるのかもしれません。試していませんが。

仕方がないので、コードを replace して XML パースができる状態にしてしまいます。取り出すときのコードを見る限り img と br が不正になってしまうので、ピンポイントでそこだけ変えています。まあ、jQuery が使えるので、インジェクションする javascript のほうで、適当に XML 形式に直す関数を作って eval するのがよいでしょうね。

■パース済みの XML から ExDoc を使う

数年前からちまちまと作っている/使っているライブラリですが ExDoc で値を取り出します。ExDoc はこういう適当な XML データから適当に値を取り出すのに便利なライブラリです。

BookList GetBooks(string html)
{
    var doc = ExDocument.LoadXml(html);
    var books = new BookList();

    ExElements items  = doc * "div" % "class" == "sakuhin open-detail";
    foreach (var it in items)
    {
        var book = new Book();
        book.Title = ((ExElement)(it * "div" % "class" == "rdtx")).Value.Trim();
        book.Author = ((ExElement)(it * "div" % "class" == "color888")).Value.Trim();
        book.ASIN = it * "div" % "data-baid";
        book.IconUrl = it * "img" % "src";
        book.IconUrl.Replace("thumbnail", "cover");
        books.Add(book);
    }
    return books;
}

■見た目は Kindle Launcher と一緒

UI を考えるのが面倒だったというのもありますが、データバインドさせて GridView で表示しているところは一緒です。Kindle API を使っても、絶版マンガ図書館の HTML を直接弄っても同じように使える、ということです。後は青空文庫まわりで「えあ草子」か「青空文庫リーダー・ライト」を個別呼び出しとか。

カテゴリー: C#, WinRT | [WinRT] ストアアプリの WebView の中身を取得する方法 はコメントを受け付けていません

[WinRT] デスクトップアプリからストアプリへアプリケーションデータを通じて連携させる

Kindle Launcher ネタの続きです。

Windows ストアアプリを公開するときのアカウントを「企業アカウント」にしておくと、デスクトップアプリもストアに紹介を載せることができます。そのために年間9.800円也を支払っていたのですが、いやいやほとんど使わないしやめようと思っていたら、つい先日無料になりました。なので、アカウント自体は放っておくことにしましょう。ちなみに今、出している moonmile solutions のアカウントは「個人アカウント」なのです。企業アカウントの場合、諸々の審査があってブランド名を変えられないので、こっちを使っています。まあ、個人上行主の屋号を変えればいいだけなんですけどね。

ストアプリからデスクトップを扱えなくて、逆にデスクトップからストアアプリへの通知はどうやるのか?という問題が諸々ありますが、ひとつは「プロトコル」を通じて送る方法です。幸いにしてストアアプリのほうはプロトコルを手軽に登録するので、アプリ特有のプロトコルを作っておいて、デスクトップからストアアプリを呼び出せば良いのです。ただし、これの場合は、いちいちストアアプリを起動することになります。
もうひとつの方法は、ストアアプリの「アプリケーションデータ」にデスクトップから直接書き込んでしまうことです。あまり見かけない方法(推奨されない方法?)なのですが、ローミング自体は OS からそういう手段を取っているし、アプリID 自体は固有で作られる(バージョンアップしても変わらない)のでこれを使うのが手軽でしょう。

■ストアアプリのパッケージファミリ名

Package.appxmanifest ファイルをダブルクリックして「パッケージ化」タブを表示させると、パッケージファミリ名が分かります。

ストアアプリをインストールしたときに使われるキーで、アプリで使うアプリケーションデータの位置にも使われています。ストアアプリのデータは、

C:UsersmasudaAppDataLocalPackages37659MoonmileSolutions.KindleLauncher_fjkmb51crq5v0

のように、ログインユーザーの AppData に下に作られるので、ここに直接書き込んでしまいます。Kindle Launcher は、このフォルダの LocalState に書籍の画像ファイルを保存していますが、購入一覧のツールのツールは MyBookshelf.xml で書いています。ここのパッケージ名は Kindle Launcher が公開されている限り同じものが使われるので、このファイルを他にも利用することができます。

アクセス権はユーザーの下にあるので、いくらでも書き込みが可能ですね。他のストアアプリを操作することもできます。RoamingState フォルダに書き込むと、自動的にローミングしてくれるので、適当なストアアプリの RoamingState に書いておいて自動的に同期されるようにする、という裏ワザもできます。

■ストアアプリで起動時に購入履歴を表示する

複数ページの場合は、App.xaml.cs に書くほうがいいのですが、Kindle Launcher は1枚しかないので、MainPage.xaml.cs の LoadState に書いてしまっています。

private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    // TODO: バインド可能なアイテムのコレクションを this.DefaultViewModel["Items"] に割り当てます
    GetMyBookshelf();
    vm.Title = "";
}
async void GetMyBookshelf()
{
    // MyBookshelf.xml があれば読み込む
    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
    var path = folder + "\MyBookshelf.xml";

    try
    {
        using (var sr = await folder.OpenStreamForReadAsync("MyBookshelf.xml"))
        {
            var se = new XmlSerializer(typeof(BookList));
            var books = se.Deserialize(sr) as BookList;
            this.vm.Books = books;
        }
    }
    catch
    {
        // 無い場合はそのまま
    }
}

ローカルのアプリケーションフォルダから読み込んでいます。ここでは XmlSerializer を使っていますが、DataContractSerializer を使ってもよいでしょう。気を付けないといけないのは、ファイル自体は「壊れていることがある」ので、必ず復帰ができるようにしておくことです。ここでは try-catch で例外を取得することと、catch した例外は無視することですね。何かエラーがあっても何事もなかったように進まないといけない「フェールセーフ」な作りになります。

カテゴリー: C#, WinRT | [WinRT] デスクトップアプリからストアプリへアプリケーションデータを通じて連携させる はコメントを受け付けていません

[WinRT] Kindle の購入リストを取得する

Kindle Launcher ネタの続きです。

どうせならば、購入済みの一覧を拾ってきて、それに対してセカンダリタイルを付けれたほうが便利ですよね。ということで、購入リストを Kindle Cloud Reader から取得します。
Kindle Cloud Reader 自体がブラウザなので、なんとかなるだろう、という予想のもとに進めて方針を立てます。

  1. WebBrowser を使って Kindle Cloud Reader を表示する。
  2. 中身の DOM を拾って ASIN のリストを取得する。

な感じで ok でしょうってことで。

実際やってみると、いくつか難関があって、

  • WebBrowser で接続するときに User-Agent を偽装しないといけない。
  • WebBrowser 出 iframe 内ので HTML をどうやって取るのか?

というところです。本来はWindows ストアアプリとして作りたいところですが、WinRT の WebView と使うとまともに動かないので、WPF の WebBrowser を使います。HttpClient で直接データを持ってきてもよいのですが、購入リスト自体が iframe になっているのと、HTML のパースが面倒なのでパスします。そのうち、HTML のパーサーを作って試してみましょう。

■WebBrowser で User-Agent を偽装する

何故、偽装が必要かというと、そのまま WebBrowser で呼び出したときには、

こんな画面が出て接続ができません。そんな訳で、User-Agent を偽装しています。

private void OnClickCheck(object sender, RoutedEventArgs e)
{
    // url を取得
    string url = "http://read.amazon.co.jp" + "?dummy=" + DateTime.Now.Ticks.ToString();
    string ua = "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko"; // IE
    wb.Navigate(new Uri(url), null, null, ua);
}

偽装自体は簡単で、Navigate メソッドの第4引数に設定します。ここにヘッダを追加できるので、複数追加したいときは “rn” で追加すればよいそうです。すると、初回だけ Kindle のログイン画面が出て、その後はブラウザの cookie(かな?)が使われます。
URL に dummy を渡しているのはキャッシュが使われないようにするためです。

■購入リストを取得する

購入リストは iframe の中にあります。WebBrowser.Document が mshtml.HTMLDocument にあたるので、これを使って DOM で解析していきます。mshtml 自体は、参照設定で「アセンブリ」→「拡張」にある「Microsoft.mshtml」をチェックします。

結構、手間取ったのですが、iframe の中身を取るためには、doc.frames.item(0) な感じで iframe を取ってきて、mshtml.HTMLWindow2 で iframe のウィンドウを取得します。その後は、再び document プロパティで解析するというパターンですね。

private void OnClickGetList(object sender, RoutedEventArgs e)
{
    var doc = wb.Document as mshtml.HTMLDocument;
    var iwin = doc.frames.item(0) as mshtml.HTMLWindow2;
    var idoc = iwin.document as mshtml.HTMLDocument;
    var titles = idoc.getElementById("titles_inner_wrapper");

    vm.Books.Clear();
    var books = vm.Books;
    foreach (mshtml.IHTMLElement it in titles.children)
    {
        var id = it.getAttribute("id") as string;
        books.Add(GetBook(it));
        Debug.WriteLine(id);
    }
}

上記の方法は、WPF の WebBrowser でやった方法ですが、Windows デスクトップアプリの WebBrowser では次のようになります。既に、HtmlDocument が定義されているのでそのまま使います。iframe に対しては、GetElementById を使っては取れないので、Frames コレクションから直接取ってきます。

private void button2_Click(object sender, EventArgs e)
{
    var doc = webBrowser1.Document;
    // var iframe = doc.GetElementById(&quot;KindleLibraryIFrame&quot;);
    var iwin = doc.Window.Frames[0];
    var idoc = iwin.Document;
    var titles = idoc.GetElementById(&quot;titles_inner_wrapper&quot;);
    var ids = new List<string>();
    foreach ( HtmlElement it in titles.Children ) {
        ids.Add(it.Id);
        Debug.WriteLine(it.Id);
    }
}

購入一覧の場合は、最初の frame にあるので無条件に 0 を指定していますが、複数の iframe がある場合はチェックが必要でしょう。

こうすることで、WebBrowser を使って Kindle の購入一覧を取得できます。

お次は、取得した履歴をどうやってストアアプリと連携させるか、のトリックを紹介しましょう。

カテゴリー: C#, WPF | [WinRT] Kindle の購入リストを取得する はコメントを受け付けていません