2012/12/29

High DPI Cursor Changer

High DPI Windows 8 Cursor Setで作成したカーソルファイルの導入と設定を自動化するツールとして、High DPI Cursor Changerを作成しました。Windows 8でDPIを200%以上に上げたときにカーソルの輪郭が汚くなる問題に対応するものです。

DPIを200%にしたときの見た目はこんな感じです。標準のカーソルに似せていますが、全てスクラッチで、個人的に気になった点は変えたりもしているので、微妙に違いがあります。

現状、DPIを200%以上にしている人は少ないかもしれませんが、おいおい需要が増えてくるものと予想して。

プロジェクトサイト: High DPI Cursor Changer 英語 / 日本語 at SourceForge.net
実行ファイル from SourceForge.net

2012/12/21

ReadyNASにSSD

大容量SSDの価格低下もそこそこ進んでいるので、先々はNASにもSSDを入れることを考えて、ReadyNAS Ultra 2にIntel X25-M G1を入れてみるテスト。

1. SMART

HDDと入れ替えると、HDDと同様にRAIDiatorがインストールされて(X-RAID2を指定)普通に起動。FrontViewからSSDの情報も見られる。
Intel X25-M in FrontView

温度は1C/33Fと出ているが、RAIDar Protocolで返ってくるのを見ると-1C/31Fで、食い違っていた。

ここからSMARTも一応表示される。
Intel X25-M SMART in FrontView

が、この中に温度はない。また、寿命に関係するAvailable Reserved SpaceとMedia Wearout Indicatorは項目には出ているが、正しい値は取れてない。

このSMARTはPCに接続してCrystalDiskInfo、SSD Toolbox、Smartmotoolsで見たときには、それぞれこんな感じ。
Intel X25-M SMART by CrystalDiskInfo
Intel X25-M SMART by SSD Toolbox
Intel X25-M SMART by Smartmontools

温度

先に温度について考えると、一見して明らかなように、このSSDはSMARTのデータに温度(IDは16進数でC2、10進数で194)を含んでいない。SSDの中には0Cと返すものもあるようだが、このSSDはそもそも温度のデータを返さない。

したがって、ReadyNASが示す1C/33Fあるいは-1C/31Fはダミーの数字で、それもたぶん-1C/31Fの方が本来の数字で(コードの世界では該当するものが存在しないときに-1を返したりする)、その摂氏のマイナス符号を認識し損ねて1Cと取った上で華氏をそれに合わせて計算したのが1C/33Fではないか、という気がする。

寿命

意外だったのはReadyNASがIntelのSSDに特有のAvailable Reserved Space(E8)とMedia Wearout Indicator(E9)を認識したことで、このIDが示すもののデータを持っていなければこの名前は出てこないはず……。

と、ReadyNASではたぶん字数を減らすために「e」を抜いて「Available Reservd Space」となっているが、上にある「Reallocated Sector Count」は字数が多いにもかかわらずそのままで、少し不自然。一方、この「Available Reservd Space」はSmartmontoolsの表記と同じで、「Intel Internal」も同じ。ということから、ReadyNASは標準的な項目以外はSmartmontoolsと同じデータを利用しているのではないか、という推測を立て得る。

それはともかくとして、値の方は出してくるものを間違えていて、意味を成してないわけだが。この当たり、各ベンダー独自の仕様に合わせてデータを処理するようにしないと意味のある数字は拾い出せない、という当然のことを示している。

2. 評価

NASの動作としてはごく普通で、スピンダウン時でもスピンアップを待つことなくすぐにアクセスできる(SSDの場合にスピンダウンが実際にどう動いているのかは未確認)。

という意味で普通に使う分には問題なさそうだが、SMARTの監視は実質的にできず、各ベンダーの仕様がばらばらである限り、NAS側の対応もなかなか進まないような気がするので(add-onでも出てくれば別だが)、SSDの状態が気になるなら時々外してPCに接続してチェックするしかないと思う。

[追記1]

そういえばSmartmontoolsはたいていのLinuxディストリビューションに含まれている……ことを思い出して、ReadyNASのSSHアクセスを有効にしてログインすると、しっかりRAIDiatorにも入っていた。

これでSMARTを見てみると、PCとの接続時と同じように表示される。
Intel X25-M SMART by Smartmontools on RAIDiator (Linux) through SSH

したがって、ReadyNASに入れたままでも、これで定期的にチェックするようにすればSMARTの値を監視できることになる。

以上、後はしいて言えばTrimがどうなるかという点を除けば、ReadyNASでSSDを使う上での障害は実質的になさそうということが分かった。

[追記2]

WindowsのクライアントPCからSMARTの値をチェックするためのスクリプトを書いてみた。PuTTYを使ってSSHアクセスし、smartctlを実行して、結果からSMARTの目的の項目を拾い出してCSV形式で保存する。

手順としては、
  1. ReadyNASに「Enable Root SSH Access」Add-onをインストールしてSSHアクセスを可能にする。
  2. PuTTYをダウンロードして適当なフォルダーに置く。使うのはコマンドライン用のPLINK.EXE。
  3. 以下の内容のバッチファイルを作成して、ファイル名はここでは「smartctl.bat」として同じフォルダーに保存する。
    plink nas-XX-XX-XX -l root -pw netgear1 ^
     "smartctl -A -f brief /dev/sda" > result.txt
    
    • nas-XX-XX-XXはReadyNASのホスト名かIPアドレス
    • -pwの後ろは管理者パスワード
    • /dev/sdaは1番目のディスクの意味

  4. 以下の内容のVBScriptを作成して、ファイル名はここでは「checksmart.vbs」として同じフォルダーに保存する。ATTRIBUTESで目的の項目を指定。
    Option Explicit
    
    Dim FILE_BAT 'Batch file to execute smartctl through SSH
    Dim FILE_RLT 'File to store result of smartctl temporarily
    Dim FILE_RCD 'File to record (append) result of smartctl
    FILE_BAT = "smartctl.bat"
    FILE_RLT = "result.txt" 'Must be the same file in batch file
    FILE_RCD = "record.csv"
    
    Dim ATTRIBUTES 'SMART attributes (separated by space)
    ATTRIBUTES = "Available_Reservd_Space Media_Wearout_Indicator"
    
    'Execute smartctl through SSH
    Dim objShell
    Set objShell = WScript.CreateObject("WScript.Shell")
    Dim result 'Return value to avoid error in using Run method
    result = objShell.Run(FILE_BAT, 0, True)
    Set objShell = Nothing
    
    'Process result of smartctl
    Dim strBuf
    
    Dim objFSO
    Dim objFile
    Set objFSO = WScript.CreateObject("Scripting.FileSystemObject")
    Set objFile = objFSO.OpenTextFile(FILE_RLT)
    strBuf = objFile.ReadAll
    Set objFile = Nothing
    Set objFSO = Nothing
    
    Dim strLines
    strLines = Split(strBuf, vbLf)
    
    Dim strSer 'Array to hold names of attributes
    strSer = Split(ATTRIBUTES)
    
    Dim strVal 'Array to hold values of attributes
    ReDim strVal(UBound(strSer))
    
    Dim objRegExp
    Set objRegExp = New RegExp
    objRegExp.Pattern = " \d{3} " 'Pattern of value of attributes
    objRegExp.IgnoreCase = True
    objRegExp.Global = True
    Dim i
    Dim j
    For i = 0 To UBound(strLines)
        For j = 0 To UBound(strSer)
            If 0 < InStr(1, strLines(i), strSer(j), 1) Then
                Dim objMatches
                Set objMatches = objRegExp.Execute(strLines(i))
                If 0 < objMatches.Count Then
                    strVal(j) = objMatches(0).Value
                End If
                Set objMatches = Nothing
            End If
        Next
    Next
    Set objRegExp = Nothing
    
    'Record result of smartctl
    Dim strRec
    strRec = Now() & ","
    For j = 0 To UBound(strVal)
        strRec = strRec & Trim(strVal(j)) & ","
    Next
    
    Set objFSO = WScript.CreateObject("Scripting.FileSystemObject")
    If Not objFSO.FileExists(FILE_RCD) Then
        'If first record, add header line
        strRec = "Date," & Join(strSer, ",") & "," & vbCrLf & strRec
    End If
    Set objFile = objFSO.OpenTextFile(FILE_RCD, 8, True)
    objFile.Write(strRec & vbCrLf)
    Set objFile = Nothing
    Set objFSO = Nothing
    
    (smartctl.batの内容をFILE_BATの部分に直接書いても良さそうなものだが、そうするとなぜかrecord.txtがホスト側に作成されてしまうので、分けた。)
