近頃のWindowsには、Windows Animation Managerというアニメーション支援のライブラリが搭載されています(MSDNライブラリ:Windows Animation API の概要)。これを試しに使ってみました。

Windows Animation Managerの概要

Windows Animation Managerが提供している根本的な機能は、最も単純に言ってしまえば変数の値を徐々に変化させていくこと(遷移 (Transition)と呼ばれる:たとえば、一定速度や加速・減速、放物線など)です。

一見簡単に思えることですが、様々な場合について考え出すと自分でコードをすべて書くのは面倒そうです。たとえば、2つ以上の変数を扱う場合や、あるアニメーションの完了前に次のアニメーションの開始要求が来た場合などが考えられます。MSDNライブラリのストーリーボードのスケジューリングに登場する図を見ていてそう思いました。そのため、このように1つのライブラリとして成立するのではないでしょうか。

アプリケーションの実装方針

とにもかくにも、ライブラリが提供している機能は変数の遷移までなので、変数の値をUIに反映させることはアプリ側の実装になります。そのタイミングの面で、大きく2つの方法があるようです(MSDNライブラリのWindows Animation の使用より)。

アプリケーション駆動型アニメーション
ゲームのような60fpsなどで動くアプリなら、変数の値を毎フレーム読み取って使用させると良いです。
タイマー駆動型アニメーション
そうでないアプリなら、Windows Animation Managerが提供するタイマー(MSDNライブラリ: UIAnimationTimer)が使えます。アニメーションの進行中、適宜コールバックされるので、都度変数の値を読み取って使用します。

このアニメーション対象の変数は、アニメーション変数と呼び、ライブラリで提供されているオブジェクトを使用しなくてはなりません。これはIUIAnimationVaribleインタフェースで表されます。倍精度浮動小数点数(GetValue)または32ビット整数(GetIntegerValue)として、値を読み取れます。

動作環境

Windows 7/Server 2008 R2以上およびWindows Vista/Server 2008にプラットフォーム更新プログラム(KB971664)を入れた環境で利用できます。

のちに機能拡張が行われているようで、IUIAnimationManager2など、Windows 8/Server 2012およびWindows 7/Server 2008 R2のプラットフォーム更新プログラム(KB2670838)が要件になっているものもあります。これらについては、今回は扱いませんでした。

MSDNライブラリの記述を見る限り、Windowsストアアプリでも利用できるようです。

サンプルプログラム

こんなサンプルプログラムを書いてみました。タイマー駆動型でウィンドウの高さ・幅を動かす処理をアニメーションさせています。

#define _WIN32_WINNT _WIN32_WINNT_VISTA
 
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
 
#include <string>
#include <windows.h>
#include <wtsapi32.h>
#include <UIAnimation.h>
 
#include <comdef.h>
 
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
#include <atlwin.h>
 
#include <atlapp.h>
#include <atlmisc.h>
#include <atlcrack.h>
#include <atlctrls.h>
 
#pragma comment(lib, "wtsapi32.lib")
 
_COM_SMARTPTR_TYPEDEF(IUIAnimationManager, __uuidof(IUIAnimationManager));
_COM_SMARTPTR_TYPEDEF(IUIAnimationTimer, __uuidof(IUIAnimationTimer));
_COM_SMARTPTR_TYPEDEF(IUIAnimationTransitionLibrary, __uuidof(IUIAnimationTransitionLibrary));
_COM_SMARTPTR_TYPEDEF(IUIAnimationVariable, __uuidof(IUIAnimationVariable));
_COM_SMARTPTR_TYPEDEF(IUIAnimationStoryboard, __uuidof(IUIAnimationStoryboard));
_COM_SMARTPTR_TYPEDEF(IUIAnimationTransition, __uuidof(IUIAnimationTransition));
_COM_SMARTPTR_TYPEDEF(IUIAnimationTimerUpdateHandler, __uuidof(IUIAnimationTimerUpdateHandler));
 
static const int WIDTH_CHECK_OFF = 100;
static const int WIDTH_CHECK_ON = 300;
 
