チュートリアル: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でも似た話になってしまって、単なる作りこみになるのでやめました。

WPF3Dことはじめ

WPFの3Dクラスがややこしい。

wpf3d

左から。WPFのWindowやGridに3D表示されるUIElementはViewPort3D。このViewPort3DにはCameraプロパティがあって3Dモデルを映している形にります。その3DモデルはModelVisual3Dオブジェクトですが、通常は複数のオブジェクトを保有するのでModel3DGroupでグループ化してやる必要があります。

Model3DGroupの中にはLight(AmbientLightやDirectionalLight)と目的の3Dモデル(Geometry3Dオブジェクト)を保有する形になる。3Dモデルはまず3頂点で三角形をつくる、これがMeshGeometry3D、それにMaterial(テクスチャ)を貼り付けてGeometry3Dを作る。

 

1.3DオブジェクトをXAMLで作る

3DオブジェクトをXAMLコードでカキカキするのはほぼ不可能、モデラーを使います。Blenderがメジャーぽいけど使いにくかったのでメタセコイアで。初心者向けの書籍もAmazon中古で格安になっていたりするのでお勧め。

metasequoia

モデルを作成後ファイル保存(.mqo)し、Identityで開きXAMLで保存します。アニメーションも作りたい場合はIdentityで作ります。

Identity_

できたXAMLを張り付ければ3Dオブジェクトは完成。

Test.xaml

<Viewbox x:Name="Viewbox_test" 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/interactivedesigner/2006" xmlns:c="http://schemas.openxmlformats.org/markup-compatibility/2006" c:Ignorable="d">
	<Viewport3D x:Name="Viewport3D_test" Width="400" Height="300">
		<Viewport3D.Resources>
			<ResourceDictionary>
				<MaterialGroup x:Key="Material_test_NoMaterial" >
					<DiffuseMaterial>
						<DiffuseMaterial.Brush>
							<SolidColorBrush Color="#cccccc" Opacity="1.000000"/>
						</DiffuseMaterial.Brush>
					</DiffuseMaterial>
				</MaterialGroup>
				<Transform3DGroup x:Key="Transform_Character_test_Character_0" >
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
					<ScaleTransform3D ScaleX="1" ScaleY="1" ScaleZ="1"/>
					<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
						<RotateTransform3D.Rotation>
							<AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
						</RotateTransform3D.Rotation>
					</RotateTransform3D>
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
				</Transform3DGroup>
				<Transform3DGroup x:Key="Transform_Model_test_obj1" >
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
					<ScaleTransform3D CenterX="0.000000" CenterY="0.000000" CenterZ="0.000000" ScaleX="1" ScaleY="1" ScaleZ="1"/>
					<RotateTransform3D CenterX="0.000000" CenterY="0.000000" CenterZ="0.000000">
						<RotateTransform3D.Rotation>
							<AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
						</RotateTransform3D.Rotation>
					</RotateTransform3D>
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
				</Transform3DGroup>
				<MeshGeometry3D x:Key="Geometry_test_obj1_0"
					TriangleIndices="2,1,0 3,2,0 5,4,1 2,5,1 7,6,4 5,7,4 3,0,6 7,3,6 1,4,6 0,1,6 5,2,3 7,5,3 "
					Positions="-160.219193,132.025406,117.890900 161.003204,132.025406,117.890900 202.421402,-139.093903,117.890800 -201.637405,-139.093903,117.890800 161.003204,132.025299,-117.890999 202.421402,-139.093994,-117.891098 -160.219193,132.025299,-117.890999 -201.637405,-139.093994,-117.891098 "
				/>
			</ResourceDictionary>
		</Viewport3D.Resources>

		<Viewport3D.Camera>
			<PerspectiveCamera x:Name="Camera_test" Position="955.806458,986.842163,1417.041260" LookDirection="-0.484275,-0.500000,-0.717968" UpDirection="0.000000,1.000000,0.000000" FieldOfView="20.000000" NearPlaneDistance="1" FarPlaneDistance="10000" />
		</Viewport3D.Camera>

		<ModelVisual3D>
			<ModelVisual3D.Content>
				<Model3DGroup>
					<AmbientLight Color="#646464" />
					<DirectionalLight x:Name="Light_test" Color="#FFFFFF" Direction="-0.484275,-0.500000,-0.717968" />
				</Model3DGroup>
			</ModelVisual3D.Content>
		</ModelVisual3D>
		<ModelVisual3D x:Name="Character_test_Character_0" Transform="{StaticResource Transform_Character_test_Character_0}">
			<ModelVisual3D.Content>
				<Model3DGroup>
					<Model3DGroup x:Name="Model_test_obj1" Transform="{StaticResource Transform_Model_test_obj1}">
						<GeometryModel3D x:Name="Geometry_Model_test_obj1_0" Geometry="{StaticResource Geometry_test_obj1_0}" Material="{StaticResource Material_test_NoMaterial}"/>
					</Model3DGroup>
				</Model3DGroup>
			</ModelVisual3D.Content>
		</ModelVisual3D>
	</Viewport3D>

	<Viewbox.Resources>
		<Storyboard Duration="Forever" FillBehavior="HoldEnd" BeginTime="0:0:0" x:Key="Animation_test_Animation_0">
		</Storyboard>
	</Viewbox.Resources>

	<Viewbox.Triggers>
		<EventTrigger RoutedEvent="Viewport3D.Loaded">
			<EventTrigger.Actions>
				<BeginStoryboard Storyboard="{StaticResource Animation_test_Animation_0}" />
			</EventTrigger.Actions>
		</EventTrigger>
	</Viewbox.Triggers>
