雑記

 ここでは YKToolkit 開発にまつわる日記的なものを掲載しています。 いわば自分用メモみたいなものもあれば、「こんなテク使ったよ」的なことも載せます。

2015.04.10 - 子ウィンドウを作ってみた

 唐突に更新された YKToolkit.Controls.dll Ver.1.11.0.0 ですが、 ParentPanel および ChildWindow コントロールがなんの説明もなく追加されています。 これらは子ウィンドウを実現するためのコントロールです。 さすがに誰も使えないだろうと思うので、 とりあえずこちらにサンプルコードを載せておきます。ああ、説明書も更新しなきゃ...

 そもそも子ウィンドウとはなんなのか。

 Excel や Word なんかで複数のファイルを開いたとき、 それぞれのファイルに対してウィンドウが与えられますよね。 そのウィンドウは Excel 本体のウィンドウの中であれば自由に移動できるし、リサイズもできます。 そんなウィンドウをここでは子ウィンドウと呼んでいます。

Fig.1 : Excel では複数のファイルを子ウィンドウで切り替えられる

 こういう子ウィンドウを WPF でもできないかな〜なんて思ってしまったのがきっかけで、 ただなんとなく作ってしまったのが今回の更新に含まれています。 そんなわけでかなり中途半端かつオレオレコントロールになっています。

 とにかく子ウィンドウを表示したいんだい!という場合は Canvas コントロールの上に ChildWindow コントロールを配置します。

<YK:Window x:Class="WpfApplication1.Views.MainView"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:YK="clr-namespace:YKToolkit.Controls;assembly=YKToolkit.Controls"
           Title="MainView" Height="400" Width="500">
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="ファイル (_F)" />
            <MenuItem Header="編集 (_E)" />
            <MenuItem Header="オプション (_O)" />
            <MenuItem Header="ヘルプ (_H)" />
        </Menu>

        <Canvas>
            <YK:ChildWindow Title="child1">
                <TextBlock Text="ひと〜つ人の世を憂い" />
            </YK:ChildWindow>

            <YK:ChildWindow Title="child2">
                <TextBlock Text="ふたつ不埒な悪行三昧" />
            </YK:ChildWindow>

            <YK:ChildWindow Title="child3">
                <TextBlock Text="みっつ見てみぬふり" />
            </YK:ChildWindow>
        </Canvas>
    </DockPanel>
</YK:Window>
Code 1 : 子ウィンドウ単体で表示してみる
実行してみると、次のように 3 つの子ウィンドウが表示されます。
Fig.2 : 3 つの ChildWindow が操作できる
一応ウィンドウのようなコントロール外観となっており、 タイトルバーをドラッグすれば移動できるし、 境界線をドラッグすればリサイズもできるようになっています。 最大化ボタンを押せば Canvas コントロールいっぱいにリサイズされます。
Fig.3 : 最大化された子ウィンドウ
しかし、閉じるボタンを押しても閉じてくれません。ここからじわじわとオレオレ仕様に突入します。

 閉じるボタンが効かない現象についてなんのフォローもなく、唐突に ParentPanel を導入します。 ParentPanel は IChildWindow インターフェースを実装したクラスを対象とした ItemsSource プロパティを持っています。 というわけでまずサンプルとして次のような Person クラスを用意します。

namespace WpfApplication1.Models
{
    using System;
    using YKToolkit.Controls;

    public class Person : IChildWindow
    {
        #region IChildWindow のメンバ
        /// <summary>
        /// 子ウィンドウとしてのアクティブ状態を取得または設定します。
        /// </summary>
        public bool IsActive { get; set; }

        private bool isClosed;
        /// <summary>
        /// 子ウィンドウが閉じられたかどうかを確認または設定します。
        /// </summary>
        public bool IsClosed
        {
            get { return isClosed; }
            set
            {
                if (isClosed != value)
                {
                    isClosed = value;
                    if (isClosed)
                        RaiseClosed();
                }
            }
        }

        /// <summary>
        /// 子ウィンドウのタイトルを取得または設定します。
        /// </summary>
        public string Title { get; set; }
        #endregion IChildWindow のメンバ

        #region イベント定義
        /// <summary>
        /// 子ウィンドウがとじられたときに発生します。
        /// </summary>
        public event EventHandler<EventArgs> Closed;

        /// <summary>
        /// Closed イベントを発行します。
        /// </summary>
        private void RaiseClosed()
        {
            var h = Closed;
            if (h != null)
                h(this, EventArgs.Empty);
        }
        #endregion イベント定義

        /// <summary>
        /// 名前を取得または設定します。
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 年齢を取得または設定します。
        /// </summary>
        public int Age { get; set; }
    }
}
Code 2 : IChildWindow インターフェースを実装した Person クラス
サンプルとして動けばいいので INotifyPropertyChanged とかあんまり考えていません。それから Closed イベントが定義してあるところでなんとなくお察しいただけるでしょうか。

 次にこれを扱う ViewModel 側のコードです。

