« [Windows ストア アプリ] 8.1 に 「再ターゲット」 すると、ナニが変わるのか? #win8dev_jp | トップページ | [Windows 8.1] Windows ストアでギフト カードが利用可能に! 10/18発売 #win8jp »

2013年9月24日 (火)

[Windows ストア アプリ] 8 と 8.1 のプロジェクトで、 できるだけたくさんのソース ファイルを共用してみる #win8dev_jp

前の記事で、8 から 8.1 に 「再ターゲット」 (retarget) してもソース コードは変化しないと分かりました。

となれば、 共通の DLL に置いたり、 ソース コードを共用するにしてもプロジェクト間でリンクしたりして、 できるだけファイルの重複は避けたいものです。

とはいうものの、 Win8.1 用の Windows ストア アプリでは、 最低限でも、 ビュー幅の変更に対応しなければなりません。
※ @IT の記事 「大きく変わるWindowsストア・アプリ開発 ~ ビュー状態に関連する変更点 (3/6)」 参照

Win8用の Windows ストア アプリは、  4つの VisualState (Landscape, Filled, Snapped, FullScreenPortrait) に対応した画面レイアウトを持っています。 それを Win8.1用に変換したプロジェクトでは、 たとえば次のような場合に画面レイアウトをデフォルト動作から変更したくなります。

  • 横置きで、 Snapped (幅 320px) より広く Filled (幅 1024px) より狭いとき ⇒ 思い通りに Snapped か Filled にしたい
  • 横置きで画面の高さが 1024px 未満なのに、 縦長のビューのとき ⇒ FullScreenPortrait にはしたくない

そのためには、 LayoutAwarePage クラスの DetermineVisualState メソッドをオーバーライドして、 適切な VisualState を返してやればよいのです。

…よいのですが、 しかし DetermineVisualState メソッドの中では、 新しいビュー サイズが分かりません。 このメソッドを実行している時点では、 まだビューのサイズは変更されておらず、 元のままなのです。 古い情報に基づいて VisualState を算出しても、 おかしな動作をするだけです (1ステップ遅れて画面レイアウトが切り替わる)。

さて、 どうしたものかということで、 Win8 用にも使えるようにしたまま、 LayoutAwarePage クラスを改造しちゃいます。 f(^^;

LayoutAwarePage クラスの DetermineVisualState メソッドの中で、 次に変わる新しいビュー サイズが分かればいいわけですから、 それを DetermineVisualState メソッドの引数に渡してもらうようにします (Size 型の newSize 引数)。
どうやって newSize を与えるかというと…、

  • DetermineVisualState メソッドを呼び出すのは、ほとんどが WindowSizeChanged メソッド。 ここでは、 メソッドの引数に渡された WindowSizeChangedEventArgs の Size プロパティが、 これから適用される予定の新しいサイズ。 なので、 この新しいサイズを DetermineVisualState メソッドまで引き回せばいい。
  • DetermineVisualState メソッドを呼び出している残りは、 StartLayoutUpdates メソッドから。 これはページがロードされたときに呼び出されるメソッドだから、 現在のサイズ (this.Frame.RenderSize) を DetermineVisualState メソッドに渡せばいい。

ということで、 LayoutAwarePage クラスを次のように改修します。
改修したメソッドだけを示します。 メソッドの順序は見やすいように入れ替えてあります。 変更した部分は赤色で。

// this.Loaded で呼び出される
public void StartLayoutUpdates(object sender, RoutedEventArgs e)
{
  var control = sender as Control;
  if (control == null) return;
  if (this._layoutAwareControls == null)
  {
    // 更新の対象となるコントロールがある場合、ビューステートの変更の待機を開始します
    Window.Current.SizeChanged += this.WindowSizeChanged;
    this._layoutAwareControls = new List<Control>();
  }
  this._layoutAwareControls.Add(control);

  // コントロールの最初の表示状態を設定します
  VisualStateManager.GoToState(control, DetermineVisualState(ApplicationView.Value, this.Frame.RenderSize), false);
}

// Window.Current.SizeChanged で呼び出される
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
  this.InvalidateVisualState(e.Size);
}

public void InvalidateVisualState(Size newSize)
{
  if (this._layoutAwareControls != null)
  {
    string visualState = DetermineVisualState(ApplicationView.Value, newSize);
    foreach (var layoutAwareControl in this._layoutAwareControls)
    {
      VisualStateManager.GoToState(layoutAwareControl, visualState, false);
    }
  }
}

protected virtual string DetermineVisualState(ApplicationViewState viewState, Size newSize)
{
  return viewState.ToString();
}

