コンソールアプリを.NET Framework 4.5とNETCore2.1に両対応させつつ、GlobalTool化してみた

「.NET core グローバル ツール」っていうのを使うと、簡単にコンソールアプリを配布&インストールできるんだってー「マジで?!」 ということでEmptyKeysUI_Generatorの中のekUiGenを.NET Framework 4.5とNETCore2.1に両対応させて、NETCore2.1のほうをGlobalToolパッケージ化しました。

  1. イケル気がする(気のせい)
    最初は、ekUiGen.csprojの中のTargetFrameworkタグをTargetFrameworksにしてnetcoreapp2.1を追加すればイケルなと思っていたのですが、うまくいきませんでした。 「PackAsToolはNETCore2.1以降じゃねえとだめだゴルァ!」(意訳)とのこと。GlobalToolはNETCore2.1だけでいいのよ、NET4.5は普通のコンソールアプリにしたいの。誰か過去にやってないのかしら?とググってみたところ、.NET Core Global Tools Configuration | JvRというサイトを発見しました。

  2. やったか?(やってない)
    先人の知恵に従い、csprojを変更。ビルド後イベントに dotnet pack /p:GlobalTool=true -c Release を入れてみたが、失敗。 ビルドしてから、dotnet pack /p:GlobalTool=true -c Releaseを実行したところ、無事GlobalToolができました。めでたしめでたし。

  3. 実行できねえ(地獄への入り口)
    さっそくできたnupkgをインストールだぜヒャッハー!! インストール方法はここを参照 実行してみたらこんなエラーが

    A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in 'C:\Program Files\dotnet'.

    何じゃあこりゃああああ

  4. .NetCoreアプリとしてもダメ
    ググってみたものの、なかなか対処法がわからない。しかし、「それ、.NetCoreアプリとして動くの?」(意訳)という一文に従ってみると、

    A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in '(ユーザーフォルダが含まれているので省略)'. Failed to run as a self-contained app. If this should be a framework-dependent app, add the (省略)\ekuigen.runtimeconfig.json file specifying the appropriate framework.

    あかんやん

  5. よくわからんけど動いた
    プロジェクトファイル(*.csproj)に

<PropertyGroup>
    <OutputType>Exe</OutputType>
</PropertyGroup>

を入れたところ動いた。どこからこの一文を持ってきたんだ私。

この格闘の末のプロジェクトがこちら github.com そのうち本家にプルリクエスト投げる予定

MonoGameでいまさら日本語表示 WpfFontPipeline改造してみた

やってみたくなったのでMonoGameで日本語表示させてみた(4/7不具合修正)

続・真・簡単(かもしれない)日本語表示
カスタムコンテンツプロセッサーを利用した高度な日本語表示を行う をもとにしてコンテンツプロセッサを作り、それを利用して日本語表示してみた。

f:id:reniris:20190319181228p:plain
いちおうCrossPlatformにしてみた
f:id:reniris:20190319181229p:plain
文字がちっちゃいけどAndroidでも表示できた

以下にコンテンツプロセッサとサンプルを公開している github.com

やってみた結果、日本語表示ひとつでめんどくさい処理が必要とかMonoGameが日本で普及しないわけがわかった。 WpfFontPipeline.dllはサンプルではプロジェクトフォルダに含めてたけど、NuGetとかにした場合はどーすんだろ、PackageReferenceだとグローバルキャッシュの場所とか取り方わかんないし、いまどきpackages.configとかあんまり使いたくないしなぁ。

ほんとはDrawTextだけじゃなくてEmptyKeysとかでもサンプル作ってみたかったのだが、XAMLを使うのにEXEを噛ませる必要があって、PackageReference形式だと死ぬという致命的な欠陥があったのでお蔵入りになりました。 MonoGameでいいGUIライブラリないかしら。Myraはフォント変える方法わかんないし

Idle Miner Tycoonイベント攻略

最近Idle Miner Tycoonにハマッているのですが、イベント無理ゲーじゃね?と思っていたところにいい感じの攻略法を見つけたので紹介します。 今回紹介する攻略法は、以下のサイトを翻訳してコピペ改変したものです。 www.reddit.com

イベント鉱山攻略法

1研究する

「全鉱山収入がx%増加」をできれば3つGetしよう。スキルレベルが高ければなお良し。

2 スーパーマネージャーをGetする

Dr. Steiner、Blingsley、Turnerを最低1人できれば3人Getしよう。スキルを使えばキャッシュが大量に手に入ります。

以下原文改変

イベント鉱山はしばしば「不可能」または「時間がかかりすぎる」または「課金」とみなされており、新しいプレイヤーにとってはこれが本当である可能性が高い。 しかし、イベントの鉱山を非常にスムーズに動かすためにはいくつかのことがあります。

1.メインゲームをプレイする(研究用)

これは鈍いアドバイスのように見えるかもしれませんが、これはEvent Minesで成功するためには最も重要なことです。 あなたのイベントの鉱山の攻略は「研究」からのものです!

緑色のスキルツリーでは、「全鉱山収入がx%増加」という3つのクラスターを持っています。 できるだけ早くこれらの3つのクラスタのそれぞれに1つのPointを入れてください! これはあなたのイベント鉱山の収入に対しておよそ60%の利益をもたらすだけでなく、あなたの他のすべての鉱山(本土を含む)にももたらします。

通常のゲームでは、ツリー上の他のすべてのスキルノードに1ポイントを打つことになるでしょう。 それが達成されると、あなたのすべての鉱山で合計収入が169.5%増加するように、「すべての収入」ノードを最大限に使い始めるでしょう。 少ないように見えるかもしれませんが、イベント鉱山の基本的な収入増です。

スキルツリーの残りの部分を見ると、ゲームをさらに進めるのに役立つものは何もありません。 最大のインセンティブは、「あなたの倉庫/エレベーターマネージャーのアップグレードコストx%削減」という赤スキルになります。

青スキルツリーでは、素敵な所得向上のために2つの大きなクラスターを最大限に活用しようとしています。 それらを過小評価しないでください! Shaft-Unlockの時間短縮にどのくらいのポイントを得ているかにもよりますが、これは常に良いことです。