これで、このchecksmart.vbsを定期的に実行するように設定すれば、結果がCSVファイルに記録されていく。

2012/11/14

AppUserModelIDをC#から操作する

Windows 7以降で利用されるAppUserModelIDを含んだショートカットをC#から操作するコードを書いてみた。

1. 背景

Windows 8のWindowsストアアプリでは常駐型アプリは難しいので、常駐型の監視アプリを使おうとする場合、(x86版であれば)デスクトップアプリを動かしておいて何かあればWindowsストアアプリの画面にトースト通知を出すのが一つの解になると思うが、このトースト通知をデスクトップアプリから送るにはAppUserModelIDを含んだショートカットがスタートメニューにあることが条件になっている。

このAppUserModelIDは普通に張ったショートカットには含まれないもので、これを含むショートカットの作成はアプリのインストーラにやらせることをMicrosoftは推奨している。が、そうは言ってもインストーラの作ったショートカットをユーザーが消してしまう可能性もあるので確実性に欠けるし、常にインストーラを必要とするのはあまり便利ではない。

一方で、AppUserModelIDを含んだショートカットを読み書きする方法は.NET Frameworkでは提供されておらず、ショートカットでよく使われるWSHのWshShortcutオブジェクトでもAppUserModelIDは対象に入っていない。一応C++での方法は示されているので、であればと、これに沿ってC#からWin32とCOMを使って操作するコード(ラッパークラス)を書いた次第。

2. コーディング

C#あるいはVBからCOMのIShellLinkインターフェイスを使ってショートカットを読み書きする例は既に幾つかあって、これをベースにさせていただいた。
また、C++でAppUserModelIDを読み書きする例は以下のとおり。
方針としては、IShellLinkを使う例を基本的に踏襲しつつ(IPersistFileインターフェイスはSystem.Runtime.InteropServices.ComTypes.IPersistFileの方を使う)、IShellLinkはAppUserModelIDに対応してないので、さらに下のIPropertyStoreインターフェイスを使って直にプロパティをいじるというもの。

ただ、プロパティの値を格納するPROPVARIANT構造体が複雑で、これに難儀していたところ、そもそものデスクトップアプリからトースト通知を送るサンプルに(正確には、その利用するWindows API Code Packに)答えがあった。
これにあるPROPVARIANTを扱うクラスは長いものだが、出し入れする値の内容を文字列に限定してしまえば(AppUserModelIDは文字列)、さほどでもない。

IPropertyStoreに必要なコードを整理すると、まずIPropertyStore自体は以下のとおり。
// IPropertyStore Interface
[ComImport,
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
    uint GetCount([Out] out uint cProps);
    uint GetAt([In] uint iProp, out PropertyKey pkey);
    uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
    uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
    uint Commit();
}
この中でプロパティのキーを格納するPropertyKey構造体。
// PropertyKey Structure
// Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1 
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct PropertyKey
{
    #region Fields

    private Guid formatId;    // Unique GUID for property
    private Int32 propertyId; // Property identifier (PID)

    #endregion

    #region Public Properties

    public Guid FormatId
    {
        get
        {
            return formatId;
        }
    }

    public Int32 PropertyId
    {
        get
        {
            return propertyId;
        }
    }

    #endregion

    #region Constructor

    public PropertyKey(Guid formatId, Int32 propertyId)
    {
        this.formatId = formatId;
        this.propertyId = propertyId;
    }

    public PropertyKey(string formatId, Int32 propertyId)
    {
        this.formatId = new Guid(formatId);
        this.propertyId = propertyId;
    }

    #endregion
}
で、プロパティの値を格納するPropVariantクラス(とそれに必要な関数)。
// PropVariant Class (only for string value)
// Narrowed down from PropVariant.cs of Windows API Code Pack 1.1
// Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11
// /interop-with-propvariants-in-net.aspx
[StructLayout(LayoutKind.Explicit)]
private sealed class PropVariant : IDisposable
{
    #region Fields

    [FieldOffset(0)]
    ushort valueType;     // Value type 

    // [FieldOffset(2)]
    // ushort wReserved1; // Reserved field
    // [FieldOffset(4)]
    // ushort wReserved2; // Reserved field
    // [FieldOffset(6)]
    // ushort wReserved3; // Reserved field

    [FieldOffset(8)]
    IntPtr ptr;           // Value

    #endregion

    #region Public Properties

    // Value type (System.Runtime.InteropServices.VarEnum)
    public VarEnum VarType
    {
        get { return (VarEnum)valueType; }
        set { valueType = (ushort)value; }
    }

    // Whether value is empty or null
    public bool IsNullOrEmpty
    {
        get
        {
            return (valueType == (ushort)VarEnum.VT_EMPTY ||
                    valueType == (ushort)VarEnum.VT_NULL);
        }
    }

    // Value (only for string value)
    public string Value
    {
        get
        {
            return Marshal.PtrToStringUni(ptr);
        }
    }

    #endregion

    #region Constructor

    public PropVariant()
    { }

    // Construct with string value
    public PropVariant(string value)
    {
        if (value == null)
            throw new ArgumentException("Failed to set value.");

        valueType = (ushort)VarEnum.VT_LPWSTR;
        ptr = Marshal.StringToCoTaskMemUni(value);
    }

    #endregion

    #region Destructor

    ~PropVariant()
    {
        Dispose();
    }

    public void Dispose()
    {
        PropVariantClear(this);
        GC.SuppressFinalize(this);
    }

    #endregion
}

[DllImport("Ole32.dll", PreserveSig = false)]
private extern static void PropVariantClear([In, Out] PropVariant pvar);
また、AppUserModelIDのキーはFormatIDとPropIDからPropertyKeyを使って以下のように定義できる。
// Name = System.AppUserModel.ID
// ShellPKey = PKEY_AppUserModel_ID
// FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
// PropID = 5
// Type = String (VT_LPWSTR)
private readonly PropertyKey AppUserModelIDKey = 
    new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);
