WPFのカスタムコントロール制作と詳解

近頃ようやく使われだした感のあるWPFですが（例えばゆっくりムービーメーカー4でも使われてると見られる）、WPFではより高機能なカスタマイズを提供するため、カスタムコントロールというものがあります。

ユーザーコントロールは既存のコントロールを組み合わせたもので、手軽で簡単に作ることが出来ます。

作例：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/3cad6f02-94d7-57f2-a91d-d2f82492dcd0.png)

https://qiita.com/EndOfData/items/17ddac0b26f3302f103d


これに対し、カスタムコントロールはより詳細な外観の定義やカスタム動作をコードビハインドに負担させる事なく記述可能です。

***Microsoft Learn***

https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/controls/creating-a-control-that-has-a-customizable-appearance?view=netframeworkdesktop-4.8

***トグルボタンの例***

https://shikaku-sh.hatenablog.com/entry/wpf-how-to-create-customcontrol-togglebutton


以下からはVisual Studio2022を前提に話を進めます。

# カスタムコントロールの自作

### git repository
```sass
 git clone https://github.com/Sheephuman/CustomControlTest.git
 ```


今回は自作のアプリケーション開発も兼ねて、ユーザーコントロール以上に詳細な外観と機能を備えたカスタムコントロールを制作します。

仕様
・↓キー押下でプルダウンリスト(PopUp)の表示
・スペルチェック機能
・テストを兼ねて外観も弄る


## 手順1　プロジェクトにカスタムコントロールを追加する

いつもの「追加」からですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/e41e569a-f888-1d0e-b048-31a61c4099b9.png)

名前は```PulldownTextBoxExUltimetSheep.cs```とします。


## コードビハインド
以下のCodeが記述されます。

```Cs

   public class PulldownTextBoxExUltimetSheep : Control
   {
       static PulldownTextBoxExUltimetSheep()
       {
           DefaultStyleKeyProperty.OverrideMetadata(typeof(PulldownTextBoxExUltimetSheep), new FrameworkPropertyMetadata(typeof(PulldownTextBoxExUltimetSheep)));
   //カスタム コントロールのデフォルト スタイルを設定
   //DefaultStyleKeyProperty` メタデータをオーバーライドすることで、コントロールを特定のリソース ディクショナリ内のスタイルに関連付けることになります。

       }
   }
```

## カスタムコントロール外観の定義
これが結構、めんどうなんですね。


```PullDownTextBoxStyle.xaml```を設置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/80bcebaf-a4ae-7a63-a192-a560a9abbf86.png)


### リソースディクショナリを定義

うんざりするほど縦に長くなりますが諦めよう(笑)
```xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApp11">

    <Style TargetType="{x:Type local:PulldownTextBoxExUltimetSheep}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:PulldownTextBoxExUltimetSheep}">
                    <Border Background="White" 
                            CornerRadius="30" 
                            BorderThickness="1" 
                            BorderBrush="Gray">

                        <TextBox x:Name="PART_TextBox"
                           
                                 SpellCheck.IsEnabled="True" 
                            AcceptsReturn="True">
                            <TextBox.Style>
                                <Style TargetType="TextBox">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="TextBox">
                                                <Border CornerRadius="30" 
                            Background="{TemplateBinding Background}" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="{TemplateBinding BorderThickness}">
                                                    <ScrollViewer x:Name="PART_ContentHost"/>
                                                </Border>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </TextBox.Style>


                        </TextBox>
                        
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>




```
### Mermaidで書いてみた
分かりやすいかどうかは微妙なところです('ω')
参考

https://qiita.com/Qiita/items/c07f3262d8f3b25f06c9

```mermaid
classDiagram
  ResourceDictionary --|> Style
  Style --|> Setter
  Setter --|> ControlTemplate
  ControlTemplate --|> Border
  Border --|> TextBox
  TextBox --|> TextBoxStyle
  TextBoxStyle --|> InnerStyle
  InnerStyle --|> InnerControlTemplate
  InnerControlTemplate --|> InnerBorder
  InnerBorder --|> ScrollViewer

  class ResourceDictionary{
    +xmlns
    +xmlns:x
    +xmlns:local
  }

  class Style{
    +TargetType
  }

  class Setter{
    +Property
    +Value
  }

  class ControlTemplate{
    +TargetType
  }

  class Border{
    +Background
    +CornerRadius
    +BorderThickness
    +BorderBrush
  }

  class TextBox{
    +x:Name
    +SpellCheck.IsEnabled
    +AcceptsReturn
  }

  class TextBoxStyle{
  }

  class InnerStyle{
    +TargetType
  }

  class InnerControlTemplate{
    +TargetType
  }

  class InnerBorder{
    +CornerRadius
    +Background
    +BorderBrush
    +BorderThickness
  }

  class ScrollViewer{
    +x:Name
  }