2.スーパーマネージャーを取得する(本土鉱山)

これらの本土鉱山はf ***として退屈であるため、これは達成するには厄介なものですが、報酬は狂気です。 残念ながら、これはすべて運しだいです。 そして、それらの本土鉱山にかなりの時間を費やす必要があります。 私は、本当に頻繁に! 6番目のスーパーマネージャーの費用は17Kで、最後のメインランドマインは1.68kです。これは第6マネージャーのメインランド鉱山の11倍です。 まだ7番目を手に入れることについて考えたくない。

3つの最も貴重なスーパーマネージャーを手に入れるべきです

Dr. Steiner

倉庫に3倍のビームを直接ビームこれは絶対的な獣です。 5分のアクティブタイムで、あなたはこの小さな友達から多くの収入を得ることができます。 彼はまた、あなたが活動している間あなたの鉱山のシャフトをアップグレードすることができ、彼の報酬も同様に増加するという意味で動的にスケールします。

Blingsley

1分ごとに5秒ごとに利益を得るこれは短期間で最も強力なものです。 しかし、それはあなたの鉱山の適切なレベルであなたのエレベーターと倉庫を持つことに依存しています。 あなたの最も有益な鉱山シャフトでこのマネージャーを使用することで、現在よりも1つの通貨の層を簡単に得ることができます。

Turner

効果の終わりに10倍のリソースを手に入れましょう。これはかなり簡単なものです。 Dr. Steinerと同様に、鉱山シャフトの10倍の速度で倉庫に直接収入を得ることができます。 彼の所要時間は短いので、あなたの最初の鉱山労働者が無駄な収入にならないようにしてください。 彼のクールダウンも非常に短いので、ゲームを非常に積極的にプレイすると#1になる可能性があります。

では、スーパーマネージャーはなぜそんなに強いのですか? まあ、理由はかなり単純です。 Elevator and Warehouseと比較して、あなたの鉱山軸へのレベルを取得することはかなり安いです。 あなたが数時間プレイした後にログインするときに意味します。エレベーターや倉庫にはまだお金を入れないでください。 すべてのお金をあなたの最も有益な鉱山のシャフトに入れてください! それを済ませたら、ターナー氏を最初に選ぶことをお勧めします。 これにより、あなたはきれいな収入を得ることができます。

次に、Dr. Steinerに行く。 彼と一緒にあなたは今、非常に強い安定した収入を得るでしょう。 最初の3〜4分は、あなたのマインシャフトをさらに強くするだけです。 最後の2分間が到着したら、私はあなたのエレベーターと倉庫レベルをあなたの新しい鉱山シャフト所得レベルまでスピードアップすることを提案します。

エレベーターと倉庫をあなたの鉱山シャフトと同じレベルにすることは、Blingsleyを起動するのに最適なタイミングです。 そんなにお金! この資金を3つのプラットフォームすべてに均等に費やし、すべてのマネージャーが再び使用したい場合は30分後に戻ってください。 または、あなたの他の鉱山を10分間プレイして、クールダウンがわずか15分であれば、Turnerをもう一度使用してください。

3.ブーストを利用する

あなたが本当に最後に向かって苦労しているなら、これは最後の手段です。 以前のすべてのイベント鉱山から集めた収入の増加、毎日のログインボーナス、そして遠征が便利になりました。

できるだけ多くのブーストを積み重ねてください! 各色のアクティブなブーストをアクティブにすることができます! 2倍の収入。 赤から5倍の収入。 グレイからの10倍の収入。 それ以上のものはもう一つの素晴らしいボーナスです !

また、あなたのスーパーマネージャーとこれらのブーストを組み合わせてください。 スーパーマネージャーは、これらの所得向上をまったく新しいレベルに引き上げます。

4.辛抱強さ - あきらめないでください

あなたがゲームを始めているとき、Event Minesは不可能に近づいています。 研究についての最初のポイントは、私が作ることができる最大のものです。 すべての鉱山の収入を増やすことは、それほど簡単ではありません。 早期にイベント鉱山を行うためにブーストを使用しなければならないと感じたら、ブーストを使用しないでください! 最初のEvent Mineをスキップして、次のイベントが来るのを待ってください。 「AR」「AS」「AT」の通貨単位にするためにブーストを使用する必要がある場合は、使用しているブースト数に関係なく、必要な「AX」にすることはできません。

だからメインゲームを続けてください。 あなたの研究を進めてください。 メインランド鉱山を再生し、あなたのスーパーマネージャーを圧倒してください。 あなたが少なくとも5つを持っている前にスーパーマネージャーを再登録しないでください。 最初の4を取得するのはかなり速いはずです。 5番手を取ることはちょっとした仕事ですが、6番手からスタートするのは本当に面倒です。 もしSteinerさんがあなたの5番目のマネージャーになれないのであれば、Steinerのために少しでもお金をかけて欲しいです。

5.課金する

このゲームをサポートするための優れた投資を探している場合は、永続的な2倍のパッシブボーナスを獲得してください。 ダブル・インカムとダブル・パッシブ・インカムはおそらくあなたが得ることができるあなたのお金のための最も狂った価値です。 彼らが乗法的であることを考えれば、コイン・インカム・ブーストの報酬も増加し、スーパー・マネジャーをさらに強力にすることができます。

最終的な言葉

あなたがゲームを楽しんでいることを願っています。 そして、これらのヒントがイベント鉱山を少しでも簡単に手伝ってくれることを願っています。 そのゴールデントロフィーに達することは素晴らしい気分で、あなたが勝つ500xブーストはあなたのメイン鉱山のスーパーブーストを可能にします。 2000xの価値がある5分のブーストが得られれば、あなたはどれほど遠くまで押し込むことができるのか驚くでしょう - そして、これらのブーストを持つことは、実際にインスタント・キャッシュ・マネーを使う価値があります。

だから、みんなを遊ぶ。 鉱山が開く前にまだ少し時間がかかります。 あなたの研究を進めてください。 もう1人のスーパーマネージャーを獲得し、それらのイベントの鉱山を打ち負かす!