namespace WpfApplication1.ViewModels
{
    using YKToolkit.Bindings;
    using WpfApplication1.Models;
    using System.Collections.ObjectModel;

    public class MainViewModel : NotificationObject
    {
        private ObservableCollection<Person> people = new ObservableCollection<Person>();
        /// <summary>
        /// 人物データを取得または設定します。
        /// </summary>
        public ObservableCollection<Person> People
        {
            get { return people; }
            set { SetProperty(ref people, value); }
        }

        private DelegateCommand addCommand;
        /// <summary>
        /// 人物データ追加コマンドを取得します。
        /// </summary>
        public DelegateCommand AddCommand
        {
            get
            {
                return addCommand ?? (addCommand = new DelegateCommand(_ => AddPerson()));
            }
        }

        /// <summary>
        /// 人物データを追加します。
        /// </summary>
        private void AddPerson()
        {
            var person = new Person
            {
                Title = "人物データ " + People.Count.ToString(),
                Name = "田中" + People.Count.ToString() + "子",
                Age = 16 + People.Count,
            };
            person.Closed += OnPersonClosed;
            People.Add(person);
        }

        /// <summary>
        /// 人物データクローズイベントハンドラ
        /// </summary>
        /// <param name="sender">イベント発行元</param>
        /// <param name="e">イベント引数</param>
        private void OnPersonClosed(object sender, System.EventArgs e)
        {
            var person = sender as Person;
            if (person != null)
            {
                person.Closed -= OnPersonClosed;
                People.Remove(person);
            }
        }

    }
}
Code 3 : Person のコレクションとこれを追加/削除するメソッド
Person クラスの追加は AddCommand コマンドで、 削除は Closed イベントが発生したときにおこないます。

 そして有無を言わさず View の XAML を次のように書きます。

<YK:Window x:Class="WpfApplication1.Views.MainView"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:YK="clr-namespace:YKToolkit.Controls;assembly=YKToolkit.Controls"
           Title="MainView" Height="400" Width="500">
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="ファイル (_F)" />
            <MenuItem Header="編集 (_E)" />
            <MenuItem Header="オプション (_O)" />
            <MenuItem Header="ヘルプ (_H)" />
        </Menu>

        <Button Content="Add Person" DockPanel.Dock="Top" Command="{Binding AddCommand}" />

        <YK:ParentPanel ItemsSource="{Binding People}">
            <YK:ParentPanel.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Name, StringFormat='{}氏名 : {0}'}" />
                        <TextBlock Text="{Binding Age, StringFormat='{}年齢 : {0}'}" />
                    </StackPanel>
                </DataTemplate>
            </YK:ParentPanel.ItemTemplate>
        </YK:ParentPanel>
    </DockPanel>
</YK:Window>
Code 4 : ParentPanel による子ウィンドウ表示
ParentPanel の ItemsSource プロパティに People プロパティをデータバインドしています。 また、ItemTemplate プロパティには、子ウィンドウのコンテンツ表示に対する DataTemplate が指定できます。

 そんなわけで実行結果がこちら。

Fig.4 : ボタンを押すと子ウィンドウが追加されていく
Fig.5 : 閉じるボタンを押すと子ウィンドウが消えていく

 以上子ウィンドウ表示のための ParentPanel および ChildWindow コントロールでした... で終わるのもアレなので、少し雑談を。

 そもそも子ウィンドウを WPF で実現させようと思ったきっかけは、冒頭でも書いたように、 Excel や Word なんかで見るいわゆる MDI 形式なアプリケーションを使っていた時です。 WPF は確か公式でも MDI はサポートしないよと言われてしまったフレームワークですが、 別にできないよとは言われていません。 つまり、サポートされなければ自作すればいいじゃないというスタンスです。 というわけで MDI 形式に耐えられる枠組みを考えようかと。 そして真っ先に手を付けられるのがこの子ウィンドウの実現でした。 そもそもこれができないと MDI の枠組みを考えたところで無意味ですし。 で、とりあえずそれっぽいものができて嬉しくなったのでついうっかりライブラリに追加してしまいました。反省。

 MDI として機能させるためには何が必要か。

  • 子ウィンドウの表示 <- とりあえずできた
  • 子ウィンドウの管理 <- ViewModel が持つコレクション
  • 切り替えのためのウィンドウメニュー <- 機能は ViewModel で実現
  • タブ機能 <- TabControl をカスタマイズ
 なんだ、後はほとんど ViewModel でやることじゃん。 TabControl のカスタマイズはライブラリとして用意したほうがいいのかなぁ。めんどいなぁ。

 こうして考えてみると後は MDI として作り込むための雛形があればもうできるんじゃないか。 というわけでやってみる ... 時間はないので適当に作ったバイナリエディタのスクショを投下して逃げます。 MDI を完成させるのは実業務でない限りやる気も気合いも出ないなぁ。

Fig.6 : なんちゃって MDI バイナリエディタ (ウィンドウ管理機能がない)

Designed by CSS.Design Sample