```




:::note 
詳しい解説

### 1. \<ResourceDictionary>要素
```xml


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApp11">
```


xmlns: XAMLの基本的な名前空間であり、UI要素の定義に必要です。
xmlns:x: XAMLの言語構文（属性など）を利用するための名前空間です。
xmlns:local: 現在のプロジェクト（ここではWpfApp11）の名前空間を指定します。


 ### 2. \<Style\> 要素
 
 



### App.xaml内で指定のリソースディクショナリを参照させる

```TargetType="{x:Type local:PulldownTextBoxExUltimetSheep}"```で、このスタイルがPulldownTextBoxExUltimetSheepクラスに適用されることを指定しています。

### 1. \<Setter>要素

特定のプロパティに値を設定する役割
```xml
<Setter Property="Template">
```

### 2. \<ControlTemplate>要素
ControlTemplateは、コントロールの外観を細かく定義するためのテンプレートです。
```xml
TargetType="{x:Type local:PulldownTextBoxExUltimetSheep}"
```
により、PulldownTextBoxExUltimetSheep専用のテンプレートであることが示されています。

### 3. \<Border>要素
Border要素は、コントロールの外側に表示される枠を定義します。

### 5. \<TextBox>要素
###  \<TextBox.Style>内の\<ControlTemplate>要素を解説
これにより、テキストボックスを詳細にカスタマイズできます。

```xml
     <TextBox.Style>
         <Style TargetType="TextBox">
             <Setter Property="Template">
                 <Setter.Value>
                     <ControlTemplate TargetType="TextBox">
                         <Border CornerRadius="30" 
         Background="{TemplateBinding Background}" 
         BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}">
                             <ScrollViewer x:Name="PART_ContentHost"/>
                         </Border>
                     </ControlTemplate>
                 </Setter.Value>
             </Setter>
         </Style>
     </TextBox.Style>
```

#### 1.\<Style TargetType="TextBox"> 
このスタイルがTextBoxに適用されることを明示しています。

#### 2.\<Setter Property="Template">
Templateプロパティの値をカスタマイズしています。ControlTemplateを使ってTextBoxのビジュアルを定義しています。
#### 3.\<ControlTemplate TargetType="TextBox">
TargetType="TextBox"により、このテンプレートがTextBoxに適用されることを示します。このテンプレートによってTextBoxの見た目が指定されます。


:::
リソースディクショナリの構造は長くなりがちですが、簡略化する方法があります。よく使うスタイルやテンプレートをリソースディクショナリに別途定義し、それを必要に応じて参照できます。




## 実装

```cs
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

namespace WpfApp11
{

    public class PulldownTextBoxExUltimetSheep : TextBox
    {
        private ListBox _dropdownList = null!;
        private TextBox? _textBox = null!;
// null!;: ここでの null! は、C# 8.0 以降の「null 許容性」機能の一部です。null! は、nullable 変数に対して「この変数は必ず後で初期化される」とコンパイラに伝えるために使います。この場合、コンパイラが警告を出さないようにするための手法です。
    