EX1

通常の鉱山では、できるだけ深く掘削し、できるだけ深く鉱山をアップグレードして、別の鉱山をロック解除することができます。 すすぎ、繰り返します。 そして、ほとんどの場合、真実である - 時折、鉱山のシャフトは、ある限界(400,500,600など)を越えたときに、1〜2レベル以上の性能を発揮しますが、通常、最も深いものが最も古いものです。

イベント鉱山では、私はより良い仕事をするための別の戦略を見つけました。 できるだけ早く深く掘り下げてください。障壁には時間がかかってしまいます。新しい選手にとっては、できるだけ早く始めることが大切です。 しかし、それらの深いレベルを最大化するのではなく、最も高い(サーフェスに最も近い)800に達することに集中してください。800までの最後の鉱山はあなたの一番の恩人、エレベーターの短い旅、 これらはあなたの一番の徴兵者になるので、あなたはそのレベルで強調した3人のスーパーマネージャーの1人を使用することができます。彼が生み出す収入は彼の直下の鉱山シャフトを水平にするために使用できます。 それらが770+に達したら、そのレベルであなたの次のスーパーマネージャーをアクティブにして繰り返します。

開発者は、新しいイベントのパターンを変更する際に、異なる方法でバランスを取ることができる可能性がありますが、これは最後のいくつかではうまくいきました。 立ち上げ後数時間以内に確認できるはずです。

EX2 友達からの助けを得る

友人を追加すると、あなたの所得に+ 5%の増額が与えられます。無料で最大100%増(2倍の恒久的な増額)です。 リンクをクリックして人を追加するには、少し時間がかかります。 あなたの友人のリンクをスレッドに投稿すると、他のプレイヤーもあなたにゲームを追加することができ、少なくとも20人の友人を獲得するプロセスをスピードアップします。

もう1つの利点は、追加の現金、ボーナス乗数、さらに追加のスキルポイントを獲得するための良い方法である遠征へのアクセスです。

提督業プラグインを作ってみよう(5)ウィンドウ表示タイミングを変更しよう

現在は、プラグインがロードされたタイミングで艦隊情報ウィンドウが表示されていますが、艦これの起動を検知して、そのタイミングで艦隊情報ウィンドウを表示するように変更します。

1.艦これの開始を監視して、開始されたらメッセージを投げる

艦これが始まると、KanColleModel.IsRegisteredプロパティがtrueになるのでそれを監視すれば艦これの開始を検知できます。実際のコードを以下に記載します。

  • InformationViewModel.cs
public class InformationViewModel : Livet.ViewModel
{
    //艦これの情報はここからみんな持ってきます
    private KanColleModel Kancolle { get; } = KanColleModel.Current;

    private PropertyChangedEventListener listener;

    /// <summary>
    /// 艦隊情報
    /// </summary>
    public FleetViewModel[] Fleets { get; private set; }

    public InformationViewModel()
    {
        listener = new PropertyChangedEventListener(Kancolle);
        //艦これの開始を監視して、開始されたらRegister()を呼ぶ
        listener.RegisterHandler(() => Kancolle.IsRegistered, (s, e) => Register());
        //艦これの艦隊情報の変化を監視して、変化があったらUpdateFleets()を呼ぶ
        listener.RegisterHandler(() => Kancolle.Fleets, (s, e) => UpdateFleets());

        this.CompositeDisposable.Add(listener);
    }

    public void Initialize()
    {
    }

    /// <summary>
    /// 艦これが始まったときに呼ばれる
    /// </summary>
    private void Register()
    {
        Messenger.Raise(new InteractionMessage("InfoShow"));
    }

    /// <summary>
    /// 艦隊情報が更新されたときに呼ばれる
    /// </summary>
    private void UpdateFleets()
    {
        var fleets = this.Kancolle.Fleets;

        if (fleets == null) return;

        Fleets = fleets.Select(f => new FleetViewModel(f)).ToArray();
        RaisePropertyChanged(nameof(Fleets));
    }
}

2.メッセージをViewで受け取ってコードビハインドを呼ぶ

  1. ソリューションエクスプローラーでプロジェクトを右クリックして「Blendでデザイン」を選択
  2. InformationWindow.xamlをアクティブにしたら、 「アセット」パネルを表示して、「ビヘイビアー」「Livet」「LivetCallMethodAction」の順に選択する f:id:reniris:20180910013145p:plain
  3. 選択した「LivetCallMethodAction」を「オブジェクトとタイムライン」パネルの「MetroWindow」にドラッグする
  4. XAMLの「LivetCallMethodAction」をクリックして「プロパティ」パネルのTriggerTypeの横にある「新規」ボタンを押す
  5. 出てきた「オブジェクトの選択」ダイアログから「InteractionMessageTrigger」を選択して「OK」ボタンを押す f:id:reniris:20180910014308p:plain
  6. 「プロパティ」パネルからトリガー MessageKeyに「InfoShow」Messengerは「データバインディングの作成」を選択して「Info.Messenger」を選択してOKを押す f:id:reniris:20180910014617p:plain

  7. 「プロパティ」パネルのその他の指定からMethodNameに「ShowWindow」MethodTargetは「データバインディングの作成」を選択してバインドの種類「RelativeSource FindAncestor」先祖の型とレベル「InformationWindow」を選択してOKを押す f:id:reniris:20180910015907p:plain

  8. コードビハインドでウィンドウを表示する関数を実装 InformationWindow.xaml.csに「ShowWindow」という名前の引数、返り値なしの自分自身を表示する関数を実装

実際のコードを以下に記載します

  • InformationWindow.xaml
