ぴーさんログ

だいたいXamarin.Formsのブログ

艦これ二期のスクリーンショットを撮るChrome拡張(ver.1.4)

chrome.google.com

前回の問題点を改善して、高DPIの環境だったり、スクロールやズームをしていても原寸大のスクリーンショットが撮れるようになりました。

ついでにオプションで保存形式を選択できるようにして

申し訳程度のアイコンを設定しました。

f:id:ticktack623:20180824202442j:plain

艦これ第二期がスタートしたのでスクリーンショットを撮るChrome拡張を作った

8月17日(金)に艦これ第二期がスタートして、FlashからHTML5に移行しました。リニューアルオープンです。

ゲーム画面が 800x480 から 1200x720 になりました。大きい!

それに合わせて艦娘の立ち絵などの画像素材も高解像度化されました。う、美しすぎる!

綺麗になったら艦娘たちのスクリーンショットたくさん撮りたいですね。

という訳で艦これ二期のスクリーンショットを撮るChrome拡張を作ってみました。ボタンひと押しでスクショが撮れます。 (初Chrome拡張たのしかったです)

せっかく作ったので公開します。

chrome.google.com

ソースコード

制限事項

スクロールしたりウィンドウを縮小していると正しく取れない

Chrome APIでタブの見えている範囲をキャプチャしてゲーム画面部分を切り抜いています。 なのでスクロールするとずれちゃいます。(スクロール量を取得すれば対応可能?)

高DPI環境で使うと正しく撮れない

例えばMacBook Proのような高DPIで画面が拡大表示されている環境。

切り抜く範囲をDPIに応じて調整すれば対応できるけど、拡大された画像を縮小し直すことになるので画質劣化しそう。

スクショを撮るたびにいちいち保存場所を選ばされる

作り始める前はFileSystem APIでローカルに保存できると思っていたのですがChrome拡張からは使えないようなので、諦めてファイルとしてダウンロード形に落ち着きました。

保存場所を選ぶダイアログが出ている間も次のスクショが撮れるので連写は可能。

アイコンが無い

せっかく公開したので何か作ります。

試したけどダメだったアプローチ

ゲーム画面を描画しているcanvas要素から直接描画内容を画像化

canvas.toDataURL()しても真っ黒な画像しか取得できませんでした。

ゲーム側で使ってるPixiJS(?)の設定でレンダラーのバッファを保持するように使っていないと取得できない模様。

Xamarin.Formsのトロフィーをもらいました

Xamarin.FormsチームがGitHubリポジトリのコントリビューター宛にトロフィーを贈ってくれました。やったぜ。

(P3PPPは私のGitHubアカウント名)

(ちなみにトロフィーの形はXamarin Universityの認定トロフィーと同じだったりする)

届くまでの紆余曲折

2018年 5月下旬

David(※)「Xamarin.Formsリポジトリにコントリビュートありがとう!贈り物をしたいのでmailing address(住所)を教えてもらえますか?」
ワイ「(細かいやり取りをするための連絡先をGitHub経由で聞いてるのかな?)、これです(e-mailアドレス)」
David「physical mailing addressを教えて?」
ワイ「アッハイ、ここ宛てでお願いします。」

※Xamarin.Formsのプログラムマネージャー

2018年 7月頭

David「アメリカ国外に発送するには荷受人の電話番号が必要みたい、教えてください。こういうことをするのは初めてで、学びがあります。」
ワイ「この番号でー。」

2018年 7月中旬

UPS(配送業者)「アメリカからの荷物が来てるんですが心当たりあります?住所が市区町村までしか入ってなくてどこに送れば?宛先がMicrosoftアワードセンター(?)になってますが会社受取にします?」
ワイ「その宛先は発送作業した部署か何かですかねぇ、普通に個人宅宛でお願いしますー。」
UPS「あー、Microsoftの従業員ではないんですね、承りましたー。」

数日後着弾

こうして見ると、ほぼ電話番号だけで届いてますね。無事に届いて本当に良かった。

Xamarin.Macのコントリビュータになりました

Xamarin.Macなアプリは作ってないんですけどね。

この時のPRが修正を受けつつマージされました。