static const int HEIGHT_CHECK_OFF = 100;
static const int HEIGHT_CHECK_ON = 300;
 
class TestWindow
  : public ATL::CWindowImpl<TestWindow>
  , public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public IUIAnimationTimerEventHandler
  , public IUIAnimationStoryboardEventHandler
{
public:
  DECLARE_WND_CLASS(TEXT("Test Window Class"));
 
public:
  // IUIAnimationTimerEventHandler実装
 
  IFACEMETHOD(OnPreUpdate)() override
  {
    return S_OK;
  }
  IFACEMETHOD(OnPostUpdate)() override
  {
    try
    {
      INT32 width, height;
      ATLENSURE_SUCCEEDED(m_animationWidth->GetIntegerValue(&width));
      ATLENSURE_SUCCEEDED(m_animationHeight->GetIntegerValue(&height));
      RECT rc;
      GetWindowRect(&rc);
      rc.right = rc.left + width;
      rc.bottom = rc.top + height;
      MoveWindow(&rc, FALSE);
    }
    catch(const ATL::CAtlException& e)
    {
      return e;
    }
 
    return S_OK;
  }
  IFACEMETHOD(OnRenderingTooSlow)(UINT) override
  {
    return S_OK;
  }
 
  // IUIAnimationStoryboardEventHandler実装
 
  IFACEMETHOD(OnStoryboardStatusChanged)(
    _In_ IUIAnimationStoryboard* /*storyboard*/,
    _In_ UI_ANIMATION_STORYBOARD_STATUS newStatus,
    _In_ UI_ANIMATION_STORYBOARD_STATUS /*previousStatus*/) override
  {
    if (newStatus == UI_ANIMATION_STORYBOARD_FINISHED)
    {
      // OnPostUpdateのMoveWindowでは再描画を抑制しているので、
      // ここで再描画を要求している。
      Invalidate();
    }
    return S_OK;
  }
 
  IFACEMETHOD(OnStoryboardUpdated)(
    _In_ IUIAnimationStoryboard* /*storyboard*/) override
  {
    return S_OK;
  }
 
  TestWindow()
    : m_animationManager(__uuidof(UIAnimationManager))
    , m_animationTimer(__uuidof(UIAnimationTimer))
    , m_transitionLibrary(__uuidof(UIAnimationTransitionLibrary))
  {
  }
 
  HRESULT FinalConstruct()
  {
    try
    {
      ATLENSURE_SUCCEEDED(m_animationManager->CreateAnimationVariable(
        WIDTH_CHECK_OFF, &m_animationWidth));
      ATLENSURE_SUCCEEDED(m_animationManager->CreateAnimationVariable(
        HEIGHT_CHECK_OFF, &m_animationHeight));
    }
    catch (const ATL::CAtlException& e)
    {
      return e;
    }
    return S_OK;
  }
 
  BEGIN_COM_MAP(TestWindow)
    COM_INTERFACE_ENTRY(IUIAnimationTimerEventHandler)
    COM_INTERFACE_ENTRY(IUIAnimationStoryboardEventHandler)
  END_COM_MAP()
 
private:
  static const int IDC_CHECK = 1;
 
  BEGIN_MSG_MAP(TestWindow)
    COMMAND_ID_HANDLER_EX(IDC_CHECK, OnCheckClick)
    MESSAGE_HANDLER_EX(WM_SETTINGCHANGE, OnUpdateAnimationEnabled)
    MESSAGE_HANDLER_EX(WM_WTSSESSION_CHANGE, OnUpdateAnimationEnabled)
    MSG_WM_CREATE(OnCreate)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()
 
private:
  void OnCheckClick(UINT /*notifyCode*/, int /*id*/, HWND /*hwndCtl*/)
  {
    try
    {
      auto finalValue = m_check.GetCheck()
        ? SIZE{WIDTH_CHECK_ON, HEIGHT_CHECK_ON}
        : SIZE{WIDTH_CHECK_OFF, HEIGHT_CHECK_OFF};
 
      IUIAnimationStoryboardPtr storyboard;
      ATLENSURE_SUCCEEDED(m_animationManager->CreateStoryboard(&storyboard));
      ATLENSURE_SUCCEEDED(storyboard->SetStoryboardEventHandler(this));
 
      IUIAnimationTransitionPtr animationTransitionWidth;
      IUIAnimationTransitionPtr animationTransitionHeight;
      ATLENSURE_SUCCEEDED(m_transitionLibrary->CreateLinearTransition(
        0.08, finalValue.cx, &animationTransitionWidth));
      ATLENSURE_SUCCEEDED(m_transitionLibrary->CreateLinearTransition(
        0.08, finalValue.cy, &animationTransitionHeight));
 
#if 1
      // 横→縦の順で動かす
      UI_ANIMATION_KEYFRAME keyframeAfterWidth;
      ATLENSURE_SUCCEEDED(storyboard->AddTransition(
        m_animationWidth, animationTransitionWidth));
      ATLENSURE_SUCCEEDED(storyboard->AddKeyframeAfterTransition(
        animationTransitionWidth, &keyframeAfterWidth));
      ATLENSURE_SUCCEEDED(storyboard->AddTransitionAtKeyframe(
        m_animationHeight, animationTransitionHeight, keyframeAfterWidth));
#else
      // 同時に動かす
      ATLENSURE_SUCCEEDED(storyboard->AddTransition(
        m_animationWidth, animationTransitionWidth));
      ATLENSURE_SUCCEEDED(storyboard->AddTransition(
        m_animationHeight, animationTransitionHeight));
#endif
 
      IUIAnimationTimerUpdateHandlerPtr tiemrUpdaterHandler
        = m_animationManager;
      ATLENSURE_SUCCEEDED(m_animationTimer->SetTimerUpdateHandler(
        tiemrUpdaterHandler, UI_ANIMATION_IDLE_BEHAVIOR_DISABLE));
      ATLENSURE_SUCCEEDED(m_animationTimer->SetTimerEventHandler(this));
 
      UI_ANIMATION_SECONDS secondsNow;
      ATLENSURE_SUCCEEDED(m_animationTimer->GetTime(&secondsNow));
      ATLENSURE_SUCCEEDED(storyboard->Schedule(secondsNow));
    }
    catch(const ATL::CAtlException& e)
    {
      try
      {
        OutputDebugString(ATL::AtlGetErrorDescription(e));
      }
      catch(...)
      {
      }
    }
  }
 
  LRESULT OnUpdateAnimationEnabled(UINT, WPARAM, LPARAM)
  {
    BOOL isAnimationEnabled = FALSE;
    SystemParametersInfo(
      SPI_GETCLIENTAREAANIMATION, 0, &isAnimationEnabled, 0);
    m_animationManager->SetAnimationMode(
      isAnimationEnabled && !GetSystemMetrics(SM_REMOTESESSION)
      ? UI_ANIMATION_MODE_SYSTEM_DEFAULT
      : UI_ANIMATION_MODE_DISABLED);
    return 0;
  }
 
  LRESULT OnCreate(const CREATESTRUCT*)
  {
    SetWindowPos(nullptr, 0, 0, WIDTH_CHECK_OFF, HEIGHT_CHECK_OFF,
      SWP_NOZORDER | SWP_NOMOVE);
 
    RECT rcCheck{0, 0, 80, 20};
    if (m_check.Create(*this, rcCheck, nullptr,
      BS_AUTOCHECKBOX | WS_VISIBLE | WS_CHILD, 0, IDC_CHECK) == nullptr)
    {
      return -1;
    }
    WTSRegisterSessionNotification(*this, NOTIFY_FOR_THIS_SESSION);
    OnUpdateAnimationEnabled(0, 0, 0);
    return 0;
  }
 
  void OnDestroy()
  {
    WTSUnRegisterSessionNotification(*this);
    PostQuitMessage(0);
  }
 
  IUIAnimationManagerPtr m_animationManager;
  IUIAnimationTimerPtr m_animationTimer;
  IUIAnimationTransitionLibraryPtr m_transitionLibrary;
  IUIAnimationVariablePtr m_animationWidth;
  IUIAnimationVariablePtr m_animationHeight;
 
  WTL::CButton m_check;
};
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int cmdShow)
{
  auto hr = CoInitialize(nullptr);
  if (FAILED(hr))
    return hr;
 
  try
  {
    ATL::CComObjectStackEx<TestWindow> wnd;
    if (!wnd.Create(nullptr, nullptr,
      TEXT("Animation sample"),
      WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME))
    {
      return -1;
    }
    wnd.ShowWindow(cmdShow);
    wnd.UpdateWindow();
 
    WTL::CMessageLoop msgLoop;
    auto result = msgLoop.Run();
    CoUninitialize();
    return result;
  }
  catch(...)
  {
    return -1;
  }
}

