2012年4月29日日曜日

wpf : カスタムコントロールを追加するもう1つの方法

普通、カスタムコントロールを作成するときはソリューションエクスプローラーから新しい項目を追加しますよね? その方法で追加すると「Themes/Generic.xaml」と「カスタムコントロール名.cs」のファイルが追加されます。 これを編集していくのが普通のやり方です。

ですが、コレだとやりたい事ができないケースもあるようです。 今回は、Generic.xamlのStyleにEventSetterを追加したらx:Classが無いとダメだと怒られてしまいました。 それをキッカケに「x:Classの設定ってどう書くの?」と色々検索して試してみたらカスタムコントロールを追加するもう1つの方法が見つかりました。 この方法なら問題なくEventSetterも使えます。

その方法はこんな感じ。

  1. ソリューションエクスプローラーから新規「ウィンドウ」を追加。 ファイル名は「カスタムコントロール名.xaml」でよい。
  2. カスタムコントロール名.xamlを開き、ルート要素のタグをWindowから継承元コントロール名に変える。 ルート要素の属性からWindow用の属性(Title、Width、Heightなど)を削除。 ルート要素の子のContent(Grid)も削除。
  3. カスタムコントロール名.xaml.csを開き、カスタムコントロールクラスの継承元をWindowから継承元コントロール名に書きかえる。

ちょっとだけ面倒だけど簡単です。

実際にやってみるときの様子はこうなります。 例えば、ListBoxを継承したTestListBoxを作る場合は。

  1. ソリューションエクスプローラーから新規ウィンドウを追加。 ファイル名は「TestListBox.xaml」とする。
  2. TestListBox.xamlを開き、ルート要素のタグをWindowからListBoxに変える。 ルート要素の属性からWindow用の属性(Title、Width、Height)を削除。 ルート要素の子のContent(Grid)も削除。
    <ListBox x:Class="WpfApplication1.TestListBox"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="TestListBox" Height="300" Width="300">
        <Grid>
            
        </Grid>
    </ListBox>
  3. TestListBox.xaml.csを開き、TestListBoxクラスの継承元をWindowからListBoxに書きかえる。
    using ~略~
    
    namespace WpfApplication1
    {
        public partial class TestListBox : ListBox
        {
            public TestListBox()
            {
                InitializeComponent();
            }
        }
    }

これでOK。 この書き換えをした直後のビルドでエラーが出ることもあるようですが、2度続けてビルドしたらエラーは出なくなりました。 名前解決の何かでしょうか? まぁとにかく、1度エラーなしで通るようになれば後は普通に編集できます。

カスタムコントロールクラスのコンストラクタにあるInitializeComponentメソッドは「そのままでいいの?」と思ったけど大丈夫でした。 カスタムコントロールどころか、適当なテキストファイルにxamlを書いてからリネームしてもルート要素がコントロールでさえあれば自動生成されるみたいです。 自動生成されたコードはobjフォルダの「コントロール名.g.i.cs」というファイルを見れば確認できます。

あと、普通にカスタムコントロールを追加したときには、静的コンストラクタにメタデータの上書きコードが書かれていますよね?

static カスタムコントロールクラス名()
{
    DefaultStyleKeyProperty.OverrideMetadata(
            typeof(カスタムコントロールクラス名),
            new FrameworkPropertyMetadata(
                    typeof(カスタムコントロールクラス名)
            )
    );
}

コレについては勉強不足でよく分かってません。 ただ、親クラスとなるコントロールを元にした改造コントロールを作るだけなら不要のようです。

とにかくこれで土台は完成。 あとは、MainWindowを書くようにカスタムコントロールを書いて行きます。 ↓は適当な例です。

<ListBox
        x:Class="WpfApplication1.TestListBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        SelectionChanged="ListBox_SelectionChanged"
>
    <ListBox.Resources>
        <Style TargetType="ListBoxItem">
            <EventSetter Event="MouseDoubleClick" Handler="OnMouseDoubleClick"/>
            <Setter Property="FontSize" Value="24"/>
        </Style>
    </ListBox.Resources>
    <ListBox.Template>
        <ControlTemplate>
            <Border Name="border" BorderBrush="DarkGreen" BorderThickness="2" CornerRadius="4">
                <StackPanel IsItemsHost="True" />
            </Border>
        </ControlTemplate>
    </ListBox.Template>
</ListBox>

クラスライブラリを作りたいときは前の投稿も参照。

------------------------

2012年5月12日 追記

どうやらこの書き方でカスタムコントロールを作ると制限があるようです。 カスタムコントロールと使う側、両方のxamlでResourcesを書くと例外が発生しました。

例外が出るのはこういう書き方をした場合です。 まずはカスタムコントロール側で普通にResourcesを記述。

<TabControl
        x:Class="UserControls.MyTabControl"
        ~その他属性~
>
    <TabControl.Resources>
        ~リソースを書く~
    </TabControl.Resources>
</TabControl>

カスタムコントロールを使う側で、カスタムコントロールのResourcesを「上書き」すると例外が発生します。

<Window x:Class="~略~"
        xmlns:appCtrls="clr-namespace:UserControls"
        ~その他属性~
>
    <appCtrls:MyTabControl>
        <appCtrls:MyTabControl.Resources>
            ~リソースを書く~
        </appCtrls:MyTabControl.Resources>
    </appCtrls:MyTabControl>
</Window>

発生するのはXamlParseExceptionです。 InnerExceptionを見るとInvalidOperationException(ResourceDictionaryインスタンスを再初期化することはできません)との記述が。

もちろん、Window.Resourcesなど他のリソースには影響はありません。 ダメなのはカスタムコントロール.Resourcesの重複だけです。 カスタムコントロールのResources以外のプロパティが重複しても影響なし、使う側の値で上書きされます。

というわけでこの追加方法をとるならカスタムコントロール.xamlでResourcesは書かないようにしましょう。 リソースが必要な場合はC#のコードで読み込んだり、マージしたりする必要があります。 カスタムコントロールを使う側のResourcesを禁止にするっていう仕様もあり ... 無いか?

それにしても、普通にGeneric.xamlに書くよりファイルの整理がしやすいと思ったんですが、思わぬ落とし穴ですなぁ。

------------------------

2012年5月15日 追記