これらを使ってAppUserModelIDをShellLinkクラスのプロパティとして加えた。なお、VerifySucceededは戻り値を見て失敗なら例外を出すメソッド。ショートカットにAppUserModelIDのプロパティが存在しない場合、Valueはnullとなる。
// AppUserModelID to be used for Windows 7 or later.
public string AppUserModelID
{
    get
    {
        using (PropVariant pv = new PropVariant())
        {
            VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv));

            if (pv.Value == null)
                return "Null";
            else
                return pv.Value;
        }
    }
    set
    {
        using (PropVariant pv = new PropVariant(value))
        {
            VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv));
            VerifySucceeded(PropertyStore.Commit());
        }
    }
}
このShellLinkクラスを使うアプリも合わせた全体のコードはこちらに載せた。なお、IShellLink自体の機能は本題ではないので、最低限必要なものに絞ってある。

アプリの見た目は以下のようなもので、ショートカット先のターゲットファイルのパス、実行時のオプション、AppUserModelIDの読み書きが可能。

実際に使うには作り込みの必要があると思うが、叩き台としてはこんなものかと。

2012/11/06

SSDかどうかをC#から判別する

ある物理的なドライブがSSDかどうかを判別する方法は色々あり得るが、Microsoftが示したアルゴリズム(Windows 7 Disk Defragmenter User Interface Overview)にしたがってNyaRuRuさんがC++のコードを書かれていたので(SSDなら動作を変えるアプリケーションを作る)、同じことをC#からWin32を使って行うコードを書いてみた。

内容的にはほぼNyaRuRuさんのコードのままで、以下の方法の2本立て。
  • Windows 7以降で使えて、管理者権限が不要な「no seek penalty」を利用する。
  • それ以前のOSでも使えるが、管理者権限が必要な「nominal media rotation rate」を利用する。
まずWin32の宣言は以下のとおり。使用するWin32の関数はCreateFileとDeviceIoControlで、後はそれに必要な定数と構造体。
// For CreateFile to get handle to drive
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;

// CreateFile to get handle to drive
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
    [MarshalAs(UnmanagedType.LPWStr)]
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    IntPtr lpSecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    IntPtr hTemplateFile);

// For control codes
private const uint FILE_DEVICE_MASS_STORAGE = 0x0000002d;
private const uint IOCTL_STORAGE_BASE = FILE_DEVICE_MASS_STORAGE;
private const uint FILE_DEVICE_CONTROLLER = 0x00000004;
private const uint IOCTL_SCSI_BASE = FILE_DEVICE_CONTROLLER;
private const uint METHOD_BUFFERED = 0;
private const uint FILE_ANY_ACCESS = 0;
private const uint FILE_READ_ACCESS = 0x00000001;
private const uint FILE_WRITE_ACCESS = 0x00000002;

private static uint CTL_CODE(uint DeviceType, uint Function,
     uint Method, uint Access)
{
    return ((DeviceType << 16) | (Access << 14) |
    (Function << 2) | Method);
}

// For DeviceIoControl to check no seek penalty
private const uint StorageDeviceSeekPenaltyProperty = 7;
private const uint PropertyStandardQuery = 0;

[StructLayout(LayoutKind.Sequential)]
private struct STORAGE_PROPERTY_QUERY
{
    public uint PropertyId;
    public uint QueryType;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public byte[] AdditionalParameters;
}

[StructLayout(LayoutKind.Sequential)]
private struct DEVICE_SEEK_PENALTY_DESCRIPTOR
{
    public uint Version;
    public uint Size;
    [MarshalAs(UnmanagedType.U1)]
    public bool IncursSeekPenalty;
}

// DeviceIoControl to check no seek penalty
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
   SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    ref STORAGE_PROPERTY_QUERY lpInBuffer,
    uint nInBufferSize,
    ref DEVICE_SEEK_PENALTY_DESCRIPTOR lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);

// For DeviceIoControl to check nominal media rotation rate
private const uint ATA_FLAGS_DATA_IN = 0x02;

[StructLayout(LayoutKind.Sequential)]
private struct ATA_PASS_THROUGH_EX
{
    public ushort Length;
    public ushort AtaFlags;
    public byte PathId;
    public byte TargetId;
    public byte Lun;
    public byte ReservedAsUchar;
    public uint DataTransferLength;
    public uint TimeOutValue;
    public uint ReservedAsUlong;
    public IntPtr DataBufferOffset;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] PreviousTaskFile;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] CurrentTaskFile;
}

[StructLayout(LayoutKind.Sequential)]
private struct ATAIdentifyDeviceQuery
{
    public ATA_PASS_THROUGH_EX header;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
    public ushort[] data;
}

// DeviceIoControl to check nominal media rotation rate
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
   SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    ref ATAIdentifyDeviceQuery lpInBuffer,
    uint nInBufferSize,
    ref ATAIdentifyDeviceQuery lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);
これを受けた1つ目のメソッドが「no seek penalty」を利用する「HasNoSeekPenalty」で、物理ドライブ名を「\\\\.\\PhysicalDrive0」の形にした文字列を引数に取る。QA@ITで質問させていただいたもの(SSDかどうかをC#から判別する方法)と基本的に同じ。あまり似た例がなかったので、ほぼスクラッチ。
// Method for no seek penalty
private static void HasNoSeekPenalty(string sDrive)
{
    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        0, // No access to drive
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_STORAGE_QUERY_PROPERTY = CTL_CODE(
        IOCTL_STORAGE_BASE, 0x500,
        METHOD_BUFFERED, FILE_ANY_ACCESS); // From winioctl.h

    STORAGE_PROPERTY_QUERY query_seek_penalty =
        new STORAGE_PROPERTY_QUERY();
    query_seek_penalty.PropertyId = StorageDeviceSeekPenaltyProperty;
    query_seek_penalty.QueryType = PropertyStandardQuery;

    DEVICE_SEEK_PENALTY_DESCRIPTOR query_seek_penalty_desc =
        new DEVICE_SEEK_PENALTY_DESCRIPTOR();

    uint returned_query_seek_penalty_size;

    bool query_seek_penalty_result = DeviceIoControl(
        hDrive,
        IOCTL_STORAGE_QUERY_PROPERTY,
        ref query_seek_penalty,
        (uint)Marshal.SizeOf(query_seek_penalty),
        ref query_seek_penalty_desc,
        (uint)Marshal.SizeOf(query_seek_penalty_desc),
        out returned_query_seek_penalty_size,
        IntPtr.Zero);

    hDrive.Close();

    if (query_seek_penalty_result == false)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        if (query_seek_penalty_desc.IncursSeekPenalty == false)
        {
            Console.WriteLine("This drive has NO SEEK penalty.");
        }
        else
        {
            Console.WriteLine("This drive has SEEK penalty.");
        }
    }
}
(注)GetErrorMessageはエラーメッセージを得るためのメソッド。以下同じ。