この DetermineVisualState メソッドを、 Win8.1 用のプロジェクトでは、 継承したページでオーバーライドしてやるわけですが…

パーシャル クラスとして実装しましょう。 そうすれば、 ページのコード本体はプロジェクト間リンクで済みます。 (^^)
このアプリは、幅が 960 未満のときに Fill にされちゃうと画面が切れて使い物にならないので、 スナップかポートレイトに切り替えてみます。

// MainPage.xaml.Win81.cs
public sealed partial class MainPage
{
  protected override string DetermineVisualState(Windows.UI.ViewManagement.ApplicationViewState viewState, Size newSize)
  {
    var info = DisplayInformation.GetForCurrentView();
    if (info.CurrentOrientation == DisplayOrientations.Landscape
        || info.CurrentOrientation == DisplayOrientations.LandscapeFlipped)
    {
      // タブレットを横置きで使ってるときは…
      if (newSize.Width < 768.0)
      {

 // ビューの幅が 768 未満なら有無を言わさずスナップ状態
        return "Snapped";
      }
      else if (newSize.Width < 960.0)
      {
        // 幅が 768 以上 960 未満なら…
        // 高さが 1024 以上あればポートレイトにし、 それ未満ならスナップにする。
        return (newSize.Height < 1024.0) ? "Snapped" : "FullScreenPortrait";
      }
      // 幅が 960 以上なら Fill 状態で OK (←デフォルトの挙動なので、ここでは何もしない)
    }
    return base.DetermineVisualState(viewState, newSize);
  }
}

これで、 LayoutAwarePage クラスと MainPage.xaml/.xaml.cs の 3ファイルが、 Win8 用プロジェクトと Win8.1 用プロジェクトで共用できるようになりました。

閑話休題。

こうやって共通に使えるファイルが増えてくると、 その部分は別の共用プロジェクトにまとめたくなります。 だって、 プロジェクト間リンクを張るのって、 けっこう面倒なんですもん f(^^;

すると、 クラスのファイルとかはいいんですが、 xaml ファイルが問題になります。 例えば、 StandardStyles.xaml を共用プロジェクトに移動したとしましょう。 App.xaml から参照できるんでしょうか?

共用プロジェクトの名前が BluewaterSoft.ClaudiaTimer.Common で、 その Common フォルダーに StandardStyles.xaml を配置したとすると、 App.xaml は次のように書き換えれば OK です。
あ、 もちろんこの共用プロジェクトへの参照設定も必要です f(^^;

<ResourceDictionary.MergedDictionaries>
  <!--<ResourceDictionary Source="Common/StandardStyles.xaml"/>-->
  <!-- ↓ こう書き換える -->
  <ResourceDictionary Source="ms-appx:///BluewaterSoft.ClaudiaTimer.Common/Common/StandardStyles.xaml" />
</ResourceDictionary.MergedDictionaries>

さて、 こんなふうにして、 Win8.1 用プロジェクト内のファイルを、 共用プロジェクトに移したり、 Win8 用プロジェクトへのリンクに置き換えていくと、 ほとんど空っぽに近くなります。
App.xaml/.xaml.cs は、 リンクに置き換えるとアプリが起動しなくなるようで、 これは残さざるを得ないのですが、 その他には、 プロジェクトの設定関連のファイルと、 Win8.1 用に追加したパーシャル クラスだけで済みます。

例えば、 クラウディア タイマーのソリューションはこんな感じになりました。

20130923_win8win81projects01

Win8.1用プロジェクトの中で、 実体のあるファイルは赤色のマークを付けた 5箇所だけです。
App.xaml/.xaml.cs はコピペするしかなさそうなので、 基本部分以外は App.xaml.Extension.cs というパーシャル クラス に持って行って、 その部分はリンクにしてあります。

※ Win8.1用クラウディア タイマーがストアの審査に通ったら、 全ソースを公開しますね。

|

« [Windows ストア アプリ] 8.1 に 「再ターゲット」 すると、ナニが変わるのか? #win8dev_jp | トップページ | [Windows 8.1] Windows ストアでギフト カードが利用可能に! 10/18発売 #win8jp »

プログラミング」カテゴリの記事

* プログラミング ( Metro スタイル )」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/58256577

この記事へのトラックバック一覧です: [Windows ストア アプリ] 8 と 8.1 のプロジェクトで、 できるだけたくさんのソース ファイルを共用してみる #win8dev_jp:

« [Windows ストア アプリ] 8.1 に 「再ターゲット」 すると、ナニが変わるのか? #win8dev_jp | トップページ | [Windows 8.1] Windows ストアでギフト カードが利用可能に! 10/18発売 #win8jp »