Windows Forms でコントロールをアニメーションさせる
Animator
クラスの作成
時にコントロールをアニメーションさせたくなることがあります。と言っても、そんな派手なものは必要ありません。たとえばエレベーターの中で現在の階を見てしまう時を思い出してください。ほんの少しの待ち時間がある時、それを短く感じさせ、ユーザーのストレスを軽減するのにアニメーションは効果的です。また、フェードインを使えば描画の際のちらつきを無くすこともできます。
コントロールをアニメーションさせるのは簡単です。時間を置いて少しずつプロパティを変化させればいいのです。しかし、ユーザーそれぞれの環境で処理速度の違いがありますから、適当にループしたのでは速すぎたり遅すぎたりします。
そこでタイマーを使います。一定時間毎にプロパティを変化させればどの環境でもだいたい同じように見えるはずです。この処理をサポートさせるために作ったのが、次に挙げる Animator
クラスです。
public static class Animator
{
/// <summary>
/// 1 フレームの時間とフレーム数を指定してアニメーション機能を提供します。
/// </summary>
/// <param name="interval">1 フレームの時間をミリ秒単位で指定します。</param>
/// <param name="frequency">
/// frequency はコールバックが呼ばれる回数から 1 を引いたものです。例えば frequency が 10 の時には 11 回呼ばれます。
/// </param>
/// <param name="callback">
/// bool callback(int frame, int frequency) の形でコールバックを指定します。
/// frame は 0 から frequency の値まで 1 ずつ増加します。
/// frequency は引数と同じものです。
/// </param>
public static void Animate(int interval, int frequency, Func<int, int, bool> callback)
{
var timer = new System.Windows.Forms.Timer();
timer.Interval = interval;
int frame = 0;
timer.Tick += (sender, e) =>
{
if (callback(frame, frequency) == false || frame >= frequency)
{
timer.Stop();
}
frame++;
};
timer.Start();
}
/// <summary>
/// 持続時間を指定してアニメーション機能を提供します。
/// </summary>
/// <param name="duration">持続時間をミリ秒単位で指定します。</param>
/// <param name="callback">
/// bool callback(int frame, int frequency) の形でコールバックを指定します。
/// frame は 0 から frequency の値まで 1 ずつ増加します。
/// frequency はコールバックが呼ばれる回数から 1 を引いたものです。例えば frequency が 10 の時には 11 回呼ばれます。
/// </param>
public static void Animate(int duration, Func<int, int, bool> callback)
{
const int interval = 25;
if (duration < interval) duration = interval;
Animate(25, duration / interval, callback);
}
}
Form
フェードインする Animator
の使い方は簡単です。例えば Form
を一つ作り、そのコンストラクタと Load
イベントハンドラを次のように書き換えてみてください。これでフェードインする Form
の完成です。簡単でしょ?
partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Opacity = 0;
}
private void Form1_Load(object sender, EventArgs e)
{
Animator.Animate(150, (frame, frequency) =>
{
if (!Visible || IsDisposed) return false;
Opacity = (double)frame / frequency;
return true;
});
}
}
まずコンストラクタで Opacity
を 0 にしています。Opacity
というのは、Form
の透明度を取得・設定するプロパティです。これを 0 にすると完全に透明になり、1 にすると完全に不透明になります。そして Load
イベントハンドラで Animator.Animate()
メソッドを呼びます。
第一引数の 150
はアニメーションの持続時間です。単位はミリ秒ですので、150 ミリ秒かけてフェードインせよという意味になります。
第二引数はラムダ式でコールバックを指定しています。コールバックの引数 frame
は、コールバックが呼ばれた回数を示します。これが 0 の時、コールバックは初めて呼ばれました。1 の時は 2 回目、2 の時は 3 回目というふうに、ゼロベースの数字がコールバックに渡されます。frequency
は呼ばれる回数を表しますが、例えば 10 回呼ばれる時には 9 になります。なぜこんなややこしい仕様になっているかというと、frame / frequency
が 0 から 1 までの値を取るようにするためです。
if (!Visible || IsDisposed) return false;
は、アニメーション中にフォームが閉じられた時、アニメーションを中止せよとの命令です。コールバックは、続行するときには true
を返し、キャンセルするときには false
を返してください。
次の Opacity = (double)frame / frequency;
で Opacity
の値が最初は 0 に、最後は 1 になります。
丸く現れるフォーム
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
radius = (int)(Math.Sqrt(Width * Width + Height * Height) / 2);
ox = Width / 2;
oy = Height / 2;
Region = new Region(new GraphicsPath());
}
private int radius { get; set; }
private int ox { get; set; }
private int oy { get; set; }
private void Form_Load(object sender, EventArgs e)
{
Animator.Animate(300, (frame, resolution) =>
{
if (!Visible || IsDisposed) return false;
var graphicsPath = new GraphicsPath();
var r = radius * frame / resolution;
graphicsPath.AddEllipse(new Rectangle(ox - r, oy - r, r * 2, r * 2));
Region = new Region(graphicsPath);
if (frame == resolution) Region = null;
return true;
});
}
}
コントロールには、Region
というプロパティがあります。このプロパティを変更すると、コントロールを好きな形にすることができます。上記コードは、コンストラクタで Region
を無にし、Load
で 300 ミリ秒かけて円形のフォームを次第に大きくしていき、最後に Region = null
で通常の形に戻しています。
Form
に限らずコントロールには Region
プロパティがあり、これを使えば、星形だろうが渦巻だろうが名前を言ってはいけないあのネズミだろうが、好きな形のコントロールを作ることができます。
あまり時間をとったり派手な動きをしたりするアニメーションはかえってユーザーのストレスを増大させることになりますので、ほどほどにしなければなりません(Windows のメニューに動きが追加された時の気持ちを思い出してください)が、効果的な場面でのほんの少しのアニメーションは使いどころがありますので、どうぞ使ってみてください。
おまけ: 流れ星
using (var form = new Form())
{
form.FormBorderStyle = FormBorderStyle.None;
form.StartPosition = FormStartPosition.Manual;
var graphicsPath = new GraphicsPath();
graphicsPath.AddString("★", new FontFamily("メイリオ"), 0, 50, new Point(0, 0), StringFormat.GenericDefault);
form.Region = new Region(graphicsPath);
form.Left = 100;
form.Top = 100;
form.BackColor = Color.White;
form.Load += (sender, e) =>
{
Animator.Animate(300, (frame, frequency) =>
{
if (!form.Visible || form.IsDisposed) return false;
form.Left = 100 + 800 * frame / frequency;
form.Top = 100 + 800 * frame / frequency;
if (frame == frequency) form.Close();
return true;
});
};
form.ShowDialog();
}
執筆日: 2017/03/28
Discussion