近頃ようやく使われだした感のあるWPFですが(例えばゆっくりムービーメーカー4でも使われてると見られる)、WPFではより高機能なカスタマイズを提供するため、カスタムコントロールというものがあります。
ユーザーコントロールは既存のコントロールを組み合わせたもので、手軽で簡単に作ることが出来ます。
これに対し、カスタムコントロールはより詳細な外観の定義やカスタム動作をコードビハインドに負担させる事なく記述可能です。
Microsoft Learn
トグルボタンの例
以下からはVisual Studio2022を前提に話を進めます。
カスタムコントロールの自作
git repository
git clone https://github.com/Sheephuman/CustomControlTest.git
今回は自作のアプリケーション開発も兼ねて、ユーザーコントロール以上に詳細な外観と機能を備えたカスタムコントロールを制作します。
仕様
・↓キー押下でプルダウンリスト(PopUp)の表示
・スペルチェック機能
・テストを兼ねて外観も弄る
手順1 プロジェクトにカスタムコントロールを追加する
名前はPulldownTextBoxExUltimetSheep.cs
とします。
コードビハインド
以下のCodeが記述されます。
public class PulldownTextBoxExUltimetSheep : Control
{
static PulldownTextBoxExUltimetSheep()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PulldownTextBoxExUltimetSheep), new FrameworkPropertyMetadata(typeof(PulldownTextBoxExUltimetSheep)));
//カスタム コントロールのデフォルト スタイルを設定
//DefaultStyleKeyProperty` メタデータをオーバーライドすることで、コントロールを特定のリソース ディクショナリ内のスタイルに関連付けることになります。
}
}
カスタムコントロール外観の定義
これが結構、めんどうなんですね。
PullDownTextBoxStyle.xaml
を設置します。
リソースディクショナリを定義
うんざりするほど縦に長くなりますが諦めよう(笑)
<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 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側
カスタムコントロールと比較するため、通常のテキストボックスも置いてみた
<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の言語バーを英語にする必要があります。