diff --git a/Windows.Shell/ShellObjects/ComObjWrapper.cs b/Windows.Shell/ShellObjects/ComObjWrapper.cs new file mode 100644 index 00000000..2b2cef11 --- /dev/null +++ b/Windows.Shell/ShellObjects/ComObjWrapper.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Vanara.Windows.Shell +{ + /// + /// + /// + /// + /// + /// + public abstract class ComObjWrapper : IDisposable, IEquatable, INotifyPropertyChanged where TObj : ComObjWrapper where TComType : class + { + /// The internal reference to the COM object. + protected TComType iObj; + + /// Initializes a new instance of the class. + /// The base interface. + protected ComObjWrapper(TComType baseInterface) => iObj = baseInterface ?? throw new ArgumentNullException(nameof(baseInterface)); + + /// Occurs when a property value changes. + public virtual event PropertyChangedEventHandler PropertyChanged; + + /// Gets the COM interface supporting this type. + /// The COM interface. + public virtual TComType ComInterface => iObj; + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public virtual void Dispose() + { + Marshal.ReleaseComObject(iObj); + iObj = null; + } + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is TObj view ? Equals(view) : obj is TComType com && Equals(com)); + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + public abstract bool Equals(TComType other); + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + public virtual bool Equals(TObj other) => Equals(other.iObj); + + /// 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 abstract override int GetHashCode(); + + /// Called when a property's value has changed. + /// Name of the property. + protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); + } +} \ No newline at end of file diff --git a/Windows.Shell/ShellObjects/ShellBrowser.cs b/Windows.Shell/ShellObjects/ShellBrowser.cs new file mode 100644 index 00000000..1a60c92a --- /dev/null +++ b/Windows.Shell/ShellObjects/ShellBrowser.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using Vanara.Extensions; +using Vanara.PInvoke; +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + internal class ShellBrowser : IShellBrowser, IOleCommandTarget, Shell32.IServiceProvider + { + private readonly ShellView shellView; + + internal ShellBrowser(ShellView view) => shellView = view ?? throw new ArgumentNullException(nameof(view)); + + HRESULT IShellBrowser.BrowseObject(IntPtr pidl, SBSP wFlags) + { + if (wFlags.IsFlagSet(SBSP.SBSP_PARENT)) + { + shellView.NavigateParent(); + } + else if (wFlags.IsFlagSet(SBSP.SBSP_NAVIGATEBACK)) + { + shellView.NavigateBack(); + } + else if (wFlags.IsFlagSet(SBSP.SBSP_NAVIGATEFORWARD)) + { + shellView.NavigateForward(); + } + else + { + shellView.Navigate(new ShellFolder(pidl)); + } + return HRESULT.S_OK; + } + + HRESULT IShellBrowser.ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL; + + HRESULT Ole32.IOleWindow.ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL; + + HRESULT IOleCommandTarget.Exec(in Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, in object pvaIn, ref object pvaOut) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.GetControlWindow(FCW id, out HWND phwnd) + { + phwnd = HWND.NULL; + return HRESULT.E_NOTIMPL; + } + + HRESULT IShellBrowser.GetViewStateStream(STGM grfMode, out IStream ppStrm) + { + ppStrm = null; + return HRESULT.E_NOTIMPL; + } + + HRESULT IShellBrowser.GetWindow(out HWND phwnd) + { + phwnd = shellView.shellViewWindow; + return HRESULT.E_NOTIMPL; + } + + HRESULT Ole32.IOleWindow.GetWindow(out HWND phwnd) => shellView.iObj.GetWindow(out phwnd); + + HRESULT IShellBrowser.InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.QueryActiveShellView(out IShellView ppshv) + { + ppshv = null; + return HRESULT.E_NOTIMPL; + } + + HRESULT Shell32.IServiceProvider.QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject) + { + if (riid == typeof(IOleCommandTarget).GUID) + { + ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IOleCommandTarget)); + return HRESULT.S_OK; + } + else if (riid == typeof(IShellBrowser).GUID) + { + ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IShellBrowser)); + return HRESULT.S_OK; + } + else + { + ppvObject = IntPtr.Zero; + return HRESULT.E_NOINTERFACE; + } + } + + HRESULT IOleCommandTarget.QueryStatus(in Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, OLECMDTEXT pCmdText) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret) + { + pret = default; + return HRESULT.E_NOTIMPL; + } + + HRESULT IShellBrowser.SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.SetToolbarItems(ComCtl32.TBBUTTON[] lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL; + + HRESULT IShellBrowser.TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL; + } +} \ No newline at end of file diff --git a/Windows.Shell/ShellObjects/ShellContextMenu.cs b/Windows.Shell/ShellObjects/ShellContextMenu.cs new file mode 100644 index 00000000..9e6654da --- /dev/null +++ b/Windows.Shell/ShellObjects/ShellContextMenu.cs @@ -0,0 +1,278 @@ +// Credit due to Gong-Shell from which this was largely taken. +#if !NETCOREAPP3_1 +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Vanara.PInvoke; +using static Vanara.PInvoke.Shell32; +using static Vanara.PInvoke.User32; + +namespace Vanara.Windows.Shell +{ + /// Provides support for displaying the context menu of a shell item. + /// + /// Use this class to display a context menu for a shell item, either as a popup menu, or as a main menu. + /// + /// To display a popup menu, simply call with the parent control and the position at which the menu should + /// be shown. + /// + /// + /// To display a shell context menu in a Form's main menu, call the method to populate the menu. In addition, you + /// must intercept a number of special messages that will be sent to the menu's parent form. To do this, you must override like so: + /// + /// + ///protected override void WndProc(ref Message m) { + ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) { + ///base.WndProc(ref m); + ///} + ///} + /// + /// Where m_ContextMenu is the being shown. + /// Standard menu commands can also be invoked from this class, for example and . + /// + public class ShellContextMenu + { + private const int m_CmdFirst = 0x8000; + private readonly IContextMenu2 m_ComInterface2; + private readonly IContextMenu3 m_ComInterface3; + private readonly MessageWindow m_MessageWindow; + + /// Initialises a new instance of the class. + /// The item to which the context menu should refer. + public ShellContextMenu(ShellItem item) : this(new ShellItem[] { item }) { } + + /// Initialises a new instance of the class. + /// The items to which the context menu should refer. + public ShellContextMenu(ShellItem[] items) + { + var pidls = new IntPtr[items.Length]; + ShellFolder parent = null; + + for (var n = 0; n < items.Length; ++n) + { + pidls[n] = ILFindLastID((IntPtr)items[n].PIDL); + + if (parent is null) + { + if (items[n] == ShellFolder.Desktop) + { + parent = ShellFolder.Desktop; + } + else + { + parent = items[n].Parent; + } + } + else + { + if (items[n].Parent != parent) + { + throw new Exception("All shell items must have the same parent"); + } + } + } + + ComInterface = parent.IShellFolder.GetUIObjectOf(HWND.NULL, pidls); + m_ComInterface2 = ComInterface as IContextMenu2; + m_ComInterface3 = ComInterface as IContextMenu3; + m_MessageWindow = new MessageWindow(this); + } + + /// Gets the underlying COM interface. + public IContextMenu ComInterface { get; set; } + + /// Handles context menu messages when the is displayed on a Form's main menu bar. + /// + /// + /// To display a shell context menu in a Form's main menu, call the method to populate the menu with the + /// shell item's menu items. In addition, you must intercept a number of special messages that will be sent to the menu's parent + /// form. To do this, you must override like so: + /// + /// + ///protected override void WndProc(ref Message m) { + ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) { + ///base.WndProc(ref m); + ///} + ///} + /// + /// Where m_ContextMenu is the being shown. + /// + /// The message to handle. + /// + /// if the message was a Shell Context Menu message, if not. If the method returns + /// false, then the message should be passed down to the base class's method. + /// + public bool HandleMenuMessage(ref Message m) + { + try + { + if ((m.Msg == (int)WindowMessage.WM_COMMAND) && ((int)m.WParam >= m_CmdFirst)) + { + InvokeCommand((int)m.WParam - m_CmdFirst); + return true; + } + else + { + if (m_ComInterface3 != null) + { + m_ComInterface3.HandleMenuMsg2((uint)m.Msg, m.WParam, m.LParam, out IntPtr result); + m.Result = result; + return true; + } + else if (m_ComInterface2 != null) + { + m_ComInterface2.HandleMenuMsg((uint)m.Msg, m.WParam, m.LParam); + m.Result = IntPtr.Zero; + return true; + } + } + } + catch { } + return false; + } + + /// Invokes the Copy command on the shell item(s). + public void InvokeCopy() => InvokeVerb("copy"); + + /// Invokes the Copy command on the shell item(s). + public void InvokeCut() => InvokeVerb("cut"); + + /// Invokes the Delete command on the shell item(s). + public void InvokeDelete() + { + try + { + InvokeVerb("delete"); + } + catch (COMException e) + { + // Ignore the exception raised when the user cancels a delete operation. + if (e.ErrorCode != (HRESULT)(Win32Error)Win32Error.ERROR_CANCELLED && + e.ErrorCode != HRESULT.COPYENGINE_E_USER_CANCELLED) + { + throw; + } + } + } + + /// Invokes the Paste command on the shell item(s). + public void InvokePaste() => InvokeVerb("paste"); + + /// Invokes the Rename command on the shell item. + public void InvokeRename() => InvokeVerb("rename"); + + /// Invokes the specified verb on the shell item(s). + public void InvokeVerb(string verb) + { + var invoke = new CMINVOKECOMMANDINFOEX(); + invoke.cbSize = (uint)Marshal.SizeOf(invoke); + invoke.lpVerb = new SafeResourceId(verb); + ComInterface.InvokeCommand(invoke); + } + + /// Populates a with the context menu items for a shell item. + /// + /// If this method is being used to populate a Form's main menu then you need to call in the Form's + /// message handler. + /// + /// The menu to populate. + public void Populate(Menu menu) + { + RemoveShellMenuItems(menu); + ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, CMF.CMF_EXPLORE); + } + + /// Shows a context menu for a shell item. + /// The parent control. + /// The position on that the menu should be displayed at. + public void ShowContextMenu(Control control, Point pos) + { + using var menu = new ContextMenu(); + Populate(menu); + var command = TrackPopupMenuEx(menu.Handle, TrackPopupMenuFlags.TPM_RETURNCMD, pos.X, pos.Y, m_MessageWindow.Handle); + if (command > 0) + { + InvokeCommand((int)command - m_CmdFirst); + } + } + + private void InvokeCommand(int index) + { + var invoke = new CMINVOKECOMMANDINFOEX(index) { nShow = ShowWindowCommand.SW_SHOWNORMAL }; + m_ComInterface2.InvokeCommand(invoke); + } + + private void RemoveShellMenuItems(Menu menu) + { + const int tag = 0xAB; + + var menuInfo = new MENUINFO(); + menuInfo.cbSize = (uint)Marshal.SizeOf(menuInfo); + menuInfo.fMask = MenuInfoMember.MIM_MENUDATA; + + var itemInfo = new MENUITEMINFO(); + itemInfo.cbSize = (uint)Marshal.SizeOf(itemInfo); + itemInfo.fMask = MenuItemInfoMask.MIIM_ID | MenuItemInfoMask.MIIM_SUBMENU; + + // First, tag the managed menu items with an arbitary value (0xAB). + TagManagedMenuItems(menu, tag); + + var remove = new List(); + var count = GetMenuItemCount(menu.Handle); + for (uint n = 0; n < count; ++n) + { + GetMenuItemInfo(menu.Handle, n, true, ref itemInfo); + + if (itemInfo.hSubMenu.IsNull) + { + // If the item has no submenu we can't get the tag, so check its ID to determine if it was added by the shell. + if (itemInfo.wID >= m_CmdFirst) remove.Add(n); + } + else + { + GetMenuInfo(itemInfo.hSubMenu, ref menuInfo); + if ((int)menuInfo.dwMenuData != tag) remove.Add(n); + } + } + + // Remove the unmanaged menu items. + remove.Reverse(); + foreach (var position in remove) + { + DeleteMenu(menu.Handle, (uint)position, MenuFlags.MF_BYPOSITION); + } + } + + private void TagManagedMenuItems(Menu menu, int tag) + { + var info = new MENUINFO(); + info.cbSize = (uint)Marshal.SizeOf(info); + info.fMask = MenuInfoMember.MIM_MENUDATA; + info.dwMenuData = (UIntPtr)tag; + + foreach (MenuItem item in menu.MenuItems) + { + SetMenuInfo(item.Handle, info); + } + } + + private class MessageWindow : Control + { + private readonly ShellContextMenu m_Parent; + + public MessageWindow(ShellContextMenu parent) => m_Parent = parent; + + protected override void WndProc(ref Message m) + { + if (!m_Parent.HandleMenuMessage(ref m)) + { + base.WndProc(ref m); + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Windows.Shell/ShellObjects/ShellNavigationHistory.cs b/Windows.Shell/ShellObjects/ShellNavigationHistory.cs new file mode 100644 index 00000000..699c0376 --- /dev/null +++ b/Windows.Shell/ShellObjects/ShellNavigationHistory.cs @@ -0,0 +1,96 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Linq; +using Vanara.Collections; +using Vanara.Extensions; +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + /// The navigation log is a history of the locations visited by a shell view object. + public class ShellNavigationHistory : Vanara.Collections.IHistory + { + private readonly Vanara.Collections.History pidls = new Collections.History(); + + internal ShellNavigationHistory() + { + } + + /// Occurs when [collection changed]. + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// Occurs when a property value changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek backward; otherwise, . + public bool CanSeekBackward => pidls.CanSeekBackward; + + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek forward; otherwise, . + public bool CanSeekForward => pidls.CanSeekForward; + + /// Gets the items in the history. + /// The number of items. + public int Count => pidls.Count; + + /// Gets the shell object in the Locations collection pointed to by CurrentLocationIndex. + public ShellItem Current => new ShellItem(pidls.Current); + + /// + /// Adds the specified item as the last history entry and sets the property to + /// it's value. + /// + /// The item to add to the history. + public void Add(ShellItem item) => pidls.Add(item.PIDL, true); + + /// Clears the history of all items. + public void Clear() => pidls.Clear(); + + /// Returns an enumerator that iterates through the collection. + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() => pidls.Select(p => new ShellItem(p)).GetEnumerator(); + + /// Gets a specified number of items starting at a location within the history. + /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable. + /// The reference point within the history at which to start fetching items. + /// A read-only list of items. + public IReadOnlyList GetItems(int count, SeekOrigin origin) => (IReadOnlyList)new List(pidls.GetItems(count, origin).Select(ShIFromPIDL)); + + /// + /// Seeks through the history a given number of items starting at a known location within the history. This updates the property. + /// + /// The number of items to move. This value can be negative to search backwards or positive to search forwards. + /// The reference point within the history at which to start seeking. + /// The value at the new current pointer position. + public ShellItem Seek(int count, SeekOrigin origin) => ShIFromPIDL(pidls.Seek(count, origin)); + + /// Seeks one position backwards. + /// The value at the new current pointer position. + public ShellItem SeekBackward() => ShIFromPIDL(pidls.SeekBackward()); + + /// Seeks one position forwards. + /// The value at the new current pointer position. + public ShellItem SeekForward() => ShIFromPIDL(pidls.SeekForward()); + + /// + /// Adds the specified item as the last history entry and sets the property to + /// it's value. + /// + /// The item to add to the history. + /// + /// indicates to remove all items forward of the current pointer; leaves the history intact. + /// + void IHistory.Add(ShellItem item, bool removeForwardItems) => pidls.Add(item?.PIDL, removeForwardItems); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal void Add(PIDL pidl) => pidls.Add(pidl, true); + + private static ShellItem ShIFromPIDL(PIDL pidl) => pidl is null ? null : new ShellItem(pidl); + } +} \ No newline at end of file diff --git a/Windows.Shell/ShellObjects/ShellView.cs b/Windows.Shell/ShellObjects/ShellView.cs new file mode 100644 index 00000000..52671208 --- /dev/null +++ b/Windows.Shell/ShellObjects/ShellView.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Vanara.InteropServices; +using Vanara.PInvoke; +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + /// Event argument for The Navigated event + public class NavigatedEventArgs : EventArgs + { + /// Initializes a new instance of the class. + /// The folder. + public NavigatedEventArgs(ShellFolder folder) => NewLocation = folder; + + /// The new location of the explorer browser + public ShellItem NewLocation { get; private set; } + } + + /// Event argument for The Navigating event + public class NavigatingEventArgs : CancelEventArgs + { + /// Initializes a new instance of the class. + /// The pending location. + public NavigatingEventArgs(ShellItem pendingLocation) => PendingLocation = pendingLocation; + + /// The location being navigated to. + public ShellItem PendingLocation { get; private set; } + } + + /// Event argument for the NavigatinoFailed event + public class NavigationFailedEventArgs : EventArgs + { + /// The location the browser would have navigated to. + public ShellItem FailedLocation { get; set; } + } + + /// + /// + /// + /// + /// + /// + public class ShellView : Control, INotifyPropertyChanged + { + internal IShellView iObj; + internal HWND shellViewWindow; + private ShellFolder currentFolder; + private IShellBrowser iBrowser; + + /// Creates a new from a shell folder and assigns it to a window. + /// The shell folder. + /// The owner window. + /// A new instance for the supplied shell folder. + public ShellView(ShellFolder folder, IWin32Window owner) : this(CreateViewObject(folder, owner?.Handle ?? IntPtr.Zero)) => Navigate(folder); + + /// Initializes a new instance of the class. + public ShellView() : this(ShellFolder.Desktop, null) { } + + /// Initializes a new instance of the class. + /// The base interface. + internal ShellView(IShellView baseInterface) + { + iObj = baseInterface; + History = new ShellNavigationHistory(); + Items = new ShellItemArray(GetItemArray(iObj, SVGIO.SVGIO_ALLVIEW)); + } + + /// Fires when the Items collection changes. + [Category("Action"), Description("Items changed.")] + public event EventHandler ItemsChanged; + + /// Fires when the ExplorerBorwser view has finished enumerating files. + [Category("Behavior"), Description("View is done enumerating files.")] + public event EventHandler ItemsEnumerated; + + /// + /// Fires when a navigation has been 'completed': no Navigating listener has canceled, and the ExplorerBorwser has created a new + /// view. The view will be populated with new items asynchronously, and ItemsChanged will be fired to reflect this some time later. + /// + [Category("Action"), Description("Navigation complete.")] + public event EventHandler Navigated; + + /// Fires when a navigation has been initiated, but is not yet complete. + [Category("Action"), Description("Navigation initiated, but not complete.")] + public event EventHandler Navigating; + + /// + /// Fires when either a Navigating listener cancels the navigation, or if the operating system determines that navigation is not possible. + /// + [Category("Action"), Description("Navigation failed.")] + public event EventHandler NavigationFailed; + + /// Occurs when a property value changes. + [Category("Behavior"), Description("Property changed.")] + public event PropertyChangedEventHandler PropertyChanged; + + /// Fires when the item selected in the view has changed (i.e., a rename ). This is not the same as SelectionChanged. + [Category("Action"), Description("Selected item has changed.")] + public event EventHandler SelectedItemModified; + + /// Fires when the SelectedItems collection changes. + [Category("Behavior"), Description("Selection changed.")] + public event EventHandler SelectionChanged; + + /// Gets or sets the currently being browsed by the . + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ShellFolder CurrentFolder { get => currentFolder ??= new ShellFolder(GetFolderForView(iObj)); set => Navigate(value); } + + /// A set of flags that indicate the options for the folder. + public FOLDERFLAGS Flags => iObj.GetCurrentInfo().fFlags; + + /// Contains the navigation history of the ExplorerBrowser + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ShellNavigationHistory History { get; private set; } + + /// Gets all the items in the view. + /// An array with all the items. + public ShellItemArray Items { get; private set; } + + /// Gets or sets the currently selected items in the view. + /// An array with the selected items. + /// All items must belong to the folder hosted by this view. - SelectedItems + public ShellItem[] SelectedItems + { + get => Array.ConvertAll(GetItems(iObj, SVGIO.SVGIO_SELECTION), i => new ShellItem(i)); + set + { + // Deselect all + iObj.SelectItem(default, SVSIF.SVSI_DESELECTOTHERS); + if (value is null || value.Length == 0) return; + // Get parent folder of view items + PIDL pidl = CurrentFolder.PIDL; + // Ensure all have this parent + if (!value.All(shi => shi.PIDL.Parent.Equals(pidl))) + throw new ArgumentException("All items must belong to the folder hosted by this view.", nameof(SelectedItems)); + // Select all provided + foreach (ShellItem item in value) + iObj.SelectItem((IntPtr)item.PIDL, SVSIF.SVSI_SELECT); + } + } + + /// + /// Retrieves a handle to one of the windows participating in in-place activation (frame, document, parent, or in-place object window). + /// + /// The window handle. + public HWND WindowHandle => shellViewWindow; + + /// Folder view mode. + public FOLDERVIEWMODE ViewMode => iObj.GetCurrentInfo().ViewMode; + + /// Gets the default size of the control. + protected override Size DefaultSize => new Size(250, 200); + + private IShellBrowser Browser => iBrowser ??= new ShellBrowser(this); + + /// Implements the operator !=. + /// The left. + /// The right. + /// The result of the operator. + public static bool operator !=(ShellView left, ShellView right) => !(left == right); + + /// Implements the operator ==. + /// The left. + /// The right. + /// The result of the operator. + public static bool operator ==(ShellView left, ShellView right) => EqualityComparer.Default.Equals(left, right); + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + /// + public bool Equals(IShellView other) + { + HWND w1 = HWND.NULL, w2 = HWND.NULL; + other?.GetWindow(out w1); + iObj?.GetWindow(out w2); + return w1 == w2; + } + + /// Determines whether the specified , is equal to this instance. + /// The to compare with this instance. + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is ShellView sv && Equals(sv.iObj)) || (obj is IShellView isv && Equals(isv)); + + /// 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() => (int)(IntPtr)WindowHandle; + + /// + /// Clears the Explorer Browser of existing content, fills it with content from the specified container, and adds a new point to the + /// Travel Log. + /// + /// The shell container to navigate to. + public void Navigate(ShellFolder folder) + { + if (!OnNavigating(folder)) return; + + ShellFolder previous = currentFolder; + currentFolder = folder; + + try + { + RecreateShellView(); + History.Add(folder.PIDL); + OnNavigated(); + } + catch (Exception) + { + currentFolder = previous; + RecreateShellView(); + throw; + } + OnPropertyChanged(nameof(CurrentFolder)); + } + + /// + /// Navigates to the last item in the navigation history list. This does not change the set of locations in the navigation log. + /// + public bool NavigateBack() { if (History.CanSeekBackward) { Navigate(new ShellFolder(History.SeekBackward())); return true; } return false; } + + /// + /// Navigates to the next item in the navigation history list. This does not change the set of locations in the navigation log. + /// + /// True if the navigation succeeded, false if it failed for any reason. + public bool NavigateForward() { if (History.CanSeekForward) { Navigate(new ShellFolder(History.SeekForward())); return true; } return false; } + + /// Navigates to the parent of the currently displayed folder. + public void NavigateParent() { if (CurrentFolder != ShellFolder.Desktop) Navigate(CurrentFolder.Parent); } + + /// Refreshes the view's contents in response to user input. + public override void Refresh() => iObj.Refresh(); + + /// Saves the Shell's view settings so the current state can be restored during a subsequent browsing session. + public void SaveState() => iObj.SaveViewState(); + + /// Raises the event. + protected internal virtual void OnItemsChanged() => ItemsChanged?.Invoke(this, EventArgs.Empty); + + /// Raises the event. + protected internal virtual void OnItemsEnumerated() => ItemsEnumerated?.Invoke(this, EventArgs.Empty); + + /// Raises the event. + protected internal virtual void OnNavigated() => Navigated?.Invoke(this, new NavigatedEventArgs(CurrentFolder)); + + /// Raises the event. + protected internal virtual bool OnNavigating(ShellFolder pendingLocation) + { + var e = new NavigatingEventArgs(pendingLocation); + Navigating?.Invoke(this, e); + return !e.Cancel; + } + + /// Raises the event. + protected internal virtual void OnNavigationFailed(NavigationFailedEventArgs nfevent) + { + if (nfevent?.FailedLocation is null) return; + NavigationFailed?.Invoke(this, nfevent); + } + + /// Raises the event. + protected internal virtual void OnSelectedItemModified() => SelectedItemModified?.Invoke(this, EventArgs.Empty); + + /// Raises the event. + protected internal virtual void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty); + + /// + /// Releases the unmanaged resources used by the and its child controls and optionally + /// releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!shellViewWindow.IsNull) + { + User32.DestroyWindow(shellViewWindow); + shellViewWindow = HWND.NULL; + } + if (iBrowser != null) + { + Marshal.ReleaseComObject(iBrowser); + iBrowser = null; + } + if (Items != null) + { + Items.Dispose(); + Items = null; + } + if (History != null) + { + History = null; + } + if (iObj != null) + { + Marshal.ReleaseComObject(iObj); + iObj = null; + } + } + base.Dispose(disposing); + } + + /// Raises the method. + protected override void OnCreateControl() + { + base.OnCreateControl(); + CreateShellView(); + OnNavigated(); + } + + /// Raises the event. + /// The instance containing the event data. + protected override void OnResize(EventArgs eventargs) + { + base.OnResize(eventargs); + User32.SetWindowPos(shellViewWindow, HWND.HWND_TOP, 0, 0, ClientRectangle.Width, ClientRectangle.Height, 0); + } + + /// Processes Windows messages. + /// The Windows to process. + protected override void WndProc(ref Message m) + { + const int CWM_GETISHELLBROWSER = 0x407; + + // Windows 9x sends the CWM_GETISHELLBROWSER message and expects the IShellBrowser for the window to be returned or an Access + // Violation occurs. This is pseudo-documented in knowledge base article Q157247. + if (m.Msg == CWM_GETISHELLBROWSER) + { + m.Result = Marshal.GetComInterfaceForObject(iBrowser, typeof(IShellBrowser)); + } + else + { + base.WndProc(ref m); + } + } + + private static IShellView CreateViewObject(ShellFolder folder, HWND owner) => + folder?.iShellFolder.CreateViewObject(owner); + + private static PIDL GetFolderForView(IShellView iView) + { + PIDL pidl = GetItems(iView, SVGIO.SVGIO_ALLVIEW).FirstOrDefault(); + pidl?.RemoveLastId(); + return pidl; + } + + private static IShellItemArray GetItemArray(IShellView iView, SVGIO uItem) => ((IFolderView)iView).Items(uItem); + + private static PIDL[] GetItems(IShellView iView, SVGIO uItem) + { + using ComReleaser ido = ComReleaserFactory.Create(iView.GetItemObject(uItem)); + var shdo = new ShellDataObject(ido.Item); + return shdo.GetShellIdList(); + } + + private void CreateShellView() + { + IShellView prev = iObj; + iObj = CreateViewObject(CurrentFolder, Handle); + Items = new ShellItemArray(GetItemArray(iObj, SVGIO.SVGIO_ALLVIEW)); + try + { + var fsettings = new FOLDERSETTINGS(ViewMode, Flags); + shellViewWindow = iObj.CreateViewWindow(prev, fsettings, Browser, ClientRectangle); + } + catch (COMException ex) + { + // If the operation was cancelled by the user (for example because an empty removable media drive was selected, then + // "Cancel" pressed in the resulting dialog) convert the exception into something more meaningfil. + if (ex.ErrorCode == unchecked((int)0x800704C7U)) + { + throw new OperationCanceledException("User cancelled.", ex); + } + } + + iObj.UIActivate(SVUIA.SVUIA_ACTIVATE_NOFOCUS); + + if (DesignMode) User32.EnableWindow(shellViewWindow, false); + + if (prev != null) prev.DestroyViewWindow(); + } + + private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private void RecreateShellView() + { + if (iObj != null) + { + CreateShellView(); + OnNavigated(); + } + } + } +} \ No newline at end of file