2014/10/04

Windows 10 TPのバージョン判定

Windows 10 TP(Technical Preview)のバージョン判定について一応確認してみた。

1. 判定方法


内部バージョンが6.4というのは分かっているので、これをC#のコードから判定する方法として以下を試してみた。
.NET FrameworkのSystem.Environment.OSVersionは内部的にGetVersion/GetVersionExを呼び出しているようなので、これに準じる。

なお、GetVersion/GetVersionExについては、Windows 8.1と同様にマニフェストファイルでcompatibilityの記述が必要となる。

2. コード


それぞれのコードは以下のとおり。

GetVersionEx
public static Version GetOsVersionByGetVersionEx()
{
  var info = new OSVERSIONINFOEX();
  info.dwOSVersionInfoSize = (uint)Marshal.SizeOf(info);

  var result = GetVersionEx(ref info);

  return result
    ? new Version((int)info.dwMajorVersion, (int)info.dwMinorVersion)
    : null;
}
マニフェストファイルのcompatibilityにWindows 10のIDを含めている。
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
  <application>
    <!-- The ID below indicates application support for Windows 7 -->
    <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>

    <!-- The ID below indicates application support for Windows 8 -->
    <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>

    <!-- The ID below indicates application support for Windows 8.1 -->
    <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>

    <!-- The ID below indicates application support for Windows 10 -->
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
  </application>
</compatibility>
RtlGetVersion
public static Version GetOsVersionByRtlGetVersion()
{
  var info = new OSVERSIONINFOEX();
  info.dwOSVersionInfoSize = (uint)Marshal.SizeOf(info);

  var result = RtlGetVersion(ref info);

  return (result == 0) // STATUS_SUCCESS
    ? new Version((int)info.dwMajorVersion, (int)info.dwMinorVersion)
    : null;
}
NetWkstaGetInfo
public static Version GetOsVersionByNetWkstaGetInfo()
{
  IntPtr buff = IntPtr.Zero;

  try
  {
    var result = NetWkstaGetInfo(null, 100, out buff);

    if (result == 0) // NERR_Success
    {
      var info = (WKSTA_INFO_100)Marshal.PtrToStructure(buff, typeof(WKSTA_INFO_100));

      if (info.platform_id == 500) // PLATFORM_ID_NT
        return new Version((int)info.ver_major, (int)info.ver_minor);
    }
  }
  finally
  {
    if (buff != IntPtr.Zero)
      NetApiBufferFree(buff);
  }

  return null;
}
[修正]

IntPtrをNetApiBufferFreeで解放するよう修正した。

WMI
public static Version GetOsVersionByWmi()
{
  var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem");

  var os = searcher.Get().Cast<ManagementObject>().FirstOrDefault();

  if ((os != null) && (os["OsType"] != null) && (os["Version"] != null))
  {
    if (os["OsType"].ToString() == "18") // WINNT
      return new Version(os["Version"].ToString());
  }

  return null;
}
VerifyVersionInfo

引数majorとminorでバージョン番号を指定し、それとの比較をする形。
public static bool? IsOsEqualOrNewerByVerifyVersionInfo(int major, int minor)
{
  var info = new OSVERSIONINFOEX();
  info.dwMajorVersion = (uint)major;
  info.dwMinorVersion = (uint)minor;
  info.dwOSVersionInfoSize = (uint)Marshal.SizeOf(info);

  ulong cm = 0;
  cm = VerSetConditionMask(cm, VER_MAJORVERSION, VER_GREATER_EQUAL);
  cm = VerSetConditionMask(cm, VER_MINORVERSION, VER_GREATER_EQUAL);

  var result = VerifyVersionInfoW(ref info, VER_MAJORVERSION | VER_MINORVERSION, cm);

  if (result)
    return true;

  return (Marshal.GetLastWin32Error() == 1150) // ERROR_OLD_WIN_VERSION
    ? false
    : (bool?)null;
}
Win32の宣言を含むコード全体はレポジトリに置いた。

3. テスト結果


実機にインストールしたWindows 10 TP上でのテスト結果は予想通りで、どの方法でもバージョン判定はできている。

マニフェストファイル中のOSのIDをコメントアウトして変えてみると、

GetVersionExはサポートしている上限のOSのバージョン番号を返しているのが分かる。以上で確認終了。

4. 備考


MSDNのGetVersion/GetVersionExの説明にはWindows 8.1後のOSでは変更されるか利用できなくなる可能性があるのでVersion Helper APIを使うように書かれている。一方、C#の観点から言えば、Version Helper APIは要はC++のマクロだから使えないし、GetVersion/GetVersionExを使っているSystem.Environment.OSVersionはどうなるのかということになって、先行き不透明な状態にある。まあ様子を見て使える手の中から対応を考えるしかないと思う。

参考までに、Version Helper APIと同じ機能を.NET Framework用に書いた例。

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を作り上げることができると思います。