diff --git a/build/common.props b/build/common.props index 4bc6c1ef..8fb2cbb3 100644 --- a/build/common.props +++ b/build/common.props @@ -3,7 +3,7 @@ 1.0.0 - 5.8.1 + 5.8.2 2.2.2 2.8.8 2.8.0 diff --git a/changelog.txt b/changelog.txt index 41aee7c7..f2487033 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,8 @@ --------------------------------------------------------------------------------------------------- +Version: 5.8.2 +Game Versions: v1.0.0,v1.0.1,v1.0.2,v1.0.3,v1.1.0,v1.1.1,v1.1.2,v1.1.3,v1.1.4,v1.1.5,v1.2.0,v1.2.1 +* Added Preset Import/Export/Deleting +--------------------------------------------------------------------------------------------------- Version: 5.8.1 Game Versions: v1.0.0,v1.0.1,v1.0.2,v1.0.3,v1.1.0,v1.1.1,v1.1.2,v1.1.3,v1.1.4,v1.1.5,v1.2.0,v1.2.1 * Fixed missing Preset saving diff --git a/src/MCM.Abstractions/GameFeatures/IFileSystemProvider.cs b/src/MCM.Abstractions/GameFeatures/IFileSystemProvider.cs index 64ec8fdd..03079c33 100644 --- a/src/MCM.Abstractions/GameFeatures/IFileSystemProvider.cs +++ b/src/MCM.Abstractions/GameFeatures/IFileSystemProvider.cs @@ -40,7 +40,8 @@ interface IFileSystemProvider GameFile[] GetFiles(GameDirectory directory, string searchPattern); GameFile? GetFile(GameDirectory directory, string fileName); GameFile GetOrCreateFile(GameDirectory directory, string fileName); - bool WriteData(GameFile file, byte[] data); + bool WriteData(GameFile file, byte[]? data); byte[]? ReadData(GameFile file); + string? GetSystemPath(GameFile file); } } \ No newline at end of file diff --git a/src/MCM.Abstractions/Presets/JsonSettingsPreset.cs b/src/MCM.Abstractions/Presets/JsonSettingsPreset.cs index 5ebd1b22..5525d7cd 100644 --- a/src/MCM.Abstractions/Presets/JsonSettingsPreset.cs +++ b/src/MCM.Abstractions/Presets/JsonSettingsPreset.cs @@ -40,6 +40,14 @@ private sealed class PresetContainer : PresetContainerDefinition public BaseSettings? Settings { get; set; } } + public static string? GetPresetId(string content) + { + var container = JsonConvert.DeserializeObject(content); + if (container is null) return null; + + return container.Id; + } + public static JsonSettingsPreset? FromFile(BaseSettings settings, GameFile file) => FromFile(settings.Id, file, settings.CreateNew); public static JsonSettingsPreset? FromFile(string settingsId, GameFile file, Func getNewSettings) { diff --git a/src/MCM.Bannerlord/GameFeatures/FileSystemProvider.cs b/src/MCM.Bannerlord/GameFeatures/FileSystemProvider.cs index 57134e70..ed210961 100644 --- a/src/MCM.Bannerlord/GameFeatures/FileSystemProvider.cs +++ b/src/MCM.Bannerlord/GameFeatures/FileSystemProvider.cs @@ -64,9 +64,13 @@ public GameFile GetOrCreateFile(GameDirectory directory, string fileName) return new GameFile(directory, fileName); } - public bool WriteData(GameFile file, byte[] data) + public bool WriteData(GameFile file, byte[]? data) { var baseFile = new TWPlatformFilePath(new TWPlatformDirectoryPath((PlatformFileType) file.Owner.Type, file.Owner.Path), file.Name); + + if (data is null) + return PlatformFileHelper.DeleteFile(baseFile); + return PlatformFileHelper.SaveFile(baseFile, data) == SaveResult.Success; } @@ -75,5 +79,11 @@ public bool WriteData(GameFile file, byte[] data) var baseFile = new TWPlatformFilePath(new TWPlatformDirectoryPath((PlatformFileType) file.Owner.Type, file.Owner.Path), file.Name); return !PlatformFileHelper.FileExists(baseFile) ? null : PlatformFileHelper.GetFileContent(baseFile); } + + public string? GetSystemPath(GameFile file) + { + var baseFile = new TWPlatformFilePath(new TWPlatformDirectoryPath((PlatformFileType) file.Owner.Type, file.Owner.Path), file.Name); + return PlatformFileHelper.GetFileFullPath(baseFile); + } } } \ No newline at end of file diff --git a/src/MCM.Publish/_Module/ModuleData/Languages/EN/sta_strings.xml b/src/MCM.Publish/_Module/ModuleData/Languages/EN/sta_strings.xml index 58f5be95..16ed5b17 100644 --- a/src/MCM.Publish/_Module/ModuleData/Languages/EN/sta_strings.xml +++ b/src/MCM.Publish/_Module/ModuleData/Languages/EN/sta_strings.xml @@ -43,5 +43,9 @@ + + + + \ No newline at end of file diff --git a/src/MCM.UI/GUI/Brushes/SettingsBrush.xml b/src/MCM.UI/GUI/Brushes/SettingsBrush.xml index 6ca6ec5a..294011df 100644 --- a/src/MCM.UI/GUI/Brushes/SettingsBrush.xml +++ b/src/MCM.UI/GUI/Brushes/SettingsBrush.xml @@ -1,4 +1,10 @@  + + + + + + diff --git a/src/MCM.UI/GUI/Prefabs/ModOptionsPageView.xml b/src/MCM.UI/GUI/Prefabs/ModOptionsPageView.xml index caa8ad5d..5b4255b0 100644 --- a/src/MCM.UI/GUI/Prefabs/ModOptionsPageView.xml +++ b/src/MCM.UI/GUI/Prefabs/ModOptionsPageView.xml @@ -82,6 +82,11 @@ + + + + @@ -98,24 +103,6 @@ - - - - - - - - - - - - - - diff --git a/src/MCM.UI/GUI/Prefabs/ModOptionsView.xml b/src/MCM.UI/GUI/Prefabs/ModOptionsView.xml index 38dfb998..5554e170 100644 --- a/src/MCM.UI/GUI/Prefabs/ModOptionsView.xml +++ b/src/MCM.UI/GUI/Prefabs/ModOptionsView.xml @@ -112,6 +112,11 @@ + + + + @@ -128,24 +133,6 @@ - - - - - - - - - - - - - - diff --git a/src/MCM.UI/GUI/ViewModels/ModOptionsVM.cs b/src/MCM.UI/GUI/ViewModels/ModOptionsVM.cs index 136f6fa2..e93e0c88 100644 --- a/src/MCM.UI/GUI/ViewModels/ModOptionsVM.cs +++ b/src/MCM.UI/GUI/ViewModels/ModOptionsVM.cs @@ -18,11 +18,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using TaleWorlds.Core; using TaleWorlds.Engine; using TaleWorlds.Library; using TaleWorlds.Localization; @@ -372,64 +374,226 @@ public void ExecuteSelect(SettingsVM? viewModel) } } - public void ExecuteSaveAsPreset() + private void RefreshPresetList() { - if (SelectedMod?.SettingsInstance is not { } settings) return; - var settingsSnapshot = settings.CopyAsNew(); + if (SelectedMod is null) return; - var fileSystem = GenericServiceProvider.GetService(); - if (fileSystem is null) return; + SelectedMod.ReloadPresetList(); + DoPresetsSelectorCopyWithoutEvents(() => + { + PresetsSelectorCopy.Refresh(SelectedMod.PresetsSelector.ItemList.Select(x => x.OriginalItem), SelectedMod.PresetsSelector.SelectedIndex); + }); + } - InformationManager.ShowTextInquiry(InquiryDataUtils.CreateTextTranslatable( - "{=ModOptionsVM_SaveAsPreset}Save As Preset", - "{=ModOptionsVM_SaveAsPresetDesc}Choose the name of the preset", + private void OverridePreset(Action onOverride) + { + InformationManager.ShowInquiry(InquiryDataUtils.CreateTranslatable( + "{=ModOptionsVM_OverridePreset}Preset Already Exists", + "{=ModOptionsVM_OverridePresetDesc}Preset already exists! Do you want to override it?", true, true, - "{=5Unqsx3N}Confirm", + "{=aeouhelq}Yes", "{=3CpNUnVl}Cancel", - input => - { - var hasSet = new HashSet(System.IO.Path.GetInvalidFileNameChars()); - var sb = new StringBuilder(); - foreach (var c in input) sb.Append(hasSet.Contains(c) ? '_' : c); - var id = sb.ToString(); + onOverride, () => { })); + } + + public void ExecuteManagePresets() + { + const string savePreset = "save_preset"; + const string importPreset = "import_preset"; + const string exportPreset = "export_preset"; + const string deletePreset = "delete_preset"; - var presetsDirectory = fileSystem.GetOrCreateDirectory(fileSystem.GetModSettingsDirectory(), "Presets"); - var settingsDirectory = fileSystem.GetOrCreateDirectory(presetsDirectory, settingsSnapshot.Id); + if (SelectedMod?.SettingsInstance is not { } settings) return; + + var fileSystem = GenericServiceProvider.GetService(); + if (fileSystem is null) return; + + if (PresetsSelectorCopy.SelectedItem?.OriginalItem is null) return; - var filename = $"{id}.json"; + void SaveAsPreset(GameDirectory settingsDirectory) + { + var settingsSnapshot = settings.CopyAsNew(); - void SavePreset() + InformationManager.ShowTextInquiry(InquiryDataUtils.CreateTextTranslatable( + "{=ModOptionsVM_SaveAsPreset}Save As Preset", + "{=ModOptionsVM_SaveAsPresetDesc}Choose the name of the preset", + true, true, + "{=5Unqsx3N}Confirm", + "{=3CpNUnVl}Cancel", + input => { - var presetFile = fileSystem.GetOrCreateFile(settingsDirectory, filename); + var hasSet = new HashSet(System.IO.Path.GetInvalidFileNameChars()); + var sb = new StringBuilder(); + foreach (var c in input) sb.Append(hasSet.Contains(c) ? '_' : c); + var id = sb.ToString(); - var preset = new JsonSettingsPreset(settingsSnapshot.Id, id, input, presetFile, () => null!); - preset.SavePreset(settingsSnapshot); + var filename = $"{id}.json"; - SelectedMod.ReloadPresetList(); - DoPresetsSelectorCopyWithoutEvents(() => + void SavePreset() { - PresetsSelectorCopy.Refresh(SelectedMod.PresetsSelector.ItemList.Select(x => x.OriginalItem), SelectedMod.PresetsSelector.SelectedIndex); - }); + var presetFile = fileSystem.GetOrCreateFile(settingsDirectory, filename); + + var preset = new JsonSettingsPreset(settingsSnapshot.Id, id, input, presetFile, () => null!); + preset.SavePreset(settingsSnapshot); + + RefreshPresetList(); + } + + if (fileSystem.GetFile(settingsDirectory, filename) is not null) + { + // Override file? + OverridePreset(SavePreset); + return; + } + + SavePreset(); + }, () => { })); + } + + void ImportNewPreset(GameDirectory settingsDirectory) + { + var dialog = new OpenFileDialog + { + Title = "Import Preset", + Filter = "MCM Preset (.json)|*.json", + CheckFileExists = true, + CheckPathExists = true, + ReadOnlyChecked = true, + Multiselect = false, + ValidateNames = true, + }; + if (dialog.ShowDialog()) + { + var content = File.ReadAllText(dialog.FileName); + var presetId = JsonSettingsPreset.GetPresetId(content); + + void CopyFile() + { + var presetFile = fileSystem.GetOrCreateFile(settingsDirectory, $"{presetId}.json"); + var path = fileSystem.GetSystemPath(presetFile); + if (path is null) return; + try + { + File.Copy(dialog.FileName, path, true); + } + catch (Exception) { /* ignore */ } } + var filename = $"{presetId}.json"; if (fileSystem.GetFile(settingsDirectory, filename) is not null) { - // Override file? - InformationManager.ShowInquiry(InquiryDataUtils.CreateTranslatable( - "{=ModOptionsVM_OverridePreset}Preset Already Exists", - "{=ModOptionsVM_OverridePresetDesc}Preset already exists! Do you want to override it?", - true, true, - "{=aeouhelq}Yes", - "{=3CpNUnVl}Cancel", - () => - { - SavePreset(); - }, () => { })); + OverridePreset(CopyFile); return; } - SavePreset(); - }, () => { })); + CopyFile(); + } + + RefreshPresetList(); + } + + void ExportPreset(GameFile presetFile) + { + var path = fileSystem.GetSystemPath(presetFile); + if (path is null) return; + + var dialog = new SaveFileDialog + { + Title = "Export Preset", + Filter = "MCM Preset (.json)|*.json", + FileName = System.IO.Path.GetFileName(path), + + ValidateNames = true, + + OverwritePrompt = true, + }; + if (dialog.ShowDialog()) + { + try + { + File.Copy(path, dialog.FileName, true); + } + catch (Exception) { /* ignore */ } + } + } + + void DeletePreset(GameFile presetFile) + { + fileSystem.WriteData(presetFile, null); + + RefreshPresetList(); + } + + void OnActionSelected(List selected) + { + var selectedPresetKey = PresetsSelectorCopy.SelectedItem?.OriginalItem; + if (selectedPresetKey is null) return; + + var presetsDirectory = fileSystem.GetOrCreateDirectory(fileSystem.GetModSettingsDirectory(), "Presets"); + var settingsDirectory = fileSystem.GetOrCreateDirectory(presetsDirectory, settings.Id); + + var filename = $"{selectedPresetKey.Id}.json"; + + switch (selected[0].Identifier) + { + case savePreset: + { + SaveAsPreset(settingsDirectory); + break; + } + case importPreset: + { + ImportNewPreset(settingsDirectory); + break; + } + case exportPreset: + { + var presetFile = fileSystem.GetFile(settingsDirectory, filename); + if (presetFile is null) return; + ExportPreset(presetFile); + break; + } + + case deletePreset: + { + var presetFile = fileSystem.GetFile(settingsDirectory, filename); + if (presetFile is null) return; + DeletePreset(presetFile); + break; + } + } + } + + var inquiries = new List + { + new(importPreset, new TextObject("{=ModOptionsVM_ManagePresetsImport}Import a new Preset").ToString(), null) + }; + + if (PresetsSelectorCopy.SelectedItem.OriginalItem.Id == "custom") + { + inquiries.Add(new(savePreset, new TextObject("{=ModOptionsVM_SaveAsPreset}Save As Preset").ToString(), null)); + } + + if (PresetsSelectorCopy.SelectedItem.OriginalItem.Id is not "custom" and not "default") + { + inquiries.Add(new(exportPreset, new TextObject("{=ModOptionsVM_ManagePresetsExport}Export Preset '{PRESETNAME}'", new Dictionary() + { + { "PRESETNAME", PresetsSelectorCopy.SelectedItem.OriginalItem.Name } + }).ToString(), null)); + inquiries.Add(new(deletePreset, new TextObject("{=ModOptionsVM_ManagePresetsDelete}Delete Preset '{PRESETNAME}'", new Dictionary() + { + { "PRESETNAME", PresetsSelectorCopy.SelectedItem.OriginalItem.Name } + }).ToString(), null)); + } + + MBInformationManager.ShowMultiSelectionInquiry(InquiryDataUtils.CreateMultiTranslatable( + "{=ModOptionsVM_ManagePresets}Manage Presets", "", + inquiries, + true, + 1, 1, + "{=5Unqsx3N}Confirm", + "{=3CpNUnVl}Cancel", + OnActionSelected, _ => { })); } public override void OnFinalize() diff --git a/src/MCM.UI/MCM.UI.csproj b/src/MCM.UI/MCM.UI.csproj index 29f5f5ec..725b0fb6 100644 --- a/src/MCM.UI/MCM.UI.csproj +++ b/src/MCM.UI/MCM.UI.csproj @@ -8,6 +8,7 @@ MCM.UI Debug;Release $(DefineConstants) + true diff --git a/src/MCM.UI/Utils/InquiryDataUtils.cs b/src/MCM.UI/Utils/InquiryDataUtils.cs index 7336aaa6..5616bb2c 100644 --- a/src/MCM.UI/Utils/InquiryDataUtils.cs +++ b/src/MCM.UI/Utils/InquiryDataUtils.cs @@ -1,8 +1,10 @@ using HarmonyLib.BUTR.Extensions; using System; +using System.Collections.Generic; using System.Linq; +using TaleWorlds.Core; using TaleWorlds.Library; using TaleWorlds.Localization; @@ -21,12 +23,25 @@ private delegate InquiryData V2Delegate(string titleText, string text, bool isAf private static readonly V2Delegate? V2 = AccessTools2.GetConstructorDelegate(typeof(InquiryData), typeof(V2Delegate).GetMethod("Invoke").GetParameters().Select(x => x.ParameterType).ToArray()); + private delegate TextInquiryData V1TextDelegate(string titleText, string text, bool isAffirmativeOptionShown, bool isNegativeOptionShown, string affirmativeText, string negativeText, Action affirmativeAction, Action negativeAction, bool shouldInputBeObfuscated = false, Func>? textCondition = null, string soundEventPath = "", string defaultInputText = ""); private static readonly V1TextDelegate? V1Text = AccessTools2.GetConstructorDelegate(typeof(TextInquiryData), typeof(V1TextDelegate).GetMethod("Invoke").GetParameters().Select(x => x.ParameterType).ToArray()); + + private delegate MultiSelectionInquiryData V1MultiDelegate(string titleText, string descriptionText, List inquiryElements, bool isExitShown, int maxSelectableOptionCount, + string affirmativeText, string negativeText, Action> affirmativeAction, Action> negativeAction, string soundEventPath = ""); + private static readonly V1MultiDelegate? V1Multi = + AccessTools2.GetConstructorDelegate(typeof(MultiSelectionInquiryData), typeof(V1MultiDelegate).GetMethod("Invoke").GetParameters().Select(x => x.ParameterType).ToArray()); + + private delegate MultiSelectionInquiryData V2MultiDelegate(string titleText, string descriptionText, List inquiryElements, bool isExitShown, int minSelectableOptionCount, int maxSelectableOptionCount, + string affirmativeText, string negativeText, Action> affirmativeAction, Action> negativeAction, string soundEventPath = ""); + private static readonly V2MultiDelegate? V2Multi = + AccessTools2.GetConstructorDelegate(typeof(MultiSelectionInquiryData), typeof(V2MultiDelegate).GetMethod("Invoke").GetParameters().Select(x => x.ParameterType).ToArray()); + + public static InquiryData? Create(string titleText, string text, bool isAffirmativeOptionShown, bool isNegativeOptionShown, string affirmativeText, string negativeText, Action affirmativeAction, Action negativeAction) { if (V1 is not null) @@ -53,5 +68,19 @@ private delegate TextInquiryData V1TextDelegate(string titleText, string text, b public static TextInquiryData? CreateTextTranslatable(string titleText, string text, bool isAffirmativeOptionShown, bool isNegativeOptionShown, string affirmativeText, string negativeText, Action affirmativeAction, Action negativeAction) => CreateText(new TextObject(titleText).ToString(), new TextObject(text).ToString(), isAffirmativeOptionShown, isNegativeOptionShown, new TextObject(affirmativeText).ToString(), new TextObject(negativeText).ToString(), affirmativeAction, negativeAction); + + public static MultiSelectionInquiryData? CreateMulti(string titleText, string descriptionText, List inquiryElements, bool isExitShown, int minSelectableOptionCount, int maxSelectableOptionCount, string affirmativeText, string negativeText, Action> affirmativeAction, Action> negativeAction) + { + if (V1Multi is not null) + return V1Multi(titleText, descriptionText, inquiryElements, isExitShown, maxSelectableOptionCount, affirmativeText, negativeText, affirmativeAction, negativeAction); + + if (V2Multi is not null) + return V2Multi(titleText, descriptionText, inquiryElements, isExitShown, minSelectableOptionCount, maxSelectableOptionCount, affirmativeText, negativeText, affirmativeAction, negativeAction); + + return null; + } + + public static MultiSelectionInquiryData? CreateMultiTranslatable(string titleText, string descriptionText, List inquiryElements, bool isExitShown, int minSelectableOptionCount, int maxSelectableOptionCount, string affirmativeText, string negativeText, Action> affirmativeAction, Action> negativeAction) => + CreateMulti(new TextObject(titleText).ToString(), new TextObject(descriptionText).ToString(), inquiryElements, isExitShown, minSelectableOptionCount, maxSelectableOptionCount, new TextObject(affirmativeText).ToString(), new TextObject(negativeText).ToString(), affirmativeAction, negativeAction); } } \ No newline at end of file diff --git a/src/MCM.UI/Utils/OpenSaveDialogs.cs b/src/MCM.UI/Utils/OpenSaveDialogs.cs new file mode 100644 index 00000000..1522fd53 --- /dev/null +++ b/src/MCM.UI/Utils/OpenSaveDialogs.cs @@ -0,0 +1,2012 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +namespace MCM.UI.Utils; + +/// The StrPtr structure represents a LPTSTR. +[StructLayout(LayoutKind.Sequential), DebuggerDisplay("{ptr}, {ToString()}")] +file struct StrPtrAuto : IEquatable, IEquatable, IEquatable +{ + private IntPtr ptr; + + /// Initializes a new instance of the struct. + /// The string value. + public StrPtrAuto(string s) => ptr = StringHelper.AllocString(s); + + /// Initializes a new instance of the struct. + /// Number of characters to reserve in memory. + public StrPtrAuto(uint charLen) => ptr = StringHelper.AllocChars(charLen); + + /// Gets a value indicating whether this instance is equivalent to null pointer or void*. + /// true if this instance is null; otherwise, false. + public bool IsNull => ptr == IntPtr.Zero; + + /// Assigns a string pointer value to the pointer. + /// The string pointer value. + public void Assign(IntPtr stringPtr) { Free(); ptr = stringPtr; } + + /// Assigns a new string value to the pointer. + /// The string value. + public void Assign(string s) => StringHelper.RefreshString(ref ptr, out var _, s); + + /// Assigns a new string value to the pointer. + /// The string value. + /// The character count allocated. + /// true if new memory was allocated for the string; false if otherwise. + public bool Assign(string s, out uint charsAllocated) => StringHelper.RefreshString(ref ptr, out charsAllocated, s); + + /// Assigns an integer to the pointer for uses such as LPSTR_TEXTCALLBACK. + /// The value to assign. + public void AssignConstant(int value) { Free(); ptr = (IntPtr)value; } + + /// Frees the unmanaged string memory. + public void Free() { StringHelper.FreeString(ptr); ptr = IntPtr.Zero; } + + /// Indicates whether the specified string is or an empty string (""). + /// + /// if the value parameter is or an empty string (""); otherwise, . + /// + public bool IsNullOrEmpty => ptr == IntPtr.Zero || StringHelper.GetString(ptr, CharSet.Auto, 1) == string.Empty; + + /// Performs an implicit conversion from to . + /// The instance. + /// The result of the conversion. + public static implicit operator string?(StrPtrAuto p) => p.IsNull ? null : p.ToString(); + + /// Performs an explicit conversion from to . + /// The instance. + /// The result of the conversion. + public static explicit operator IntPtr(StrPtrAuto p) => p.ptr; + + /// Performs an implicit conversion from to . + /// The pointer. + /// The result of the conversion. + public static implicit operator StrPtrAuto(IntPtr p) => new() { ptr = p }; + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// true if the specified is equal to this instance; otherwise, false. + public bool Equals(IntPtr other) => EqualityComparer.Default.Equals(ptr, other); + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// true if the specified is equal to this instance; otherwise, false. + public bool Equals(string? other) => EqualityComparer.Default.Equals(this, other); + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// true if the specified is equal to this instance; otherwise, false. + public bool Equals(StrPtrAuto other) => Equals(other.ptr); + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// true if the specified is equal to this instance; otherwise, false. + public override bool Equals(object obj) => obj switch + { + null => IsNull, + string s => Equals(s), + StrPtrAuto p => Equals(p), + IntPtr p => Equals(p), + _ => base.Equals(obj), + }; + + /// Returns a hash code for this instance. + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() => ptr.GetHashCode(); + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() => StringHelper.GetString(ptr) ?? "null"; + + /// Determines whether two specified instances of are equal. + /// The first pointer or handle to compare. + /// The second pointer or handle to compare. + /// if equals ; otherwise, . + public static bool operator ==(StrPtrAuto left, StrPtrAuto right) => left.Equals(right); + + /// Determines whether two specified instances of are not equal. + /// The first pointer or handle to compare. + /// The second pointer or handle to compare. + /// if does not equal ; otherwise, . + public static bool operator !=(StrPtrAuto left, StrPtrAuto right) => !left.Equals(right); +} + +/// A safe class that represents an object that is pinned in memory. +/// +file static class StringHelper +{ + /// Allocates a block of memory allocated from the unmanaged COM task allocator sufficient to hold the number of specified characters. + /// The number of characters, inclusive of the null terminator. + /// The method used to allocate the memory. + /// The character set. + /// The address of the block of memory allocated. + public static IntPtr AllocChars(uint count, Func memAllocator, CharSet charSet = CharSet.Auto) + { + if (count == 0) return IntPtr.Zero; + var sz = GetCharSize(charSet); + var ptr = memAllocator((int)count * sz); + if (count > 0) + { + if (sz == 1) + Marshal.WriteByte(ptr, 0); + else + Marshal.WriteInt16(ptr, 0); + } + return ptr; + } + + /// Allocates a block of memory allocated from the unmanaged COM task allocator sufficient to hold the number of specified characters. + /// The number of characters, inclusive of the null terminator. + /// The character set. + /// The address of the block of memory allocated. + public static IntPtr AllocChars(uint count, CharSet charSet = CharSet.Auto) => AllocChars(count, Marshal.AllocCoTaskMem, charSet); + + /// Copies the contents of a managed object to a block of memory allocated from the unmanaged COM task allocator. + /// The managed object to copy. + /// The character set. + /// The address, in unmanaged memory, where the parameter was copied to, or 0 if a null object was supplied. + public static IntPtr AllocSecureString(SecureString? s, CharSet charSet = CharSet.Auto) + { + if (s == null) return IntPtr.Zero; + if (GetCharSize(charSet) == 2) + return Marshal.SecureStringToCoTaskMemUnicode(s); + return Marshal.SecureStringToCoTaskMemAnsi(s); + } + + /// Copies the contents of a managed object to a block of memory allocated from a supplied allocation method. + /// The managed object to copy. + /// The character set. + /// The method used to allocate the memory. + /// The address, in unmanaged memory, where the parameter was copied to, or 0 if a null object was supplied. + public static IntPtr AllocSecureString(SecureString? s, CharSet charSet, Func memAllocator) => AllocSecureString(s, charSet, memAllocator, out _); + + /// Copies the contents of a managed object to a block of memory allocated from a supplied allocation method. + /// The managed object to copy. + /// The character set. + /// The method used to allocate the memory. + /// Returns the number of allocated bytes for the string. + /// The address, in unmanaged memory, where the parameter was copied to, or 0 if a null object was supplied. + public static IntPtr AllocSecureString(SecureString? s, CharSet charSet, Func memAllocator, out int allocatedBytes) + { + allocatedBytes = 0; + if (s == null) return IntPtr.Zero; + var chSz = GetCharSize(charSet); + var encoding = chSz == 2 ? Encoding.Unicode : Encoding.UTF8; + var hMem = AllocSecureString(s, charSet); + var str = chSz == 2 ? Marshal.PtrToStringUni(hMem) : Marshal.PtrToStringAnsi(hMem); + Marshal.FreeCoTaskMem(hMem); + if (str == null) return IntPtr.Zero; + var b = encoding.GetBytes(str); + var p = memAllocator(b.Length); + Marshal.Copy(b, 0, p, b.Length); + allocatedBytes = b.Length; + return p; + } + + /// Copies the contents of a managed String to a block of memory allocated from the unmanaged COM task allocator. + /// A managed string to be copied. + /// The character set. + /// The allocated memory block, or 0 if is null. + public static IntPtr AllocString(string? s, CharSet charSet = CharSet.Auto) => charSet == CharSet.Auto ? Marshal.StringToCoTaskMemAuto(s) : (charSet == CharSet.Unicode ? Marshal.StringToCoTaskMemUni(s) : Marshal.StringToCoTaskMemAnsi(s)); + + /// Copies the contents of a managed String to a block of memory allocated from a supplied allocation method. + /// A managed string to be copied. + /// The character set. + /// The method used to allocate the memory. + /// The allocated memory block, or 0 if is null. + public static IntPtr AllocString(string? s, CharSet charSet, Func memAllocator) => AllocString(s, charSet, memAllocator, out _); + + /// + /// Copies the contents of a managed String to a block of memory allocated from a supplied allocation method. + /// + /// A managed string to be copied. + /// The character set. + /// The method used to allocate the memory. + /// Returns the number of allocated bytes for the string. + /// The allocated memory block, or 0 if is null. + public static IntPtr AllocString(string? s, CharSet charSet, Func memAllocator, out int allocatedBytes) + { + if (s == null) { allocatedBytes = 0; return IntPtr.Zero; } + var b = s.GetBytes(true, charSet); + var p = memAllocator(b.Length); + Marshal.Copy(b, 0, p, allocatedBytes = b.Length); + return p; + } + + /// + /// Zeros out the allocated memory behind a secure string and then frees that memory. + /// + /// The address of the memory to be freed. + /// The size in bytes of the memory pointed to by . + /// The memory freer. + public static void FreeSecureString(IntPtr ptr, int sizeInBytes, Action memFreer) + { + if (IsValue(ptr)) return; + var b = new byte[sizeInBytes]; + Marshal.Copy(b, 0, ptr, b.Length); + memFreer(ptr); + } + + /// Frees a block of memory allocated by the unmanaged COM task memory allocator for a string. + /// The address of the memory to be freed. + /// The character set of the string. + public static void FreeString(IntPtr ptr, CharSet charSet = CharSet.Auto) + { + if (IsValue(ptr)) return; + if (GetCharSize(charSet) == 2) + Marshal.ZeroFreeCoTaskMemUnicode(ptr); + else + Marshal.ZeroFreeCoTaskMemAnsi(ptr); + } + + /// Gets the encoded bytes for a string including an optional null terminator. + /// The string value to convert. + /// if set to true include a null terminator at the end of the string in the resulting byte array. + /// The character set. + /// A byte array including encoded as per and the optional null terminator. + public static byte[] GetBytes(this string value, bool nullTerm = true, CharSet charSet = CharSet.Auto) => + GetBytes(value, GetCharSize(charSet) == 1 ? Encoding.UTF8 : Encoding.Unicode, nullTerm); + + /// Gets the encoded bytes for a string including an optional null terminator. + /// The string value to convert. + /// The character encoding. + /// if set to true include a null terminator at the end of the string in the resulting byte array. + /// A byte array including encoded as per and the optional null terminator. + public static byte[] GetBytes(this string value, Encoding enc, bool nullTerm = true) + { + var chSz = GetCharSize(enc); + var ret = new byte[enc.GetByteCount(value) + (nullTerm ? chSz : 0)]; + enc.GetBytes(value, 0, value.Length, ret, 0); + if (nullTerm) + enc.GetBytes(new[] { '\0' }, 0, 1, ret, ret.Length - chSz); + return ret; + } + + /// Gets the number of bytes required to store the string. + /// The string value. + /// if set to true include a null terminator at the end of the string in the count if does not equal null. + /// The character set. + /// The number of bytes required to store . Returns 0 if is null. + public static int GetByteCount(this string value, bool nullTerm = true, CharSet charSet = CharSet.Auto) => + GetByteCount(value, GetCharSize(charSet) == 1 ? Encoding.UTF8 : Encoding.Unicode, nullTerm); + + /// Gets the number of bytes required to store the string. + /// The string value. + /// The character encoding. + /// if set to true include a null terminator at the end of the string in the count if does not equal null. + /// The number of bytes required to store . Returns 0 if is null. + public static int GetByteCount(this string value, Encoding enc, bool nullTerm = true) => + value is null ? 0 : enc.GetByteCount(value) + (nullTerm ? GetCharSize(enc) : 0); + + /// Gets the size of a character defined by the supplied . + /// The character set to size. + /// The size of a standard character, in bytes, from . + public static int GetCharSize(CharSet charSet = CharSet.Auto) => charSet == CharSet.Auto ? Marshal.SystemDefaultCharSize : (charSet == CharSet.Unicode ? UnicodeEncoding.CharSize : 1); + + /// Gets the size of a character defined by the supplied . + /// The character encoding type. + /// The size of a standard character, in bytes, from . + public static int GetCharSize(Encoding enc) => enc.GetByteCount(new[] { '\0' }); + + /// + /// Allocates a managed String and copies all characters up to the first null character or the end of the allocated memory pool from a string stored in unmanaged memory into it. + /// + /// The address of the first character. + /// The character set of the string. + /// If known, the total number of bytes allocated to the native memory in . + /// + /// A managed string that holds a copy of the unmanaged string if the value of the parameter is not null; + /// otherwise, this method returns null. + /// + public static string? GetString(IntPtr ptr, CharSet charSet = CharSet.Auto, long allocatedBytes = long.MaxValue) + { + if (IsValue(ptr)) return null; + var sb = new StringBuilder(); + unsafe + { + var chkLen = 0L; + if (GetCharSize(charSet) == 1) + { + for (var uptr = (byte*)ptr; chkLen < allocatedBytes && *uptr != 0; chkLen++, uptr++) + sb.Append((char)*uptr); + } + else + { + for (var uptr = (ushort*)ptr; chkLen + 2 <= allocatedBytes && *uptr != 0; chkLen += 2, uptr++) + sb.Append((char)*uptr); + } + } + return sb.ToString(); + } + + /// + /// Allocates a managed String and copies all characters up to the first null character or at most characters from a string stored in unmanaged memory into it. + /// + /// The address of the first character. + /// The number of characters to copy. + /// The character set of the string. + /// + /// A managed string that holds a copy of the unmanaged string if the value of the parameter is not null; + /// otherwise, this method returns null. + /// + public static string? GetString(IntPtr ptr, int length, CharSet charSet = CharSet.Auto) => GetString(ptr, charSet, length * GetCharSize(charSet)); + + /// Indicates whether a specified string is , empty, or consists only of white-space characters. + /// The string to test. + /// + /// if the parameter is or , or if + /// value consists exclusively of white-space characters. + /// + public static bool IsNullOrWhiteSpace(string? value) => value is null || value.All(c => char.IsWhiteSpace(c)); + + /// Refreshes the memory block from the unmanaged COM task allocator and copies the contents of a new managed String. + /// The address of the first character. + /// Receives the new character length of the allocated memory block. + /// A managed string to be copied. + /// The character set of the string. + /// true if the memory block was reallocated; false if set to null. + public static bool RefreshString(ref IntPtr ptr, out uint charLen, string? s, CharSet charSet = CharSet.Auto) + { + FreeString(ptr, charSet); + ptr = AllocString(s, charSet); + charLen = s == null ? 0U : (uint)s.Length + 1; + return s != null; + } + + /// Writes the specified string to a pointer to allocated memory. + /// The string value. + /// The pointer to the allocated memory. + /// The resulting number of bytes written. + /// if set to true include a null terminator at the end of the string in the count if does not equal null. + /// The character set of the string. + /// If known, the total number of bytes allocated to the native memory in . + public static void Write(string? value, IntPtr ptr, out int byteCnt, bool nullTerm = true, CharSet charSet = CharSet.Auto, long allocatedBytes = long.MaxValue) + { + if (value is null) + { + byteCnt = 0; + return; + } + if (ptr == IntPtr.Zero) throw new ArgumentNullException(nameof(ptr)); + var bytes = GetBytes(value, nullTerm, charSet); + if (bytes.Length > allocatedBytes) + throw new ArgumentOutOfRangeException(nameof(allocatedBytes)); + byteCnt = bytes.Length; + Marshal.Copy(bytes, 0, ptr, byteCnt); + } + + private static bool IsValue(IntPtr ptr) => ptr.ToInt64() >> 16 == 0; +} + +/// +/// An error code returned by the CommDlgExtendedError function. +/// +/// +/// +/// +/// Error code +/// Meaning +/// +/// +/// CDERR +/// General error codes that can be returned for any of the common dialog box functions. +/// +/// +/// PDERR +/// Error codes returned for the PrintDlg function. +/// +/// +/// +/// +/// CFERR +/// Error codes returned for the ChooseFont function. +/// +/// +/// FNERR +/// Error codes returned for the GetOpenFileName and GetSaveFileName functions. +/// +/// +/// FRERR +/// Error codes returned for the FindText and ReplaceText functions. +/// +/// +/// +file enum ERR : uint +{ + /// + /// The dialog box could not be created. The common dialog box function's call to the DialogBox function failed. For example, + /// this error occurs if the common dialog box call specifies an invalid window handle. + /// + CDERR_DIALOGFAILURE = 0xFFFF, + + /// + /// The common dialog box function failed to find a specified resource. + /// + CDERR_FINDRESFAILURE = 0x0006, + + /// + /// The common dialog box function failed during initialization. This error often occurs when sufficient memory is not available. + /// + CDERR_INITIALIZATION = 0x0002, + + /// + /// The common dialog box function failed to load a specified resource. + /// + CDERR_LOADRESFAILURE = 0x0007, + + /// + /// The common dialog box function failed to load a specified string. + /// + CDERR_LOADSTRFAILURE = 0x0005, + + /// + /// The common dialog box function failed to lock a specified resource. + /// + CDERR_LOCKRESFAILURE = 0x0008, + + /// + /// The common dialog box function was unable to allocate memory for internal structures. + /// + CDERR_MEMALLOCFAILURE = 0x0009, + + /// + /// The common dialog box function was unable to lock the memory associated with a handle. + /// + CDERR_MEMLOCKFAILURE = 0x000A, + + /// + /// The ENABLETEMPLATE flag was set in the Flags member of the initialization structure for the corresponding common + /// dialog box, but you failed to provide a corresponding instance handle. + /// + CDERR_NOHINSTANCE = 0x0004, + + /// + /// The ENABLEHOOK flag was set in the Flags member of the initialization structure for the corresponding common + /// dialog box, but you failed to provide a pointer to a corresponding hook procedure. + /// + CDERR_NOHOOK = 0x000B, + + /// + /// The ENABLETEMPLATE flag was set in the Flags member of the initialization structure for the corresponding common dialog + /// box, but you failed to provide a corresponding template. + /// + CDERR_NOTEMPLATE = 0x0003, + + /// + /// The RegisterWindowMessage function returned an error code when it was called by the common dialog box function. + /// + CDERR_REGISTERMSGFAIL = 0x000C, + + /// + /// The lStructSize member of the initialization structure for the corresponding common dialog box is invalid. + /// + CDERR_STRUCTSIZE = 0x0001, + + /// + /// The PrintDlg function failed when it attempted to create an information context. + /// + PDERR_CREATEICFAILURE = 0x100A, + + /// + /// You called the PrintDlg function with the DN_DEFAULTPRN flag specified in the wDefault member of the DEVNAMES structure, + /// but the printer described by the other structure members did not match the current default printer. This error occurs when + /// you store the DEVNAMES structure, and the user changes the default printer by using the Control Panel. + /// To use the printer described by the DEVNAMES structure, clear the DN_DEFAULTPRN flag and call PrintDlg again. + /// To use the default printer, replace the DEVNAMES structure (and the structure, if one exists) with NULL; and call PrintDlg again. + /// + PDERR_DEFAULTDIFFERENT = 0x100C, + + /// + /// The data in the DEVMODE and DEVNAMES structures describes two different printers. + /// + PDERR_DNDMMISMATCH = 0x1009, + + /// + /// The printer driver failed to initialize a DEVMODE structure. + /// + PDERR_GETDEVMODEFAIL = 0x1005, + + /// + /// The PrintDlg function failed during initialization, and there is no more specific extended error code to describe the failure. + /// This is the generic default error code for the function. + /// + PDERR_INITFAILURE = 0x1006, + + /// + /// The PrintDlg function failed to load the device driver for the specified printer. + /// + PDERR_LOADDRVFAILURE = 0x1004, + + /// + /// A default printer does not exist. + /// + PDERR_NODEFAULTPRN = 0x1008, + + /// + /// No printer drivers were found. + /// + PDERR_NODEVICES = 0x1007, + + /// + /// The PrintDlg function failed to parse the strings in the [devices] section of the WIN.INI file. + /// + PDERR_PARSEFAILURE = 0x1002, + + /// + /// The [devices] section of the WIN.INI file did not contain an entry for the requested printer. + /// + PDERR_PRINTERNOTFOUND = 0x100B, + + /// + /// The PD_RETURNDEFAULT flag was specified in the Flags member of the PRINTDLG structure, but the hDevMode or hDevNames member was not NULL. + /// + PDERR_RETDEFFAILURE = 0x1003, + + /// + /// The PrintDlg function failed to load the required resources. + /// + PDERR_SETUPFAILURE = 0x1001, + + /// + /// The size specified in the nSizeMax member of the CHOOSEFONT structure is less than the size specified in the nSizeMin member. + /// + CFERR_MAXLESSTHANMIN = 0x2002, + + /// + /// No fonts exist. + /// + CFERR_NOFONTS = 0x2001, + + /// + /// The buffer pointed to by the lpstrFile member of the OPENFILENAME structure is too small for the file name specified + /// by the user. The first two bytes of the lpstrFile buffer contain an integer value specifying the size required to receive + /// the full name, in characters. + /// + FNERR_BUFFERTOOSMALL = 0x3003, + + /// + /// A file name is invalid. + /// + FNERR_INVALIDFILENAME = 0x3002, + + /// + /// An attempt to subclass a list box failed because sufficient memory was not available. + /// + FNERR_SUBCLASSFAILURE = 0x3001, + + /// + /// A member of the FINDREPLACE structure points to an invalid buffer. + /// + FRERR_BUFFERLENGTHZERO = 0x4001, +} + +/// +/// A set of bit flags you can use to initialize the dialog box. When the dialog box returns, it sets these flags to indicate the +/// user's input. +/// +[Flags] +file enum OFN +{ + /// + /// The File Name list box allows multiple selections. If you also set the OFN_EXPLORER flag, the dialog box uses the + /// Explorer-style user interface; otherwise, it uses the old-style user interface. + /// + /// If the user selects more than one file, the lpstrFile buffer returns the path to the current directory followed by the file + /// names of the selected files. The nFileOffset member is the offset, in bytes or characters, to the first file name, and the + /// nFileExtension member is not used. For Explorer-style dialog boxes, the directory and file name strings are NULL separated, + /// with an extra NULL character after the last file name. This format enables the Explorer-style dialog boxes to return long + /// file names that include spaces. For old-style dialog boxes, the directory and file name strings are separated by spaces and + /// the function uses short file names for file names with spaces. You can use the FindFirstFile function to convert between + /// long and short file names. + /// + /// + /// If you specify a custom template for an old-style dialog box, the definition of the File Name list box must contain the + /// LBS_EXTENDEDSEL value. + /// + /// + OFN_ALLOWMULTISELECT = 0x00000200, + + /// + /// If the user specifies a file that does not exist, this flag causes the dialog box to prompt the user for permission to + /// create the file. If the user chooses to create the file, the dialog box closes and the function returns the specified name; + /// otherwise, the dialog box remains open. If you use this flag with the OFN_ALLOWMULTISELECT flag, the dialog box allows the + /// user to specify only one nonexistent file. + /// + OFN_CREATEPROMPT = 0x00002000, + + /// + /// Prevents the system from adding a link to the selected file in the file system directory that contains the user's most + /// recently used documents. To retrieve the location of this directory, call the SHGetSpecialFolderLocation function with the + /// CSIDL_RECENT flag. + /// + OFN_DONTADDTORECENT = 0x02000000, + + /// Enables the hook function specified in the lpfnHook member. + OFN_ENABLEHOOK = 0x00000020, + + /// + /// Causes the dialog box to send CDN_INCLUDEITEM notification messages to your OFNHookProc hook procedure when the user opens a + /// folder. The dialog box sends a notification for each item in the newly opened folder. These messages enable you to control + /// which items the dialog box displays in the folder's item list. + /// + OFN_ENABLEINCLUDENOTIFY = 0x00400000, + + /// + /// Enables the Explorer-style dialog box to be resized using either the mouse or the keyboard. By default, the Explorer-style + /// Open and Save As dialog boxes allow the dialog box to be resized regardless of whether this flag is set. This flag is + /// necessary only if you provide a hook procedure or custom template. The old-style dialog box does not permit resizing. + /// + OFN_ENABLESIZING = 0x00800000, + + /// + /// The lpTemplateName member is a pointer to the name of a dialog template resource in the module identified by the hInstance + /// member. If the OFN_EXPLORER flag is set, the system uses the specified template to create a dialog box that is a child of + /// the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses the template to create an + /// old-style dialog box that replaces the default dialog box. + /// + OFN_ENABLETEMPLATE = 0x00000040, + + /// + /// The hInstance member identifies a data block that contains a preloaded dialog box template. The system ignores + /// lpTemplateName if this flag is specified. If the OFN_EXPLORER flag is set, the system uses the specified template to create + /// a dialog box that is a child of the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses + /// the template to create an old-style dialog box that replaces the default dialog box. + /// + OFN_ENABLETEMPLATEHANDLE = 0x00000080, + + /// + /// Indicates that any customizations made to the Open or Save As dialog box use the Explorer-style customization methods. For + /// more information, see Explorer-Style Hook Procedures and Explorer-Style Custom Templates. + /// + /// By default, the Open and Save As dialog boxes use the Explorer-style user interface regardless of whether this flag is set. + /// This flag is necessary only if you provide a hook procedure or custom template, or set the OFN_ALLOWMULTISELECT flag. + /// + /// + /// If you want the old-style user interface, omit the OFN_EXPLORER flag and provide a replacement old-style template or hook + /// procedure. If you want the old style but do not need a custom template or hook procedure, simply provide a hook procedure + /// that always returns FALSE. + /// + /// + OFN_EXPLORER = 0x00080000, + + /// + /// The user typed a file name extension that differs from the extension specified by lpstrDefExt. The function does not use + /// this flag if lpstrDefExt is NULL. + /// + OFN_EXTENSIONDIFFERENT = 0x00000400, + + /// + /// The user can type only names of existing files in the File Name entry field. If this flag is specified and the user enters + /// an invalid name, the dialog box procedure displays a warning in a message box. If this flag is specified, the + /// OFN_PATHMUSTEXIST flag is also used. This flag can be used in an Open dialog box. It cannot be used with a Save As dialog box. + /// + OFN_FILEMUSTEXIST = 0x00001000, + + /// + /// Forces the showing of system and hidden files, thus overriding the user setting to show or not show hidden files. However, a + /// file that is marked both system and hidden is not shown. + /// + OFN_FORCESHOWHIDDEN = 0x10000000, + + /// Hides the Read Only check box. + OFN_HIDEREADONLY = 0x00000004, + + /// + /// For old-style dialog boxes, this flag causes the dialog box to use long file names. If this flag is not specified, or if the + /// OFN_ALLOWMULTISELECT flag is also set, old-style dialog boxes use short file names (8.3 format) for file names with spaces. + /// Explorer-style dialog boxes ignore this flag and always display long file names. + /// + OFN_LONGNAMES = 0x00200000, + + /// + /// Restores the current directory to its original value if the user changed the directory while searching for files. + /// This flag is ineffective for GetOpenFileName. + /// + OFN_NOCHANGEDIR = 0x00000008, + + /// + /// Directs the dialog box to return the path and file name of the selected shortcut (.LNK) file. If this value is not + /// specified, the dialog box returns the path and file name of the file referenced by the shortcut. + /// + OFN_NODEREFERENCELINKS = 0x00100000, + + /// + /// For old-style dialog boxes, this flag causes the dialog box to use short file names (8.3 format). Explorer-style dialog + /// boxes ignore this flag and always display long file names. + /// + OFN_NOLONGNAMES = 0x00040000, + + /// Hides and disables the Network button. + OFN_NONETWORKBUTTON = 0x00020000, + + /// The returned file does not have the Read Only check box selected and is not in a write-protected directory. + OFN_NOREADONLYRETURN = 0x00008000, + + /// + /// The file is not created before the dialog box is closed. This flag should be specified if the application saves the file on + /// a create-nonmodify network share. When an application specifies this flag, the library does not check for write protection, + /// a full disk, an open drive door, or network protection. Applications using this flag must perform file operations carefully, + /// because a file cannot be reopened once it is closed. + /// + OFN_NOTESTFILECREATE = 0x00010000, + + /// + /// The common dialog boxes allow invalid characters in the returned file name. Typically, the calling application uses a hook + /// procedure that checks the file name by using the FILEOKSTRING message. If the text box in the edit control is empty or + /// contains nothing but spaces, the lists of files and directories are updated. If the text box in the edit control contains + /// anything else, nFileOffset and nFileExtension are set to values generated by parsing the text. No default extension is added + /// to the text, nor is text copied to the buffer specified by lpstrFileTitle. If the value specified by nFileOffset is less + /// than zero, the file name is invalid. Otherwise, the file name is valid, and nFileExtension and nFileOffset can be used as if + /// the OFN_NOVALIDATE flag had not been specified. + /// + OFN_NOVALIDATE = 0x00000100, + + /// + /// Causes the Save As dialog box to generate a message box if the selected file already exists. The user must confirm whether + /// to overwrite the file. + /// + OFN_OVERWRITEPROMPT = 0x00000002, + + /// + /// The user can type only valid paths and file names. If this flag is used and the user types an invalid path and file name in + /// the File Name entry field, the dialog box function displays a warning in a message box. + /// + OFN_PATHMUSTEXIST = 0x00000800, + + /// + /// Causes the Read Only check box to be selected initially when the dialog box is created. This flag indicates the state of the + /// Read Only check box when the dialog box is closed. + /// + OFN_READONLY = 0x00000001, + + /// + /// Specifies that if a call to the OpenFile function fails because of a network sharing violation, the error is ignored and the + /// dialog box returns the selected file name. If this flag is not set, the dialog box notifies your hook procedure when a + /// network sharing violation occurs for the file name specified by the user. If you set the OFN_EXPLORER flag, the dialog box + /// sends the CDN_SHAREVIOLATION message to the hook procedure. If you do not set OFN_EXPLORER, the dialog box sends the + /// SHAREVISTRING registered message to the hook procedure. + /// + OFN_SHAREAWARE = 0x00004000, + + /// + /// Causes the dialog box to display the Help button. The hwndOwner member must specify the window to receive the HELPMSGSTRING + /// registered messages that the dialog box sends when the user clicks the Help button. An Explorer-style dialog box sends a + /// CDN_HELP notification message to your hook procedure when the user clicks the Help button. + /// + OFN_SHOWHELP = 0x00000010, +} + +/// A set of bit flags you can use to initialize the dialog box. +[Flags] +file enum OFN_EX +{ + /// + /// If this flag is set, the places bar is not displayed. If this flag is not set, Explorer-style dialog boxes include a places + /// bar containing icons for commonly-used folders, such as Favorites and Desktop. + /// + OFN_EX_NOPLACESBAR = 0x00000001, +} + +/// +/// +/// [Starting with Windows Vista, the Open and Save As common dialog boxes have been superseded by the Common Item +/// Dialog. We recommended that you use the Common Item Dialog API instead of these dialog boxes from the Common Dialog Box Library.] +/// +/// +/// Receives notification messages sent from the dialog box. The function also receives messages for any additional controls that +/// you defined by specifying a child dialog template. The OFNHookProc hook procedure is an application-defined or library-defined +/// callback function that is used with the Explorer-style Open and Save As dialog boxes. +/// +/// +/// The LPOFNHOOKPROC type defines a pointer to this callback function. OFNHookProc is a placeholder for the +/// application-defined function name. +/// +/// +/// +/// A handle to the child dialog box of the Open or Save As dialog box. Use the GetParent function to get the handle +/// to the Open or Save As dialog box. +/// +/// The identifier of the message being received. +/// Additional information about the message. The exact meaning depends on the value of the Arg2 parameter. +/// +/// Additional information about the message. The exact meaning depends on the value of the Arg2 parameter. If the Arg2 parameter +/// indicates the WM_INITDIALOG message, Arg4 is a pointer to an OPENFILENAME structure containing the values specified when the +/// dialog box was created. +/// +/// +/// If the hook procedure returns zero, the default dialog box procedure processes the message. +/// If the hook procedure returns a nonzero value, the default dialog box procedure ignores the message. +/// +/// For the CDN_SHAREVIOLATION and CDN_FILEOK notification messages, the hook procedure should return a nonzero value to indicate +/// that it has used the SetWindowLong function to set a nonzero DWL_MSGRESULT value. +/// +/// +/// +/// +/// If you do not specify the OFN_EXPLORER flag when you create an Open or Save As dialog box, and you want a +/// hook procedure, you must use an old-style OFNHookProcOldStyle hook procedure. In this case, the dialog box will have the +/// old-style user interface. +/// +/// +/// When you use the GetOpenFileName or GetSaveFileName functions to create an Explorer-style Open or Save As dialog +/// box, you can provide an OFNHookProc hook procedure. To enable the hook procedure, use the OPENFILENAME structure that you passed +/// to the dialog creation function. Specify the pointer to the hook procedure in the lpfnHook member and specify the +/// OFN_ENABLEHOOK flag in the Flags member. +/// +/// +/// If you provide a hook procedure for an Explorer-style common dialog box, the system creates a dialog box that is a child of the +/// default dialog box. The hook procedure acts as the dialog procedure for the child dialog. This child dialog is based on the +/// template you specified in the OPENFILENAME structure, or it is a default child dialog if no template is specified. The child +/// dialog is created when the default dialog procedure is processing its WM_INITDIALOG message. After the child dialog processes +/// its own WM_INITDIALOG message, the default dialog procedure moves the standard controls, if necessary, to make room for +/// any additional controls of the child dialog. The system then sends the CDN_INITDONE notification message to the hook procedure. +/// +/// +/// The hook procedure does not receive messages intended for the standard controls of the default dialog box. You can subclass the +/// standard controls, but this is discouraged because it may make your application incompatible with later versions. However, the +/// Explorer-style common dialog boxes provide a set of messages that the hook procedure can use to monitor and control the dialog. +/// These include a set of notification messages sent from the dialog, as well as messages that you can send to retrieve information +/// from the dialog. For a complete list of these messages, see Explorer-Style Hook Procedures. +/// +/// +/// If the hook procedure processes the WM_CTLCOLORDLG message, it must return a valid brush handle to painting the background of +/// the dialog box. In general, if it processes any WM_CTLCOLOR* message, it must return a valid brush handle to painting the +/// background of the specified control. +/// +/// +/// Do not call the EndDialog function from the hook procedure. Instead, the hook procedure can call the PostMessage function to +/// post a WM_COMMAND message with the IDCANCEL value to the dialog box procedure. Posting IDCANCEL closes the dialog +/// box and causes the dialog box function to return FALSE. If you need to know why the hook procedure closed the dialog box, +/// you must provide your own communication mechanism between the hook procedure and your application. +/// +/// +// https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nc-commdlg-lpofnhookproc LPOFNHOOKPROC Lpofnhookproc; UINT_PTR +// Lpofnhookproc( HWND Arg1, UINT Arg2, WPARAM Arg3, LPARAM Arg4 ) {...} +[UnmanagedFunctionPointer(CallingConvention.Winapi)] +file delegate IntPtr LPOFNHOOKPROC(IntPtr Arg1, uint Arg2, IntPtr Arg3, IntPtr Arg4); + +/// +/// +/// [Starting with Windows Vista, the Open and Save As common dialog boxes have been superseded by the Common Item +/// Dialog. We recommended that you use the Common Item Dialog API instead of these dialog boxes from the Common Dialog Box Library.] +/// +/// +/// Contains information that the GetOpenFileName and GetSaveFileName functions use to initialize an Open or Save As +/// dialog box. After the user closes the dialog box, the system returns information about the user's selection in this structure. +/// +/// +/// +/// For compatibility reasons, the Places Bar is hidden if Flags is set to OFN_ENABLEHOOK and lStructSize is OPENFILENAME_SIZE_VERSION_400. +/// +// https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamea typedef struct tagOFNA { DWORD lStructSize; +// HWND hwndOwner; HINSTANCE hInstance; LPCSTR lpstrFilter; LPSTR lpstrCustomFilter; DWORD nMaxCustFilter; DWORD nFilterIndex; LPSTR +// lpstrFile; DWORD nMaxFile; LPSTR lpstrFileTitle; DWORD nMaxFileTitle; LPCSTR lpstrInitialDir; LPCSTR lpstrTitle; DWORD Flags; +// WORD nFileOffset; WORD nFileExtension; LPCSTR lpstrDefExt; LPARAM lCustData; LPOFNHOOKPROC lpfnHook; LPCSTR lpTemplateName; +// LPEDITMENU lpEditInfo; LPCSTR lpstrPrompt; void *pvReserved; DWORD dwReserved; DWORD FlagsEx; } OPENFILENAMEA, *LPOPENFILENAMEA; +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] +file struct OPENFILENAME +{ + /// + /// Type: DWORD + /// The length, in bytes, of the structure. Use + /// sizeof (OPENFILENAME) + /// for this parameter. + /// + /// + public uint lStructSize; + + /// + /// Type: HWND + /// + /// A handle to the window that owns the dialog box. This member can be any valid window handle, or it can be NULL if the + /// dialog box has no owner. + /// + /// + public IntPtr hwndOwner; // TODO: HWND + + /// + /// Type: HINSTANCE + /// + /// If the OFN_ENABLETEMPLATEHANDLE flag is set in the Flags member, hInstance is a handle to a memory + /// object containing a dialog box template. If the OFN_ENABLETEMPLATE flag is set, hInstance is a handle to a + /// module that contains a dialog box template named by the lpTemplateName member. If neither flag is set, this member is + /// ignored. If the OFN_EXPLORER flag is set, the system uses the specified template to create a dialog box that is a + /// child of the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses the template to + /// create an old-style dialog box that replaces the default dialog box. + /// + /// + public IntPtr hInstance; // TODO: HINSTANCE + + /// + /// Type: LPCTSTR + /// + /// A buffer containing pairs of null-terminated filter strings. The last string in the buffer must be terminated by two + /// NULL characters. + /// + /// + /// The first string in each pair is a display string that describes the filter (for example, "Text Files"), and the second + /// string specifies the filter pattern (for example, ".TXT"). To specify multiple filter patterns for a single display string, + /// use a semicolon to separate the patterns (for example, ".TXT;.DOC;.BAK"). A pattern string can be a combination of valid + /// file name characters and the asterisk (*) wildcard character. Do not include spaces in the pattern string. + /// + /// + /// The system does not change the order of the filters. It displays them in the File Types combo box in the order + /// specified in lpstrFilter. + /// + /// If lpstrFilter is NULL, the dialog box does not display any filters. + /// + /// In the case of a shortcut, if no filter is set, GetOpenFileName and GetSaveFileName retrieve the name of the .lnk file, not + /// its target. This behavior is the same as setting the OFN_NODEREFERENCELINKS flag in the Flags member. To + /// retrieve a shortcut's target without filtering, use the string + /// "All Files\0*.*\0\0" + /// . + /// + /// + [MarshalAs(UnmanagedType.LPTStr)] + public string lpstrFilter; + + /// + /// Type: LPTSTR + /// + /// A static buffer that contains a pair of null-terminated filter strings for preserving the filter pattern chosen by the user. + /// The first string is your display string that describes the custom filter, and the second string is the filter pattern + /// selected by the user. The first time your application creates the dialog box, you specify the first string, which can be any + /// nonempty string. When the user selects a file, the dialog box copies the current filter pattern to the second string. The + /// preserved filter pattern can be one of the patterns specified in the lpstrFilter buffer, or it can be a filter + /// pattern typed by the user. The system uses the strings to initialize the user-defined file filter the next time the dialog + /// box is created. If the nFilterIndex member is zero, the dialog box uses the custom filter. + /// + /// If this member is NULL, the dialog box does not preserve user-defined filter patterns. + /// + /// If this member is not NULL, the value of the nMaxCustFilter member must specify the size, in characters, of + /// the lpstrCustomFilter buffer. + /// + /// + public StrPtrAuto lpstrCustomFilter; + + /// + /// Type: DWORD + /// + /// The size, in characters, of the buffer identified by lpstrCustomFilter. This buffer should be at least 40 characters + /// long. This member is ignored if lpstrCustomFilter is NULL or points to a NULL string. + /// + /// + public uint nMaxCustFilter; + + /// + /// Type: DWORD + /// + /// The index of the currently selected filter in the File Types control. The buffer pointed to by lpstrFilter + /// contains pairs of strings that define the filters. The first pair of strings has an index value of 1, the second pair 2, and + /// so on. An index of zero indicates the custom filter specified by lpstrCustomFilter. You can specify an index on input + /// to indicate the initial filter description and filter pattern for the dialog box. When the user selects a file, + /// nFilterIndex returns the index of the currently displayed filter. If nFilterIndex is zero and + /// lpstrCustomFilter is NULL, the system uses the first filter in the lpstrFilter buffer. If all three + /// members are zero or NULL, the system does not use any filters and does not show any files in the file list control of + /// the dialog box. + /// + /// + public uint nFilterIndex; + + /// + /// Type: LPTSTR + /// + /// The file name used to initialize the File Name edit control. The first character of this buffer must be NULL + /// if initialization is not necessary. When the GetOpenFileName or GetSaveFileName function returns successfully, this buffer + /// contains the drive designator, path, file name, and extension of the selected file. + /// + /// + /// If the OFN_ALLOWMULTISELECT flag is set and the user selects multiple files, the buffer contains the current + /// directory followed by the file names of the selected files. For Explorer-style dialog boxes, the directory and file name + /// strings are NULL separated, with an extra NULL character after the last file name. For old-style dialog boxes, + /// the strings are space separated and the function uses short file names for file names with spaces. You can use the + /// FindFirstFile function to convert between long and short file names. If the user selects only one file, the lpstrFile + /// string does not have a separator between the path and file name. + /// + /// + /// If the buffer is too small, the function returns FALSE and the CommDlgExtendedError function returns + /// FNERR_BUFFERTOOSMALL. In this case, the first two bytes of the lpstrFile buffer contain the required size, in + /// bytes or characters. + /// + /// + public StrPtrAuto lpstrFile; + + /// + /// Type: DWORD + /// + /// The size, in characters, of the buffer pointed to by lpstrFile. The buffer must be large enough to store the path and + /// file name string or strings, including the terminating NULL character. The GetOpenFileName and GetSaveFileName + /// functions return FALSE if the buffer is too small to contain the file information. The buffer should be at least 256 + /// characters long. + /// + /// + public uint nMaxFile; + + /// + /// Type: LPTSTR + /// The file name and extension (without path information) of the selected file. This member can be NULL. + /// + public StrPtrAuto lpstrFileTitle; + + /// + /// Type: DWORD + /// + /// The size, in characters, of the buffer pointed to by lpstrFileTitle. This member is ignored if lpstrFileTitle + /// is NULL. + /// + /// + public uint nMaxFileTitle; + + /// + /// Type: LPCTSTR + /// The initial directory. The algorithm for selecting the initial directory varies on different platforms. + /// Windows 7: + /// + /// + /// + /// If lpstrInitialDir has the same value as was passed the first time the application used an Open or Save + /// As dialog box, the path most recently selected by the user is used as the initial directory. + /// + /// + /// + /// Otherwise, if lpstrFile contains a path, that path is the initial directory. + /// + /// + /// Otherwise, if lpstrInitialDir is not NULL, it specifies the initial directory. + /// + /// + /// + /// If lpstrInitialDir is NULL and the current directory contains any files of the specified filter types, the + /// initial directory is the current directory. + /// + /// + /// + /// Otherwise, the initial directory is the personal files directory of the current user. + /// + /// + /// Otherwise, the initial directory is the Desktop folder. + /// + /// + /// Windows 2000/XP/Vista: + /// + /// + /// If lpstrFile contains a path, that path is the initial directory. + /// + /// + /// Otherwise, lpstrInitialDir specifies the initial directory. + /// + /// + /// + /// Otherwise, if the application has used an Open or Save As dialog box in the past, the path most recently used + /// is selected as the initial directory. However, if an application is not run for a long time, its saved selected path is discarded. + /// + /// + /// + /// + /// If lpstrInitialDir is NULL and the current directory contains any files of the specified filter types, the + /// initial directory is the current directory. + /// + /// + /// + /// Otherwise, the initial directory is the personal files directory of the current user. + /// + /// + /// Otherwise, the initial directory is the Desktop folder. + /// + /// + /// + public StrPtrAuto lpstrInitialDir; + + /// + /// Type: LPCTSTR + /// + /// A string to be placed in the title bar of the dialog box. If this member is NULL, the system uses the default title + /// (that is, Save As or Open). + /// + /// + public StrPtrAuto lpstrTitle; + + /// + /// Type: DWORD + /// + /// A set of bit flags you can use to initialize the dialog box. When the dialog box returns, it sets these flags to indicate + /// the user's input. This member can be a combination of the following flags. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// OFN_ALLOWMULTISELECT 0x00000200 + /// + /// The File Name list box allows multiple selections. If you also set the OFN_EXPLORER flag, the dialog box uses the + /// Explorer-style user interface; otherwise, it uses the old-style user interface. If the user selects more than one file, the + /// lpstrFile buffer returns the path to the current directory followed by the file names of the selected files. The nFileOffset + /// member is the offset, in bytes or characters, to the first file name, and the nFileExtension member is not used. For + /// Explorer-style dialog boxes, the directory and file name strings are NULL separated, with an extra NULL character after the + /// last file name. This format enables the Explorer-style dialog boxes to return long file names that include spaces. For + /// old-style dialog boxes, the directory and file name strings are separated by spaces and the function uses short file names + /// for file names with spaces. You can use the FindFirstFile function to convert between long and short file names. If you + /// specify a custom template for an old-style dialog box, the definition of the File Name list box must contain the + /// LBS_EXTENDEDSEL value. + /// + /// + /// + /// OFN_CREATEPROMPT 0x00002000 + /// + /// If the user specifies a file that does not exist, this flag causes the dialog box to prompt the user for permission to + /// create the file. If the user chooses to create the file, the dialog box closes and the function returns the specified name; + /// otherwise, the dialog box remains open. If you use this flag with the OFN_ALLOWMULTISELECT flag, the dialog box allows the + /// user to specify only one nonexistent file. + /// + /// + /// + /// OFN_DONTADDTORECENT 0x02000000 + /// + /// Prevents the system from adding a link to the selected file in the file system directory that contains the user's most + /// recently used documents. To retrieve the location of this directory, call the SHGetSpecialFolderLocation function with the + /// CSIDL_RECENT flag. + /// + /// + /// + /// OFN_ENABLEHOOK 0x00000020 + /// Enables the hook function specified in the lpfnHook member. + /// + /// + /// OFN_ENABLEINCLUDENOTIFY 0x00400000 + /// + /// Causes the dialog box to send CDN_INCLUDEITEM notification messages to your OFNHookProc hook procedure when the user opens a + /// folder. The dialog box sends a notification for each item in the newly opened folder. These messages enable you to control + /// which items the dialog box displays in the folder's item list. + /// + /// + /// + /// OFN_ENABLESIZING 0x00800000 + /// + /// Enables the Explorer-style dialog box to be resized using either the mouse or the keyboard. By default, the Explorer-style + /// Open and Save As dialog boxes allow the dialog box to be resized regardless of whether this flag is set. This flag is + /// necessary only if you provide a hook procedure or custom template. The old-style dialog box does not permit resizing. + /// + /// + /// + /// OFN_ENABLETEMPLATE 0x00000040 + /// + /// The lpTemplateName member is a pointer to the name of a dialog template resource in the module identified by the hInstance + /// member. If the OFN_EXPLORER flag is set, the system uses the specified template to create a dialog box that is a child of + /// the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses the template to create an + /// old-style dialog box that replaces the default dialog box. + /// + /// + /// + /// OFN_ENABLETEMPLATEHANDLE 0x00000080 + /// + /// The hInstance member identifies a data block that contains a preloaded dialog box template. The system ignores + /// lpTemplateName if this flag is specified. If the OFN_EXPLORER flag is set, the system uses the specified template to create + /// a dialog box that is a child of the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses + /// the template to create an old-style dialog box that replaces the default dialog box. + /// + /// + /// + /// OFN_EXPLORER 0x00080000 + /// + /// Indicates that any customizations made to the Open or Save As dialog box use the Explorer-style customization methods. For + /// more information, see Explorer-Style Hook Procedures and Explorer-Style Custom Templates. By default, the Open and Save As + /// dialog boxes use the Explorer-style user interface regardless of whether this flag is set. This flag is necessary only if + /// you provide a hook procedure or custom template, or set the OFN_ALLOWMULTISELECT flag. If you want the old-style user + /// interface, omit the OFN_EXPLORER flag and provide a replacement old-style template or hook procedure. If you want the old + /// style but do not need a custom template or hook procedure, simply provide a hook procedure that always returns FALSE. + /// + /// + /// + /// OFN_EXTENSIONDIFFERENT 0x00000400 + /// + /// The user typed a file name extension that differs from the extension specified by lpstrDefExt. The function does not use + /// this flag if lpstrDefExt is NULL. + /// + /// + /// + /// OFN_FILEMUSTEXIST 0x00001000 + /// + /// The user can type only names of existing files in the File Name entry field. If this flag is specified and the user enters + /// an invalid name, the dialog box procedure displays a warning in a message box. If this flag is specified, the + /// OFN_PATHMUSTEXIST flag is also used. This flag can be used in an Open dialog box. It cannot be used with a Save As dialog box. + /// + /// + /// + /// OFN_FORCESHOWHIDDEN 0x10000000 + /// + /// Forces the showing of system and hidden files, thus overriding the user setting to show or not show hidden files. However, a + /// file that is marked both system and hidden is not shown. + /// + /// + /// + /// OFN_HIDEREADONLY 0x00000004 + /// Hides the Read Only check box. + /// + /// + /// OFN_LONGNAMES 0x00200000 + /// + /// For old-style dialog boxes, this flag causes the dialog box to use long file names. If this flag is not specified, or if the + /// OFN_ALLOWMULTISELECT flag is also set, old-style dialog boxes use short file names (8.3 format) for file names with spaces. + /// Explorer-style dialog boxes ignore this flag and always display long file names. + /// + /// + /// + /// OFN_NOCHANGEDIR 0x00000008 + /// + /// Restores the current directory to its original value if the user changed the directory while searching for files. This flag + /// is ineffective for GetOpenFileName. + /// + /// + /// + /// OFN_NODEREFERENCELINKS 0x00100000 + /// + /// Directs the dialog box to return the path and file name of the selected shortcut (.LNK) file. If this value is not + /// specified, the dialog box returns the path and file name of the file referenced by the shortcut. + /// + /// + /// + /// OFN_NOLONGNAMES 0x00040000 + /// + /// For old-style dialog boxes, this flag causes the dialog box to use short file names (8.3 format). Explorer-style dialog + /// boxes ignore this flag and always display long file names. + /// + /// + /// + /// OFN_NONETWORKBUTTON 0x00020000 + /// Hides and disables the Network button. + /// + /// + /// OFN_NOREADONLYRETURN 0x00008000 + /// The returned file does not have the Read Only check box selected and is not in a write-protected directory. + /// + /// + /// OFN_NOTESTFILECREATE 0x00010000 + /// + /// The file is not created before the dialog box is closed. This flag should be specified if the application saves the file on + /// a create-nonmodify network share. When an application specifies this flag, the library does not check for write protection, + /// a full disk, an open drive door, or network protection. Applications using this flag must perform file operations carefully, + /// because a file cannot be reopened once it is closed. + /// + /// + /// + /// OFN_NOVALIDATE 0x00000100 + /// + /// The common dialog boxes allow invalid characters in the returned file name. Typically, the calling application uses a hook + /// procedure that checks the file name by using the FILEOKSTRING message. If the text box in the edit control is empty or + /// contains nothing but spaces, the lists of files and directories are updated. If the text box in the edit control contains + /// anything else, nFileOffset and nFileExtension are set to values generated by parsing the text. No default extension is added + /// to the text, nor is text copied to the buffer specified by lpstrFileTitle. If the value specified by nFileOffset is less + /// than zero, the file name is invalid. Otherwise, the file name is valid, and nFileExtension and nFileOffset can be used as if + /// the OFN_NOVALIDATE flag had not been specified. + /// + /// + /// + /// OFN_OVERWRITEPROMPT 0x00000002 + /// + /// Causes the Save As dialog box to generate a message box if the selected file already exists. The user must confirm whether + /// to overwrite the file. + /// + /// + /// + /// OFN_PATHMUSTEXIST 0x00000800 + /// + /// The user can type only valid paths and file names. If this flag is used and the user types an invalid path and file name in + /// the File Name entry field, the dialog box function displays a warning in a message box. + /// + /// + /// + /// OFN_READONLY 0x00000001 + /// + /// Causes the Read Only check box to be selected initially when the dialog box is created. This flag indicates the state of the + /// Read Only check box when the dialog box is closed. + /// + /// + /// + /// OFN_SHAREAWARE 0x00004000 + /// + /// Specifies that if a call to the OpenFile function fails because of a network sharing violation, the error is ignored and the + /// dialog box returns the selected file name. If this flag is not set, the dialog box notifies your hook procedure when a + /// network sharing violation occurs for the file name specified by the user. If you set the OFN_EXPLORER flag, the dialog box + /// sends the CDN_SHAREVIOLATION message to the hook procedure. If you do not set OFN_EXPLORER, the dialog box sends the + /// SHAREVISTRING registered message to the hook procedure. + /// + /// + /// + /// OFN_SHOWHELP 0x00000010 + /// + /// Causes the dialog box to display the Help button. The hwndOwner member must specify the window to receive the HELPMSGSTRING + /// registered messages that the dialog box sends when the user clicks the Help button. An Explorer-style dialog box sends a + /// CDN_HELP notification message to your hook procedure when the user clicks the Help button. + /// + /// + /// + /// + public OFN Flags; + + /// + /// Type: WORD + /// + /// The zero-based offset, in characters, from the beginning of the path to the file name in the string pointed to by + /// lpstrFile. For the ANSI version, this is the number of bytes; for the Unicode version, this is the number of + /// characters. For example, if lpstrFile points to the following string, "c:\dir1\dir2\file.ext", this member contains + /// the value 13 to indicate the offset of the "file.ext" string. If the user selects more than one file, nFileOffset is + /// the offset to the first file name. + /// + /// + public ushort nFileOffset; + + /// + /// Type: WORD + /// + /// The zero-based offset, in characters, from the beginning of the path to the file name extension in the string pointed to by + /// lpstrFile. For the ANSI version, this is the number of bytes; for the Unicode version, this is the number of + /// characters. Usually the file name extension is the substring which follows the last occurrence of the dot (".") character. + /// For example, txt is the extension of the filename readme.txt, html the extension of readme.txt.html. Therefore, if + /// lpstrFile points to the string "c:\dir1\dir2\readme.txt", this member contains the value 20. If lpstrFile + /// points to the string "c:\dir1\dir2\readme.txt.html", this member contains the value 24. If lpstrFile points to the + /// string "c:\dir1\dir2\readme.txt.html.", this member contains the value 29. If lpstrFile points to a string that does + /// not contain any "." character such as "c:\dir1\dir2\readme", this member contains zero. + /// + /// + public ushort nFileExtension; + + /// + /// Type: LPCTSTR + /// + /// The default extension. GetOpenFileName and GetSaveFileName append this extension to the file name if the user fails to type + /// an extension. This string can be any length, but only the first three characters are appended. The string should not contain + /// a period (.). If this member is NULL and the user fails to type an extension, no extension is appended. + /// + /// + public StrPtrAuto lpstrDefExt; + + /// + /// Type: LPARAM + /// + /// Application-defined data that the system passes to the hook procedure identified by the lpfnHook member. When the + /// system sends the WM_INITDIALOG message to the hook procedure, the message's lParam parameter is a pointer to the + /// OPENFILENAME structure specified when the dialog box was created. The hook procedure can use this pointer to get the + /// lCustData value. + /// + /// + public IntPtr lCustData; + + /// + /// Type: LPOFNHOOKPROC + /// + /// A pointer to a hook procedure. This member is ignored unless the Flags member includes the OFN_ENABLEHOOK flag. + /// + /// + /// If the OFN_EXPLORER flag is not set in the Flags member, lpfnHook is a pointer to an + /// OFNHookProcOldStyle hook procedure that receives messages intended for the dialog box. The hook procedure returns + /// FALSE to pass a message to the default dialog box procedure or TRUE to discard the message. + /// + /// + /// If OFN_EXPLORER is set, lpfnHook is a pointer to an OFNHookProc hook procedure. The hook procedure receives + /// notification messages sent from the dialog box. The hook procedure also receives messages for any additional controls that + /// you defined by specifying a child dialog template. The hook procedure does not receive messages intended for the standard + /// controls of the default dialog box. + /// + /// + [MarshalAs(UnmanagedType.FunctionPtr)] + public LPOFNHOOKPROC lpfnHook; + + /// + /// Type: LPCTSTR + /// + /// The name of the dialog template resource in the module identified by the hInstance member. For numbered dialog box + /// resources, this can be a value returned by the MAKEINTRESOURCE macro. This member is ignored unless the + /// OFN_ENABLETEMPLATE flag is set in the Flags member. If the OFN_EXPLORER flag is set, the system uses + /// the specified template to create a dialog box that is a child of the default Explorer-style dialog box. If the + /// OFN_EXPLORER flag is not set, the system uses the template to create an old-style dialog box that replaces the + /// default dialog box. + /// + /// + [MarshalAs(UnmanagedType.LPTStr)] + public string lpTemplateName; + + /// + /// Type: void* + /// This member is reserved. + /// + private IntPtr pvReserved; + + /// + /// Type: DWORD + /// This member is reserved. + /// + private uint dwReserved; + + /// + /// Type: DWORD + /// A set of bit flags you can use to initialize the dialog box. Currently, this member can be zero or the following flag. + /// + /// + /// Value + /// Meaning + /// + /// + /// OFN_EX_NOPLACESBAR 0x00000001 + /// + /// If this flag is set, the places bar is not displayed. If this flag is not set, Explorer-style dialog boxes include a places + /// bar containing icons for commonly-used folders, such as Favorites and Desktop. + /// + /// + /// + /// + public OFN_EX FlagsEx; +} + +file class PInvoke +{ + /// + /// + /// [Starting with Windows Vista, the Open and Save As common dialog boxes have been superseded by the Common Item + /// Dialog. We recommended that you use the Common Item Dialog API instead of these dialog boxes from the Common Dialog Box Library.] + /// + /// + /// Creates an Open dialog box that lets the user specify the drive, directory, and the name of a file or set of files to be opened. + /// + /// + /// + /// Type: LPOPENFILENAME + /// + /// A pointer to an OPENFILENAME structure that contains information used to initialize the dialog box. When GetOpenFileName + /// returns, this structure contains information about the user's file selection. + /// + /// + /// + /// Type: BOOL + /// + /// If the user specifies a file name and clicks the OK button, the return value is nonzero. The buffer pointed to by the + /// lpstrFile member of the OPENFILENAME structure contains the full path and file name specified by the user. + /// + /// + /// If the user cancels or closes the Open dialog box or an error occurs, the return value is zero. To get extended error + /// information, call the CommDlgExtendedError function, which can return one of the following values. + /// + /// + /// + /// + /// The Explorer-style Open dialog box provides user-interface features that are similar to the Windows Explorer. You can + /// provide an OFNHookProc hook procedure for an Explorer-style Open dialog box. To enable the hook procedure, set the + /// OFN_EXPLORER and OFN_ENABLEHOOK flags in the Flags member of the OPENFILENAME structure and specify the + /// address of the hook procedure in the lpfnHook member. + /// + /// + /// Windows continues to support the old-style Open dialog box for applications that want to maintain a user-interface + /// consistent with the old-style user-interface. To display the old-style Open dialog box, enable an OFNHookProcOldStyle + /// hook procedure and ensure that the OFN_EXPLORER flag is not set. + /// + /// To display a dialog box that allows the user to select a directory instead of a file, call the SHBrowseForFolder function. + /// Note, when selecting multiple files, the total character limit for the file names depends on the version of the function. + /// + /// + /// ANSI: 32k limit + /// + /// + /// Unicode: no restriction + /// + /// + /// Examples + /// For an example, see Opening a File. + /// + // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getopenfilenamea BOOL GetOpenFileNameA( LPOPENFILENAMEA + // Arg1 ); + [DllImport("comdlg32.dll", SetLastError = false, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetOpenFileName(ref OPENFILENAME Arg1); + + /// + /// + /// [Starting with Windows Vista, the Open and Save As common dialog boxes have been superseded by the Common Item + /// Dialog. We recommended that you use the Common Item Dialog API instead of these dialog boxes from the Common Dialog Box Library.] + /// + /// Creates a Save dialog box that lets the user specify the drive, directory, and name of a file to save. + /// + /// + /// Type: LPOPENFILENAME + /// + /// A pointer to an OPENFILENAME structure that contains information used to initialize the dialog box. When GetSaveFileName + /// returns, this structure contains information about the user's file selection. + /// + /// + /// + /// Type: BOOL + /// + /// If the user specifies a file name and clicks the OK button and the function is successful, the return value is nonzero. + /// The buffer pointed to by the lpstrFile member of the OPENFILENAME structure contains the full path and file name + /// specified by the user. + /// + /// + /// If the user cancels or closes the Save dialog box or an error such as the file name buffer being too small occurs, the + /// return value is zero. To get extended error information, call the CommDlgExtendedError function, which can return one of the + /// following values: + /// + /// + /// + /// + /// The Explorer-style Save dialog box that provides user-interface features that are similar to the Windows Explorer. You + /// can provide an OFNHookProc hook procedure for an Explorer-style Save dialog box. To enable the hook procedure, set the + /// OFN_EXPLORER and OFN_ENABLEHOOK flags in the Flags member of the OPENFILENAME structure and specify the + /// address of the hook procedure in the lpfnHook member. + /// + /// + /// Windows continues to support old-style Save dialog boxes for applications that want to maintain a user-interface + /// consistent with the old-style user-interface. To display the old-style Save dialog box, enable an OFNHookProcOldStyle + /// hook procedure and ensure that the OFN_EXPLORER flag is not set. + /// + /// Examples + /// For an example, see Creating an Enhanced Metafile. + /// + // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getsavefilenamea BOOL GetSaveFileNameA( LPOPENFILENAMEA + // Arg1 ); + [DllImport("comdlg32.dll", SetLastError = false, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetSaveFileName(ref OPENFILENAME Arg1); + + /// + /// Returns a common dialog box error code. This code indicates the most recent error to occur during the execution of one of the + /// common dialog box functions. + /// + /// + /// Type: DWORD + /// + /// If the most recent call to a common dialog box function succeeded, the return value is undefined. If the common dialog box + /// function returned FALSE because the user closed or canceled the dialog box, the return value is zero. Otherwise, the + /// return value is a nonzero error code. + /// + /// + /// The CommDlgExtendedError function can return general error codes for any of the common dialog box functions. In addition, + /// there are error codes that are returned only for a specific common dialog box. All of these error codes are defined in Cderr.h. + /// The following general error codes can be returned for any of the common dialog box functions. + /// + /// + /// + /// Return code/value + /// Description + /// + /// + /// CDERR_DIALOGFAILURE 0xFFFF + /// + /// The dialog box could not be created. The common dialog box function's call to the DialogBox function failed. For example, this + /// error occurs if the common dialog box call specifies an invalid window handle. + /// + /// + /// + /// CDERR_FINDRESFAILURE 0x0006 + /// The common dialog box function failed to find a specified resource. + /// + /// + /// CDERR_INITIALIZATION 0x0002 + /// The common dialog box function failed during initialization. This error often occurs when sufficient memory is not available. + /// + /// + /// CDERR_LOADRESFAILURE 0x0007 + /// The common dialog box function failed to load a specified resource. + /// + /// + /// CDERR_LOADSTRFAILURE 0x0005 + /// The common dialog box function failed to load a specified string. + /// + /// + /// CDERR_LOCKRESFAILURE 0x0008 + /// The common dialog box function failed to lock a specified resource. + /// + /// + /// CDERR_MEMALLOCFAILURE 0x0009 + /// The common dialog box function was unable to allocate memory for internal structures. + /// + /// + /// CDERR_MEMLOCKFAILURE 0x000A + /// The common dialog box function was unable to lock the memory associated with a handle. + /// + /// + /// CDERR_NOHINSTANCE 0x0004 + /// + /// The ENABLETEMPLATE flag was set in the Flags member of the initialization structure for the corresponding common dialog box, but + /// you failed to provide a corresponding instance handle. + /// + /// + /// + /// CDERR_NOHOOK 0x000B + /// + /// The ENABLEHOOK flag was set in the Flags member of the initialization structure for the corresponding common dialog box, but you + /// failed to provide a pointer to a corresponding hook procedure. + /// + /// + /// + /// CDERR_NOTEMPLATE 0x0003 + /// + /// The ENABLETEMPLATE flag was set in the Flags member of the initialization structure for the corresponding common dialog box, but + /// you failed to provide a corresponding template. + /// + /// + /// + /// CDERR_REGISTERMSGFAIL 0x000C + /// The RegisterWindowMessage function returned an error code when it was called by the common dialog box function. + /// + /// + /// CDERR_STRUCTSIZE 0x0001 + /// The lStructSize member of the initialization structure for the corresponding common dialog box is invalid. + /// + /// + /// The following error codes can be returned for the PrintDlg function. + /// + /// + /// Return code/value + /// Description + /// + /// + /// PDERR_CREATEICFAILURE 0x100A + /// The PrintDlg function failed when it attempted to create an information context. + /// + /// + /// PDERR_DEFAULTDIFFERENT 0x100C + /// + /// You called the PrintDlg function with the DN_DEFAULTPRN flag specified in the wDefault member of the DEVNAMES structure, but the + /// printer described by the other structure members did not match the current default printer. This error occurs when you store the + /// DEVNAMES structure, and the user changes the default printer by using the Control Panel. To use the printer described by the + /// DEVNAMES structure, clear the DN_DEFAULTPRN flag and call PrintDlg again. To use the default printer, replace the DEVNAMES + /// structure (and the structure, if one exists) with NULL; and call PrintDlg again. + /// + /// + /// + /// PDERR_DNDMMISMATCH 0x1009 + /// The data in the DEVMODE and DEVNAMES structures describes two different printers. + /// + /// + /// PDERR_GETDEVMODEFAIL 0x1005 + /// The printer driver failed to initialize a DEVMODE structure. + /// + /// + /// PDERR_INITFAILURE 0x1006 + /// + /// The PrintDlg function failed during initialization, and there is no more specific extended error code to describe the failure. + /// This is the generic default error code for the function. + /// + /// + /// + /// PDERR_LOADDRVFAILURE 0x1004 + /// The PrintDlg function failed to load the device driver for the specified printer. + /// + /// + /// PDERR_NODEFAULTPRN 0x1008 + /// A default printer does not exist. + /// + /// + /// PDERR_NODEVICES 0x1007 + /// No printer drivers were found. + /// + /// + /// PDERR_PARSEFAILURE 0x1002 + /// The PrintDlg function failed to parse the strings in the [devices] section of the WIN.INI file. + /// + /// + /// PDERR_PRINTERNOTFOUND 0x100B + /// The [devices] section of the WIN.INI file did not contain an entry for the requested printer. + /// + /// + /// PDERR_RETDEFFAILURE 0x1003 + /// + /// The PD_RETURNDEFAULT flag was specified in the Flags member of the PRINTDLG structure, but the hDevMode or hDevNames member was + /// not NULL. + /// + /// + /// + /// PDERR_SETUPFAILURE 0x1001 + /// The PrintDlg function failed to load the required resources. + /// + /// + /// The following error codes can be returned for the ChooseFont function. + /// + /// + /// Return code/value + /// Description + /// + /// + /// CFERR_MAXLESSTHANMIN CFERR_MAXLESSTHANMIN + /// + /// The size specified in the nSizeMax member of the CHOOSEFONT structure is less than the size specified in the nSizeMin member. + /// + /// + /// + /// CFERR_NOFONTS 0x2001 + /// No fonts exist. + /// + /// + /// The following error codes can be returned for the GetOpenFileName and GetSaveFileName functions. + /// + /// + /// Return code/value + /// Description + /// + /// + /// FNERR_BUFFERTOOSMALL 0x3003 + /// + /// The buffer pointed to by the lpstrFile member of the OPENFILENAME structure is too small for the file name specified by the + /// user. The first two bytes of the lpstrFile buffer contain an integer value specifying the size required to receive the full + /// name, in characters. + /// + /// + /// + /// FNERR_INVALIDFILENAME 0x3002 + /// A file name is invalid. + /// + /// + /// FNERR_SUBCLASSFAILURE 0x3001 + /// An attempt to subclass a list box failed because sufficient memory was not available. + /// + /// + /// The following error code can be returned for the FindText and ReplaceText functions. + /// + /// + /// Return code/value + /// Description + /// + /// + /// FRERR_BUFFERLENGTHZERO 0x4001 + /// A member of the FINDREPLACE structure points to an invalid buffer. + /// + /// + /// + // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror DWORD CommDlgExtendedError(); + [DllImport("comdlg32.dll", SetLastError = false, ExactSpelling = true)] + public static extern ERR CommDlgExtendedError(); +} + +internal abstract class FileDialog +{ + public const int MAX_FILE_LENGTH = 2048; + + /// + /// Specifies that the user can type only valid paths and file names. If this flag is + /// used and the user types an invalid path and file name in the File Name entry field, + /// a warning is displayed in a message box. + /// + public bool CheckPathExists { get; set; } = false; // OFN_PATHMUSTEXIST + + /// + /// Gets or sets the current file name filter string, + /// which determines the choices that appear in the "Save as file type" or + /// "Files of type" box at the bottom of the dialog box. + /// + /// This is an example filter string: + /// Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*" + /// + /// + /// Thrown in the setter if the new filter string does not have an even number of tokens + /// separated by the vertical bar character '|' (that is, the new filter string is invalid.) + /// + /// + /// If DereferenceLinks is true and the filter string is null, a blank + /// filter string (equivalent to "|*.*") will be automatically substituted to work + /// around the issue documented in Knowledge Base article 831559 + /// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API. + /// + public string? Filter { get; set; } = "All files(*.*)\0\0"; + + /// + /// Gets or sets the index of the filter currently selected in the file dialog box. + /// + /// NOTE: The index of the first filter entry is 1, not 0. + /// + public int FilterIndex { get; set; } = 1; + + /// + /// Gets or sets the initial directory displayed by the file dialog box. + /// + public string? InitialDirectory { get; set; } = null; + + /// + /// Gets or sets a string shown in the title bar of the file dialog. + /// If this property is null, a localized default from the operating + /// system itself will be used (typically something like "Save As" or "Open") + /// + public string? Title { get; set; } = "Open a file..."; + + /// + /// Gets or sets a value indicating whether the dialog box accepts only valid + /// Win32 file names. + /// + public bool ValidateNames { get; set; } = false; // OFN_NOVALIDATE + + public bool ShowHidden { get; set; } = false; + + + public abstract bool ShowDialog(); +} + +internal class SaveFileDialog : FileDialog +{ + /// + /// Restores the current directory to its original value if the user + /// changed the directory while searching for files. + /// + public bool RestoreDirectory { get; set; } // OFN_NOCHANGEDIR + + /// + /// Gets or sets a value indicating whether the dialog box prompts the user for + /// permission to create a file if the user specifies a file that does not exist. + /// + /// + /// Callers must have UIPermission.AllWindows to call this API. + /// + public bool CreatePrompt { get; set; } = false; // OFN_CREATEPROMPT + + /// + /// Gets or sets a value indicating whether the Save As dialog box displays a + /// warning if the user specifies a file name that already exists. + /// + /// + /// Callers must have UIPermission.AllWindows to call this API. + /// + public bool OverwritePrompt { get; set; } = false; // OFN_OVERWRITEPROMPT + + public string? FileName { get; set; } + + public override bool ShowDialog() + { + var fileName = Marshal.ReAllocCoTaskMem(Marshal.StringToCoTaskMemUni(FileName ?? string.Empty), MAX_FILE_LENGTH); + //using var fileName = new SafeCoTaskMemString(FileName ?? string.Empty, MAX_FILE_LENGTH); + //using var fileTitle = new SafeCoTaskMemString(MAX_FILE_LENGTH); + var ofn = new OPENFILENAME + { + lStructSize = (uint) Marshal.SizeOf(), + lpstrFilter = $"{Filter?.Replace("|", "\0")}\0", + nFilterIndex = 1, + lpstrFileTitle = default, + nMaxFileTitle = 0, + lpstrInitialDir = string.IsNullOrEmpty(InitialDirectory) ? default : new StrPtrAuto(InitialDirectory!), + lpstrTitle = string.IsNullOrEmpty(Title) ? default : new StrPtrAuto(Title!), + lpstrFile = fileName, + nMaxFile = MAX_FILE_LENGTH, + Flags = OFN.OFN_EXPLORER + }; + + if (CheckPathExists) + ofn.Flags |= OFN.OFN_PATHMUSTEXIST; + if (!ValidateNames) + ofn.Flags |= OFN.OFN_NOVALIDATE; + if (ShowHidden) + ofn.Flags |= OFN.OFN_FORCESHOWHIDDEN; + + if (RestoreDirectory) + ofn.Flags |= OFN.OFN_NOCHANGEDIR; + if (CreatePrompt) + ofn.Flags |= OFN.OFN_CREATEPROMPT; + if (OverwritePrompt) + ofn.Flags |= OFN.OFN_OVERWRITEPROMPT; + + var result = PInvoke.GetSaveFileName(ref ofn); + if (result) + FileName = Marshal.PtrToStringUni(fileName); + + Marshal.FreeCoTaskMem(fileName); + + return result; + } +} + +internal class OpenFileDialog : FileDialog +{ + /// + /// Gets or sets a value indicating whether + /// the dialog box displays a warning if the + /// user specifies a file name that does not exist. + /// + public bool CheckFileExists { get; set; } = false; // OFN_FILEMUSTEXIST + + /// + /// Gets or sets an option flag indicating whether the + /// dialog box allows multiple files to be selected. + /// + public bool Multiselect { get; set; } = false; // OFN_ALLOWMULTISELECT + + /// + /// Gets or sets a value indicating whether the read-only + /// check box is selected. + /// + public bool ReadOnlyChecked { get; set; } = false; // OFN_READONLY + + /// + /// Gets or sets a value indicating whether the dialog + /// contains a read-only check box. + /// + public bool ShowReadOnly { get; set; } = false; // OFN_HIDEREADONLY + + /// + /// Gets or sets a string containing the full path of the file selected in + /// the file dialog box. + /// + public string? FileName => FileNames?.Length > 0 ? FileNames[0] : null; + + /// + /// Gets the file names of all selected files in the dialog box. + /// + public string[]? FileNames { get; protected set; } = null; + + public override bool ShowDialog() + { + FileNames = null; + + var file = Marshal.AllocHGlobal(MAX_FILE_LENGTH * Marshal.SystemDefaultCharSize); + for (var i = 0; i < MAX_FILE_LENGTH * Marshal.SystemDefaultCharSize; i++) + Marshal.WriteByte(file, i, 0); + + var fileTitle = string.IsNullOrEmpty(FileName) ? IntPtr.Zero : Marshal.ReAllocCoTaskMem(Marshal.StringToCoTaskMemUni(FileName ?? string.Empty), MAX_FILE_LENGTH); + var ofn = new OPENFILENAME + { + lStructSize = (uint) Marshal.SizeOf(), + lpstrFilter = Filter?.Replace("|", "\0") + "\0", + nFilterIndex = 1, + lpstrFileTitle = fileTitle, + nMaxFileTitle = MAX_FILE_LENGTH, + lpstrInitialDir = string.IsNullOrEmpty(InitialDirectory) ? default : new StrPtrAuto(InitialDirectory!), + lpstrTitle = string.IsNullOrEmpty(Title) ? default : new StrPtrAuto(Title!), + lpstrFile = file, + nMaxFile = MAX_FILE_LENGTH, + Flags = OFN.OFN_EXPLORER + }; + + if (CheckPathExists) + ofn.Flags |= OFN.OFN_PATHMUSTEXIST; + if (!ValidateNames) + ofn.Flags |= OFN.OFN_NOVALIDATE; + if (ShowHidden) + ofn.Flags |= OFN.OFN_FORCESHOWHIDDEN; + + if (CheckFileExists) + ofn.Flags |= OFN.OFN_FILEMUSTEXIST; + if (Multiselect) + ofn.Flags |= OFN.OFN_ALLOWMULTISELECT; + if (ReadOnlyChecked) + ofn.Flags |= OFN.OFN_READONLY; + if (!ShowReadOnly) + ofn.Flags |= OFN.OFN_HIDEREADONLY; + + var result = PInvoke.GetOpenFileName(ref ofn); + if (result) + { + var filePointer = file; + var pointer = (long) filePointer; + var fileStr = Marshal.PtrToStringAuto(filePointer); + var strList = new List(); + + // Retrieve file names + while (fileStr.Length > 0) + { + strList.Add(fileStr); + + pointer += fileStr.Length * Marshal.SystemDefaultCharSize + Marshal.SystemDefaultCharSize; + filePointer = (IntPtr) pointer; + fileStr = Marshal.PtrToStringAuto(filePointer); + } + + if (strList.Count > 1) + { + FileNames = new string[strList.Count - 1]; + for (var i = 1; i < strList.Count; i++) + { + FileNames[i - 1] = Path.Combine(strList[0], strList[i]); + } + } + else + { + FileNames = strList.ToArray(); + } + } + + if (fileTitle != IntPtr.Zero) Marshal.FreeCoTaskMem(fileTitle); + + return result; + } +} \ No newline at end of file