.NET でファイルの詳細情報の日時を変更する

# はじめに

僕は以前 Delphi でファイルの日時を変更する Windows アプリを作りました。
[簡単エクスプローラ拡張 EzExpEx]
(http://hp.vector.co.jp/authors/VA029585/EzExpEx/)
これは残念なことにユニコードの名称のファイルを扱えないんですよね。
最新の Delphi でビルドし直せばいいのですが、僕が持っている Delphi ではだめなんです。
これを VB.NET で作り直したいとずっと思っていました。

僕のアプリが扱えるファイルの日時は
・ファイル自体の作成日時、更新日時
・Exif 情報の撮影日時、更新日時
・PDF ファイルの作成日時、更新日時
・詳細情報の作成日時、更新日時

ファイル自体の日時を変更するのは .NET の標準機能で対応できますが、ファイルの詳細情報の日時などはどうでしょう。

## ファイルの概要情報の日時を変更する

Microsoft Office のファイルなどは、Windows が管理するファイルそのものの日時と別に、ファイルの詳細情報に作成日時、更新日時を持っています。

ファイルの詳細情報の日時は、.NET の標準機能で変更できません。
取得するのは、楽な方法があるようです。
※この記事を書くために調べていたところ、Windows API CodePack を使って変更もできそうです。
[ファイル プロパティの取得 | C# プログラミング解説 | so-zou.jp]
(https://so-zou.jp/software/tech/programming/c-sharp/stream/file/property.htm)

僕が Delphi で作ったアプリは、Windows の IPropertyStorage を使っていました。
[サンプル: "EXCEL&WORDファイルのプロパティ取得" | Delphi Users' Forum]
(http://delfusa.main.jp/delfusafloor/archive/www.nifty.ne.jp_forum_fdelphi/samples/00915.html)
調べていくと C# で書かれたコードは見つけられました。
[ぬるり。: C# と NTFS ストリームの甘くなくもない関係 | ぬるりことNullReferenceExceptionに怯えながら日々書き連ねる.NETのコード片(主にC#)]
(http://hongliang.seesaa.net/article/16600421.html)
これを VB.NET で書き直すことにしました。C# のコードも用意しました。

# IPropertyStorage を使用する

VB.NET および C# で IPropertyStorage を使用できるようにします。

```vb:VB.NET
    <ComImport, Guid("0000013A-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IPropertySetStorage
        Function Create() As Integer
        <PreserveSig>
        Function Open(ByRef FormatId As Guid, Mode As Integer, ByRef Storage As IPropertyStorage) As Integer
        Function Delete() As Integer
        Function [Enum]() As Integer
    End Interface

    <DllImport("ole32.dll", SetLastError:=True, CharSet:=CharSet.Unicode, CallingConvention:=CallingConvention.Winapi)>
    Public Shared Function StgOpenStorageEx(Name As String, Mode As Integer, Format As Integer, Attributes As Integer, Options As IntPtr, Reserved As IntPtr, <[In]> ByRef Iid As Guid, <Out> ByRef Opened As IPropertySetStorage) As Integer
    End Function

    <ComImport, Guid("00000138-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IPropertyStorage
        <PreserveSig>
        Function ReadMultiple(Count As Integer, <MarshalAs(UnmanagedType.LPArray), [In]> Specs() As PropSpec, <MarshalAs(UnmanagedType.LPArray), Out> Variants() As PropVariant) As Integer
        Function WriteMultiple(Count As Integer, <MarshalAs(UnmanagedType.LPArray), [In]> Specs() As PropSpec, <MarshalAs(UnmanagedType.LPArray), [In]> Variants() As PropVariant, NameFirstId As Integer) As Integer
        Function DeleteMultiple() As Integer
        Function ReadPropertyNames() As Integer
        Function WritePropertyNames() As Integer
        Function DeletePropertyNames() As Integer
        Function Commit() As Integer
        Function Revert() As Integer
        Function [Enum]() As Integer
        Function SetTimes() As Integer
        Function SetClass() As Integer
        Function Stat() As Integer
    End Interface

    Public Structure PropSpec
        Public Type As Integer
        Public Id As Integer
    End Structure

    Public Structure PropVariant
        Public Type As Short
        Public Reserved1 As Short
        Public Reserved2 As Short
        Public Reserved3 As Short
        Public Value As Long
    End Structure

    <DllImport("ole32.dll", SetLastError:=True, CharSet:=CharSet.Unicode, CallingConvention:=CallingConvention.Winapi)>
    Public Shared Function PropVariantClear(<[In]> ByRef [Variant] As PropVariant) As Integer
    End Function

    Public Const STGM_CREATE = &H1000
    Public Const STGM_READ = &H0
    Public Const STGM_WRITE = &H1
    Public Const STGM_READWRITE = &H2
    Public Const STGM_SHARE_EXCLUSIVE = &H10
    Public Const STGFMT_FILE = &H3
    Public Const STGFMT_ANY = &H4
    Public Const PRSPEC_PROPID = &H1
    Public Const VT_FILETIME = &H40
```
```csharp:C#
        [ComImport, Guid("0000013A-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPropertySetStorage
        {
            int Create();
            [PreserveSig]
            int Open([In] ref Guid formatId, int mode, out IPropertyStorage storage);
            int Delete();
            int Enum();
        }

        [DllImport("ole32.dll", CharSet = CharSet.Unicode)]
        public static extern int StgOpenStorageEx(string name, int mode, int format, int attributes, IntPtr options, IntPtr reserved, [In] ref Guid iid, out IPropertySetStorage opened);

        [ComImport, Guid("00000138-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPropertyStorage
        {
            [PreserveSig]
            int ReadMultiple(int count, [MarshalAs(UnmanagedType.LPArray), In] PropSpec[] specs, [MarshalAs(UnmanagedType.LPArray), Out] PropVariant[] variants);
            [PreserveSig]
            int WriteMultiple(int count, [MarshalAs(UnmanagedType.LPArray), In] PropSpec[] specs, [MarshalAs(UnmanagedType.LPArray), In] PropVariant[] variants, int nameFirstId);
            int DeleteMultiple();
            int ReadPropertyNames();
            int WritePropertyNames();
            int DeletePropertyNames();
            int Commit();
            int Revert();
            int Enum();
            int SetTimes();
            int SetClass();
            int Stat();
        }
        public struct PropSpec
        {
            public int Kind;
            public int Id;
        }
        public struct PropVariant
        {
            public short Type;
            public short Reserved1, Reserved2, Reserved3;
            public long Value;
        }

        [DllImport("ole32.dll", CharSet = CharSet.Unicode)]
        public static extern int PropVariantClear([In] ref PropVariant variant);

        public const int STGM_CREATE = 0x1000;
        public const int STGM_READ = 0x0;
        public const int STGM_WRITE = 0x1;
        public const int STGM_READWRITE = 0x2;
        public const int STGM_SHARE_EXCLUSIVE = 0x10;
        public const int STGFMT_FILE = 0x3;
        public const int STGFMT_ANY = 0x4;
        public const int PRSPEC_PROPID = 0x1;
        public const int VT_FILETIME = 0x40;
```
# ファイルの詳細情報の日時を取得する

```vb:VB.NET
        Dim Path As String = "●●●●.doc"    '対象とするファイルのパス
        Dim CreateedTime As DateTime    '作成日時を返す
        Dim CreatedValid As Boolean = False    '作成日時を取得できたか返す
        Dim LastSavedTime As DateTime    '更新日時を返す
        Dim LastSavedValid As Boolean = False     '更新日時を取得できたか返す

        If Not System.IO.File.Exists(Path) Then
            Exit Function
        End If
        Try
            Dim PropSetStrge As IPropertySetStorage = Nothing
            Dim PropStrge As IPropertyStorage = Nothing
            Try
                Dim Guid As New Guid("0000013A-0000-0000-C000-000000000046")
                Marshal.ThrowExceptionForHR(StgOpenStorageEx(Path, STGM_READ + STGM_SHARE_EXCLUSIVE, STGFMT_ANY, 0, IntPtr.Zero, IntPtr.Zero, Guid, PropSetStrge))

                Dim FmtId As New Guid("F29F85E0-4FF9-1068-AB91-08002B27B3D9")
                Marshal.ThrowExceptionForHR(PropSetStrge.Open(FmtId, STGM_READ + STGM_SHARE_EXCLUSIVE, PropStrge))

                Dim Specs(1) As PropSpec
                Specs(0).Type = PRSPEC_PROPID
                Specs(0).Id = 12
                Specs(1).Type = PRSPEC_PROPID
                Specs(1).Id = 13
                Dim Variants(1) As PropVariant
                Marshal.ThrowExceptionForHR(PropStrge.ReadMultiple(Specs.Length, Specs, Variants))
                CreatedTime = DateTime.FromFileTime(Variants(0).Value)
                CreatedValid = True
                LastSavedTime = DateTime.FromFileTime(Variants(1).Value)
                LastSavedValid = True
            Finally
                If PropSetStrge IsNot Nothing Then Marshal.ReleaseComObject(PropSetStrge)
                If PropStrge IsNot Nothing Then Marshal.ReleaseComObject(PropStrge)
            End Try
        Catch ex As Exception
            Exit Function
        End Try
```
```csharp:C#
            string Path = "●●●●.doc";    // 対象とするファイルのパス
            DateTime CreatedTime;    // 作成日時を返す
            bool CreatedValid = false;    // 作成日時を取得できたか返す
            DateTime LastSavedTime;    // 更新日時を返す
            bool LastSavedValid = false;    // 更新日時を取得できたか返す

            if (!System.IO.File.Exists(Path)) {
                return false;
            }
            try {
                IPropertySetStorage PropSetStrg = null;
                IPropertyStorage PropStrg = null;
                try {
                    Guid iid = new Guid("0000013A-0000-0000-C000-000000000046");
                    Marshal.ThrowExceptionForHR(StgOpenStorageEx(Path, STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_ANY, 0, IntPtr.Zero, IntPtr.Zero, ref iid, out PropSetStrg));

                    Guid fmtid = new Guid("F29F85E0-4FF9-1068-AB91-08002B27B3D9");
                    Marshal.ThrowExceptionForHR(PropSetStrg.Open(ref fmtid, STGM_READ | STGM_SHARE_EXCLUSIVE, out PropStrg));

                    PropSpec[] specs = new PropSpec[2];
                    specs[0].Kind =PRSPEC_PROPID;
                    specs[0].Id = 12;
                    specs[1].Kind = PRSPEC_PROPID;
                    specs[1].Id = 13;
                    PropVariant[] variants = new PropVariant[2];
                    Marshal.ThrowExceptionForHR(PropStrg.ReadMultiple(specs.Length, specs, variants));
                    CreatedTime = DateTime.FromFileTime(variants[0].Value);
                    CreatedValid = true;
                    LastSavedTime = DateTime.FromFileTime(variants[1].Value);
                    LastSavedValid = true;
                }
                finally {
                    if (PropStrg != null) Marshal.ReleaseComObject(PropStrg);
                    if (PropSetStrg != null) Marshal.ReleaseComObject(PropSetStrg);
                }
            }
            catch (Exception ex) {
                return false;
            }
```

# ファイルの詳細情報の日時を変更する

```vb:VB.NET
        Dim Path As String = "●●●●.doc"    '対象とするファイルのパス
        Dim CreatedTime As DateTime    '作成する更新日時を指定する
        Dim CreatedValid As Boolean = True    '作成日時を変更するか指定する
        Dim LastSavedTime As DateTime    '変更する更新日時を指定する
        Dim LastSavedValid As Boolean = True     '更新日時を変更するか指定する

        If Not System.IO.File.Exists(Path) Then
            Exit Function
        End If
        Try
            Dim PropSetStrge As IPropertySetStorage = Nothing
            Dim PropStrge As IPropertyStorage = Nothing
            Try
                Dim IId As New Guid("0000013A-0000-0000-C000-000000000046")
                Marshal.ThrowExceptionForHR(StgOpenStorageEx(Path, STGM_READWRITE + STGM_SHARE_EXCLUSIVE, STGFMT_ANY, 0, IntPtr.Zero, IntPtr.Zero, IId, PropSetStrge))

                Dim FmtId As New Guid("F29F85E0-4FF9-1068-AB91-08002B27B3D9")
                Marshal.ThrowExceptionForHR(PropSetStrge.Open(FmtId, STGM_READWRITE + STGM_SHARE_EXCLUSIVE, PropStrge))

                Dim Specs(1) As PropSpec
                Dim Variants(1) As PropVariant
                If CreatedValid Then
                    Specs(0).Type = PRSPEC_PROPID
                    Specs(0).Id = 12
                    Variants(0).Type = VT_FILETIME
                    Variants(0).Value = CreatedTime.ToFileTime
                End If
                If LastSavedValid Then
                    Specs(1).Type = PRSPEC_PROPID
                    Specs(1).Id = 13
                    Variants(1).Type = VT_FILETIME
                    Variants(1).Value = LastSavedTime.ToFileTime
                End If
                Marshal.ThrowExceptionForHR(PropStrge.WriteMultiple(Specs.Length, Specs, Variants, 0))
            Finally
                If PropSetStrge IsNot Nothing Then Marshal.ReleaseComObject(PropSetStrge)
                If PropStrge IsNot Nothing Then Marshal.ReleaseComObject(PropStrge)
            End Try
        Catch ex As Exception
            Exit Function
        End Try
```
```csharp:C#
            string Path = "●●●●.doc";    // 対象とするファイルのパス
            DateTime CreatedTime;    // 変更する作成日時を指定する
            bool CreatedValid = true;    // 作成日時を変更するか指定する
            DateTime LastSavedTime;    // 変更する更新日時を指定する
            bool LastSavedValid = true;    // 更新日時を変更するか指定する

            if (!System.IO.File.Exists(Path)) {
                return false;
            }
            try {
                IPropertySetStorage PropSetStrg = null;
                IPropertyStorage PropStrg = null;
                try {
                    Guid guid = new Guid("0000013A-0000-0000-C000-000000000046");
                    Marshal.ThrowExceptionForHR(StgOpenStorageEx(Path, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, STGFMT_ANY, 0, IntPtr.Zero, IntPtr.Zero, ref guid, out PropSetStrg));

                    Guid fmtid = new Guid("F29F85E0-4FF9-1068-AB91-08002B27B3D9");
                    Marshal.ThrowExceptionForHR(PropSetStrg.Open(ref fmtid, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, out PropStrg));

                    PropSpec[] specs = new PropSpec[2];
                    PropVariant[] variants = new PropVariant[2];
                    if (CreatedValid) {
                        specs[0].Kind = PRSPEC_PROPID;
                        specs[0].Id = 12;
                        variants[0].Type = VT_FILETIME;
                        variants[0].Value = CreatedTime.ToFileTime();
                    }
                    if (LastSavedValid) {
                        specs[1].Kind = PRSPEC_PROPID;
                        specs[1].Id = 13;
                        variants[1].Type = VT_FILETIME;
                        variants[1].Value = LastSavedTime.ToFileTime();
                    }
                    Marshal.ThrowExceptionForHR(PropStrg.WriteMultiple(specs.Length, specs, variants, 0));
                }
                finally {
                    if (PropStrg != null) Marshal.ReleaseComObject(PropStrg);
                    if (PropSetStrg != null) Marshal.ReleaseComObject(PropSetStrg);
                }
            }
            catch (Exception ex) {
                return false;
            }
```