同様に戻り値がNSObjectになってしまっているAPI1がたまにあるらしいので、見つけた人はxamarin-maciosのリポジトリにissue登録してあげてください。NSObjectになっている箇所と、正しい型が載っているAppleのドキュメントのリンクを載せてissue登録すれば、バージョンアップのタイミングで対応してくれると思います。

なんだったらTwitterで@ticktackmobileに教えてくれるのでも良いです。

参考までにPRコメントのやりとり(超意訳)

ワイ「NSObjectから正しい型に変更するPR作ったやで」
中の人A「これじゃあ破壊的変更になるから取り込めないやで」
中の人B「API互換性を維持するために#if XAMCORE_4_0みたいなバージョン分岐作るとええで」
中の人C「ブランチ修正しといたで、これでいこか」
中の人D「ちょっとまって、古いバージョンでも正しい型で受け取れるメソッド追加するのはどうやろ」
中の人C「悪くないやん、ええんちゃう」
GitHub「マージしたやで」
ワイ「やったぜ」

#if XAMCORE_4_0の部分は次のバージョン番号によって変わると思うので、新しめのコミットから探して真似すればPRもすんなり通るんじゃないかな。


  1. たぶんヘッダーファイルではid型で定義されているのだと思う。

ASUS ZenFone 4s MAXでXamarin.Androidの実機デバッグができない場合のトラブルシューティング

ASUS ZenFoneでXamarin.Androidアプリの実機デバッグをする際、「Mono Shared Runtimeのインストールがブロックされて失敗する」というのは既知の問題があります。

Can't deploy on device (Android MarshMallow) — Xamarin Forums

ZenFone 2や3では「Auto-start Manager」アプリのブロッキングを無効化すれば良いらしいです。

ZenFone 4sでも同様の問題が発生しますが、「Auto-start Manager」アプリが無くなっているので解決手順が異なります。

  1. 「モバイルマネージャー」アプリを開く。
  2. 「PowerMaster」を開く。
  3. 「節電オプション」を開く。
  4. 「自動軌道によりアプリを自動拒否する」をオフにする。

これで OK!

Visual Studio 2017で作成したXamarin.FormsプロジェクトをVisual Studio 2015でビルドする

さて、現時点のVisual Studio 2017(ver. 15.6.5)でXamarin.Formsプロジェクトを新規作成すると結構新しくてイケイケなプロジェクトをはいてくれます。

  • packages.configファイルが無い
  • 共通コードが.NET Standard 2.0(またはShared project)

これをVisual Studio 2015で開くと...

f:id:ticktack623:20180409235548j:plain

プロジェクトファイルが読み込めず、ビルドできません。

理由は...

  • .NET Standard、ましてや2.0なんて対応してない。
  • packages.configに代わり、PackageReferenceというフォーマットが使われている。VS 2015(というかMSBuild 14では読めない)

逆マイグレーション

VS 2015でビルドできるようにするために

  • .NET StandardプロジェクトをPCLプロジェクトで作りなおす。
  • iOSやAndroidのcsprojファイルからPackageReference要素を削る。(参照していたパッケージをメモしておく)
  • packagesフォルダの中身を削除する(だいたいslnファイルと同じ階層にあるやつ)
  • 空のpackages.configファイルを追加する(必要ないかも)
  • PackageReferenceで参照していたパッケージをNuGetパッケージマネージャで追加しなおす。
  • アセンブリの参照方法が変わって通らなくなったコードが出た場合は修正する。

まとめ

特別な理由がない限りVS 2015を投げ捨てましょう。そろそろ限界です。

Xamarin.Forms VisualStateManager Support

Xamarin.Forms 3.0.0からVisualStateManager、およびVisualStateが追加されます。

VisualStateManager.GoToState()を呼ぶと対応するVisualStateのSetterが適用される機能です。 これにより、Viewの状態に対応する見た目を宣言的に扱うことが可能となります。

基本的な使い方

