C#のWPFでWaitingCircleコントロールを作る

「読み込み中」とか「しばらくお待ちください」な時に表示される円がくるくるアニメーションしてるようなやつを作る。

↓こんなやつ。


XAML

<UserControl x:Class="WpfApplication1.WaitingCircle"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApplication1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Viewbox Stretch="Uniform">
        <Canvas x:Name="MainCanvas" Width="100" Height="100">
            <Canvas.RenderTransform>
                <RotateTransform x:Name="MainTrans" CenterX="50" CenterY="50"/>
            </Canvas.RenderTransform>
        </Canvas>
    </Viewbox>
</UserControl>

コントロールのサイズに合わせて子要素を拡大縮小する (9行目)

Canvasの中にくるくる回る円の要素を書き込んでいくが、座標が計算しやすいように100x100のサイズで作っていく。

但し、このWaitingCircleコントロールがどのようなサイズで配置されてもいいように、Viewboxコントロールを使ってサイズの調整を行う。

StretchプロパティにUniformを指定して、縦横比を保ちつつサイズいっぱいに引き伸ばす。

 

 

描画領域を作る (10行目)

座標が計算しやすいように100x100のサイズにしたCanvasコントロールを準備。

Canvasの中にくるくる回る円の要素を作っていくが、座標は計算で求める方が楽なのでコードの方で行う。

 

 

アニメーションの為の回転変換を用意する (11~13行目)

Canvasを回転させるアニメーションを作る為RotateTransformを用意する。

アニメーションの細かい設定はコードの方で行う。

 

 

 

 


コード

    public partial class WaitingCircle : UserControl
    {
        public static readonly DependencyProperty CircleColorProperty =
            DependencyProperty.Register(
                "CircleColor", // プロパティ名を指定
                typeof(Color), // プロパティの型を指定
                typeof(WaitingCircle), // プロパティを所有する型を指定
                new UIPropertyMetadata(Color.FromRgb(90, 117, 153),
                    (d, e) => {(d as WaitingCircle).OnCircleColorPropertyChanged(e); }));
        public Color CircleColor
        {
            get { return (Color)GetValue(CircleColorProperty); }
            set { SetValue(CircleColorProperty, value); }
        }


        public WaitingCircle()
        {
            InitializeComponent();

            double cx = 50.0;
            double cy = 50.0;
            double r  = 45.0;
            int    cnt = 14;
            double deg = 360.0 / (double)cnt;
            double degS = deg * 0.2;
            for (int i = 0; i < cnt; ++i)
            {
                var si1 = Math.Sin((270.0 - (double)i * deg) / 180.0 * Math.PI);
                var co1 = Math.Cos((270.0 - (double)i * deg) / 180.0 * Math.PI);
                var si2 = Math.Sin((270.0 - (double)(i + 1) * deg + degS) / 180.0 * Math.PI);
                var co2 = Math.Cos((270.0 - (double)(i + 1) * deg + degS) / 180.0 * Math.PI);
                var x1 = r * co1 + cx;
                var y1 = r * si1 + cy;
                var x2 = r * co2 + cx;
                var y2 = r * si2 + cy;

                var path = new Path();
                path.Data = Geometry.Parse(string.Format("M {0},{1} A {2},{2} 0 0 0 {3},{4}", x1, y1, r, x2, y2));
                path.Stroke = new SolidColorBrush(Color.FromArgb((byte)(255 - (i * 256 / cnt)), CircleColor.R, CircleColor.G, CircleColor.B));
                path.StrokeThickness = 10.0;
                MainCanvas.Children.Add(path);
            }

            var kf = new DoubleAnimationUsingKeyFrames();
            kf.RepeatBehavior = RepeatBehavior.Forever;
            for (int i = 0; i < cnt; ++i)
            {
                kf.KeyFrames.Add(new DiscreteDoubleKeyFrame()
                {
                    KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(i * 80)),
                    Value = i * deg
                });
            }
            MainTrans.BeginAnimation(RotateTransform.AngleProperty, kf);
        }

        public void OnCircleColorPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (null == MainCanvas) return;
            if (null == MainCanvas.Children) return;
            
            foreach (var child in MainCanvas.Children)
            {
                var shp = child as Shape;
                var sb  = shp.Stroke as SolidColorBrush;
                var a   = sb.Color.A;
                shp.Stroke = new SolidColorBrush(Color.FromArgb(a, CircleColor.R, CircleColor.G, CircleColor.B));
            }
        }
    }

円の色を変えられるプロパティを追加 (3~14行目)

円の色を指定するCircleColorを依存関係プロパティとして作成。

依存関係プロパティについてはこちらを参照。

 

 

円を書く (21~43行目)

円弧の透明度を変えながら複数作成して円を作る。

cx,cy  ・・・円の中心座標

r        ・・・円の半径

cnt   ・・・円の分割数(14分割)

deg  ・・・分割した1つ分の角度

degS ・・・円弧と円弧の隙間の角度

 

円弧はPathクラスを使って作成する。

PathクラスのDataプロパティはパス マークアップ構文を使って作成。

マークアップ構文で円弧を書く場合は以下のようになる。

M [始点X],[始点Y] A [円半径X],[円半径Y] [回転角] [180度以上の時1] [正の角の時1] [終点X],[終点Y]

 

 

アニメーションを作成する (45~55行目)

Canvasを回転させるアニメーションを作る。

DiscreteDoubleKeyFrameを使うと補間せずに値が変わるので、フレーム毎の移動量を円弧の角度(deg)と合わせるといい感じになる。

RotateTransformのBeginAnimationメソッドを使って、Angleプロパティへのアニメーションを開始する。(55行目)

 

 

円の色を変えられた時の処理 (58~70行目)

色が変更されたらCanvas内の要素(円弧)のstrokeプロパティのブラシを再作成するが、アルファ値だけは元の値を使ってR,G,Bのみを変更する。

 

 

コメントをお書きください

コメント: 3
  • #1

    来訪者 (金曜日, 12 1月 2018 18:54)

    wpfで本機能を実現できました。
    コードがシンプルでユーザコントロールでもあったのでとても使いやすく助かりました。
    ありがとうございました。

  • #2

    2 (月曜日, 17 2月 2020 11:57)

    助かりました。

  • #3

    来訪者2 (金曜日, 03 2月 2023 14:40)

    13フレームから0フレームに移るときに1フレーム分とびませんか?

    このままのコードで実行すると、1周後に一瞬かくつく感じがします。
    0フレーム時の状態が一瞬しかないからだと思いますので、
    Forの後に、14番目のフレームとして、Value=0のフレームを同じように登録するとスムーズに
    回転するようになります。

    kf.KeyFrames.Add(new DiscreteDoubleKeyFrame()
    {
    KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cnt * 80)),
    Value = 0
    });