提督業プラグインを作ってみよう(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を再起動しないといけなかったりと、まだまだですね。 これらの問題は次回以降解決していきます。

提督業プラグインを作ってみよう(1)とりあえずウインドウを出そう

1.プロジェクトを作ろう

メニューから「新規作成」「プロジェクト」を選んで、「WPFユーザーコントロールライブラリ」を選びます。プロジェクト名は適当なものを入力しておいてください。

f:id:reniris:20180905192254p:plain

2.余計なファイルを消してプロジェクト構造を整理しよう

作成したプロジェクトから

f:id:reniris:20180905193235p:plain

UserControl1.xamlを削除して「Models」「Views」「ViewModels」フォルダを追加します。

f:id:reniris:20180905205757p:plain

3.Nugetから必要なライブラリをインストールしよう

KanColleViewer.Composition(必須) KanColleViewer.PluginAnalyzer(ほぼ必須) KanColleWrapper(艦これの情報を取るなら必要) LivetCask(この連載では使います) をNugetからインストールしましょう

4.プラグイン本体を実装しよう

プロジェクト直下にクラスファイルをひとつ、Viewsフォルダにユーザーコントロールひとつと、ウインドウひとつ、ViewModelsフォルダにクラスひとつを追加します。

メインの情報表示ユーザーコントロール(まだ空)

<UserControl x:Class="プロジェクト名.Views.Information"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:プロジェクト名.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
            
    </Grid>
</UserControl>

メインのウインドウ(Informationが入っているだけ)

<Window x:Class="プロジェクト名.Views.InformationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        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"
        Title="InformationWindow" Height="450" Width="800">
    <local:Information/>
</Window>

メインのViewModel(まだ空)

class InformationWindowViewModel : Livet.ViewModel
{
    public InformationWindowViewModel()
    {

    }
}

プラグイン本体

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

    public Plugin()
    {
    }

    //プラグインが読み込まれたときに呼ばれる
    public void Initialize()
    {
        infovm = new ViewModels.InformationViewModel();
        info = new Views.InformationWindow
        {
            DataContext = infovm
        };
        info.Show();
    }

    public void Dispose()
    {
        //ここに終了時の処理を書く
    }
}

ソースはブログに乗せるために余計なものを省いています。 これらのソースをいい感じに修正してビルドしたdllをKanColleViewerのPluginsフォルダにコピーすると、空っぽのウインドウが出るようになります。

参考サイト

KanColleViewer プラグインの作り方とか – CAT EARS

艦ぶらっぽい提督業プラグインを作ってみた

艦これ二期は画面がでっかくなってキレイになりましたが、私の環境でKanColleViewerだとメインのブラウザ部分がほとんどを占めてしまい、他がさっぱり見えません。 そこで、今は亡き艦ぶらのようなコンパクトで見やすい艦隊情報ウインドウを出すというプラグインを作りました。 プラグイン本体とソースはGitHubで公開しています。 github.com

次の記事でKanColleViewerプラグインの作り方について書きます。

艦これ専ブラ2期対応できるかな?(IE以外のブラウザコンポーネントについて)

ほとんどの艦これ専ブラにはIEコンポーネントが使われていますが、2期ではIEが非対応になったので、専ブラは壊滅状態です。
そこで、IE以外のブラウザコンポーネントについて調べてみました。
Chrome(おすすめ)
github.com
使い方
qiita.com

FireFox(ちょっと古い)
github.com

Edge(Windows 10 1803以降にしか対応してない)
blogs.windows.com
使い方
www.atmarkit.co.jp

参考URL
Embed Firefox/Gecko in WPF/C# - Stack Overflow
https://www.codeproject.com/Questions/611755/Embed-Chrome-or-firefox-Browser-in-Csharp-net-Form