Windows 8 アプリ入門2 【ポイントと課題】



先日、Windows 8 RTM版が公開されました。
この時期にMetroスタイルのいうワードが使えなくなるという事態に困惑している一方で、「メトロ」という響きにもだいぶ愛着が湧いてきていた分、残念でもあります。
今回は、これまでのXAML/C#を使ったWindows 8のアプリ開発についてポイントや課題をまとめます。

アプリにおける日本語フォントとStandardStyles

Windows 8アプリでは「Meiryo UI」という日本語フォントが推奨されています。

■ 文字のズレと潰れ


「Meiryo UI」を使用する場合、使用するテキストによっては「文字のズレ」や「にじみや潰れ」がないか注意が必要です。
現状では、使うFontSizeや文字によっては見た目があまりよくない状態になることがあるようです。上図では、左はFontSizeが42(ページヘッダーテキストの推奨サイズ)のときの「各種」というテキスト、右はボタンにおけるBoldの潰れやにじみ、文字の縦ズレの例です。
現状では、事態を回避するため、実機で見え方を確認してFontSizeを調節するなどの対応をとっています。
今後の改善に期待します。

■ 日本語とStandardStylesのスタイル


欧文推奨フォントである「Segoe UI」ではThin,Extra Light,Semi Light,…,Extra Blackなど11種類のFontoWeightを指定できます。一方、日本語推奨フォントの「Meiryo UI」のFontWeightは「Normal」「Bold」の2種類です。Meiryo UIでは、Thin~Mediumに指定されるとNormalに、Semi Bold~Extra Boldに指定されるとBoldとして表示されます。
テンプレートを使用した場合に最初から使用されているStandardStyles.xamlのStyleの中には「Normal」「Bold」以外に指定してあるFontWeightプロパティも多数存在します。


そこで、ページヘッダータイトルなどに日本語と英字が混じる場合には、FontWeightをNormalかBoldに指定し直す必要があります。
(英字部分だけ細くなってしまったりする)
上図は、左が英字のみLightになった状態(StandardStylesのスタイルを使用した場合)、右はNormalに揃えた状態です。

日本語と英数字の混じるテキストにおけるTextTrimming

Windows 8のTextBlockには、範囲に入らないテキストを「…」(3点リーダー)で省略する「TextTrimming」プロパティがあります。文字数の上限をレイアウト上設ける必要がある場合は、上記のプロパティを設定すると便利です。
しかしWPFなどと違い、設定できるのは「WordEllipsis(単語単位)」「None(省略なし)」であり、CharacterEllipsis(文字単位)はありません。
ここで問題になってくるのは日本語と英数字が混在したテキストの場合です。日本語は1文字=1単語とみなされるため問題ないのですが、英数字の場合はスペース区切りの単語で一括りになり、省略時も単語単位で省略されます。(例外として一文すべてが英数字のスペースなしの場合は1文字=1単語とみなされる)
つまり日本語と英数字が混在するテキストの場合、英数字のかたまりが1つの単語とみなされるためテキストの大部分が省略されてしまう危険性があります。また、その場合には省略される前の横幅が保たれるため「…」の右側に大きく余白が残ることになります。
現状では、テキスト自体を文字数でトリミングしておくか、自分でトリミングを実装することで対応しています。これも日本語ならではの問題かと思います。

ハイコントラストテーマとテーマリソースの利用


Windows 8ユーザは任意で、以下の操作をおこなうことでハイコントラストの配色テーマを選択できます。

  • コントロールパネル―デスクトップのカスタマイズ―個人設定―ハイコントラストテーマを選択
  • 設定チャーム―PC設定の変更―簡単操作―ハイ コントラストをオン

Windows 8アプリにおいても、以下のテーマリソースを利用する組み込みスタイルなどを利用している場合には、ユーザがハイコントラストテーマを設定した場合にハイコントラスト用のスタイルが適用されます。

C:\Program Files (x86)\Windows Kits\8.0\Include\winrt\xaml\design\themeresources.xaml

アプリの配色をおこなう際には、ハイコントラストのテーマ配色においても可読性や視認性が保たれるか確認をおこなう必要があります。
また、アプリの配色をハイコントラストに変更させない方法として、themeresources.xamlに定義されたスタイルをアプリケーション側で再定義し直すことで、ハイコントラストのテーマに関係なく一定の配色に固定できます。

ランドスケープとポートレートのレイアウト切替

アプリの表示はデバイスの向き(ランドスケープ[横]かポートレート[縦])に合わせて適切なレイアウトに変更する必要があります。XAMLではルートグリッドのビジュアルステートに適切なストーリーボードを登録することで、簡単にレイアウトの変化を記述できます。
一方、ItemTemplateやItemContainerStyleやItemSource、各Selectorなどをデバイスの向きによって切り替えたい場合には、C#で記述することも可能です。

■ 現在のデバイスの向きを取得する

DisplayProperties.CurrentOrientation

取得できるのは、以下の5通りです。

  • DisplayOrientations.Landscape (横)
  • DisplayOrientations.LandscapeFlipped (逆向き横)
  • DisplayOrientations.Portrait (縦)
  • DisplayOrientations.PortraitFlipped (逆向き縦)
  • DisplayOrientations.None (向き情報なし)

デバイスの向きを変更することで発生するイベントを2つ紹介します。

■ OrientationChangedイベントを使用する

OrientationChangedイベントは、デバイスの向きが変更されたときに発生します。

public Sample01()
{
   this.InitializeComponent();
   OrientationGroupStyleChanged();   //初期状態で設定が必要な場合
   DisplayProperties.OrientationChanged += DisplayProperties_OrientationChanged;
}
void DisplayProperties_OrientationChanged(object sender)
{
   OrientationGroupStyleChanged();
}
void OrientationGroupStyleChanged()
{
   System.Diagnostics.Debug.WriteLine(DisplayProperties.CurrentOrientation);
}