基本的な使い方を見てみましょう。入力された文字数に応じてEntryの背景色を変える、新規パスワード入力欄風のサンプルです。

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VisualStateManagerSample.BasicSample">

    <StackLayout VerticalOptions="Center" Padding="20">
        <Label Text="enter new password" />
        <Entry x:Name="_entry" IsPassword="true">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="LengthStates">
                        <VisualState x:Name="None" />
                        <VisualState x:Name="Short">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Red" />
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Medium">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Yellow" />
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Long">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Green" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </VisualStateManager.VisualStateGroups>
        </Entry>
    </StackLayout>
</ContentPage>

VisualStateManager.VisualStateGroupsAttachedPropertyを通じてVisualStateをセットします。 VisualStateGroupsはStyleからセットすることが可能なので、VisualStateを含めてViewの規定のスタイルを作ることができます。

using System;
using System.Collections.Generic;
using Xamarin.Forms;

namespace VisualStateManagerSample
{
    [Xamarin.Forms.Xaml.XamlCompilation(Xamarin.Forms.Xaml.XamlCompilationOptions.Compile)]
    public partial class BasicSample : ContentPage
    {
        public BasicSample()
        {
            InitializeComponent();

            VisualStateManager.GoToState(_entry, "None");

            _entry.TextChanged += (s, e) =>
            {
                if (string.IsNullOrEmpty(_entry.Text))
                {
                    VisualStateManager.GoToState(_entry, "None");
                    return;
                }

                var length = _entry.Text.Length;
                if (length <= 4)
                {
                    VisualStateManager.GoToState(_entry, "Short");
                    return;
                }
                
                if (4 < length && length < 8)
                {
                    VisualStateManager.GoToState(_entry, "Medium");
                    return;
                }
                
                if (length >= 8)
                {
                    VisualStateManager.GoToState(_entry, "Long");
                    return;
                }
            };
        }
    }
}

コードビハインドではEntryのテキストが更新されるたびに、VisualStateManager.GoToState()で長さに応じたVisualStateに遷移しています。 同じViusalStateに遷移した場合はSetter適用がスキップされるため、パフォーマンスを気にして厳密に管理する必要はありません。

このサンプル実行結果はこんな感じ。

f:id:ticktack623:20180331162740g:plain

VisualStateGroup

VisualStateVisualStateGroupにまとめられた中で排他的に遷移します。

<VisualStateGroupList>
    <VisualStateGroup x:Name="Gourp1">
        <VisualState x:Name="On" />
        <VisualState x:Name="Off" />
    </VisualStateGroup>
    <VisualStateGroup x:Name="Gourp2">
        <VisualState x:Name="None" />
        <VisualState x:Name="Hightlited" />
    </VisualStateGroup>
</VisualStateGroupList>

上のようにVisualStateが設定されたViewに対して次の順にGoToSatateを実行した場合...

  1. VisualStateManager.GoToState(view, "On")
  2. VisualStateManager.GoToState(view, "Highlited")
  3. VisualStateManager.GoToState(view, "Off")

結果は次のようになります。

  1. VisualStateManager.GoToState(view, "On")
    • Group1/OnのSetterが適用される。
    • Group2のSetterは何も適用されない。
  2. VisualStateManager.GoToState(view, "Highlited")
    • Group2/HightlitedのSetterが適用される。
    • Group1/OnのSetterは解除されない。
  3. VisualStateManager.GoToState(view, "Off")
    • Group1/OnのSetterが解除される。
    • Group1/OffのSetterが適用される。
    • Group2/HightlitedのSetterは解除されず、適用されたまま。

「Setterが適用されていない初期状態に戻す」ということがしたい場合は、Setterを持たないVisualStateを用意しておくと良いです。

Style、Trigger、VisualStateの比較

Xamarin.FormsにはSetterクラスの集まりによってVisualElementのプロパティを方法として、StyleTriggerが以前から存在します。

これらとVisualStateにはどのような違いがあるでしょう。

(便宜上Setterの塊をカタカナ表記でスタイル呼ぶことにします)

Style

  • 特定のクラスに対して規定のスタイルを設定できる。
  • 他のStyleを引き継ぐことができる。(BasedOn)

Trigger

  • イベントの発火やプロパティの変化を契機にVisualElementのプロパティをセットする。
  • EnterActoins、ExitActionsを実行できる。

VisualState

  • プロパティの設定値をVisualStateという纏まった単位で管理できる。
  • VisualStateを変更する判定処理とVisualStateによって変更されるスタイルが分離できる。