</Viewbox>

ここまでコード0行。

2.3Dオブジェクトの操作

3Dオブジェクトではほぼ必須と言える回転・拡大縮小処理をビヘイビアとして用意。もっと完成度の高いビヘイビアが標準であるかと思ったのですが、見つかりませんでした。

 Wpf3DModelOperateBehavior.cs

namespace Wpf3DTestApp
{
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media.Media3D;
    using System.Windows.Interactivity;

    public class Wpf3DModelOperateBehavior : Behavior<Viewport3D>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseWheel += AssociatedObject_MouseWheel;
            this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
            this.AssociatedObject.MouseDown += AssociatedObject_MouseDown;
            this.AssociatedObject.MouseUp += AssociatedObject_MouseUp;
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseWheel += AssociatedObject_MouseWheel;
            this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
            base.OnDetaching();
        }

        void AssociatedObject_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            var viewport = sender as Viewport3D;
            if (viewport == null){
                return;
            }
            var camera = viewport.Camera as PerspectiveCamera;
            if (camera == null){
                return;
            }
            camera.FieldOfView += e.Delta / 100;
        }

        bool dragging = false;
        Point startPoint;
        double startangle;
        void AssociatedObject_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            dragging = false;
        }

        void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            dragging = true;
            startPoint = e.GetPosition(sender as Viewport3D);
            var axisRot = GetRot(sender);
            if (axisRot == null)
            {
                return;
            }
            startangle = axisRot.Angle;
        }

        void AssociatedObject_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (!dragging){
                return;
            }
            var axisRot = GetRot(sender);
            if (axisRot == null){
                return;
            }
            axisRot.Angle = startangle + (e.GetPosition(sender as Viewport3D).X - startPoint.X);
        }

        private AxisAngleRotation3D GetRot(object sender)
        {
            var viewport = sender as Viewport3D;
            if (viewport == null)
            {
                return null;
            }
            var model = viewport.Children.First();
            if (model == null || !(model is ModelVisual3D))
            {
                return null;
            }
            var transform = model.Transform;
            if (transform == null)
            {
                return null;
            }
            RotateTransform3D rotate = null;
            if (transform is RotateTransform3D)
            {
                rotate = transform as RotateTransform3D;
            }
            if (transform is Transform3DGroup)
            {
                var transgroup = transform as Transform3DGroup;
                rotate = transgroup.Children.First(trans => trans is RotateTransform3D) as RotateTransform3D;
            }
            if (rotate == null)
            {
                return null;
            }
            return rotate.Rotation as AxisAngleRotation3D;
        }

    }
}