高さと幅を動かすので、アニメーション変数を2つm_animationWidthm_animationHeightとして作っています。

アニメーションの開始部分

今回は、OnCheckClick関数にアニメーションの動作開始を直接記述してしまっています。

1回のアニメーションは、1つのストーリーボードで表現されます。ストーリーボードに遷移を追加していくことでアニメーションを定義していきます。

このプログラムでは、IUIAnimationTransitionLibrary::CreateLinearTransitionで作成したオブジェクトを遷移として登録しています。これは名前どおり線形に値が変化する遷移です。IUIAnimationTransitionLibraryに様々な組み込みの遷移を作成する関数が存在するほか、遷移の自作も可能なようです。

このほか、タイマー駆動のための処理を行っています。

最後に、IUIAnimationStoryboard::Scheduleを呼び出すことで、アニメーションが開始されます。

アニメーション変数を読み取る

タイマー駆動型の場合、IUIAnimationTimerEventHandlerを実装し、OnPostUpdateでアニメーション変数の値を読み取るコードを書くことになるようです。ご覧のとおり、このプログラムではMoveWindowしています。

なお、スムーズに見せるため、再描画を抑制する実引数FALSEを指定しています。ここで使っているのはATL::CWindowMoveWindowですが、これはそのままWindows APIのMoveWindowに渡ります。