状態遷移の判定処理とスタイルの分離がVisualStateの大きな特徴と言えるでしょう。

例えば独自のViewを定義する場合、取りうるVisualStateの名前と条件を公開しておけば、利用者は自分のアプリに合ったスタイルを自由に設定することができます。

規定のVisualState

実は、全てのViewは暗黙的にNomalDisabledFocusedの3つのVisualStateに遷移します。

それぞれのVisualStateへ遷移するタイミングは以下の通り。

  • Nomal
    • VisualStateManager.VisualStateGroups AttachedPropertyが更新されたとき。
    • ViewのIsEnabledプロパティがtrueに変更されたとき。
    • ViewのIsFocusedプロパティがfalseに変更されたとき。
  • Disabled
    • ViewのIsEnabledプロパティがfalseに変更されたとき。
  • Focused
    • ViewのIsFocusedプロパティがtrueに変更されたとき。

(フォーカス中にIsEnabled = falseしたらマズいような……、最終的にDisabledになるのかNormalになるのか、Rendereの実装に依存する?)

本当にVisualStateが変更されているか確かめてみしょう。

こんな感じのXAMLを書きます、自分ではVisualStateManager.GoToState()しません。

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:VisualStateManagerSample"
             x:Class="VisualStateManagerSample.CommonStatesSamplePage">

    <ContentPage.Resources>
        <!-- Normal <-> Disabled 確認用 for Image -->
        <Style TargetType="Image" x:Key="StatusIconStyle">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal">
                            <VisualState.Setters>
                                <Setter Property="Source" Value="{local:ImageResource VisualStateManagerSample.Images.online.png}"/>
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Disabled">
                            <VisualState.Setters>
                                <Setter Property="Source" Value="{local:ImageResource VisualStateManagerSample.Images.offline.png}"/>
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
        
        <!-- Normal <-> Disabled 確認用 for Label -->
        <Style TargetType="Label" x:Key="StatusCaptionStyle">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal">
                            <VisualState.Setters>
                                <Setter Property="TextColor" Value="Green"/>
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Disabled">
                            <VisualState.Setters>
                                <Setter Property="TextColor" Value="Gray"/>
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
        
        <!-- Normal <-> Focused 確認用 for Entry -->
        <Style TargetType="Entry" x:Key="EntryStyle">
            <Setter Property="VisualStateManager.VisualStateGroups">
                <VisualStateGroupList>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Green" />
                            </VisualState.Setters>
                        </VisualState>
                        <VisualState x:Name="Focused">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Orange" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateGroupList>
            </Setter>
        </Style>
    </ContentPage.Resources>

    <StackLayout Padding="20" Spacing="30">
        <Label Text="CommonStates Sample" />

        <!-- SwitchでImageとLabelのIsEnabledを切り替える -->
        <Grid ColumnSpacing="20">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
                   Style="{DynamicResource StatusIconStyle}"
                   HeightRequest="60" WidthRequest="60"
                   IsEnabled="{Binding IsToggled, Source={x:Reference _onlineSwitch}}" />
            <Label Grid.Row="0" Grid.Column="1"
                   Style="{DynamicResource StatusCaptionStyle}"
                   Text="@ticktackmobile" VerticalTextAlignment="End"
                   IsEnabled="{Binding IsToggled, Source={x:Reference _onlineSwitch}}" />
            <Switch x:Name="_onlineSwitch" Grid.Row="1" Grid.Column="1" IsToggled="true" />
        </Grid>
        
        <Entry x:Name="_entry" Style="{DynamicResource EntryStyle}" Placeholder="IsFocused: true = Orange, false = Green" />
    </StackLayout>
</ContentPage>

local:ImageResourceはEmbeddedResourceから画像を読み込む ためのXAMLマークアップ拡張です。(Guideに載ってるやつ)

実行結果がこちら。

Xamarin.Forms 3.0.0-pre2ではバグでIsFocusedを変更したときのVisualStateが逆になってます。(修正取り込み済み)

ticktack.hatenablog.jp

意図しないVisualStateの遷移を防ぐためNomalDisabledFocusedを使用しないのが無難ですかねー。