まずビヘイビアとは何か?はこちらを参照くださいませ。作る過程でわかったこととしては3Dの回転やスケーリングを汎用的なビヘイビアとして作るのは非常に難しそうということ。Viewport3Dから操作対象のオブジェクトネストが深いので、実際の変換を行える保証がない。3Dモデルの作り方も影響しそう。今回のビヘイビアを適用するならこんな感じ。

MainWindow.xaml(抜粋)

<Window
        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:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:me="clr-namespace:Wpf3DTestApp"
        mc:Ignorable="d" x:Class="Wpf3DTestApp.MainWindow"
        Title="MainWindow" Height="350" Width="525">
	<Grid MouseWheel="Grid_MouseWheel" MouseUp="Grid_MouseUp" MouseDown="Grid_MouseDown" MouseMove="Grid_MouseMove">
		<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="Viewbox_test" c:Ignorable="d" d:IsLocked="True">
            <Viewport3D x:Name="Viewport3D_test" Width="400" Height="300">
                <i:Interaction.Behaviors>
                    <me:Wpf3DModelOperateBehavior/>
                </i:Interaction.Behaviors>
            </Viewport3D>
		</Viewbox>
	</Grid>
</Window>

簡単な操作はこれでできるようになりました。

img

C#による最速Splash Screenの作成

C#でSplash Screenを作る方法のメモです。Splash Screenというとアプリが起動時に表示されて消えていくアレです、DLLのロードや初期化の進捗も表示するリッチなものもありますがイメージが出て・消えていくだけのシンプルなものを最短の手数で作ってみます。

1.Windows Formsの場合

WindowsFormsの場合、SplashScreenはFormを使います。

  1. Splash対象の画像をリソースに登録
  2. 新規Formを作成後、FormBorderStyleをNoneにしBackgroundに1の画像を指定
  3. 画像ファイルに合うようFormを適宜リサイズ

Program.cs

namespace WindowsFormsAppWithSplash
{
    using System;
    using System.Windows.Forms;

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            SplashForm splash = new SplashForm();
            splash.Show();
            var mainform = new Form1();
            mainform.Shown += (_, __) => splash.Close();
            Application.Run(mainform);
        }
    }
}

わずか3分!(実測)。ここで、MainFormのコンストラクタで必要なDLLのロードをする想定です。最速なのでフェードインフェードアウトはもちろんナシ。

 

2.WPFの場合

  1. Splash対象の画像をリソースに追加(WindowsFormsの場合と同じ)
  2. 画像ファイルを右クリック、プロパティのビルドアクションをSplashScreenに設定

wpf_splash

コードはなし!わずか30秒(実測)。こちらも重たい処理はMainFormで行う前提です。自作コードはありませんが、これはツールが吐き出してくれているから。

 App.g.cs(おそらくobjフォルダの中)

namespace WpfAppWithSplash {
    public partial class App : System.Windows.Application {
        /// <summary>
        /// Application Entry Point.
        /// </summary>
        [System.STAThreadAttribute()]
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
        public static void Main() {
            SplashScreen splashScreen = new SplashScreen("resources/penguins.jpg");
            splashScreen.Show(true);
            WpfAppWithSplash.App app = new WpfAppWithSplash.App();
            app.InitializeComponent();
            app.Run();
        }
    }
}

テキスト表示などもうちょっとサポートがあれば・・と思わなくもない。

C# Taskのキソ

C#には非同期処理を扱うThreadクラスがあります。Threadクラスは生身のThreadオブジェクトを表しますが、スレッドの生成・削除にはコストが掛かります。そのためThreadではなく、ThreadPoolにプールされているThreadを利用して処理を依頼するというのが定石でした。

C#4.0で追加されたTaskは内部的にはこのThreadPoolを使い、もう一段高い抽象層を作り出すのに一役買っています。ある処理が終わったら別のスレッドでこの処理を行い、、といったコードが書きやすくなっています。C#5.0で追加されたasync/awaitはこのTaskクラスをメソッド定義のように簡単に作れるようにしたものです。要はこのTaskクラス、使い方を十分理解しておく必要があるということです。

