2014/08/21

Visual Studio "14"のツールバーアイコン

Visual Studio "14" CTP3でツールバーなどのアイコンがHigh Resolution Iconsになったというので。
ウィンドウの左右について、それぞれ上がVisual Studio 2013、下がVisual Studio "14" CTP3で、DPI 150%の状態。

確かにアイコンが変わっている。ただ輪郭がまだぼけているところを見るに、たぶんリソースにラスターデータの画像ファイルを持っていて(Visual Studio Image Libraryには以前からサイズ別の画像が用意されている)、起動時にDPIを見て適当なサイズのものをImageコントロールに読み込み、それをImageコントロールが自動リサイズしているのではないかと思う。

一方、タイトルバーにある「通知」と「フィードバック」のアイコンは以前から高DPIに対応していて、こちらは輪郭のぼけが少ないところを見るに、ベクターデータだと思う。左上角の最小化などのボタンもたぶん同じ。Microsoftの田中さんの記事によればラスターデータの画像でも高解像度のもの(最大256x256ピクセル)を使っているらしい。
ちなみに、ベクターデータだから輪郭がぼけないということはなくて、論理的な描画位置が実数(Double)で管理されているのに対して、実際に表示するピクセルは整数なので、その間に端数が生じるとぼけの原因になる。これはアプリを高DPI化するには避けて通れない問題で、DPI 100%ではきっちり整数で合わせていても、拡大/縮小すると端数が必然的に出てくる。

これを抑止するには描画位置をピクセルに合わせて整数になるよう寄せてやる必要があって、そのために使えるプロパティが色々ある。
これらの効果は一筋縄では行かなくて自分でも把握し切れてないが、これらで満足する結果が得られない場合はGuidelineSetクラスを使って描画位置を明示的に指定してやる必要がある。ただし、これはかなり面倒なので、多少のぼけは諦めた方が楽かもしれない。

[修正]

タイトルバーのアイコンについて、こちらもラスターデータの画像によるものと修正した。

2014/08/01

WPFのクローム部分におけるPer-Monitor DPI対応

Per-Monitor DPI対応のラストピースともいうべき、ウィンドウのクローム部分における対応について。

1. 背景


Per-Monitor DPI対応のためにウィンドウを拡大/縮小するとき、アプリ自身からスケーリングできるのは基本的に内側のクライアント領域の部分だけで、外側のクローム部分(枠部分)はOSのコントロール下にあり、Per-Monitor DPI対応を宣言したアプリのウィンドウをOSはスケーリングしないため、結果的にクローム部分だけスケーリングから取り残されるということが起きます。

これを避けるにはクローム部分もアプリ側でコントロールすればいいわけですが、WPFの場合はWindowChromeクラスを使うことで比較的容易にこれが実現できます。これについてはぐらばくさんが基本的な説明をされています。
あえて付け足すとすれば、CaptionHeightで設定される領域はダブルクリックによる最大化の機能も担っているので、0にするより設定した方が標準ウィンドウの機能を失わずに済みます。なお、CaptionHeightの起点とウィンドウ上端の間にはResizeBorderThickness.Topが挟まるので(0でなければ)、計算するときは注意を。

2. デモ


Per-Monitor DPI対応のためのライブラリに、ビヘイビアによるもの、添付プロパティによるものに加え、WindowChromeを使ったものを"ExtendedWindow"として加えて再構成しました。Per-Monitor DPI対応の機能は共通なので、リサイズ時の問題への対策もされています。
初期設定ではWindows 8の標準ウィンドウとほとんど同じ外観です。

ここでPer-Monitor DPIを変えると、クローム部分もスケーリングされます。

どうせなのでサンプルテーマも用意してみました。

大体の項目はプロパティを通しても設定できますが、キャプションボタンのマウスオーバー時とクリック時の色は直接ResourceDictionaryを作成してThemeUriに設定する以外の方法は用意していません。といっても、実際にアプリを作ろうとすると色々手を加えないといけないと思いますが。

ちなみに、デモアプリの"Rain"をクリックするとアニメーションが表示されます。

クローム部分(のように作ってある部分)もアプリ側からコントロールできることが分かると思います。

3. まとめ


というわけで、Per-Monitor DPIに完全対応を果たしつつ、ウィンドウの全てを自由にデザインできるので、アイデア次第で魅力的なUIを作り上げることができると思います。

2014/07/21

Per-Monitor DPIのビヘイビアによる実装

WPFでPer-Monitor DPI対応を実装する方法として、これを実装したWindowから継承するやり方を取りましたが、標準のWindowにPer-Monitor DPI対応を付加する方法もあります。

