チュートリアル:WPF時計の作成(2)

Silverlightでアナログ時計作成を通してExpression Blendの使い方を学ぶチュートリアルがあります(こちら)。このWPF版をちょっと改造してみようと思います、ベースとなるチュートリアルがあって勝手に続編なのでタイトルに”(2)” がついています。

 

0.デザイン改良

もともとの時計は影をつけたり、傾斜面をつけたり良いデザインだと感じるのですが、針の部分がどうにもダサい。とは言えそんな劇的に改善できるセンスなんてありませんが、Pathを使って作ってみます。

 MainWindow.xaml(抜粋)

        <Path x:Name="hourArrow" Data="M0,0 -9,15 -6,15 0,50 6,15 9,15 z" Margin="89,58.5,146.25,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FF70DE11" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="hourTransform"/>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="minutesArrow" Data="M0,0 -5,15 -2,15 0,60 2,15 5,15 z" Margin="87,45,143,172.333" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FF1149DE" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="minutesTransform"/>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="secondArrow" Data="M0,0 -2,15 0,70 2,15 z" Margin="98.75,30.5,155,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FFF30E0E" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="secondTransform"/>
            </Path.RenderTransform>
        </Path>

これがどんな見た目なのかはのちほど。

 

1.背景を透明にする。

せっかくWPFで作るので無駄なウィンドウをなくして時計だけにします。これにはWindowのプロパティを3つセットするだけです。

 MainWindow.xaml(抜粋)

<Window x:Class="Wpf2DAnalogClock.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="275" Width="257"
		Background="Transparent" WindowStyle="None" AllowsTransparency="True" Loaded="Window_Loaded"/>

これだけで有効領域だけが残って、それ以外は透明なウィンドウができます。でもこれだけだと、ウィンドウを閉じることもできなくなってしまうので、簡単なコンテキストメニューを用意してそれだけはケアしておきます。

 MainWindow.xaml(抜粋)

	<Window.ContextMenu>
		<ContextMenu>
			<MenuItem Header="とじる" Click="MenuItem_Click"/>
		</ContextMenu>
	</Window.ContextMenu>

MainWindow.xaml.cs(抜粋)

    public partial class MainWindow : Window
    {
        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }

完成状態はのちほど。

 

2.初期時間調整をアニメーションする

せっかくアニメーションするなら起動後、時刻調整している様子をアニメーションしたらいいかと。通常の時計動作もアニメーションで実現しているので、アニメーション完了後に別のアニメーションを実行することになります。じゃKeyFrameとなりそうですが、KeyFrameはRepeatBehavior=”Forever”などとできません。よって、独立したStoryboardを一方が完了したら、もう一方を動かす、ということにします。

 MainWindow.xaml(抜粋)

    <Window.Resources>
        <!--起動後現在時刻へ移動するアニメーション-->
        <Storyboard x:Key="defaultStoryboard" x:Name="defaultStoryboard">
            <DoubleAnimation x:Name="defaultHour"
                             Storyboard.TargetName="hourTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
            <DoubleAnimation
                             Storyboard.TargetName="minutesTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
            <DoubleAnimation x:Name="defaultSecond"
                             Storyboard.TargetName="secondTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
        </Storyboard>
        <!--時計としての通常動作のアニメーション-->
        <Storyboard x:Key="clockStoryboard" x:Name="clockStoryboard">
            <DoubleAnimation x:Name="clockHour"
                             Storyboard.TargetName="hourTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="12:0:0" To="360" RepeatBehavior="Forever"/>
            <DoubleAnimation x:Name="clockMinutes"
                             Storyboard.TargetName="minutesTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="1:0:0" To="360" RepeatBehavior="Forever"/>
            <DoubleAnimation x:Name="clockSecond"
                             Storyboard.TargetName="secondTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:1:0" To="360" RepeatBehavior="Forever"/>
        </Storyboard>
    </Window.Resources>

 MainWindow.xaml.cs(抜粋)

    public partial class MainWindow : Window
    {
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            DateTime current = DateTime.Now;

            var defaultStoryboard = this.FindResource("defaultStoryboard") as Storyboard;
            // 現在時刻セット.
            var houranime = defaultStoryboard.Children[0] as DoubleAnimation;
            var minutesanime = defaultStoryboard.Children[1] as DoubleAnimation;
            var secondanime = defaultStoryboard.Children[2] as DoubleAnimation;
            houranime.To = current.Hour*30;
            minutesanime.To = current.Minute * 6;
            secondanime.To = current.Second * 6;
            if (defaultStoryboard == null){
                return;
            }
            defaultStoryboard.Completed +=(_, __) => {
                // 初期調整完了後に、通常時計動作を開始する.
                var clockStoryboard = this.FindResource("clockStoryboard") as Storyboard;
                if( clockStoryboard != null){
                    var hourClock = clockStoryboard.Children[0] as DoubleAnimation;
                    var minutesClock = clockStoryboard.Children[1] as DoubleAnimation;
                    var secondClock = clockStoryboard.Children[2] as DoubleAnimation;

                    hourClock.From = current.Hour * 30 + current.Minute / 2;
                    hourClock.To = (current.Hour + 12) * 30 + current.Minute / 2;
                    minutesClock.From = current.Minute * 6;
                    minutesClock.To = (current.Minute + 60) * 6;
                    secondClock.From = current.Second * 6;
                    secondClock.To = (current.Second + 60) * 6;
                    clockStoryboard.Begin();
                }
            };
            defaultStoryboard.Begin();
        }
    }