スレッドの処理には、スレッドの作成・開始,開始時への値の設定から始まり、進捗報告・キャンセル対応・完了時の値取得・待合わせ・例外処理といろんな要素があります。それらを盛り込んでみたソースはこちら。

 TaskBasicForm.cs

namespace Task1stTouch
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    public partial class TaskBasicForm : Form
    {
        private Task<SampleTaskResult> task;
        private CancellationTokenSource cancelSouce;
        private TaskScheduler uiTaskScheduler;
        private bool canceled;

        public TaskBasicForm(){
            InitializeComponent();
        }

        private void createAndStartbutton_Click(object sender, EventArgs e){
            // 初期化
            canceled = false;
            cancelSouce = new CancellationTokenSource();    // キャンセルサポート時に必要
            uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // UIコントロール操作に必要

            // 作成&開始
            task = Task<SampleTaskResult>.Factory.StartNew(
                TaskExecuteAsyncronously, 
                new SampleTaskArg() { Seed = 10 }, 
                cancelSouce.Token );

            // task.Waitで待てるが、UIスレッドで待つことになるためNG.
            // キャンセル時or完了時の処理はtask自体に登録しておく.
            // Task終了時に演算結果が出る場合、結果を使用する位置で待つ.
            cancelSouce.Token.Register(() => canceled = true);
            task.ContinueWith(_ => statusLabel.Text = canceled ? "キャンセルされました" : "完了しました", uiTaskScheduler);
        }

        // 非同期実行されるメソッド
        private SampleTaskResult TaskExecuteAsyncronously(object o)
        {
            // 引数取得
            SampleTaskArg arg = o as SampleTaskArg;
            for (int i = 0; i < 100; i ++)
            {
                // 進捗報告
                var progressbarUpdateTask = new Task( () => progressBar.Value = i+1);
                progressbarUpdateTask.Start(uiTaskScheduler);
                // キャンセル要求チェック
                if (cancelSouce.IsCancellationRequested){
                    return null;
                }
                // 本来したい重たい処理
                Thread.Sleep(20);
            }
            // 処理完了時の値
            return new SampleTaskResult() { Value = arg.Seed * 10 };
        }

        private void cancelButton_Click(object sender, EventArgs e)
        {
            if (cancelSouce == null){
                return;
            }
            cancelSouce.Cancel();
        }

        private void resultButton_Click(object sender, EventArgs e)
        {
            if (task == null || canceled){
                return;
            }
            // task.Resultにアクセスすると結果がまだの場合待ちます.
            statusLabel.Text = "結果は" + task.Result.Value + "です.";
        }
    }
}

dialog

まずTaskの作成と開始はそれぞれコンストラクタとStartメソッドで独立することもできますが上のように同時にもできます。非同期処理が値を返す場合はTask<T>、値を返さない場合はTaskとします。非同期処理開始後、完了をWaitで待つ例がありますが、WindowsFormsやWPF等のUIを持つアプリはメッセージポンプを回さないといけないのでWaitは禁物です。

キャンセルや進捗表示は準備が必要です。キャンセルはCancellationTokenSourceオブジェクトが必要です。非同期実行されるメソッド内部でキャンセル状態を繰り返しチェックする処理は必要です。処理が複雑になるならcancelTokenのThrowIfcancellationRequestedを使うとキャンセル要求チェック&キャンセル要求時は例外スローができ、すっきりします。

進捗表示はあまり誉められた方法ではありません。プログレスバーはUIスレッドのものなのでInvoke的なことが必要です。.NET4.5以降(?)にはEventProgress<T>というものもありますが.NET4.0時点ではこれを自作するか、上のようにUI更新タスクを投げまくるかしかありません。多分。

結果取得はtaksk.Resultだけ。非同期処理が完了していない場合、ここで待ちに入ります。そのため上で記載したとおり完了時に算出される計算結果は必要なタイミングで要求するのが良いでしょう。