2つ目のメソッドが「nominal media rotation rate」を利用する「HasNominalMediaRotationRate」で、引数は同じ。同じようにしてドライブの情報を取得する例は結構あったので(例えば、Getting Hard disk drive info with DeviceIOControl)、かなり参考にさせていただいた。
// Method for nominal media rotation rate
// (Administrative privilege is required)
private static void HasNominalMediaRotationRate(string sDrive)
{
    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        GENERIC_READ | GENERIC_WRITE, // Administrative privilege is required
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_ATA_PASS_THROUGH = CTL_CODE(
        IOCTL_SCSI_BASE, 0x040b, METHOD_BUFFERED,
        FILE_READ_ACCESS | FILE_WRITE_ACCESS); // From ntddscsi.h

    ATAIdentifyDeviceQuery id_query = new ATAIdentifyDeviceQuery();
    id_query.data = new ushort[256];

    id_query.header.Length = (ushort)Marshal.SizeOf(id_query.header);
    id_query.header.AtaFlags = (ushort)ATA_FLAGS_DATA_IN;
    id_query.header.DataTransferLength =
        (uint)(id_query.data.Length * 2); // Size of "data" in bytes
    id_query.header.TimeOutValue = 3; // Sec
    id_query.header.DataBufferOffset = (IntPtr)Marshal.OffsetOf(
        typeof(ATAIdentifyDeviceQuery), "data");
    id_query.header.PreviousTaskFile = new byte[8];
    id_query.header.CurrentTaskFile = new byte[8];
    id_query.header.CurrentTaskFile[6] = 0xec; // ATA IDENTIFY DEVICE

    uint retval_size;

    bool result = DeviceIoControl(
        hDrive,
        IOCTL_ATA_PASS_THROUGH,
        ref id_query,
        (uint)Marshal.SizeOf(id_query),
        ref id_query,
        (uint)Marshal.SizeOf(id_query),
        out retval_size,
        IntPtr.Zero);

    hDrive.Close();

    if (result == false)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        // Word index of nominal media rotation rate
        // (1 means non-rotate device)
        const int kNominalMediaRotRateWordIndex = 217;

        if (id_query.data[kNominalMediaRotRateWordIndex] == 1)
        {
            Console.WriteLine("This drive is NON-ROTATE device.");
        }
        else
        {
            Console.WriteLine("This drive is ROTATE device.");
        }
    }
}
全体を通したコードはこちらに載せた。 Visual Studio 2012で.NET Framework 4.0を対象として作成し、ThinkPad X61sにインストールしたWindows 8 ProとWindows 7上で、SSD(Intel X25-M G2)とHDDを判別できることを確認している。 まあC++のライブラリを作ってそれを利用する形でもいいが、.NETから直接使える方法があってもいいかな、ということで。

[追記] 物理ドライブ名の取得

論理ドライブ名から物理ドライブ名を取得するコードも書いてみた。WMIを使う方法もあるが、折角なのでWin32で。 まずWin32の宣言。
// For CreateFile to get handle to drive
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;

// CreateFile to get handle to drive
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
    [MarshalAs(UnmanagedType.LPWStr)]
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    IntPtr lpSecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    IntPtr hTemplateFile);

// For control codes
private const uint IOCTL_VOLUME_BASE = 0x00000056;
private const uint METHOD_BUFFERED = 0;
private const uint FILE_ANY_ACCESS = 0;

private static uint CTL_CODE(uint DeviceType, uint Function,
                             uint Method, uint Access)
{
    return ((DeviceType << 16) | (Access << 14) |
            (Function << 2) | Method);
}

// For DeviceIoControl to get disk extents
[StructLayout(LayoutKind.Sequential)]
private struct DISK_EXTENT
{
    public uint DiskNumber;
    public long StartingOffset;
    public long ExtentLength;
}

[StructLayout(LayoutKind.Sequential)]
private struct VOLUME_DISK_EXTENTS
{
    public uint NumberOfDiskExtents;
    [MarshalAs(UnmanagedType.ByValArray)]
    public DISK_EXTENT[] Extents;
}

// DeviceIoControl to get disk extents
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
           SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    IntPtr lpInBuffer,
    uint nInBufferSize,
    ref VOLUME_DISK_EXTENTS lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);
これを受けたメソッド。論理ドライブ名の文字を引数とし、始めにリムーバブルディスクなどではない固定ディスクかどうかのチェックを通す。
// Method for disk extents
private static void GetDiskExtents(char cDrive)
{
    DriveInfo di = new DriveInfo(cDrive.ToString());
    if (di.DriveType != DriveType.Fixed)
    {
        Console.WriteLine("This drive is not fixed drive.");
    }

    string sDrive = "\\\\.\\" + cDrive.ToString() + ":";

    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        0, // No access to drive
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS = CTL_CODE(
        IOCTL_VOLUME_BASE, 0,
        METHOD_BUFFERED, FILE_ANY_ACCESS); // From winioctl.h

    VOLUME_DISK_EXTENTS query_disk_extents =
        new VOLUME_DISK_EXTENTS();

    uint returned_query_disk_extents_size;

    bool query_disk_extents_result = DeviceIoControl(
        hDrive,
        IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
        IntPtr.Zero,
        0,
        ref query_disk_extents,
        (uint)Marshal.SizeOf(query_disk_extents),
        out returned_query_disk_extents_size,
        IntPtr.Zero);

    hDrive.Close();

    if (query_disk_extents_result == false ||
        query_disk_extents.Extents.Length != 1)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        Console.WriteLine("The physical drive number is: " +
                          query_disk_extents.Extents[0].DiskNumber);
    }
}
[修正] SafeFileHandleの解放

ドライブへのハンドルの解放にCloseHandle関数を使っていたが、SafeFileHandleにCloseHandleを使うと、その後にガベージコレクタがメモリを解放するときにエラーを起こすらしいと気づいたので、SafeHandle.Closeメソッドを使うように修正した。

この問題は外部コンポーネントで不規則に起こるので、なかなか原因を掴めなかったが、Stack Overflowにこれに触れたコメントがあった(How to resolve SEHException, VB .NET 2010)。MSDNなどでソースは見つけられなかったが、問題の状況によく合うし、修正してからは起こらなくなったので、当たっていたのだと思う。

2012/10/31

CrystalDiskMarkとTrim

SSDの速度を測る場合、その前の使い方(とくにライト)が影響するのはよく知られていて、Secure EraseやTrimでクリーンな状態にした後、ライトを繰り返して乱れた状態が一定以上に達すると速度低下が起こったりする。

そうした影響をなるべく防ぐための工夫は色々あり得るが、理由あって同じSSDを連続して計測する場合、その間にTrimをかけるのはどうかというアイデアを持っていた。で、最近はTrimもすっかり一般化したことだし、CrystalDiskMarkを使って試してみた(結果的には不発)。

1. 計測方法

CrystalDiskMarkを連続して実行し、その実行間にTrimをかける場合とかけない場合で差が出るかを見る。

環境
  • PC: ThinkPad X61s
  • SSD: Intel X25-M G2 80GB(OSなどで全容量の60%が埋まった状態)
  • OS: Windows 8 Pro 64bit(ドライバーはすべてOSの自動インストール)
