1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-27

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

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

作例:
image.png

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

Microsoft Learn

トグルボタンの例

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

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

git repository

 git clone https://github.com/Sheephuman/CustomControlTest.git

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

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

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

いつもの「追加」からですね。
image.png

名前はPulldownTextBoxExUltimetSheep.csとします。

コードビハインド

以下のCodeが記述されます。


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

       }
   }

カスタムコントロール外観の定義

これが結構、めんどうなんですね。

PullDownTextBoxStyle.xamlを設置します。

image.png

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

うんざりするほど縦に長くなりますが諦めよう(笑)

<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で書いてみた

分かりやすいかどうかは微妙なところです('ω')
参考

詳しい解説

1. <ResourceDictionary>要素



<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>要素

特定のプロパティに値を設定する役割

<Setter Property="Template">

2. <ControlTemplate>要素

ControlTemplateは、コントロールの外観を細かく定義するためのテンプレートです。

TargetType="{x:Type local:PulldownTextBoxExUltimetSheep}"

により、PulldownTextBoxExUltimetSheep専用のテンプレートであることが示されています。

3. <Border>要素

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

5. <TextBox>要素

<TextBox.Style>内の<ControlTemplate>要素を解説

これにより、テキストボックスを詳細にカスタマイズできます。

     <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の見た目が指定されます。

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

実装

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; // 例: スペルチェックを有効にする
            }


        }
    }
}


public override void OnApplyTemplate()
でOnApplyTemplateをオーバーライドし、

_textBox = GetTemplateChild("PART_TextBox") as TextBox;

を取得します。カスタムコントロール内のテキストボックスを取得するための処理です。

MainWindow側

カスタムコントロールと比較するため、通常のテキストボックスも置いてみた
image.png

<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>

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

Code Behind

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

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?