IEnumerableとIQueryable

ExcelをC#から扱う方法をご紹介しましたが、データ処理をする際にLINQが使えたらと思うのは私だけじゃないはず。以下のテストデータに対してクエリする方法を見ておきます。

 TestData.cs

namespace LinqToEtcetra
{
    using System;

    public class TestData
    {
        public int No { get; set; }
        public string Name { get; set; }
        public DateTime TestDate { get; set; }
        public int Score { get; set; }
    }
}

 

1.Linq To Csv

データ整理にExcelを使う場合、CSVファイルに対してLinqクエリ発行できればと思う時に使えるライブラリがこちら。早速使ってみます。まずNugetで

>PM install-package linqtocsv

としてパッケージをインストールしておきます。テストコードはこちら。

 LinqToCsvTest.cs

namespace LinqToEtcetra
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using LINQtoCSV;

    public class LinqToCsvTest
    {
        private readonly CsvContext csvContext = new CsvContext();
        private readonly CsvFileDescription fileDescription = new CsvFileDescription()
        {
            SeparatorChar = ',',
            FirstLineHasColumnNames = true,
            TextEncoding = Encoding.GetEncoding("Shift-JIS")
        };

        public void WriteFile()
        {
            var testData =  new List<TestData>(){
                new TestData(){No=1, Name="hiro",TestDate=new DateTime(2014,7,14),Score=70},
                new TestData(){No=2, Name="yuki",TestDate=new DateTime(2014,7,14),Score=85},
                new TestData(){No=3, Name="mayu",TestDate=new DateTime(2014,7,14),Score=100},
                new TestData(){No=4, Name="taku",TestDate=new DateTime(2014,7,14),Score=90},
            };
            csvContext.Write<TestData>(testData, "test.csv", fileDescription);
        }

        public void ReadFile()
        {
            var testDataSequence = csvContext.Read<TestData>("test.csv");
            // ご自由にLinqください。
            string maxScorePerson = testDataSequence.OrderByDescending(i => i.Score).First().Name;
            Console.WriteLine("最高得点は" + maxScorePerson + "さんです.");
        }
    }
}

これでもイイのですが、CSVファイルのレイアウトが変わる場合はデータ構造定義しておくことが推奨されているようです。

 TestData.cs(修正)

namespace LinqToEtcetra
{
    using System;
    using LINQtoCSV;

    public class TestData
    {
        [CsvColumn(FieldIndex=1)]
        public int No { get; set; }

        [CsvColumn(FieldIndex = 2)]
        public string Name { get; set; }

        [CsvColumn(FieldIndex = 3, OutputFormat="yyyy/MM/dd hh:mm:ss")]
        public DateTime TestDate { get; set; }

        [CsvColumn(FieldIndex = 4)]
        public int Score { get; set; }
    }
}

書きだされ、読みだされるCSVファイルはこちら。

csv_test

 

2.Linq To Excel

同じデータがExcelファイルとして用意されていたとしましょう。こちらの場合はNugetから

>PM install-package linqtoexcel

としておきます。Linq To Excelの使い方はこちら。

LinqToExcelTest.cs

namespace LinqToEtcetra
{
    using System;
    using System.Linq;
    using LinqToExcel;

    public class LinqToExcelTest
    {
        public void WriteFile()
        {
            throw new NotImplementedException("LinqToExcelではファイル出力はサポートされていません.");
        }

        public void ReadFile()
        {
            ExcelQueryFactory excel = new ExcelQueryFactory("test.xlsx");
            // ご自由にLinqください
            string maxScorePerson = excel.Worksheet("Sheet1").OrderByDescending(i => i["Score"]).First()["Name"];
            Console.WriteLine("最高得点は" + maxScorePerson + "さんです.");
        }
    }
}

想定するExcelファイルは以下です。

test_excel

LinqtoCsvの場合ファイル書き込みがサポートされていましたが、LinqToExcelの場合はサポートされていませんのPIAを使うか、NPOIを使って対応くださいませ。

 

3.IEnumerable と IQueryable