(初期時刻調整用の)StoryboardのCompletedイベントで次の(時計としての動作用の)StoryboardをBeginさせています。厳密には初期時刻調整に1sかかるのでcurrentに1s足しておいたほうがよいかとはおもいます。

まとめると、こんな感じです。

MainWindow.xaml(まとめ)

<Window x:Class="Wpf2DAnalogClock.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="275" Width="257"
		Background="Transparent" WindowStyle="None" AllowsTransparency="True" Loaded="Window_Loaded">
	<Window.ContextMenu>
		<ContextMenu>
			<MenuItem Header="とじる" Click="MenuItem_Click"/>
		</ContextMenu>
	</Window.ContextMenu>
    <Window.Resources>
        <!--起動後現在時刻へ移動するアニメーション-->
        <Storyboard x:Key="defaultStoryboard" x:Name="defaultStoryboard">
            <DoubleAnimation x:Name="defaultHour"
                             Storyboard.TargetName="hourTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
            <DoubleAnimation
                             Storyboard.TargetName="minutesTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
            <DoubleAnimation x:Name="defaultSecond"
                             Storyboard.TargetName="secondTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:0:1"/>
        </Storyboard>
        <!--時計としての通常動作のアニメーション-->
        <Storyboard x:Key="clockStoryboard" x:Name="clockStoryboard">
            <DoubleAnimation x:Name="clockHour"
                             Storyboard.TargetName="hourTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="12:0:0" To="360" RepeatBehavior="Forever"/>
            <DoubleAnimation x:Name="clockMinutes"
                             Storyboard.TargetName="minutesTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="1:0:0" To="360" RepeatBehavior="Forever"/>
            <DoubleAnimation x:Name="clockSecond"
                             Storyboard.TargetName="secondTransform"
                             Storyboard.TargetProperty="Angle"
                             Duration="0:1:0" To="360" RepeatBehavior="Forever"/>
        </Storyboard>
    </Window.Resources>
	<Grid>
		<Ellipse x:Name="shadowEllipse" Fill="#FF000000" HorizontalAlignment="Left" Height="200" VerticalAlignment="Top" Width="200" Stroke="Black" Opacity="0.3" Margin="5,5,0,0"/>
		<Ellipse x:Name="outerRimEllipse" HorizontalAlignment="Left" Height="200" VerticalAlignment="Top" Width="200" Margin="0,0,0,0">
			<Ellipse.Fill>
				<LinearGradientBrush EndPoint="1.04,1.045" StartPoint="0.036,0.039">
					<GradientStop Color="Black" Offset="0.89"/>
					<GradientStop Color="Silver" Offset="0.115"/>
				</LinearGradientBrush>
			</Ellipse.Fill>
		</Ellipse>
		<Ellipse x:Name="bevelEllipse" HorizontalAlignment="Left" Height="180" VerticalAlignment="Top" Width="180" Margin="10,10,0,0">
			<Ellipse.Fill>
				<LinearGradientBrush EndPoint="1.04,1.045" StartPoint="0.036,0.039">
					<GradientStop Color="Black" Offset="0.115"/>
					<GradientStop Color="Silver" Offset="0.89"/>
				</LinearGradientBrush>
			</Ellipse.Fill>
		</Ellipse>
		<Ellipse x:Name="centerEllipse" HorizontalAlignment="Left" Height="160" VerticalAlignment="Top" Width="160" Margin="20,20,0,0">
			<Ellipse.Fill>
				<LinearGradientBrush EndPoint="0.87,0.931" StartPoint="0.155,0.2">
					<GradientStop Color="#FF595454"/>
					<GradientStop Color="White" Offset="1"/>
				</LinearGradientBrush>
			</Ellipse.Fill>
		</Ellipse>
		<TextBlock x:Name="One" HorizontalAlignment="Left" Height="28" Margin="124,33,0,0" TextWrapping="Wrap" Text="1" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
		<TextBlock x:Name="Two" HorizontalAlignment="Left" Height="28" Margin="144,57,0,0" TextWrapping="Wrap" Text="2" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
		<TextBlock x:Name="Three" HorizontalAlignment="Left" Height="28" Margin="153,87,0,0" TextWrapping="Wrap" Text="3" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Four" HorizontalAlignment="Left" Height="28" Margin="143,120,0,0" TextWrapping="Wrap" Text="4" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Five" HorizontalAlignment="Left" Height="28" Margin="119,145,0,0" TextWrapping="Wrap" Text="5" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Six" HorizontalAlignment="Left" Height="28" Margin="86,152,0,0" TextWrapping="Wrap" Text="6" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Seven" HorizontalAlignment="Left" Height="28" Margin="53,144,0,0" TextWrapping="Wrap" Text="7" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Eight" HorizontalAlignment="Left" Height="28" Margin="28,119,0,0" TextWrapping="Wrap" Text="8" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Nine" HorizontalAlignment="Left" Height="28" Margin="20,87,0,0" TextWrapping="Wrap" Text="9" VerticalAlignment="Top" Width="27" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Ten" HorizontalAlignment="Left" Height="28" Margin="28,54,0,0" TextWrapping="Wrap" Text="10" VerticalAlignment="Top" Width="37" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Eleven" HorizontalAlignment="Left" Height="28" Margin="50,30,0,0" TextWrapping="Wrap" Text="11" VerticalAlignment="Top" Width="37" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <TextBlock x:Name="Twelve" HorizontalAlignment="Left" Height="28" Margin="81,20,0,0" TextWrapping="Wrap" Text="12" VerticalAlignment="Top" Width="37" FontFamily="Snap ITC" FontSize="21" TextAlignment="Center"/>
        <Path x:Name="hourArrow" Data="M0,0 -9,15 -6,15 0,50 6,15 9,15 z" Margin="89,58.5,146.25,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FF70DE11" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="hourTransform"/>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="minutesArrow" Data="M0,0 -5,15 -2,15 0,60 2,15 5,15 z" Margin="87,45,143,172.333" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FF1149DE" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="minutesTransform"/>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="secondArrow" Data="M0,0 -2,15 0,70 2,15 z" Margin="98.75,30.5,155,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
            <Path.Fill>
                <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
                    <GradientStop Color="#FFF30E0E" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Path.Fill>
            <Path.RenderTransform>
                <RotateTransform x:Name="secondTransform"/>
            </Path.RenderTransform>
        </Path>
    </Grid>