<metro:MetroWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:metro="http://schemes.grabacr.net/winfx/2014/controls"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:プロジェクト名.Views"
    xmlns:vm="clr-namespace:プロジェクト名.ViewModels" 
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf" 
    x:Class="プロジェクト名.Views.InformationWindow"
    mc:Ignorable="d"
    Background="{DynamicResource ThemeBrushKey}"
    Foreground="{DynamicResource ActiveForegroundBrushKey}"
    Title="InformationWindow" Height="400" Width="300" 
    d:DataContext="{d:DesignInstance {x:Type vm:InformationWindowViewModel}}" 
    Topmost="{Binding Setting.TopMost, Mode=TwoWay}" 
    WindowSettings="{Binding Setting, Mode=OneWay}" 
    IsRestoringWindowPlacement="True">

    <metro:MetroWindow.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="KanColleResource.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </metro:MetroWindow.Resources>
    
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Info.Messenger}" MessageKey="InfoShow">
            <l:LivetCallMethodAction MethodName="ShowWindow" MethodTarget="{Binding Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:InformationWindow}}}" />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>

    <DockPanel>
        <!-- キャプションバー ここから -->
        <DockPanel metro:MetroWindow.IsCaptionBar="True" DockPanel.Dock="Top">
            <Border DockPanel.Dock="Bottom"
                    Height="4" />
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
                <metro:CaptionButton 
                    Style="{DynamicResource PinButtonStyleKey}" 
                    VerticalAlignment="Top" 
                    IsChecked="{Binding Setting.TopMost, Mode=TwoWay}" />

                <metro:CaptionButton WindowAction="Minimize" VerticalAlignment="Top" >
                    <Path Style="{DynamicResource MinimizeIconKey}"/>
                </metro:CaptionButton>
            </StackPanel>

            <TextBlock Text="艦隊情報 - プラグインテスト" HorizontalAlignment="Left"
                       Style="{DynamicResource CaptionTextStyleKey}"
                       Margin="2,0,8,0" />
        </DockPanel>
        <!-- キャプションバー ここまで -->
        <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
            <local:Information DataContext="{Binding Info, Mode=OneWay}"/>
        </ScrollViewer>
    </DockPanel>
</metro:MetroWindow>
  • InformationWindow.xaml.cs
/// <summary>
/// InformationWindow.xaml の相互作用ロジック
/// </summary>
public partial class InformationWindow
{
    public InformationWindow()
    {
        InitializeComponent();
    }

    /// <summary>
    /// ウィンドウを表示する
    /// </summary>
    public void ShowWindow()
    {
        this.Show();
    }
}

3.プラグイン本体を変更

今まで艦隊情報ウィンドウを表示していた部分を削除します。(ここではわかりやすくするためにコメントアウトしています)

class Plugin : IPlugin, IDisposable
{
    private Views.InformationWindow info;  //メインのView
    private ViewModels.InformationWindowViewModel infovm;    //メインのViewModel

    public Plugin()
    {
    }

    //プラグインが読み込まれたときに呼ばれる
    public void Initialize()
    {
        SettingsHost.LoadFile();    //設定をロード

        infovm = new ViewModels.InformationWindowViewModel(nameof(Views.InformationWindow));
        info = new Views.InformationWindow
        {
            DataContext = infovm
        };
        //info.Show(); //表示タイミングを変更
    }

    public void Dispose()
    {
        //ここに終了時の処理を書く
        SettingsHost.SaveFile();    //設定をセーブ
    }
}

Livetを使うことで簡単にViewModelからViewの関数を簡単に呼ぶことが出来ました。 連載はここで終了です。

提督業プラグインを作ってみよう(4)ウィンドウ位置とサイズを保存・復元しよう

9/12追記 うっかり設定の保存・復元処理を追加し忘れていたのを修正しました。

ウィンドウ位置とサイズを保存・復元する機能自体はすでにMetroWindowに実装されています。 ただし、デフォルトの実装だと%UserProfile%\AppData\Local\ にファイルが作成されてしまうので、この連載ではプラグインdllと同じフォルダに保存するようにプログラムを改造していきます。

1.ウィンドウ位置とサイズを保存・復元するためのクラスを追加

Models\SettingsフォルダにSerializableSetting.cs、WindowSetting.csを追加します。 bool TopMostでウィンドウが最前面かどうかを、Nullable<WINDOWPLACEMENT>Placementでウィンドウ位置とサイズを保存します。

  • SerializableSetting.cs
/// <summary>
/// シリアライズする設定項目の基本クラス
/// </summary>
/// <seealso cref="System.ComponentModel.INotifyPropertyChanged" />
public abstract class SerializableSetting : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged([CallerMemberName]string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    /// <summary>
    /// シリアライズするクラスを識別するための文字列(デフォルトはクラス名)
    /// </summary>
    public virtual string CategoryName => this.GetType().Name;
}
  • WindowSetting.cs
/// <summary>
/// ウインドウ設定用クラス
/// </summary>
/// <seealso cref="MetroRadiance.UI.Controls.IWindowSettings" />
public class WindowSetting : SerializableSetting, IWindowSettings
{
    /// <summary>
    /// ウィンドウを常に最前面に表示するかどうかを示す設定値
    /// </summary>
   #region TopMost変更通知プロパティ
    private bool _TopMost;

    public bool TopMost
    {
        get
        { return _TopMost; }
        set
        { 
            if (_TopMost == value)
                return;
            _TopMost = value;
            RaisePropertyChanged(nameof(TopMost));
        }
    }
   #endregion

    /// <summary>
    /// ウィンドウの位置・サイズ・状態を示す設定値
    /// </summary>
   #region Placementプロパティ
    private WINDOWPLACEMENT? _Placement;
    public WINDOWPLACEMENT? Placement
    {
        get
        { return _Placement; }
        set
        {
            if (_Placement.Equals(value))
                return;
            _Placement = value;
        }
    }
   #endregion

    /// <summary>
    /// シリアライズするクラスを識別するための文字列(ウィンドウごとに一意な文字列にすること)
    /// </summary>
    public override string CategoryName { get; }

    public WindowSetting() : this(null) { }

    public WindowSetting(string key)
    {
        this.CategoryName = key ?? this.GetType().Name;
    }

    /// <summary>
    /// IWindowSettings.Reload
    /// </summary>
    public virtual void Reload()
    {

    }

    /// <summary>
    /// IWindowSettings.Save
    /// </summary>
    public virtual void Save()
    {

    }
}