ネット上のいろんな方が記載されているので何も新しくないのですが、IEnumerableとIQueryableの違いはクエリがどこで行われるかです。

IEnumerablevsIQueryable

クエリ結果は同じ。ちなみに先の例で、LinqToCsvはIEnumerableですが、LinqToExcelはIQueryableです。IQueryableのほうが式木作成して外部で適切なフィルタ処理が必要な分、クエリは高速化が見込まれますが作るのが大変です。LinqToCsvとLinqToExcelで差はあるのかは・・・皆様ご自身でご確認ください。

WPF 入力補完の効くTextBox

「WPFの標準コントロールが足りていない!」との声が良く聞かれます。以前WPF版NumericUpDownの自作を紹介しました。とは言え毎回毎回要望の度に作ってられないのでライブラリを使うことを考えましょう。

 

1.Extended WPF Toolkitを使ったNumericUpDown

WPFヘビーユーザーの方々はとっくにご存じでしょうが、Extended WPF ToolkitというWPFコントロールライブラリがあります。これを少し簡単にご紹介。WPFアプリケーションを新規に作成後、Nugetから

>PM install-package extended.wpf.toolkit

としてライブラリをインストール。あとはXAMLで使うだけ。

 MainWindow.xaml

<Window x:Class="WpfToolkitSampleWithCustomTextBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ewt="http://schemas.xceed.com/wpf/xaml/toolkit"
        Title="MainWindow" Height="116" Width="296">
    <Grid>
        <ewt:DecimalUpDown Height="30" Width="200" Value="20"/>
    </Grid>
</Window>

numericupdown_using_extendedwpftoolkit

このライブラリには他にもColorPicker,Calculator,MagniferなどWindowsFormsでは存在しなかったもの、自作しても作成が困難だったものがあり非常にありがたいライブラリです。

 

2.入力補完の効くTextBox

Extended Wpf Toolkiの中にWatermarkTextBoxというコントロールがあります。Watermarkは日本語でいうと”透かし”、文字が空の場合に「コレコレを入力ください」とグレー文字で表示され、入力しようとすると消えるアレです。これビヘイビアを使えばできるなと思ったと同時に、同じものを作ってもしょうがない。似たところで、入力補完が効くTextBoxを作成してみようと思います。入力補完も表示にしか関係しない部分なので、ビヘイビアがぴったりです。