</Window>

MainWindow.xaml.cs(まとめ)

namespace Wpf2DAnalogClock
{
    using System;
    using System.Windows;
    using System.Windows.Media.Animation;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            DateTime current = DateTime.Now;

            var defaultStoryboard = this.FindResource("defaultStoryboard") as Storyboard;
            // 現在時刻セット.
            var houranime = defaultStoryboard.Children[0] as DoubleAnimation;
            var minutesanime = defaultStoryboard.Children[1] as DoubleAnimation;
            var secondanime = defaultStoryboard.Children[2] as DoubleAnimation;
            houranime.To = current.Hour * 30 + current.Minute / 2;
            minutesanime.To = current.Minute * 6;
            secondanime.To = current.Second * 6;
            if (defaultStoryboard == null){
                return;
            }
            defaultStoryboard.Completed +=(_, __) => {
                // 初期調整完了後に、通常時計動作を開始する.
                var clockStoryboard = this.FindResource("clockStoryboard") as Storyboard;
                if( clockStoryboard != null){
                    var hourClock = clockStoryboard.Children[0] as DoubleAnimation;
                    var minutesClock = clockStoryboard.Children[1] as DoubleAnimation;
                    var secondClock = clockStoryboard.Children[2] as DoubleAnimation;

                    hourClock.From = current.Hour * 30 + current.Minute / 2;
                    hourClock.To = (current.Hour + 12) * 30 + current.Minute / 2;
                    minutesClock.From = current.Minute * 6;
                    minutesClock.To = (current.Minute + 60) * 6;
                    secondClock.From = current.Second * 6;
                    secondClock.To = (current.Second + 60) * 6;
                    clockStoryboard.Begin();
                }
            };
            defaultStoryboard.Begin();
        }
    }
}

動作のようす

ちゃんと背景が透過している様子がわかるかと思います。あぁもう12時まわっていた。

 

ボツネタ.カラクリ時計にする

アナログ時計の文字盤が1, 2, 3, …11, 12の順になっておらず1, 6, 11, ..など一見でたらめに並んだ時計をご存じでしょうか。最後の仕上げにこれをトライしようとして失敗しました。

構想のみ・・・

通常の時計との違いとして、時針を通常の5倍(1時間30°でなく150°)ずつ進めればよいのですが、1つ目の問題点として時刻の変わり目で一気に動かさないといけないことがあります、これは複数のアニメーションを使うことや線形補間をやめれば実現できそうです。もう1つの問題点は、起動後いつのタイミングでその移動をするかということです。タイマーを使えば当然実現できますがそれならWindowsFormsでも似た話になってしまって、単なる作りこみになるのでやめました。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中