アニメーションの完了

ここはWindows SDKのサンプルプログラムで見当たらなかった自作の処理です。MoveWindowで再描画を抑止したため、アニメーション完了時にInvalidateRect(コード上はATL::CWindowInvalidate)を呼び出したくて、この処理を追加しました。

具体的には、IUIAnimationStoryboardEventHandlerを実装し、OnStoryboardStatusChangedで完了状態かどうかの判定を行っています。

そして、OnCheckClick内でIUIAnimationStoryboard::SetStoryboardEventHandlerを呼び出すことで、これが呼び出されるようになります。

アニメーションの有効・無効

OnUpdateAnimationEnabledでは、アニメーションの有効・無効を設定しています。今回は、ユーザーがオフに設定しているかリモートデスクトップ上のセッションである場合に無効としています。

なお、前者の設定は、コントロールパネル→システムとセキュリティ→システム→システムの詳細設定→詳細設定→(パフォーマンス)設定→視覚効果→Windows内のアニメーションコントロールと要素に対応しています。余談ですが、これはWindowの複数形として解釈すべき(つまり誤訳)ではないでしょうかね。

まとめ

Windows Animation Managerを使ったプログラムを試しに書いてみました。適用例としてGDI/GDI+/Direct2Dなどを使って描画内容を変化させるものが多かったので、MoveWindowという若干異色なものを題材に選びました。

ところで、ウィンドウの大きさを変える処理については、レイヤードウィンドウにしてUpdateLayeredWindowで変更するほうがスムーズに描画されるのかもしれません。今回はそこまで試しませんでした。

ニッチなところを攻めるライブラリなのかもしれませんので、これを読んだ方も機会があれば使ってみてはいかがでしょうか、という〆にしたいと思います。


スポンサード リンク

この記事のカテゴリ

  • ⇒ Windows Animation Managerを使ってみた
  • ⇒ Windows Animation Managerを使ってみた