【C#/WPF】アニメーションの動的ロード【XAML/IronPython】
閉じる
閉じる

新しい記事を投稿しました。シェアして読者に伝えましょう

×

【C#/WPF】アニメーションの動的ロード【XAML/IronPython】

2015-01-07 15:52
    ドーモ、読者=サン。忍殺卓の人です。

    1. 今日の内容

    今回もWPF/XAML開発のTips紹介です。デスクトップマスコットの開発中にxaml、特にStoryboardを実行時ロードする手口を試したので情報公開します。C#/XAMLの基礎知識に加えてアニメーションの基本が分かってる人向けです。またIronPythonコードが出てきますが.NET慣れしてれば普通に読めると思います。

    ゆっくりWindowWalker(YWW)」のユーザの方に対しては一応「水饅頭モード」と呼ぶべき動きの実装報告になってます。「2. 動的ロードが必要な理由」のところで動画を紹介してるのでそちらから動画を御覧ください。

    目次
    2. 動的ロードが必要な理由
    3. 色々と動的だから動的言語でやっちゃえ!
    4. xamlをpythonコードに埋める(+ソースコード掲載)
    5. まとめ

    2. 動的ロードが必要な理由

    アニメーションを動的ロードしたくなったのは完全に娯楽目的です。私が作ってるデスクトップマスコット「ゆっくりWindowWalker」でキャラクターの動きとして任意のアニメーションを適用したいけど、アプリケーション本体のxamlはアセンブリ内に押し込めたままにしたい、と。実際に動かした例はこんな感じ。



    ソフトウェアとしての高い柔軟性(物理)を実現しています。大変柔らかい……アーイイ……遥かに良いです……


    上の動画ではきつねさんが製作された「うどんげ」がぴょんぴょんしてますが、これは実際にはMainWindowのTop, ScaleTransformのScaleX, ScaleYに対して後付けでアニメーションを定義して実装しています。「後付け」といってるのはアプリケーション本体ではなくてユーザサイドで編集可能なIronPythonスクリプトからアニメーションを適用してるからです。

    UMLクラス図っぽく書くとクラス構成はこんな感じになります。途中にクラスではなく外部ファイルであるIronPythonスクリプトが挟まってることに注意してください。ユーザが書き換えられるのは色がついてる部分です。





    3. 色々と動的だから動的言語でやっちゃえ!

    上のUML図にも書いてますが、アニメーションを適用するにはStoryboardの添付プロパティであるTargetとかTargetPropertyをどこかで設定しなければなりません。つまりAnimationからMainWindowとかその子要素を知る必要があります。

    しかし動的に読み込むxamlの中に直接文字列でStoryboard.Target等を書き込んで



    ...

    <DoubleAnimation Storyboard.TargetName="mainWindow" ... />
    ...




    のように設定する方法はうまくいきません。うまく行かないのは変数のスコープとかそういう事情によるものです。この問題を解決するには、

    ・アプリケーションからIronPythonへMainWindowの参照を渡して
    ・IronPythonでは
    - XAMLをもとにStoryboardを生成
    - Storyboardの中に入ってるDoubleAnimationでTargetやTargetPropertyを設定
    - StoryboardをBeginする、又はアプリケーション側に投げる

    というような手順を踏めばOKです。補助用のコードは上記の例なら20行程度書けば済みます。これはスクリプト言語で扱うのには相性が良い分量ですね。

    もちろん.NETにはC#コードの動的コンパイル機能もあるので「C#以外知らん!」とか「IronPythonライブラリを持ってくる/再配布するのが面倒!」といった場合はC#のみでなんとかすることも可能です。

    ちなみに、C#等からIronPythonをホストするときにスクリプト側へ公開する変数は「ビルトインスコープ」に置いておくと都合が良いです。これについては前の記事でチラっと書いてます。

    4. xamlをIronPythonコードに埋める

    基本のコンセプトは上で示した通りで、ここからはついでです。

    まず、そもそもxamlは文字列からロードできます。というか、普通のXAML連動アプリケーションは実際にはXAMLを実行時ロードしてるらしいので「できて当たり前」です。WPFの場合はMSDNでXamlServices.Loadメソッドの説明を見ると
    • ファイル名のstring
    • TextReader
    などを使ってxamlを読み込めることが書かれていますから、方針としては
    • xamlファイルとして分離したものを用意する
    • スクリプト内に文字列としてxamlを書き、StringReaderで読み込む
    のいずれかをすればOKです。xamlのルート要素は今回の場合Storyboardになります。

    今回の目的ではxaml + IronPythonで一つのアニメーションを提供しようとしているので、xamlを分離して再利用するモチベーションはそこまで高くありません。そこで後者の方法によって見かけ上IronPythonだけでアニメーションが実装されているようにしてみます。

    以下がIronPythonコードとMainWindowのコードビハインドです。ニコ動ブロマガの仕様でインデントが残念なことになってますがあまり気にしないでください。IronPythonコードにはもともとインデント必須の部分はありません。

    この例ではVisual Studioで"JumpingWindow"という名前のプロジェクトをWPFアプリケーションとして作り、MainWindow.xamlは全く変更していないという状況を想定しています。pyファイルは文字コードをutf-8にして保存し、exeと同じフォルダに配置してください。

    またアプリケーション側ではNuGetからIronPythonパッケージをインストールしておいてください。Visual Studioの場合ソリューションエクスプローラで「参照設定」のところを右クリックして「NuGetパッケージの管理」を選び、オンラインで検索すればすぐ出来ます。

    以上の準備をして実行するとウィンドウがぴょんぴょんします。うまく行ってれば、上の方で載せてる動画で出てくるウィンドウと同じ跳ね方をしてくれるはずです。

    このウィンドウは動きが激しく右上の「閉じる」ボタンを押して消すのにはコツが要ります。Alt+F4で消すことをオススメします。


    MainWindow.xaml.cs

    using System.Windows;

    using IronPython.Hosting;
    using Microsoft.Scripting.Hosting;

    namespace JumpingWindow
    {
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
    public MainWindow()
    {
    InitializeComponent();

    var engineMain = Python.CreateEngine();
    //ビルトインスコープに追加するとスクリプト側が複数ファイルで構成されていてもwindowが参照可能
    ScriptScope builtin = engineMain.GetBuiltinModule();
    builtin.SetVariable("window", this);

    engineMain.ExecuteFile("jumping.py");
    }

    }
    }




    jumping.py
    # -*- encoding:utf-8 -*-

    #ぴょんぴょんアニメーションの定義

    import clr
    clr.AddReference("PresentationFramework")
    clr.AddReference("System.Xaml")

    from System.IO import StringReader
    from System.Xaml import XamlServices
    from System.Windows import PropertyPath
    from System.Windows.Media.Animation import Storyboard

    xamlStr = """
    <Storyboard xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    RepeatBehavior="Forever">
    <DoubleAnimationUsingKeyFrames>
    <DiscreteDoubleKeyFrame KeyTime="0" Value="10"/>
    <EasingDoubleKeyFrame KeyTime="0:0:1" Value="300">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseIn"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    <EasingDoubleKeyFrame KeyTime="0:0:1.25" Value="450">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseOut"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="300">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseIn"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    <EasingDoubleKeyFrame KeyTime="0:0:2.5" Value="10">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseOut"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames>
    <DiscreteDoubleKeyFrame KeyTime="0" Value="300"/>
    <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="300"/>
    <EasingDoubleKeyFrame KeyTime="0:0:1.25" Value="150">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseOut"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="200">
    <EasingDoubleKeyFrame.EasingFunction>
    <QuadraticEase EasingMode="EaseIn"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    <EasingDoubleKeyFrame KeyTime="0:0:2.5" Value="300">
    <EasingDoubleKeyFrame.EasingFunction>
    <ElasticEase EasingMode="EaseOut" Springiness="3" Oscillations="5"/>
    </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
    </Storyboard>
    """

    sr = StringReader(xamlStr)
    sb = XamlServices.Load(sr)
    sr.Dispose()

    Storyboard.SetTarget(sb.Children[0], window)
    Storyboard.SetTarget(sb.Children[1], window)

    Storyboard.SetTargetProperty(sb.Children[0], PropertyPath("Top"))
    Storyboard.SetTargetProperty(sb.Children[1], PropertyPath("Height"))

    sb.Begin()




    ちなみに細かい事ですがStringReaderはIDisposableを実装しているので、使い道が無くなったらさっさとDisposeしましょう。C#のusingステートメントに似た処理(例外時でも確実にDispose)をするにはPythonでのtry-finallyを活用する必要があります。

    5. まとめ

    C#, XAML, IronPythonの連携でもってアニメーションをプログラム本体から分離する方法を紹介しました。特にWPFの一部で動的処理を適用するのにスクリプト言語に頼るとロジックの補完も簡潔に済ませられることを例示しました。今回の例ではスクリプト言語としてIronPythonを使ってますが、他のスクリプト言語でも同じようなアプローチが通用すると思います。

    やはり言語ごとに型や実行の動静は異なるので、それぞれの長所を活かすのは大切だなあと思いました(小並感


    ちなみにMainWindowの参照をスクリプトに公開したくない場合の対応策ですが、例えば
    • MainWindow側でターゲットにしたいオブジェクトの名前
    • ターゲットのプロパティを識別できる文字列
    • Storyboard
    の3点を受け取るインターフェースだけ公開し、アプリケーション側はデータに応じてアニメーションの実施/拒否を選ぶというような方針を取れば比較的簡単に実装できます。余力の有る方は是非お試しください。



    広告
    コメントを書く
    コメントをするには、
    ログインして下さい。