方法
  • CrystalDiskMark(3.0.2)を4GBで空のNTFSのパーティションに対し、テスト種類All、テスト回数9、テストサイズ2000MBの設定で30回実行。
  • 実行間には1分の間隔を挟み、Trimをかける場合は前回の実行直後にこのパーティションを対象に実行(先に示した方法で)。
  • どちらの場合も最初の実行前にTrimをかける。
  • ライト量の確認のため、一連の実行の直前と直後にCrystalDiskInfo(5.1.0 RC2)でHost Writes(総書き込み量)を取得する。Host Writesはあくまでホスト側(PC本体側)から見たライト量なので、SSD内部のライト量には直結しないが、参考にはなる。なお、この間は他のライトを伴う作業はしない(OSが勝手に行うものは除く)。
SSDのX25-Mには今更感があるが、これまでの実績から見て余裕を大幅に残したまま引退となるのは確実なので、多少消耗させてもよしという事情も背景にあり。

また、こういう計測をきっちり進めていくのは結構面倒なものだが、そこはDiskMarkStreamで楽々(自画自賛)、というよりDiskMarkStreamがあったからやる気になったというべきか。

2. 計測結果

先にどれぐらいのライト量になったかを確認すると、まずTrimをかけなかった場合の実行前と実行後のCrystalDiskInfoの結果。

次にTrimをかけた場合の実行前と実行後。
(注)E9が96から95に落ちているが、96になったのはかなり以前のことなので、意味はない。

一番最初のHost Writesの生の値は76752で、この16進数を10進数に換算すると485202になる。この値は65536セクタ、すなわち65536×512÷1024^2=32(MiB)ごとに1増えていくので、容量としては以下のようになる。

485202×32=15526464(MiB)≒14.807(TiB)

同様にそれぞれの値を計算し、実行前と実行後の差から増加量を求めると、
生の値
換算後
増加量
16進数
10進数
(MiB)
(TiB)
(MiB)
(GiB)
Trimをかけなかった場合
実行前767524852021552646414.807984320961.25
実行後7DF7A5159621651078415.746
Trimをかけた場合
実行前7E15F5164471652630415.761978496955.56
実行後858D15470251750480016.694

これらの増加量はそれぞれ9×30=270回分のテストによるものなので、1回のテストの増加量は約3.54~3.56GiBとなるが、これはテストサイズが2GBということを考えるとこんなものかと思う。どちらも全増加量は1TiB近くに上っている。

ここで、X25-M G2で速度低下を起こした後にTrimで回復した例(SSDの性能低下とTrimの効き具合を大検証)を見ると、一旦容量の90%までライトで埋めている。

方法は違うが、この全増加量は全容量80GBの12倍に当たるので、変化を起こさせるに十分なライト量ではないかと思う。というか思った、が……。

結論から言うと、CrystalDiskMarkの結果は不発だった。

まずTrimをかけなかった場合、30回の実行中、ばらつきは多少あるが、有意な変化といえるものはなかった。以下は1回目、15回目、30回目のもの。

一応、次にTrimをかけた場合、同様に以下。

こちらも有意な変化はない(Trimが効果を発揮していれば変化はなくて問題ないわけだが)。

3. まとめ

1TiB近くのライトをもってして速度低下を起こせなかったのは誤算だったが、CrystalDiskMarkを連続実行しても再度Trimが必要になるような速度低下は起こらないことが確認できたので(X25-M限定でだが)、それはそれでよしとする。

しかし……、この結果を見るに、実使用でもTrimを頻繁にかける必要はない気がしてきた。1TiBなんて普通に使う限り数箇月たっても到達しない量だと思うし。

2012/10/27

CrystalDiskMarkと雫ちゃんと

Windows 8発売の26日(というか25日深夜)にCrystalDiskMarkにも3.0.2で
Shizuku Editionが登場したので、早速、当然にDiskMarkStreamで対応。

通常版との違い、サイズなどは自動で判別する。

しかし、この麗しい姿といい、青白基調の流麗なデザインといい、これを目にすると、殺伐としたベンチマークの世界(ついあれもこれもとデータを取ろうとして、環境を整えて計測しているうちに消耗戦になってきて、あまり美しい状況にはなり難い)に涼風が吹き込まれる、というより、まとめて毒気を飛ばされてしまう危険性がある……。

その意味では機械的なマクロツールであるDiskMarkStreamはそぐわない気もするが、多少矛盾をはらんでいた方が面白い、ということにしておこう。

なお、通常版のテーマのShizukuでもエッセンスは共通。

ちなみに、Windows 8ではSSDのドライブに対してコマンドプロンプトから簡単にTrimを発行できるので(Defragの/Lオプション)、DiskMarkStreamのバッチ実行機能を使えばテスト間にTrimをすることができる。


このためのバッチファイルの内容は以下のようなもの。対象のドライブはDで、結果は後から確認するためにresult.txtというファイルに出力している。
C:\Windows\System32\Defrag.exe D: /L >> C:\Work\result.txt

ただし、Windows 8が64bit版の場合、このパスではDefrag.exeがDiskMarkStreamから見えない問題がある。これは32bitのソフト(DiskMarkStreamは32bit)を64bitのWindows上で実行するときに起こり得る問題で、この場合は以下のようになる。
C:\Windows\Sysnative\Defrag.exe D: /L >> C:\Work\result.txt


どちらの場合もDefragは管理者として実行する必要があるので、DiskMarkStreamも起動時に管理者として実行する必要がある(DiskMarkStream自体は管理者として実行される必要はないが、バッチ実行を管理者として行うトリガーとし、かつバッチ実行の度にUACの承認を求められるのを避けるため)。

[追記]

Windows 8におけるTrimについて、Microsoftの担当者(Kiran Bangalore)が答えていた(Defragging SSDs a default?)。Windows 8では(Windows 7と同様に)ファイルの削除、移動の際にTrimを発行しているが、SSDの方がリアルタイムに処理できない場合を考えて、「ディスクの最適化」で定期的にTrimを発行するようになっている(これがデフォルト)とのこと。

2012/08/31

Windows To Goのまとめ

Windows To Goの最終的な姿がWindows 8 RTMとともに明らかになったので、まとめておこうと思う。

1. 条件

使用条件は以下のとおり。狭き門となっている。

1.1. 対象ユーザー

ソフトウェアアシュアランス(SA)プログラムに入った、ボリュームライセンスのユーザー向けのEnterprise版のみ。つまり企業ユーザーの下でその社員が使う場合のみで、個人ユーザーには提供されない。


1.2. ハードウェア

PC本体側の条件はWindows 7かWindows 8の動作条件を満たしていればいいので問題にはならないが、USBメモリというか、USBドライブ側の条件は厳しい。

8月15日現在、MicrosoftがWindows To Goが使えると認定した(certified)USBドライブは、以下の2機種のみ。

いずれも一般的なUSBメモリとは違って、内部的にはSSDにUSBインターフェイスチップを付けてUSB接続にした製品で、ランダムライトが高速なのが特長だが、それ以上に門を狭くしているのはPCに接続した際にリムーバブルディスクではなく、ローカルディスクとして認識されなければならないという点。

RPまでのWindows To Goではリムーバブルディスクの場合はWindows Updateが不可という問題があったが、結局リムーバブルディスクは対象外と整理されたことになる。いずれにせよ、USBメモリでローカルディスクと認識される製品は例外的なので、ほとんどのUSBメモリは対象から外れることになる。

一方、SSDをUSBケースに収めたUSB SSDであるところのUSBドライブでも、ローカルディスクと認識されるものなら可なので(サポート外だが)、その手を厭わなければこの条件をクリアするのは難しくはない。