その一つとしてespresso3389さんが添付プロパティを使ったやり方を示されています。
これで基本的な用は足りるわけですが、ウィンドウ内の構成によってはDPIの値等を直接取得して処理したい場合もあります(例えば、画像をDPIに応じて切り替える)。

これを簡単に取得できるようにするにはどうすればいいか考えたところ、Per-Monitor DPI対応のためには要は対象Windowのインスタンスが参照できればいいので、ビヘイビアを使ったやり方があると思いついたところで、このやり方には先人がいたことを思い出しました。
これをどうせならXaml内で完結できるといいかなと思ったので、Expression Blendの(というより既に標準ライブラリと言っていいと思うSystem.Windows.Interactivityの)Behaviorとして実装を書いてみました。
これを使うとWindowのXamlで以下のような使い方ができます。
<Window x:Class="WpfPerMonitorDpiBehavior.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:local="clr-namespace:WpfPerMonitorDpiBehavior"
        Title="WPF Per-Monitor DPI Behavior"
        FontFamily="Segoe UI"
        Width="400" Height="200">
    <i:Interaction.Behaviors>
        <local:PerMonitorDpiBehavior x:Name="DpiBehavior"/>
    </i:Interaction.Behaviors>

    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid Margin="4">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <Label VerticalAlignment="Center"
                   Content="System DPI"/>
            <TextBox Grid.Column="1"
                     Width="80" Height="22" Margin="6,4"
                     Text="{Binding ElementName=DpiBehavior, Path=SystemDpi, Mode=OneWay}"/>

            <Label Grid.Row="1"
                   VerticalAlignment="Center"
                   Content="Per-Monitor DPI"/>
            <TextBox Grid.Column="1" Grid.Row="1"
                     Width="80" Height="22" Margin="6,4"
                     Text="{Binding ElementName=DpiBehavior, Path=WindowDpi, Mode=OneWay}"/>
        </Grid>
    </Grid>
</Window>
10-12行目でこのビヘイビアを指定し、35と42行目でビヘイビア内のプロパティをバインディングして表示するようにしています。

なお、ここでは簡単にするためウィンドウのリサイズにはWM_DPICHANGEDのlParamの値をそのまま使うようにしています。したがって、実用にするにはリサイズ時にサイズが狂う問題への対策が必要です。また、Per-Monitor DPI対応であることをOSに伝えるため、利用するアプリのアプリケーションマニフェストでWindows 8.1対応であること、Per-Monitor DPI対応であることを示す必要があります。

[追記] 添付プロパティによる実装

一応、同じことは添付プロパティでも可能なので、書いてみました。
クラスのインスタンス自身をプロパティとして持たせるという、よくあるパターンを添付プロパティでやったものです。WindowChromeクラスのWindowChrome添付プロパティが同じような感じです。

これでビヘイビアとよく似た使い方ができます。
<Window x:Class="WpfPerMonitorDpiProperty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfPerMonitorDpiProperty"
        Title="WPF Per-Monitor DPI Property"
        FontFamily="Segoe UI"
        Width="400" Height="200">
    <local:PerMonitorDpiProperty.AttachedProperty>        
        <local:PerMonitorDpiProperty x:Name="DpiProperty"/>
    </local:PerMonitorDpiProperty.AttachedProperty>

    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid Margin="4">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <Label VerticalAlignment="Center"
                   Content="System DPI"/>
            <TextBox Grid.Column="1"
                     Width="80" Height="22" Margin="6,4"
                     Text="{Binding ElementName=DpiProperty, Path=SystemDpi, Mode=OneWay}"/>

            <Label Grid.Row="1"
                   VerticalAlignment="Center"
                   Content="Per-Monitor DPI"/>
            <TextBox Grid.Column="1" Grid.Row="1"
                     Width="80" Height="22" Margin="6,4"
                     Text="{Binding ElementName=DpiProperty, Path=WindowDpi, Mode=OneWay}"/>
        </Grid>
    </Grid>
</Window>
こちらだとSystem.Windows.Interactivity.dllは当然不要です。

[追伸] Visual Studio 2013のデバッガーにおけるアプリケーションマニフェスト

Per-Monitor DPI対応であることをアプリケーションマニフェストで示したアプリをVS2013内のデバッガーで実行すると、Per-Monitor DPI対応にならない問題がUpdate 2まで発生していて(SetProcessDpiAwareness関数を使った場合は問題ない)、どうもデバッガーでアプリケーションマニフェストが反映されない既知の問題があったようです。この問題はUpdate 3 RCを試したところ発生しなかったので、Update 3で修正されたのだと思います。