■ SizedChangedイベントを使用する

SizedChangedイベントは、サイズが変化した時点を取得します。
ルートや各コントロールに追加することで、デバイスの回転時にイベントを発生させることができます。また、SIzedChangedイベントは初回表示時にも発生します。

  • ルートにイベンを追加
  • public Sample01()
    {
       this.InitializeComponent();
       this.SizeChanged += TMet01001_SizeChanged;
    }
    void Sample01_SizeChanged(object sender, SizeChangedEventArgs e)
    {
       System.Diagnostics.Debug.WriteLine(this.ActualHeight + ” / ” + this.ActualWidth);
    }
  • サイズ可変のコントロールにイベントを追加
  • <StackPanel HorizontalAlignment=”Stretch” SizeChanged=”TitlePanel_SizeChanged_1″ >
       &#60TextBlock x:Name=”TitleTextBlock” Text=”{Biding Text} TextTrimming=”WordEllipsis”/>
    </StackPanel>
    private void TitlePanel_SizeChanged_1(object sender, SizeChangedEventArgs e)
    {
       StackPanel panel = sender as StackPanel;
       TitleTextBlock.MaxWidth = panel.ActualWidth;
    }

解像度とスケーリング

Windows 8のアプリには解像度と画面サイズによって拡大率(100%,140%,180%)が用意されています。

■ 論理DPI(1inchあたりのドット数)を取得する

アプリにおいて解像度を確認したい場合には、論理DPIを取得します。

DisplayProperties.LogicalDpi

シミュレータで確認したところ以下のようになりました。

画面サイズと解像度 拡大率 論理DPI
10.6″1024×768 100% 96
10.6″1366×768 100% 96
10.6″1920×1080 140% 134.4
10.6″2560×1440 180% 172.8
12″1280×800 100% 96
23″1920×1080 100% 96
27″2560×1440 100% 96

■ 拡大率によってスケーリングする

自動的に解像度に合わせて画像を切り替えて表示させることができます。
拡大率によってフォルダをわける方法と、ファイル名をわけておく方法があるとのことです。後者を紹介します。
ファイル名に「.scale-***.png」をつけて同じフォルダ内に保存します。

***.scale-100.png
***.scale-140.png
***.scale-180.png

あとは、「.scale-***」を除いたファイル名(+拡張子)を指定するだけです。

***.png

びっくりするくらい簡単なのですが、本当に切り替わっているか不安。。。ということで、違う色の画像を各拡大率ファイルとして保存して上記の論理DPIを参考にシミュレータで実行してみました。
結果、しっかり違う色が表示されましたので、一安心です。

コンバーターの利用

XAMLでデータによって表示切替をしたい場合や、表示単位や表現を揃えたい時など何かと便利です。
値がNullのときにボタンを非表示にするなど、Visibilityを切り替える例です。

Visibility=”{Binding ItemId, Converter={StaticResource NullToCollapsedConverter}}”

リソースディクショナリで定義しておきます。

<cvt:NullToCollapsedConverter x:Key=”NullToCollapsedConverter”/>

ConvertとConvertBackを実装します。Convertは変換、ConvertBackは値を逆に戻すときに実装します。

class NullToCollapsedConverter : IValueConverter
{
   public object Convert(object value, Type targetType, object parameter, string language)
   {
      return (value == null) ? Visibility.Collapsed : Visibility.Visible;
   }

   public object ConvertBack(object value, Type targetType, object parameter, string language)
   {
      return new NotImplementedException();
   }
}

グルーピングしたGridViewとセマンティックズーム

グルーピングしたGridViewを使用している際、以下の状況でエラーが起こりました。原因は不明ですがOSのバグ(?)ような挙動です。

  • 2グループ目以降のグループのいずれか1つ以上に項目(項目の中は空でも可)が1つ以上ない場合、セマンティックズームでZoomOutしたのちにZoomInしようとするとエラーがでる
  • 末尾のグループに必ず項目(項目の中は空でも可)が1つ以上ない場合、セマンティックズームでZoomOutしたのちに末尾のグループをタップしてZoomInしようとするとエラーがでる

初回表示は問題なくでき、セマンティックズームでZoomOutしたのちZoomInする際に起こるエラーなのでGridViewの問題なのかSemanticZoomの問題なのかはわかりません。が、少なくとも最低でも末尾グループには最低1つ以上項目がないとセマンティックズームが使えないということで不便です。

GroupStyleSelectorの挙動

GroupStyleSelectorの挙動がおかしいという件に関しては、Googleで検索してみたところ少なからず問題としてあがっているようなのでOSのバグ(?)のような気がします。
GroupStyleSelectorにはlevelがあり、level=0はグループヘッダー部分を表しているものかと思われます。
デバッガで追いかけてみると、最初にlevel 0が渡ってきます。後にlevel 1で正しいルートを通りreturnされているようなのですが、表示はlevel 0で設定したGroupStyleに統一されてしまいます。

public class GroupStyleRoundScapeSelector : GroupStyleSelector
{
   protected override GroupStyle SelectGroupStyleCore(object group, uint level)
   {
      if (level > 0)         //level 1
      {
         var collectionViewGroup = (group as ICollectionViewGroup);
   
         if (collectionViewGroup != null)
         {
            var groupVM = collectionViewGroup.Group;
            if (groupVM is GroupStyleA)
            {
               return App.Current.Resources[“GroupStyleA”] as GroupStyle;
            }
            else
            {
               return App.Current.Resources[“GroupStyleB”] as GroupStyle;
            }
         }
         return null;
      }
      else                //level 0
      {
            return グループヘッダー部分GroupStyle;
      }
   }
}