なお、DataTraveler Ultimateについては、既存のG1のコントローラはJMicron、G2はPhisonと判明しているが、このWindows To Go用のDataTraveler WorkspaceはLSI、つまりSandForceのようなので、中身は別物らしい。

[追記1] DataTraveler Workspaceのデモ

IDFでKingstonからDataTraveler Workspaceのデモがあった。コントローラはやはりSandForce。筐体はUltimateからデザインが変更されているが、丸く寸胴な基本形状は変わっていない。



[追記2] DataTraveler Workspaceの中身

The SSD ReviewによるDataTraveler Workspaceの記事(Kingston Data Traveler Workspace Windows To Go Flash Drive Review)によると、DataTraveler Ultimateと同様にPCBを2枚重ねにした構造で、コントローラはSandForceのSF-2241。チップと筐体との間に熱伝導シートがべったり挟まれている当たり、発熱はそれなりにありそうではある。性能的には期待どおりといったところ。

[追記3] Express RC8との比較

同じThe SSD ReviewによるExpress RC8の記事(Super Talent USB3 Express RC8 100GB Flash Drive Review)によると、コントローラはSF-1222で古いにもかかわらず、同じ環境でのCrystalDiskMarkなどベンチマークの結果はDataTraveler Workspaceより上だったりして、少し意外なことになっている。

2. Express RC8

この2機種のうちDataTraveler Ultimateは限定販売なので、普通に入手できるのはExpress RC8のみとなる。このExpress RC8は1年ぐらい前から存在する製品だが、流通が限られていて影の薄い存在だったところで、いきなり抜擢された感がある。

ということで、この機会に25GBモデルを買ってみた。

SunDiskのCruzer Titanium、Green HouseのPicoDrive F3と並べてみたところ。

厚みは同じぐらいだが、面積は二回りは大きい。日本での取扱元となるらしいアーキサイトのページを見ると、この中にSandForceのSF-1222が入っている。

初接続時にCrystalDiskInfo(5.0.3)で見たところ。使用歴は検査時のものか。

ThinkPad X61sのUSB2.0ポートに差した場合の性能をCrystalDiskMark(3.0.2 Beta)で確認した。テストサイズは1000MB。シーケンシャルアクセスはPC側にボトルネックがあるので、注目するのはランダムアクセス。

やはりランダムライトは別世界の速さ。比較のためにMSD6000をUSBケースに入れた場合が以下。

MSD6000でもWindows To Goは何の支障もなく動作することが分かっているので、Express RC8の性能は十分以上ということが分かる。

性能とは関係ないが、例によって青色LEDが眩しいのは何だかな。

[追記] USB3.0との関係

自分がUSB2.0ポートで試してきた経験から断言するが、Windows To Goの動作の軽さとUSB2.0かUSB3.0かは関係ない。関係があるのは主にランダムライトである。

ここで問題はランダムライトの高速なUSBメモリがごく限られていることで、世代が進むにつれてランダムライトの激しく遅い製品が主流になってきているらしい。


で、メーカーはUSB3.0のシーケンシャルアクセスの速度を前面に出し、ランダムライトにはあえて触れないようにしているようなので、何も考えずにUSBメモリを買ってきたらランダムライトの激しく遅い製品だった、ということが普通に起こると思われるので、(認識の問題とは別に)注意が必要。

3. Windows To Go ワークスペースの作成

Windows 8 RTM評価版はEnterprise版で、Windows To Goの作成ツールがコントロールパネルにあるので、RPまでのような手順を踏まずとも簡単にWindows To GoのUSBドライブが作成できる。


Express RC8、MSD6000、PicoDrive F3を差した状態(すべてUSB2.0ポート)で、先に認識のされ方を確認しておくと、Express RC8(USB3.0_RC8と表示)はローカルディスクの認識で間違いはない。

この状態で作成ツールを起動すると、自動的に検索されて表示される。

Express RC8が選択された状態ではリストの下に何も出てないが、MSD6000(MSD-SATA6025と表示)に移動すると、「このドライブを使用すると、Windowsのパフォーマンスに影響する可能性があります。最適な結果を得るには、Windows To Go対応のUSB3.0ドライブを使用してください。」と出る。

検索した際に何らかの確認をした結果だと思うが、検索はすぐに終わったし、何を確認したのかは分からない(実は上の参考ではExpress RC8の50GBモデルを使っているが、これと同じメッセージが出ている)。この状態でも問題なく作成はできる。

さらに下のPicoDrive F3に移動すると、今度は「これはリムーバブルドライブで、Windows To Goに対応していません。必要なハードウェア仕様を満たすデバイスを選択してください。」となる。こうなると先には進めない。

この後はイメージファイルを選択して(インストール用のISOファイルをマウントし、そのドライブを検索先に指定した)進めば、何事もなく作成は終了する。

結果は、まあ当然のごとく快適に動作するWindows To GoのUSBドライブが出来た。使用容量は約10GBだったので(イメージファイル次第で変わる)、25GBの容量で問題はなかった。

4. 終わりに

割と便利に使えそうな機能なので、昨年来追いかけてきたが、条件的に個人ユーザーには縁遠いものになってしまったのは勿体ないと思う。が、元からライセンス的には扱いが難しそうだったので、そこは何とも言えない。

まあ自分のExpress RC8はOS起動用として十分な性能があることが分かったので、RTM評価版の期限が終わったら他のOSで使ってみようかと思う。

2012/08/26

High DPI Windows 8 Cursor Set

Windows 8のDPIを200%に設定した場合の問題は、結局RTMでも修正されてなかった。それはさて置き、そもそもこの問題はプログラムに問題があるというより、アイコンあるいはカーソルの画像リソースが足らないだけということに気づいた。

以下はRTM評価版のエクスプローラを見たところだが、ウィンドウのデザインは変わったが、カーソルと上向き矢印の問題は変わってない。

リンクなど他の状態のカーソルも同様。

この問題は実際にMacBook Pro RetinaにWindows 8 RTMをインストールし、DPIを200%にしたときにも確認されている。

[参考] 例
TechRepublic: Combining Windows 8 and a Retina MacBook

で、Windows 8でアイコンの表示を確かめていたときに、この輪郭が汚くなった状態は、アイコンファイルに現在のDPIで表示されるべきサイズの画像が含まれておらず、それより小さなサイズの画像が引き伸ばされて表示されたときの状態と同じことに気づいた。

いや、引き伸ばされたからといって直ちに形状が崩れる必然性はないが、引き伸ばす際に縦横比が微妙に狂ったりして、一言でいえばOSによる画像の拡大が下手なのだと思う(縮小する方は上手なのだが)。

ともかく、そういうことなら本来表示されるべきサイズ(200%なら64x64)の画像をファイルに含めてやればいいわけで、そういうカーソルファイルを作成してみた(エクスプローラの上向き矢印の方は、システムファイルに埋め込まれたリソースを使っているようで、これをいじるのは面倒なので諦めた)。

以下は作成したカーソルに変えた場合の表示。

輪郭がこのDPIにしては少し細いような気もするが、とりあえずこんなところ。

[追記1] Animated Cursor Packer