ビヘイビアとはなんぞやというとViewの表示,ユーザーインタラクションに関わる処理のことで、こちらを参照ください。(ライブラリ使おうって言っといて早速作るんかいという声が・・

 InputComplementBehavior.cs

namespace WpfToolkitSampleWithCustomTextBox
{
    using System;
    using System.Collections;
    using System.Windows;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Windows.Interactivity;

    public class InputComplementBehavior : Behavior<TextBox>
    {
        private string currentCandidate;
        private Brush defaultBrush;

        // 補完候補文字列コレクションの依存関係プロパティ
        public static DependencyProperty InputCandidatesProperty;
        static InputComplementBehavior()
        {
            InputComplementBehavior.InputCandidatesProperty =
                DependencyProperty.Register(
                "InputCandidates",
                typeof(IEnumerable),
                typeof(InputComplementBehavior),
                new FrameworkPropertyMetadata(String.Empty));
        }
        public IEnumerable InputCandidates
        {
            get { return (IEnumerable)GetValue(InputComplementBehavior.InputCandidatesProperty); }
            set { SetValue(InputComplementBehavior.InputCandidatesProperty, value); }
        }

        // Behavior<T>としての接合部
        protected override void OnAttached()
        {
            this.AssociatedObject.Initialized += AssociatedObject_Initialized;
            this.AssociatedObject.TextChanged += AssociatedObject_TextChanged;
            this.AssociatedObject.KeyDown += AssociatedObject_KeyDown;
            base.OnAttached();
        }
        protected override void OnDetaching()
        {
            this.AssociatedObject.Initialized -= AssociatedObject_Initialized;
            this.AssociatedObject.TextChanged -= AssociatedObject_TextChanged;
            this.AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
            base.OnDetaching();
        }

        // 初期化時にデフォルト背景を取得、保持しておく
        void AssociatedObject_Initialized(object sender, EventArgs e)
        {
            TextBox textBox = sender as TextBox;
            defaultBrush = textBox.Background;
        }

        // 補完処理
        void AssociatedObject_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            // 候補文字がある状態でTabキーが押されれば補完処理
            if (!string.IsNullOrEmpty(currentCandidate) && Key.Tab == e.Key)
            {
                TextBox textBox = sender as TextBox;
                textBox.Text = currentCandidate;
                textBox.CaretIndex = textBox.Text.Length;
                e.Handled = true;
            }
        }

        // 文字入力の度に候補を探し、背景描画
        void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox textBox = sender as TextBox;
            currentCandidate = GetMostProbableComplementText(textBox.Text);
            if (!String.IsNullOrEmpty(currentCandidate))
            {
                // 候補文字があるのでグレー背景で表示する
                textBox.Background = GetComplementTextBackgroundBrush();
            }
            else
            {
                // 元に戻す
                textBox.Background = defaultBrush;
            }
        }

        private string GetMostProbableComplementText(string currentText)
        {
            // 先頭からの一致を探す(カスタマイズ要素)
            if (string.IsNullOrEmpty(currentText))
            {
                return String.Empty;
            }
            foreach (var item in InputCandidates)
            {
                string text = item as String;
                if (!String.IsNullOrEmpty(text) && text.StartsWith(currentText))
                {
                    return text;
                }
            }
            return String.Empty;
        }

        private Brush GetComplementTextBackgroundBrush()
        {
            TextBox textBoxVisual = new TextBox()
            {
                Text = currentCandidate,
                BorderBrush = null,
                Foreground = new SolidColorBrush(Colors.Gray),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Center
            };
            return new VisualBrush(textBoxVisual)
            {
                Stretch = Stretch.None,
                TileMode = TileMode.None,
                AlignmentX = AlignmentX.Left,
                AlignmentY = AlignmentY.Top
            };
        }

    }
}

補完候補となる文字はバインドして設定することが容易に想像つくので、依存関係プロパティとして作成しています。このビヘイビアの使い方はこちら↓

MainWindow.xaml

<Window x:Class="WpfToolkitSampleWithCustomTextBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:me="clr-namespace:WpfToolkitSampleWithCustomTextBox"
        Title="MainWindow" Height="100" Width="300">
    <Grid>
        <TextBox Width="200" Height="25">
            <i:Interaction.Behaviors>
                <me:InputComplementBehavior>
                    <me:InputComplementBehavior.InputCandidates>
                        <x:Array Type="sys:String">
                            <sys:String>One</sys:String>
                            <sys:String>Two</sys:String>
                            <sys:String>Three</sys:String>
                            <sys:String>Four</sys:String>
                            <sys:String>Five</sys:String>
                        </x:Array>
                    </me:InputComplementBehavior.InputCandidates>
                </me:InputComplementBehavior>
            </i:Interaction.Behaviors>
        </TextBox>
    </Grid>
</Window>

このTextBoxはもちろん通常入力が可能です。

normal_usage

しかし候補文字にある文字列の一部を入力すると背景がグレーの文字が現れ、

complement_start この状態でTabキーをおすと、、

complement_done 補完が効きます!

今回は候補文字との比較を単純な登録順の前方一致でしていますが、もっとインテリジェントにもできそうです。

C#からpdfファイルの作成

C#からpdfファイルを作成するためのライブラリは様々ありますが、PDFSharpを使った方法をご紹介。PDFSharpはWindowsForms用(GDI+)用とWPF用とが分かれていますが、今回はWindowsForms用を使ってみます。

WindowsFormsアプリケーションを作成後、Nugetから

PM> install-package pdfsharp

としてライブラリをインストールします。PDFSharpを使うサンプルをアレコレ説明しようかと思ったのですが、PDFSharpのページに色々あります。線描画や矩形描画、画像の張り付けも簡単です。

