午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、サイズ可変+マウスドラッグ移動可能なEllipseをカスタムコントロールで作ってみた

昨日の発展型、ControlTemplateのCanvasの中にEllipseとTextBlockを入れてみた

結果

GIFアニメーション

期待通り、いいねえ

ユーチューブ
youtu.be


VisualTree



MainWindow.xaml



テストアプリのGIFアニメーション一覧



テストアプリのコード

2025WPF/20250117_EllipseCanvasThumb at main · gogowaten/2025WPF

github.com


テスト環境

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

EllipseThumb
CanvasThumbを継承して、依存関係プロパティを3つ追加しただけ
Fill:塗りつぶしの色
Stroke:円環の色
StrokeThickness:円環の太さ


構造は

EllipseThumbの構造
CanvasThumbのものをコピペして、Canvasの中にEllipseを追加、BasedOnでCanvasThumbのスタイルを継承、TargetTypeの変更、必要なプロパティのBinding
TargetTypeを変更しておくと、TemplateBindingでの入力候補に適切なものが表示される

指定無しだとエラーになる

TargetType指定無し


指定ありだと追加した依存関係プロパティも出てくる

TargetType指定
StyleのTargetTypeとControlTemplateのTargetTypeの両方での指定が必要


EllipseTextThumb

EllipseThumbを継承して、依存関係プロパティを追加しただけ
EllipseThumbで追加した3つの依存関係プロパティも、引き続き使える

EllipseTextthumb
追加した依存関係プロパティは2つ
TextBackground:背景色
Text:テキスト

EllipseTextThumb

  • Canvas
    • Grid
      • Ellipse
      • TextBlock

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を「できるだけ簡易」にカスタムコントロールで作ってみた - 午後わてんのブログ

gogowaten.hatenablog.com