        static PulldownTextBoxExUltimetSheep()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(PulldownTextBoxExUltimetSheep), new FrameworkPropertyMetadata(typeof(PulldownTextBoxExUltimetSheep)));
        }
        public Popup _popup;
        public PulldownTextBoxExUltimetSheep()
        {
            SetInputLanguage("en-US");

            _textBox = new TextBox();

            // スペルチェック機能を有効にする
            this.SpellCheck.IsEnabled = true;
            // KeyDownイベントの登録
            this.PreviewKeyDown += OnKeyDown;

            // PopupとListBoxの初期化
            _popup = new Popup
            {
                PlacementTarget = this,
                Placement = PlacementMode.Bottom,
                StaysOpen = false,
                IsOpen = false,
    //実験結果：IsOpenがTrueだとプロセスを終了してもPopUpが残ることがある。Closeイベントでfalseにしても同じであることから、どこかでNewされているのかもしれない。
    //困った仕様だが、PopUpはプロセスとは独立している

            };

            _dropdownList = new ListBox
            {
                Width = this.Width,
                ItemsSource = new[] { "Option1", "Option2", "Option3" }  // サンプル項目
            };
            _dropdownList.SelectionChanged += DropdownList_SelectionChanged;

            _popup.Child = _dropdownList;

            this.Loaded += PulldownTextBoxExUltimetSheep_Loaded;
            this.LostFocus += OnLostFocus;
        }

        private void SetInputLanguage(string cultureName)
        {
            CultureInfo culture = new CultureInfo(cultureName);

            if(_textBox!=null)
              _textBox.Language = System.Windows.Markup.XmlLanguage.GetLanguage(culture.IetfLanguageTag);
        }


        private void OnLostFocus(object sender, RoutedEventArgs e)
            {
                _popup.IsOpen = false;
            }

        private void PulldownTextBoxExUltimetSheep_Loaded(object sender, RoutedEventArgs e)
        {
            
        }

        private void OnKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Down)
            {
                // ↓キーでプルダウンリストを表示
                ShowDropdown();
            }
        }

        private void ShowDropdown()
        {

            _popup.IsOpen = true;
        }

        private void DropdownList_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {

            if (_dropdownList.SelectedItem != null)
            {
                if(_textBox != null)
                this._textBox.Text = _dropdownList.SelectedItem.ToString();


            }
        }


        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _textBox = new TextBox();
            _textBox = GetTemplateChild("PART_TextBox") as TextBox;

            if (_textBox != null)
            {
                // ここでPART_TextBoxに対して何か処理を行う
                _textBox.SpellCheck.IsEnabled = true; // 例: スペルチェックを有効にする
            }


        }
    }
}


```
:::note info
 ```public override void OnApplyTemplate()```
でOnApplyTemplateをオーバーライドし、  
```cs
_textBox = GetTemplateChild("PART_TextBox") as TextBox;
```
を取得します。カスタムコントロール内のテキストボックスを取得するための処理です。
:::


# MainWindow側


カスタムコントロールと比較するため、通常のテキストボックスも置いてみた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/b0c2269a-2d20-faae-21c2-624d4407c05b.png)




```xml
<Window x:Class="WpfApp11.MainWindow"
        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:WpfApp11"
        mc:Ignorable="d"
        Loaded="Window_Loaded"
        Closed="Window_Closed"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>

            <local:PulldownTextBoxExUltimetSheep 
            Margin="10"
            HorizontalAlignment="Left" 
            x:Name="PulldownTextBox" Height="50" Width="200"         
            SpellCheck.IsEnabled="True" Language="en-US">
            
                        </local:PulldownTextBoxExUltimetSheep>
            <TextBox Margin="10" SpellCheck.IsEnabled="True" HorizontalAlignment="Left"
            Width="200" Height="50" Language="en-US"/>
        </StackPanel>
    </Grid>
</Window>

```

:::note info
 Language="en-US" (**大文字小文字が区別される**)を指定することで、言語バーの設定に関係なくSpellCheckが動作するはずですが、現状ではOSの言語設定が優先されるため、機能していません。現状ではWindowsの言語設定が英語(米国)でないと動作していません。
:::


### Code Behind


```cs
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp11
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
           

        }

        private void Window_Closed(object sender, EventArgs e)
        {
            PulldownTextBox._popup.IsOpen = false;         
        }
    }
}
```


###


# 実行してみる
 こんな感じです。 textBox内の 
 \<TextBox.Style>
 　  \<Style TargetType="TextBox"> 
   　 　  \<Setter Property="Template">
要素内でBorder（枠）の丸みを設定することで丸いテキストボックスを実現しています。SplellCheck機能も実験中ですが、現状ではOSの言語バーを英語にする必要があります。
        
　![bandicam 2024-10-28 00-54-32-742_Harua_Harua.mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145714/5137014e-02a4-b2f1-b4b1-eaf81420829d.gif)