3つ以上のサイズを持つアニメーションカーソルファイルを作成できる既存のアプリがなかったので、作成したもの。サイズ別に分かれていたものを統合できれば、DPIによってファイル指定を変える必要もなくなるので。

アニメーションカーソルはRIFFファイル形式の一種だが、複数のサイズを持たせる方法が見つからなかったのでOS標準のものを覘いたところ、ファイル構造は単一のサイズだけのアニメーションカーソルと変わらず、中に含まれる個々のカーソルデータが複数のサイズの画像を持っているかどうかの違いだけのようだった。

それならと、カーソルデータの作成は他のアプリに任せ、カーソルデータ(実際はアニメーションなしのカーソルファイルそのまま)をまとめてアニメーションカーソルファイルにする部分だけを作ったのがこのアプリ。元のカーソルファイルの中身は関知しないので、単一のサイズだけのカーソルファイルを元にすれば単一のサイズのアニメーションカーソルファイルが出来、複数のサイズのカーソルファイルを元にすれば同じ複数のサイズのアニメーションカーソルが出来るという具合い。

アニメーションカーソルを作成できる既存のアプリは、カーソルデータの作成とアニメーションカーソルファイルにまとめることの両方をやるわけだが、これを分離することで、複数のサイズのカーソルファイルを用意すれば同じ複数のサイズのアニメーションカーソルファイルが作成できるようになり、サイズの数に制限がなくなるというのがポイント。

……と書きつつ、Microsoftの資料にないことなので確証はないが(アニメーションカーソルに関する情報はWin95の頃で止まっていて、その後複数のサイズを持つアニメーションカーソルが出てきてもアップデートされておらず、複数のサイズを持たせるための情報がない状態)、とりあえずうまく行っているようだからよしとする。

[参考] アニメーションカーソルのRIFFファイル形式に関する数少ない包括的な説明
O'Reilly: Microsoft RIFF
("Size is the number of bytes in the subchunk that appear after the Size field. This value is always 32."にある32は、36の間違いだと思う。この10進数に対応する位置の16進数は24なので)

[追記2]

High DPI Cursor Changerの方に統合した。

2012/08/08

Metroアプリ

デモとして作っていたMetroアプリが一応形になったので。

1. Raidar Metro Demo

Windows 8 RP上のVisual Studio 2012 RCで作ったが、もうWindows 8もVisual Studio 2012もRTMが一般の開発者向けに出る直前で、RTMでまた動作に変更がある可能性があるが……。言語はC#とXAML。

機能的にはNAS Herderのモニター機能のサブセットで、ロジック部分はVisual BasicからC#に大体そのまま移植。このメインページと設定ページの構成で、この間のページ遷移は本来スワイプで出すNavigationBarかAppBarを使うべきだが、とりあえずAppBar用のSettingsのアイコンを置いている。

対象のNASを指定する方法はHost NameかIP Addressの選択式。Scanで見つかったNASを表示する部分はGridViewで、唯一Metroらしい見た目。トースト通知はStatusにWarningが出た場合か温度が閾値を上回った場合に出るが、音声設定がうまく行かないのでデフォルト音声のまま。

これらの設定項目は、変更がある度にApplicationData.LocalSettingsに保存するようにしているので、このアプリのサスペンド時には何もしない。

Metro特有の要求の一つが4つのレイアウト(デフォルトの全画面横長のFullScreenLandscape、Snappedになった別のアプリが横に入って幅が狭まったFilled、全画面縦長のFullScreenPortrait、横に寄せたSnapped)への対応だが、とくにSnappedは要素の配置を大きく変える必要がある。

ちなみに、このレイアウト遷移をアニメーションと呼んでいるが、実際にはアニメーションするわけでも何でもなく、ただ配置が切り替わるだけ(XAMLのアニメーション機能で、いきなり最終値に変えるメソッドを待ち時間0で実行)という。

メインページを左側にSnappedにすると、こうなる。右側はデスクトップ。

やっていることは、
  • タイトルのアイコンを非表示にする。
  • Ping NASのButtonの列は、各要素を入れたGridをStackPanelに入れているが、この配置方向を横にしていたものから縦にし、サイズも合わせて変える。
  • Settingsのアイコンを非表示にする。
  • StatusのTextBoxの位置を下げる。
  • CPUなどの各項目は、各要素を3つに分けて入れたGridをStackPanelに入れているが、これも配置方向を横から縦にし、サイズを変えて位置を下げる。
  • WarningのTextBoxのサイズを変えて位置を下げる。
  • 一番下のRaw responseのTextBoxを非表示にする。
なお、このページは基本ページ(Basic Page)のテンプレートから作ったもので、タイトルのフォントが小さくなっているのはテンプレートによるもの。また、タイトルのアイコンの位置に、元のページに戻るbackButtonのアイコンが要素として存在する。これは最初のページの場合は実際には表示されないが、不要かといえばそうでもなく、ページ履歴の管理に関係しているようで、削ってしまうとSnapped状態のときにこのページに戻れなくなる。

ついでに、デスクトップの壁紙を見ていて気づいたが、Filled状態になったデスクトップは、アイコンは移動しても壁紙は移動しないらしい。

次にSnapped状態の設定ページ。Snappedになったら自動的にメインページに遷移することも考えたが、一応用意した。

やっていることは、
  • Scan NASのButtonの列も、同じくStackPanelの配置方向を横から縦にし、サイズを変える。
  • 見つかったNASを表示するGridViewは、実は同じ内容のListViewも要素として存在していて、このGridViewを表示にしてListViewを非表示にしていたものを逆にする。
  • Ping Intervalなどの各項目も、同じくStackPanelの配置方向を横から縦にし、サイズと位置を変える。
  • 一番下のRaw responseのTextBoxを非表示にする。
このGridViewとListViewを両方持っていて切り替えるという方法は、非効率な気もするが、Grid AppとSplit Appのテンプレートでやっている方法なので。なお、これらの中のGridはItemTemplateでサイズを指定しているが、実際に表示されるサイズは外のGridViewとListViewのサイズにも影響される模様。

以上、このプロジェクトファイルはNAS Herderのプロジェクトサイトに置いてある。ただし、Microsoftの審査は受けてないので開発者ライセンスのあるPCでしか実行できない。そもそも常駐できない監視アプリに何の意味があるのか、という致命的問題があるが……。

2. プロジェクトのテンプレート

アプリを作り始めるのに、どのテンプレートをベースにするのがいいのか、ということを調べるのに時間を食ったので、そのメモ。

新しいプロジェクトをWindows Metro styleで作成するときは、Blank App、Grid App、Split Appのテンプレートから選ぶわけだが、アプリの内容的にGrid AppでもSplit AppでもないとなるとBlank Appになる。ただ、Blank AppにはMetroアプリとして必須のレイアウト切り替え機能なども入ってないので、あまりベースとして使えない。