ただこのPDFSharp、日本語対応できていません。そのためDrawStringというAPIで日本語文字を書こうとしても文字化けするのです。なんてこった。そこで文字列を画像として作成して、張り付けるという乱暴な簡単な回避方法をご紹介。

 PdfSharpJapaneseExtension.cs 

namespace PdfByCsharpWinFormsApp
{
    using System;
    using System.Drawing;
    using System.IO;
    using System.Drawing.Imaging;
    using PdfSharp.Drawing;

    public static class PdfSharpJapaneseExtension
    {
        public static void DrawJapanese(this XGraphics target, String text, Font font,
                                        Brush brush, Rectangle drawRect)
        {
            Size drawSize = GetTextDrawingBmpSize(text, font);

            // Graphicsオブジェクトを使うにはBitmapの場合24bppor32bppが必要です。
            using(Bitmap tempImage = new Bitmap(drawSize.Width, drawSize.Height, PixelFormat.Format24bppRgb))
            {
                // 一時Bitmapに描画
                using (Graphics graphics = Graphics.FromImage(tempImage))
                {
                    graphics.FillRectangle(Brushes.White, new Rectangle(Point.Empty, drawSize));
                    graphics.DrawString(text, font, brush, PointF.Empty);
                }
                // 保存
                string tempImageFileName = Path.GetTempFileName() + ".bmp";
                tempImage.Save(tempImageFileName, ImageFormat.Bmp);

                // 画像ファイルからPDFへ描画
                XImage image = XImage.FromFile(tempImageFileName);
                target.DrawImage(image, new Point(drawRect.X, drawRect.Y));
            }
        }

        private static Size GetTextDrawingBmpSize(String text, Font font)
        {
            using(Bitmap graphicsImage = new Bitmap(10, 10, PixelFormat.Format24bppRgb))
            using(Graphics graphics = Graphics.FromImage(graphicsImage))
            {
                SizeF imageSize = graphics.MeasureString(text, font);
                return new Size((int)(Math.Ceiling(imageSize.Width)), (int)(Math.Ceiling(imageSize.Height)));
            }
        }
    }
}

まず文書内容とフォントから画像として張り付ける時のサイズを算出します。そのあとそれを一旦一時ファイルとして保存し、ファイルからPDFSharpの画像描画APIを使って画像を貼り付けます。これを使うコードは以下のような感じ。

Form1.cs(ボタンが1つあるだけのForm)

namespace PdfByCsharpWinFormsApp
{
    using System;
    using System.Drawing;
    using System.Diagnostics;
    using System.Windows.Forms;
    using PdfSharp.Drawing;
    using PdfSharp.Pdf;

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void testButton_Click(object sender, EventArgs e)
        {
            PdfDocument document = new PdfDocument();
            PdfPage page = document.AddPage();
            XGraphics graphics = XGraphics.FromPdfPage(page);

            // 文字列描画(PDFSharpのAPI)
            graphics.DrawString("日本語は文字化けします!", new XFont("MS UI ゴシック", 20.0f), XBrushes.Black,
                new XRect(0, 0, page.Width, page.Height), XStringFormat.TopLeft);

            // 文字列描画(今回の方法)
            graphics.DrawJapanese("日本語も文字化けしない!", new Font("MS UI ゴシック", 20.0f), Brushes.Black,
                new Rectangle(0, 100, (int)page.Width, (int)page.Height));

            const string filename = "DrawJapaneseSample.pdf";
            document.Save(filename);
            Process.Start(filename);
        }
    }
}

これを実行した結果はこんな感じ。

test_pdf_by_csharp

PDFSharpのDrawStringでは日本語文字が文字化けするのに対して、今回の方法では画像として張り付けてるだけなので文字化けしません。拡張メソッドとして作ったので似たような使い方ができています。

ただ今回の方法にはいろいろ問題が・・。拡張メソッド内で一時ファイルを削除しようとしてもロックされており削除できません。また画像として張り付けているので背景があるような場合はどうなるのか、、今回は背景を白固定で文字を描画しましたが背景に文字等があった場合は画像領域がべたで上書きされてしまいそうです。・・・怖くて確認できない。