2.設定データをファイルへの読み書きするためのクラスを追加

Models\SettingsフォルダにSettingsRoot.cs、SettingsRoot.cs、SettingsHost.csを追加します。 SettingsHost.cached_instancesに入っている設定データをファイルへ保存・復元します。

  • SettingPath.cs
/// <summary>
/// パス、ファイル名等の定数を管理するクラス
/// </summary>
public static class SettingPath
{
    /// <summary>
    /// このプラグインのあるフォルダを取得
    /// </summary>
    public static string GetDllFolder() => Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName;

    /// <summary>
    /// アセンブリ名
    /// </summary>
    public static readonly string DLLName = Assembly.GetExecutingAssembly().GetName().Name;

    /// <summary>
    /// 設定ファイル名
    /// </summary>
    private static readonly string SettingFileName = $"{DLLName}.xml";

    /// <summary>
    /// ローカルApplication Dataフォルダに 
    /// \grabacr.net\KanColleViewer\Plugins\KanburaLike\アセンブリ名.xaml
    /// を繋げたもの(コンパイルオプションで使用可能)
    /// </summary>
    private static string LocalAppFilePath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "grabacr.net", "KanColleViewer", "Plugins", DLLName, SettingFileName);

    /// <summary>
    /// このプラグインがあるフォルダに
    /// \アセンブリ名.xaml
    /// を繋げたもの(デフォルトはこれ)
    /// </summary>
    private static string PluginPath { get; } = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName,
         SettingFileName);

#if SETTING_LOCALAPP
    public static string Current { get; } = LocalAppFilePath;
#else
    public static string Current { get; } = PluginPath;
#endif
}

SettingPath.Currentで設定ファイルのフルパスが取れます。

  • SettingsRoot.cs
/// <summary>
/// 設定用データのDictionaryをシリアライズするためのクラス
/// </summary>
[SerializableAttribute()]
[XmlType(AnonymousType = true)]
[XmlRoot(Namespace = "", IsNullable = false)]
[XmlInclude(typeof(WindowSetting))]
public class SettingsRoot
{
    [XmlArrayItem(IsNullable = false)]
    public string[] Keys { get; set; }

    [XmlArrayItem(IsNullable = false)]
    public SerializableSetting[] Values { get; set; }
}

シリアライズするクラスが増えたら、XmlIncludeに追加していきます。

  • SettingsHost.cs
/// <summary>
/// 設定データをまとめてセーブ、ロードするクラス
/// </summary>
public static class SettingsHost
{
    /// <summary>
    /// 設定データのインスタンスのキャッシュ
    /// </summary>
    private static Dictionary<string, SerializableSetting> cached_instances = new Dictionary<string, SerializableSetting>();
    private static readonly object _sync = new object();

    /// <summary>
    /// 現在のcached_instancesにキャッシュされている <see cref="{T}"/>
    /// を取得します。 キャッシュがない場合は <see cref="create"/> に従って生成します。
    /// </summary>
    /// <returns></returns>
    public static T Cache<T>(Func<string, T> create, string key) where T : SerializableSetting
    {
        if (cached_instances.TryGetValue(key, out SerializableSetting obj) && obj is T) return (T)obj;

        var instance = create(key);
        cached_instances[key] = instance;

        return instance;
    }

   #region Load / Save

    /// <summary>
    /// ファイルからロード
    /// </summary>
    public static void LoadFile()
    {
        try
        {
            if (File.Exists(SettingPath.Current))
            {
                using (var stream = new FileStream(SettingPath.Current, FileMode.Open, FileAccess.Read))
                {
                    lock (_sync)
                    {
                        // 読み込み
                        var serializer = new XmlSerializer(typeof(SettingsRoot));
                        var root = (SettingsRoot)serializer.Deserialize(stream);
                        var dic = root.Keys.Zip(root.Values, (k, v) => new KeyValuePair<string, SerializableSetting>(k, v))
                            .ToDictionary(kv => kv.Key, kv => kv.Value);

                        cached_instances = dic;
                    }
                }
            }
            else
            {
                lock (_sync)
                {
                    cached_instances = new Dictionary<string, SerializableSetting>();
                }
            }
        }
        catch (Exception)
        {
            File.Delete(SettingPath.Current);
            cached_instances = new Dictionary<string, SerializableSetting>();
        }
    }

    /// <summary>
    /// ファイルにセーブ
    /// </summary>
    public static void SaveFile()
    {
        try
        {
            if (cached_instances.Count == 0) return;

            lock (_sync)
            {
                using (var stream = new FileStream(SettingPath.Current, FileMode.Create, FileAccess.ReadWrite))
                using (var writer = new StreamWriter(stream, Encoding.UTF8))
                {
                    SettingsRoot root = new SettingsRoot
                    {
                        Keys = cached_instances.Keys.ToArray(),
                        Values = cached_instances.Values.ToArray()
                    };
                    var serializer = new XmlSerializer(typeof(SettingsRoot));
                    serializer.Serialize(writer, root);

                    writer.Flush();
                }
            }
        }
        catch (Exception)
        {
            //例外が出た
        }
    }
   #endregion
}

本家KanColleViewerの設定ファイル保存には Grabacr07.KanColleViewer.Models.Settings一式と MetroTrilithon.Desktop/Serialization/SerializableProperty.csと MetroTrilithon.Desktop/Serialization/FileSettingsProvider.csが使われていますが、構造体を保存できないので、この連載では独自にクラスを実装しました。

3.ViewModelから設定データを扱うクラスを追加

ViewModels\SettingsフォルダにWindowSettingViewModel.csを追加 ViewModelsフォルダにInformationWindowViewModel.csを追加

  • WindowSettingViewModel.cs
public abstract class WindowSettingViewModel : Livet.ViewModel
{
    /// <summary>
    /// MetoroWindowのWindowSettingsにバインドするとファイルに保存できる
    /// </summary>
    public WindowSetting Setting { get; }

    public WindowSettingViewModel(string key)
    {
        Setting = SettingsHost.Cache<WindowSetting>(_ => new WindowSetting(key), key);
    }
}
  • InformationWindowViewModel.cs
