昨日の発展型、ControlTemplateのCanvasの中にEllipseとTextBlockを入れてみた
結果
期待通り、いいねえ
ユーチューブ
youtu.be
テストアプリのコード
2025WPF/20250117_EllipseCanvasThumb at main · gogowaten/2025WPF
テスト環境
- Windows 10 Home バージョン 22H2
- Visual Studio Community 2022 Version 17.12.4
- WPF
- C#
- .NET 8.0
CustomControl1.sc
using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace _20250117_EllipseCanvasThumb { public class CanvasThumb : Thumb { private readonly Thumb MyThumb; private const double MinimumSize = 1; private const double MinimumLocate = 0; private const double ThumbSize = 20; static CanvasThumb() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CanvasThumb), new FrameworkPropertyMetadata(typeof(CanvasThumb))); } public CanvasThumb() { MyThumb = new() { Width = ThumbSize, Height = ThumbSize, Cursor = Cursors.SizeNWSE }; MyThumb.DragDelta += Thumb_DragDelta; DragDelta += Thumb_DragDelta; SetInitialPosition(); } private void SetInitialPosition() { Canvas.SetLeft(MyThumb, MinimumLocate); Canvas.SetTop(MyThumb, MinimumLocate); Canvas.SetLeft(this, MinimumLocate); Canvas.SetTop(this, MinimumLocate); } private void Thumb_DragDelta(object sender, DragDeltaEventArgs e) { if (sender is Thumb t) { if (t == MyThumb) { //最小サイズ未満にならないようにThumbの移動 Canvas.SetLeft(t, Math.Max(MinimumSize, Canvas.GetLeft(t) + e.HorizontalChange)); Canvas.SetTop(t, Math.Max(MinimumSize, Canvas.GetTop(t) + e.VerticalChange)); e.Handled = true; } else if (t == this) { //最小座標未満にならないように自身の移動 Canvas.SetLeft(t, Math.Max(MinimumLocate, Canvas.GetLeft(t) + e.HorizontalChange)); Canvas.SetTop(t, Math.Max(MinimumLocate, Canvas.GetTop(t) + e.VerticalChange)); e.Handled = true; } } } public override void OnApplyTemplate() { //Templateの中のCanvasを取得してMyThumbを追加とBinding処理 base.OnApplyTemplate(); if (GetTemplateChild("PART_Canvas") is Canvas panel) { panel.Children.Add(MyThumb); //バインド //自身のサイズをソースにMyThumbの座標をバインド MyThumb.DataContext = this; _ = MyThumb.SetBinding(Canvas.LeftProperty, new Binding(nameof(Width)) { Mode = BindingMode.TwoWay }); _ = MyThumb.SetBinding(Canvas.TopProperty, new Binding(nameof(Height)) { Mode = BindingMode.TwoWay }); } } } public class EllipseThumb : CanvasThumb { public Brush Fill { get { return (Brush)GetValue(FillProperty); } set { SetValue(FillProperty, value); } } public static readonly DependencyProperty FillProperty = DependencyProperty.Register(nameof(Fill), typeof(Brush), typeof(EllipseThumb), new PropertyMetadata(null)); public Brush Stroke { get { return (Brush)GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } } public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(nameof(Stroke), typeof(Brush), typeof(EllipseThumb), new PropertyMetadata(null)); public double StrokeThickness { get { return (double)GetValue(StrokeThicknessProperty); } set { SetValue(StrokeThicknessProperty, value); } } public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(nameof(StrokeThickness), typeof(double), typeof(EllipseThumb), new PropertyMetadata(1.0)); static EllipseThumb() { DefaultStyleKeyProperty.OverrideMetadata(typeof(EllipseThumb), new FrameworkPropertyMetadata(typeof(EllipseThumb))); } public EllipseThumb() { } } public class EllipseTextThumb : EllipseThumb { public Brush TextBackground { get { return (Brush)GetValue(TextBackgroundProperty); } set { SetValue(TextBackgroundProperty, value); } } public static readonly DependencyProperty TextBackgroundProperty = DependencyProperty.Register(nameof(TextBackground), typeof(Brush), typeof(EllipseTextThumb), new PropertyMetadata(null)); public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(EllipseTextThumb), new PropertyMetadata(string.Empty)); static EllipseTextThumb() { DefaultStyleKeyProperty.OverrideMetadata(typeof(EllipseTextThumb), new FrameworkPropertyMetadata(typeof(EllipseTextThumb))); } public EllipseTextThumb() { } } }
Generic.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:_20250117_EllipseCanvasThumb"> <Style x:Key="canvasT" TargetType="{x:Type local:CanvasThumb}"> <Setter Property="Canvas.Left" Value="0"/> <Setter Property="Canvas.Top" Value="0"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:CanvasThumb}"> <Canvas x:Name="PART_Canvas" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}"> </Canvas> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type local:EllipseThumb}" BasedOn="{StaticResource canvasT}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:EllipseThumb}"> <Canvas x:Name="PART_Canvas" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}"> <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Fill="{TemplateBinding Fill}" Stroke="{TemplateBinding Stroke}" StrokeThickness="{TemplateBinding StrokeThickness}"/> </Canvas> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type local:EllipseTextThumb}" BasedOn="{StaticResource canvasT}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:EllipseTextThumb}"> <Canvas x:Name="PART_Canvas" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}"> <Grid> <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Fill="{TemplateBinding Fill}" Stroke="{TemplateBinding Stroke}" StrokeThickness="{TemplateBinding StrokeThickness}"/> <TextBlock Text="{TemplateBinding Text}" Background="{TemplateBinding TextBackground}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </Canvas> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
CanvasThumbクラスは昨日のコピペ
追加しいたクラスは2個
EllipseThumb
EllipseTextThumb
EllipseThumb
CanvasThumbを継承して、依存関係プロパティを3つ追加しただけ
Fill:塗りつぶしの色
Stroke:円環の色
StrokeThickness:円環の太さ
構造は
CanvasThumbのものをコピペして、Canvasの中にEllipseを追加、BasedOnでCanvasThumbのスタイルを継承、TargetTypeの変更、必要なプロパティのBinding
TargetTypeを変更しておくと、TemplateBindingでの入力候補に適切なものが表示される
指定無しだとエラーになる
指定ありだと追加した依存関係プロパティも出てくる
StyleのTargetTypeとControlTemplateのTargetTypeの両方での指定が必要
EllipseTextThumb
EllipseThumbを継承して、依存関係プロパティを追加しただけ
EllipseThumbで追加した3つの依存関係プロパティも、引き続き使える
追加した依存関係プロパティは2つ
TextBackground:背景色
Text:テキスト
- Canvas
- Grid
- Ellipse
- TextBlock
- Grid
GridをCanvasとの間に入れることで、TextBlockの表示位置を調節できるようになる。もし、Gridを入れないとHorizontalAlignmentでCenterを指定しても無効になる、なぜならCanvasでの位置指定はLeftとTopで行っているから
MainWindow.xaml
<Window x:Class="_20250117_EllipseCanvasThumb.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:_20250117_EllipseCanvasThumb" mc:Ignorable="d" Title="MainWindow" Height="450" Width="500"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="150"/> </Grid.ColumnDefinitions> <Canvas> <local:EllipseThumb x:Name="MyRange" Canvas.Left="150" Canvas.Top="30" Width="100" Height="100" Background="Pink" Fill="Lavender" Stroke="Gray" StrokeThickness="5"/> <local:EllipseTextThumb x:Name="EllipseTextThumb" Canvas.Left="0" Canvas.Top="0" Width="100" Height="100" Background="YellowGreen" Fill="Lavender" Stroke="Gray" StrokeThickness="5" Text="TextBlock" TextBackground="Transparent"/> </Canvas> <DockPanel Grid.Column="1"> <GroupBox DockPanel.Dock="Top" Header="{Binding Name}" DataContext="{Binding ElementName=MyRange}"> <StackPanel Margin="5"> <TextBlock Text="{Binding Path=(Canvas.Left), StringFormat=left {0:0.0}}"/> <TextBlock Text="{Binding Path=(Canvas.Top), StringFormat=top {0:0.0}}"/> <TextBlock Text="{Binding Path=ActualWidth, StringFormat=width {0:0.0}}"/> <TextBlock Text="{Binding Path=ActualHeight, StringFormat=height {0:0.0}}"/> </StackPanel> </GroupBox> <GroupBox DockPanel.Dock="Top" Header="{Binding Name}" DataContext="{Binding ElementName=EllipseTextThumb}"> <StackPanel Margin="5"> <TextBlock Text="{Binding Path=(Canvas.Left), StringFormat=left {0:0.0}}"/> <TextBlock Text="{Binding Path=(Canvas.Top), StringFormat=top {0:0.0}}"/> <TextBlock Text="{Binding Path=ActualWidth, StringFormat=width {0:0.0}}"/> <TextBlock Text="{Binding Path=ActualHeight, StringFormat=height {0:0.0}}"/> <TextBlock Text="{Binding Path=Text, StringFormat=Text {0:0.0}}"/> <TextBox Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> </GroupBox> </DockPanel> </Grid> </Window>
DockPanelの中は動作確認用なので必要ない
MainWindow.xaml.cs
using System.Windows; namespace _20250117_EllipseCanvasThumb { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
感想
できた、これでどんな要素でもサイズ可変+ドラッグ移動にできそう
最初はControlTemplateのTargetTypeを指定していせいで、追加した依存関係プロパティが出てこなくて、手動で入力したらエラーになるわで躓いていた
関連記事
前回のWPF記事は昨日の
WPF、サイズ可変+マウスドラッグ移動可能なCanvasを「できるだけ簡易」にカスタムコントロールで作ってみた - 午後わてんのブログ