そこで、プロジェクト作成後に追加できる基本ページ(Basic Page)のテンプレートにはこれらの機能が入っているので、最初のページをこれに差し替える手順。に、プロジェクト作成時に入る、XAMLのテンプレートの大元たるStandardStyles.xamlには後々にBlendで開いたときにエラーを出す問題があるので、これをMSDNのサンプルのものに差し替えることを含めた手順が以下。
  1. Visual Studioで、ファイル -> 新規作成 -> プロジェクト -> テンプレート -> Visual C# -> Windows Metro style -> Blank App (XAML) を選び、プロジェクトを作成したら一旦終了。
  2. 作成されたプロジェクトフォルダーのCommonフォルダーを開き、StandardStyles.xamlを、MSDNのいずれかのサンプルにある同名ファイルで上書き。
  3. Visual Studioでこのプロジェクトを開き、プロジェクト -> 新しい項目の追加 -> Visual C# -> Windows Metro style -> 基本ページ を選び、追加(名前はここではBasicPage1.xamlとする)。
  4. App.xamlのコードの表示を開き(C#)、 if (!rootFrame.Navigate(typeof(MainPage))) 中の MainPageBasicPage1 に修正し、ビルドして最初にBasicPage1が表示されることを確認。(ファイルの差し替えまでしない場合は、ここまで。)
  5. ソリューションエクスプローラーで(元の)MainPage.xamlを削除。
  6. BasicPage1.xamlのデザイナーの表示を開き(XAML)、 x:Class="[名前空間名].BasicPage1" 中の BasicPage1MainPage に修正。
  7. 同じくBasicPage1.xamlのコードの表示を開き(C#)、 public sealed partial class BasicPage1 中の BasicPage1MainPage に修正し、 public BasicPage1() 中の BasicPage1MainPage に修正。
  8. ソリューションエクスプローラーでBasicPage1.xamlの名前をMainPage.xamlに変更。
ただ、Visual Studio 2012もRTMで変わるだろうから、この手順もたぶん変わる。

3. よしなしごと

本格的にXAMLを使ったアプリは初めてで、最初は「何かややこしいことしてるな」という印象だったが、慣れてくるとこれはこれで合理的で面白い。スタイルやテンプレートのかけ方は、CSSを多重にかけたウェブページに似ている。

一方で、Metroアプリ作成のガイドラインなど改めて読んでいると、Metroはやはりタブレット向けのUIとの印象を強くした。その辺り、Microsoftの偉い人が話すことと、実際のMetroにはズレがある感じがする。

Metroでは比較的狭いタブレット画面でタッチ操作するために色々なガイドラインがあって、一言でいうと画面が窮屈になりがちだが、そこを(タッチ操作を駆使して)使いやすいUIにデザインすることが求められる。

また、Metroアプリの重要な方針として、「Content not Chrome」がある。つまり、従来のウィンドウの枠部分(Chrome)は表示せず(必要なときだけ出す)、ユーザーを内容(Content)に集中させよ、というものだが、これにはそもそもの前提として窮屈になりがちな画面の有効活用という要求があると思う。

これは見方を変えると、ユーザーに(OSの存在を意識させず)シングルタスクをさせろ、ということでもある。Metroではユーザーが同時に見られるアプリは基本的に1つで、Snappedで他のアプリも出しておけば一応2つになるが、まあシングルタスク+のUIといえる。

で、従来どおりのPCでは当然にウィンドウを幾つも開いてマルチタスクで使えるわけで(大型化するディスプレイを使えばなおさら)、それをシングルタスク+で使えと言われても無理がある。例えば、よく例に出る旅行サイトを使うアプリの場合、1つのアプリで見るより、色々な会社の旅行サイトを開いて見比べたり、Google Street Viewで現地の写真を見たり、交通の便を確かめたり、他の人の体験を読んだり、そういうことが同時にできた方が効率がいい。

いや、そんなことは当然で、リラックスしながらタブレットで見られることに価値がある、という言い方もできる。実際、従来はPCでやっていたがタブレットでも可能で、むしろそちらの方が便利という作業はあって、そこはタブレットが伸していくのだと思う。

ただ、そうすると対立軸はデスクトップかMetroかではなく、従来のPCかタブレットかになると思うので、それはつまり、従来のPCではデスクトップを使い、タブレットではMetroを使えばいい、という面白くも何ともない話に落ち着く。一周回って「元々そういう話だったのでは」という地点に戻るというか。

したがって、使う人、使う場面を考えて、タブレットがよければMetroアプリで行き、従来のPCがよければデスクトップアプリで行く、ということでいいのではないか、と改めて思う。

2012/07/30

はやぶさ2の近況

『はやぶさ2』については、6月の報道で、既に4月末に設計を終え、製造に入ったといわれていて、7月には「はやぶさ2プロジェクト」のギャラリーにイラストが公開されている。で、JAXA相模原キャンパス(つまりISAS)の特別公開に行ったら、早速新しい模型が展示されていた。
Hayabusa 2: 1/10 scale modelHayabusa 2: 1/10 model

昨年の1/10模型は正直あまり見るべきところがなかったが、今年は設計が固まったせいか、かなり力の入った模型になっている。

ぱっと見て『はやぶさ』と大きく違うのは、スタートラッカーが2基に増えた、高利得アンテナ(HGA)が平面の2枚に増えた、低利得アンテナ(LGA)の形状が変わった(『あかつき』などと同じ。ただし、位置はほぼ同じ)、中利得アンテナが2基から1基に減った、など。その他のアンテナが2基、太陽センサーも見える。
Hayabusa 2: 1/10 modelHayabusa 2: 1/10 model

サンプラーホーン、再突入カプセルは基本的に同じ。
Hayabusa 2: 1/10 model

高利得アンテナが薄い板になっているが、これは『あかつき』用の実物を見ても、ハニカムの板一枚という感じなので、こんなもの。
Hayabusa 2: 1/10 modelHayabusa 2: 1/10 model

中利得アンテナは2軸の可動式になっていて、これ1基で半天球をカバーできるということらしい。
Hayabusa 2: 1/10 model

イオンエンジン周辺は基本的には同じ。
Hayabusa 2: 1/10 model

サンプラーホーン側の中央に衝突装置のEFP、その脇にターゲットマーカー群があるのはいいとして、サンプラーホーンを挟んで円筒形のものが左右に2基ある。これは何かと思って調べたら、ミネルバ2は複数機が搭載されるようなので(はやぶさ2ミッションにおけるミネルバ2ローバの検討状況)、ミネルバ2のコンテナではないかと思う。観測センサーにも当然変更がある。
Hayabusa 2: 1/10 modelHayabusa 2: 1/10 model

その他、『はやぶさ』とは位置が変更された機器もある。
Hayabusa 2: 1/10 model

この他に電波無響室で電気試験中の1/2模型もあった。試験対象の低利得アンテナ(LGA)以外は発泡剤で作ったはりぼてということだったが、高利得アンテナのうち左がKaバンド、右がXバンドということが分かる。
Hayabusa 2: 1/2 model for electric test
Hayabusa 2: 1/2 model for electric test

と、プロジェクトが着々と進んでいることが見て取れたが、今年度の予算で削られた分、来年度の予算で取り返せないとまずいという事情もあって、そういえば、もう予算作成の時期ではある……。他の科学探査との優先順位とか内部的に色々事情はあるのだろうが、うまく行って『はやぶさ2』が無事1999 Ju3に旅立てることを祈る。

[追記]

KaバンドとXバンドの高利得アンテナの使い分けについては、公式リーフレット(2012年版)の「はやぶさ」「はやぶさ2」に説明がある。

なお、この1/10模型はしばらく前から同じロビーで展示されていたようなので、通常展示に戻った現在も見られると思う。