public class InformationWindowViewModel : WindowSettingViewModel
{
    public InformationViewModel Info { get; } = new InformationViewModel();

    public InformationWindowViewModel(string key) : base(key)
    {

    }
}

4.Viewと設定データをバインディングする

IsRestoringWindowPlacementをtrueに WindowSettingsにInformationWindowViewModel.Settingをバインドすることによってウィンドウ位置とサイズを保存・復元できます。 Topmost、ピン止めボタンのIsCheckedをInformationWindowViewModel.TopMostとバインドすることでウィンドウが最前面かどうかを保存・復元できます。

艦隊情報表示ユーザーコントロールのDataContextに明示的にInformationViewModelをバインドします。

  • InformationWindow.xaml
<metro:MetroWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:metro="http://schemes.grabacr.net/winfx/2014/controls"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:プロジェクト名.Views"
    xmlns:ViewModels="clr-namespace:プロジェクト名.ViewModels" 
    x:Class="プロジェクト名.Views.InformationWindow"
    mc:Ignorable="d"
    Background="{DynamicResource ThemeBrushKey}"
    Foreground="{DynamicResource ActiveForegroundBrushKey}"
    Title="InformationWindow" Height="400" Width="300" 
    d:DataContext="{d:DesignInstance {x:Type ViewModels:InformationWindowViewModel}}" 
    Topmost="{Binding Setting.TopMost, Mode=TwoWay}" 
    WindowSettings="{Binding Setting, Mode=OneWay}" 
    IsRestoringWindowPlacement="True">
    
    <metro:MetroWindow.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="KanColleResource.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </metro:MetroWindow.Resources>

    <DockPanel>
        <!-- キャプションバー ここから -->
        <DockPanel metro:MetroWindow.IsCaptionBar="True" DockPanel.Dock="Top">
            <Border DockPanel.Dock="Bottom"
                    Height="4" />
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
                <metro:CaptionButton 
                    Style="{DynamicResource PinButtonStyleKey}" 
                    VerticalAlignment="Top" 
                    IsChecked="{Binding Setting.TopMost, Mode=TwoWay}" />

                <metro:CaptionButton WindowAction="Minimize" VerticalAlignment="Top" >
                    <Path Style="{DynamicResource MinimizeIconKey}"/>
                </metro:CaptionButton>
            </StackPanel>

            <TextBlock Text="艦隊情報 - プラグインテスト" HorizontalAlignment="Left"
                       Style="{DynamicResource CaptionTextStyleKey}"
                       Margin="2,0,8,0" />
        </DockPanel>
        <!-- キャプションバー ここまで -->
        <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
            <local:Information DataContext="{Binding Info, Mode=OneWay}"/>
        </ScrollViewer>
    </DockPanel>
</metro:MetroWindow>

5.プラグイン本体を修正

メインのViewModelをInformationWindowViewModelに差し替えて、 InformationWindowViewModelの識別キーはメインのViewのクラス名にしています。

class Plugin : IPlugin, IDisposable
{
    private Views.InformationWindow info;  //メインのView
    private ViewModels.InformationWindowViewModel infovm;    //メインのViewModel

    public Plugin()
    {
    }

    //プラグインが読み込まれたときに呼ばれる
    public void Initialize()
    {
        SettingsHost.LoadFile();    //設定をロード
        infovm = new ViewModels.InformationWindowViewModel(nameof(Views.InformationWindow));
        info = new Views.InformationWindow
        {
            DataContext = infovm
        };
        info.Show();
    }

    public void Dispose()
    {
        //ここに終了時の処理を書く
        SettingsHost.SaveFile();    //設定をセーブ
    }
}

ちょっと複雑ですが、これで設定情報をXMLファイルに保存・復元できるようになります。 保存するファイル名やパスはSettingPath.cs、XMLのRootElement名はSettingsRoot.cs、XMLのElement名はSerializableSettingを継承したクラスをカスタマイズすることで変更できます。

カスタマイズ方法は以下のサイトを参考にしてください(順不同)

特殊ディレクトリーを取得する

現在実行しているDLLのパスを取得する。(GetExecutingAssembly ) - tekkの日記 C#,VB.NET

[C#]属性とテキストで構成されるXMLタグをデシリアライズする為のクラス設計 | Zero Configuration

XmlRootAttribute クラス (System.Xml.Serialization)

属性を使用した XML シリアル化の制御

参考サイト

WPF でウィンドウ位置とサイズを保存・復元しよう | grabacr.nét

KanColleViewer/source/Grabacr07.KanColleViewer/Models/Settings at develop · Grabacr07/KanColleViewer · GitHub

提督業プラグインを作ってみよう(3)デザインをカッコよくしよう

1.リソースディクショナリを追加しよう

プロジェクトのViewsフォルダにリソースディクショナリを追加して、

f:id:reniris:20180908010311p:plain ここの真ん中あたりにある 画面系プラグインのデザイナ表示に必要な Stylesをコピーしましょう。 ここではファイル名をKanColleResource.xamlとしています。

  • KanColleResource.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Colors.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.Button.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.CheckBox.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.Expander.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.FocusVisualStyle.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.PasswordBox.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.RadioButton.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.Scrollbar.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Controls.Tooltip.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Styles/Icons.xaml" />
 
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Themes/Dark.xaml" />
        <ResourceDictionary Source="pack://application:,,,/MetroRadiance;component/Themes/Accents/Purple.xaml" />
 
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Colors.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.HorizontalFlatListBox.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.ListView.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.PinButton.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.TabControl.xaml" />
        <ResourceDictionary Source="pack://application:,,,/KanColleViewer.Controls;component/Styles/Controls.Text.xaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

2.WindowクラスをMetroWindowクラスに差し替えてデザインを変えよう

上のほうにキャプションバーをつけて、ピン止めボタンと最小化ボタンをつけました。 情報表示ユーザーコントロールをスクロールできるようにするため、ContentをStackPanelではなく、DockPanelにしてあります。

変更分を以下に記載します

  • InformationWindow.xaml
<metro:MetroWindow 
    x:Class="プロジェクト名.Views.InformationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:metro="http://schemes.grabacr.net/winfx/2014/controls"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:プロジェクト名.Views"
        mc:Ignorable="d"
        Background="{DynamicResource ThemeBrushKey}"
        Foreground="{DynamicResource ActiveForegroundBrushKey}"
        Title="InformationWindow" Height="600" Width="400">
    <metro:MetroWindow.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="KanColleResource.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </metro:MetroWindow.Resources>

    <DockPanel>
        <!-- キャプションバー ここから -->
        <DockPanel metro:MetroWindow.IsCaptionBar="True" DockPanel.Dock="Top">
            <Border DockPanel.Dock="Bottom"
                    Height="4" />
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
                <metro:CaptionButton 
                    Style="{DynamicResource PinButtonStyleKey}" 
                    VerticalAlignment="Top" 
                    IsChecked="{Binding Topmost, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type metro:MetroWindow}}}" />

                <metro:CaptionButton WindowAction="Minimize" VerticalAlignment="Top" >
                    <Path Style="{DynamicResource MinimizeIconKey}"/>
                </metro:CaptionButton>
            </StackPanel>

            <TextBlock Text="艦隊情報 - プラグインテスト" HorizontalAlignment="Left"
                       Style="{DynamicResource CaptionTextStyleKey}"
                       Margin="2,0,8,0" />
        </DockPanel>
        <!-- キャプションバー ここまで -->
        <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
            <local:Information/>
        </ScrollViewer>
    </DockPanel>
</metro:MetroWindow>
  • InformationWindow.xaml.cs
//継承元は削除する
public partial class InformationWindow
{
    public InformationWindow()
    {
        InitializeComponent();
    }
}
<UserControl 
    x:Class="プロジェクト名.Views.Information"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="clr-namespace:プロジェクト名.ViewModels"
    mc:Ignorable="d" 
    d:DesignHeight="600" d:DesignWidth="400">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="KanColleResource.xaml"/>
            </ResourceDictionary.MergedDictionaries>

            <!-- 艦隊データテンプレート -->
            <DataTemplate x:Key="Fleet" DataType="{x:Type vm:ShipViewModel}">
                <StackPanel Orientation="Horizontal" DataContext="{Binding Ship}">
                    <TextBlock TextWrapping="Wrap">
                    <Run Text="{Binding Info.Name, Mode=OneWay}"/>
                    <Run Text="{Binding Level, Mode=OneWay, StringFormat=Lv.\{0:D\}}"  />
                    <Run Text="{Binding Condition, Mode=OneWay, StringFormat=(\{0:D\})}"/>
                    </TextBlock>
                </StackPanel>
            </DataTemplate>
        </ResourceDictionary>
    </UserControl.Resources>
    <StackPanel>
        <ItemsControl ItemsSource="{Binding Fleets}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Border BorderBrush="{DynamicResource ActiveForegroundBrushKey}"
                                BorderThickness="2">
                            <TextBlock>
                            <Run Text="{Binding Name}" />
                            </TextBlock>
                        </Border>
                        <ItemsControl ItemsSource="{Binding Ships}" ItemTemplate="{DynamicResource Fleet}" >
                        </ItemsControl>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</UserControl>

3.表示を確認しよう

ビルドして出来たdllをKancolleViewerのPluginsフォルダにコピーしてexeを起動してみると、 下の画像のような画面が出るようになります。

f:id:reniris:20180908022612p:plain

KancolleViewerのデザインに合わせた色合いでとても見やすいですね。ついでに閉じるボタンがなくなってKancolleViewer本体を消さない限りウインドウが消えなくなって、ピン止めボタンで最前面に固定できるようになりました。

起動するたびにウインドウサイズや位置などがリセットされてしまうので、次回はそれらをファイルに保存して使用できるようにします。

提督業プラグインを作ってみよう(2)艦隊情報を出そう

1.艦これの情報を取ってこよう

KanColleViewer本家のソース を参考にして艦これの情報を取ってくるModelクラスを作ります。 ModelsフォルダにKanColleModel.csを作りましょう。 以下にクラスの中身を記載します。

public class KanColleModel : Livet.NotificationObject, IDisposableHolder
{
   #region singleton

    public static KanColleModel Current { get; } = new KanColleModel();

   #endregion

   #region Fleets変更通知プロパティ
    private IEnumerable<Fleet> _Fleets;

    public IEnumerable<Fleet> Fleets
    {
        get
        { return _Fleets; }
        set
        {
            if (_Fleets == value)
                return;
            _Fleets = value;
            RaisePropertyChanged(nameof(Fleets));
        }
    }
   #endregion

   #region IsRegistered変更通知プロパティ
    private bool _IsRegistered = false;  //念のためこうしとく

    public bool IsRegistered
    {
        get
        { return _IsRegistered; }
        set
        {
            if (_IsRegistered == value)
                return;
            _IsRegistered = value;
            RaisePropertyChanged(nameof(IsRegistered));
        }
    }
   #endregion

    private readonly LivetCompositeDisposable compositeDisposable = new LivetCompositeDisposable();
    public ICollection<IDisposable> CompositeDisposable => this.compositeDisposable;

    /// <summary>
    /// 外からインスタンスが作れないようにコンストラクタをprivateにする
    /// </summary>
    private KanColleModel()
    {
        KanColleClient.Current
            .Subscribe(nameof(KanColleClient.IsStarted), this.RegisterHomeportListener, false)
            .AddTo(this);
    }

    /// <summary>
    /// 艦これが始まったときに一度だけ呼ばれます
    /// </summary>
    private void RegisterHomeportListener()
    {
        if (this.IsRegistered) return;

        var client = KanColleClient.Current;
        //Organization.Fleetsをチェックして、変化があったらUpdateFleetsを呼ぶ
        client.Homeport.Organization
        .Subscribe(nameof(Organization.Fleets), () => this.UpdateFleets(client.Homeport.Organization))
        .AddTo(this);

        this.IsRegistered = true;    //開始フラグを立てます
    }

    /// <summary>
    /// 艦隊に変化があったときに呼ばれる
    /// </summary>
    /// <param name="organization">organization</param>
    private void UpdateFleets(Organization organization)
    {
        //艦隊に変化があったらKanColleModel.Fleetsを更新する
        this.Fleets = organization.Fleets.Values;
    }

    public void Dispose()
    {
        this.compositeDisposable.Dispose();
    }
}

名前空間やusingは自力で何とかしましょう

2.ViewModelを実装しよう

ViewModelsフォルダにInformationViewModel.cs、FleetViewModel.cs、ShipViewModel.csを追加します。実装は以下に記載します。

InformationViewModel.cs(メインのViewModel)

public class InformationViewModel : ViewModel
{
    //艦これの情報はここからみんな持ってきます
    private KanColleModel Kancolle { get; } = KanColleModel.Current;

    private PropertyChangedEventListener listener;

    /// <summary>
    /// 艦隊情報
    /// </summary>
    public FleetViewModel[] Fleets { get; private set; }

    public InformationViewModel()
    {
        listener = new PropertyChangedEventListener(Kancolle);
        //艦これの艦隊情報の変化を監視して、変化があったらUpdateFleets()を呼ぶ
        listener.RegisterHandler(() => Kancolle.Fleets, (s, e) => UpdateFleets());

        this.CompositeDisposable.Add(listener);
    }

    /// <summary>
    /// 艦隊情報が更新されたときに呼ばれる
    /// </summary>
    private void UpdateFleets()
    {
        var fleets = this.Kancolle.Fleets;

        if (fleets == null) return;

        Fleets = fleets.Select(f => new FleetViewModel(f)).ToArray();
        RaisePropertyChanged(nameof(Fleets));
    }
}

FleetViewModel.cs(艦隊情報のViewModel)

public class FleetViewModel : ViewModel
{
   #region Name変更通知プロパティ
    private string _Name;

    public string Name
    {
        get
        { return _Name; }
        set
        {
            if (_Name == value)
                return;
            _Name = value;
            RaisePropertyChanged(nameof(Name));
        }
    }
   #endregion

    //艦隊に所属する艦娘の情報
    public ShipViewModel[] Ships { get; private set; }

    private PropertyChangedEventListener listener;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="f">f</param>
    public FleetViewModel(Fleet f)
    {

        Name = f.Name;  //艦隊名をセット
        Update(f.Ships);
        
        listener = new PropertyChangedEventListener(f);
        //艦隊の艦娘たちの変化を監視して、変化があったらUpdate()を呼ぶ
        listener.RegisterHandler(() => f.Ships, (s, e) => Update(f.Ships));
        //艦隊名の変化を監視して、変化があったら艦隊名を更新する
        listener.RegisterHandler(() => f.Name, (s, e) => Name = f.Name);
        this.CompositeDisposable.Add(listener);
    }

    /// <summary>
    /// 艦隊に所属する艦娘の状態が変わったら呼ばれる
    /// </summary>
    /// <param name="ships">ships</param>
    protected virtual void Update(IEnumerable<Ship> ships)
    {
        this.Ships = ships.Select(s => new ShipViewModel(s)).ToArray();

        RaisePropertyChanged(nameof(Ships));
    }
}

ShipViewModel.cs(単体の艦娘のViewModel) KanColleViewer本家のShipViewModel.cs を参考にします。

public class ShipViewModel : ViewModel
{
   #region Ship変更通知プロパティ
    private Ship _Ship;

    public Ship Ship
    {
        get
        { return _Ship; }
        set
        {
            if (_Ship == value)
                return;
            _Ship = value;
            RaisePropertyChanged(nameof(Ship));
        }
    }
   #endregion

    public ShipViewModel(Ship s)
    {
        this.Ship = s;
    }
}

3.Viewにバインディングしよう

一度ソリューションをビルドして、コンパイルエラーが出たらつぶしましょう。このサイトではC#WPFの基本的なことは解説しません。インターネットは広大なので、助けを求めれば、一人くらいは相談に乗ってくれる人がいるでしょう。

コンパイルエラーが出なくなったら、Information.xamlの中身を実装していきましょう。以下に記載したソースをコピペ改変して自分のプロジェクトに合わせていきましょう。 Information.xaml(メインのユーザーコントロール

<UserControl 
    x:Class="プロジェクト名.Views.Information"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="clr-namespace:プロジェクト名.ViewModels"
    mc:Ignorable="d" 
    d:DesignHeight="600" d:DesignWidth="400">
    <UserControl.Resources>
        <!-- 艦隊データテンプレート -->
        <DataTemplate x:Key="Fleet" DataType="{x:Type vm:ShipViewModel}">
            <StackPanel Orientation="Horizontal" DataContext="{Binding Ship}">
                <TextBlock TextWrapping="Wrap">
                    <Run Text="{Binding Info.Name, Mode=OneWay}"/>
                    <Run Text="{Binding Level, Mode=OneWay, StringFormat=Lv.\{0:D\}}"  />
                    <Run Text="{Binding Condition, Mode=OneWay, StringFormat=(\{0:D\})}"/>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <StackPanel>
        <ItemsControl ItemsSource="{Binding Fleets}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Background="DarkGray" Foreground="LightGray">
                            <Run Text="{Binding Name}" />
                        </TextBlock>
                        <ItemsControl ItemsSource="{Binding Ships}" ItemTemplate="{DynamicResource Fleet}" >
                        </ItemsControl>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</UserControl>

全部実装したらビルドして、生成されたdllをKanColleViewerのPluginsフォルダに上書きコピーしてKanColleViewerを起動すると、以下の画像のようなウインドウが出てくるはずです。

f:id:reniris:20180906231829p:plain

艦隊名と、艦隊に所属している艦娘の名前、レベル、Cond値が表示されます。 このままでもCond値チェックは出来ますが、デザインがイケてなかったり、ウインドウを閉じたらKanColleViewerを再起動しないといけなかったりと、まだまだですね。 これらの問題は次回以降解決していきます。