From b0718672628e3809ea24178706350b1fd4214209 Mon Sep 17 00:00:00 2001 From: Thorsten Jung Date: Fri, 2 Apr 2021 17:04:25 +0200 Subject: [PATCH] Initial commit of Vanara.Windows.Shell.ShellBrowser (#221) * Fix typo in folder name * Initial commit of ShellBrowser, as of 01-Apr-21 * Fixing brainbug --- PInvoke/{NtDll => NTDll}/Winternl.UnicodeString.cs | 0 Windows.Shell/ShellObjects/ShellBrowser.cs | 1306 +++++++++++++++++--- Windows.Shell/ShellObjects/ShellView.cs | 2 +- 3 files changed, 1140 insertions(+), 168 deletions(-) rename PInvoke/{NtDll => NTDll}/Winternl.UnicodeString.cs (100%) diff --git a/PInvoke/NtDll/Winternl.UnicodeString.cs b/PInvoke/NTDll/Winternl.UnicodeString.cs similarity index 100% rename from PInvoke/NtDll/Winternl.UnicodeString.cs rename to PInvoke/NTDll/Winternl.UnicodeString.cs diff --git a/Windows.Shell/ShellObjects/ShellBrowser.cs b/Windows.Shell/ShellObjects/ShellBrowser.cs index d67bc2a3..3cc0231b 100644 --- a/Windows.Shell/ShellObjects/ShellBrowser.cs +++ b/Windows.Shell/ShellObjects/ShellBrowser.cs @@ -1,205 +1,1177 @@ -using System; +using System.Collections.Generic; +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using System.Runtime.InteropServices; +using System.Text; using System.Windows.Forms; -using Vanara.Extensions; -using Vanara.InteropServices; +using System; + using Vanara.PInvoke; -using static Vanara.PInvoke.Ole32; using static Vanara.PInvoke.Shell32; namespace Vanara.Windows.Shell { - /// A basic implementation of IShellBrowser, IOleCommandTarget and ICommDlgBrowser. - /// - /// This implementation used a to implement: - /// - /// BrowseObject - /// GetWindow - /// OnDefaultCommand - /// OnStateChange - /// - /// - /// - /// - /// - /// - [ComVisible(true), ClassInterface(ClassInterfaceType.None)] - internal class ShellBrowser : IShellBrowser, IOleCommandTarget, Shell32.IServiceProvider, ICommDlgBrowser - { - /// The instance from initialization. - protected readonly ShellView shellView; + /// The direction argument for NavigateFromHistory() + public enum NavigationLogDirection + { + /// Navigates forward through the navigation log + Forward, - /// Initializes a new instance of the class with a instance. - /// The instance. - /// view - public ShellBrowser(ShellView view) => shellView = view ?? throw new ArgumentNullException(nameof(view)); + /// Navigates backward through the travel log + Backward + } - /// Gets or sets the progress bar associated with the view. - /// The progress bar. - public ProgressBar ProgressBar { get; set; } + /// Indicates the viewing mode of the ShellBrowser + public enum ShellBrowserViewMode + { + /// Choose the best view mode for the folder + Auto = Shell32.FOLDERVIEWMODE.FVM_AUTO, -#if NETFRAMEWORK || NETCOREAPP3_0 - /// Gets or sets the status bar associated with the view. - /// The status bar. - public StatusBar StatusBar { get; set; } + /// (New for Windows7) + Content = Shell32.FOLDERVIEWMODE.FVM_CONTENT, - /// Gets or sets the tool bar associated with the view. - /// The tool bar. - public ToolBar ToolBar { get; set; } -#endif + /// Object names and other selected information, such as the size or date last updated, are shown. + Details = Shell32.FOLDERVIEWMODE.FVM_DETAILS, - /// Gets or sets the TreeView associated with the view. - /// The TreeView. - public TreeView TreeView { get; set; } + /// The view should display medium-size icons. + Icon = Shell32.FOLDERVIEWMODE.FVM_ICON, - /// - public virtual HRESULT BrowseObject(IntPtr pidl, SBSP wFlags) - { - switch (wFlags) - { - case var f when f.IsFlagSet(SBSP.SBSP_NAVIGATEBACK): - shellView.NavigateBack(); - break; - case var f when f.IsFlagSet(SBSP.SBSP_NAVIGATEFORWARD): - shellView.NavigateForward(); - break; - case var f when f.IsFlagSet(SBSP.SBSP_PARENT): - shellView.NavigateParent(); - break; - case var f when f.IsFlagSet(SBSP.SBSP_RELATIVE): - if (ShellItem.Open(shellView.CurrentFolder.IShellFolder, pidl) is ShellFolder sf) - shellView.Navigate(sf); - break; - default: - shellView.Navigate(new ShellFolder(pidl)); - break; - } - return HRESULT.S_OK; - } + /// Object names are displayed in a list view. + List = Shell32.FOLDERVIEWMODE.FVM_LIST, - /// - public virtual HRESULT ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL; + /// The view should display small icons. + SmallIcon = Shell32.FOLDERVIEWMODE.FVM_SMALLICON, - /// - public virtual HRESULT EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL; + /// The view should display thumbnail icons. + Thumbnail = Shell32.FOLDERVIEWMODE.FVM_THUMBNAIL, - /// - public virtual HRESULT Exec([In, Optional] GuidPtr pguidCmdGroup, uint nCmdID, uint nCmdexecopt, [In, Optional] /* VARIANT* */ IntPtr pvaIn, [In, Out, Optional] /* VARIANT* */ IntPtr pvaOut) => OLECMDERR_E_NOTSUPPORTED; + /// The view should display icons in a filmstrip format. + ThumbStrip = Shell32.FOLDERVIEWMODE.FVM_THUMBSTRIP, - /// - public virtual HRESULT GetControlWindow(FCW id, out HWND phwnd) - { - phwnd = id switch - { - FCW.FCW_PROGRESS => CheckAndLoad(ProgressBar), -#if NETFRAMEWORK || NETCOREAPP3_0 - FCW.FCW_STATUS => CheckAndLoad(StatusBar), - FCW.FCW_TOOLBAR => CheckAndLoad(ToolBar), -#endif - FCW.FCW_TREE => CheckAndLoad(TreeView), - _ => HWND.NULL, - }; - return phwnd.IsNull ? HRESULT.E_NOTIMPL : HRESULT.S_OK; + /// The view should display large icons. + Tile = Shell32.FOLDERVIEWMODE.FVM_TILE + } - static HWND CheckAndLoad(Control c) => c != null && c.IsHandleCreated ? c.Handle : HWND.NULL; - } + /// Event argument for The Navigated event. + public class ShellBrowserNavigatedEventArgs : EventArgs + { + /// The new location of the ShellBrowser + public ShellFolder CurrentFolder { get; } - /// - public virtual HRESULT GetViewStateStream(STGM grfMode, out IStream ppStrm) - { - ppStrm = null; - return HRESULT.E_NOTIMPL; - } + /// Initializes a new instance of the class. + public ShellBrowserNavigatedEventArgs(ShellFolder currentFolder) + { + this.CurrentFolder = currentFolder ?? throw new ArgumentNullException(nameof(currentFolder)); + } + } - /// - public virtual HRESULT GetWindow(out HWND phwnd) - { - phwnd = shellView.shellViewWindow; - return HRESULT.S_OK; - } + /// + /// Encapsulates a -Implementation within an .
+ ///
+ /// Implements the following Interfaces:
+ /// -
+ /// -
+ /// -
+ ///
+ /// For more Information on used techniques see:
+ /// -
+ ///
+ ///
+ /// Known Issues:
+ /// - Using windows 10, the virtual Quick-Access folder doesn't get displayed properly. It has to be grouped by "Group" + /// (as shown in Windows Explorer UI), but I couldn't find the OLE-Property for this. + /// Also, if using Groups, the Frequent Files List doesn't have its Icons. Maybe we have to bind to another version + /// of ComCtrls to get this rendered properly - That's just an idea though, cause the Collapse-/Expand-Icons of the + /// Groups have the Windows Vista / Windows 7-Theme, not the Windows 10 Theme as I can see.
+ /// - DONE: Keyboard input doesn't work so far.
+ /// - DONE: Only Details-Mode should have column headers: (Using Shell32.FOLDERFLAGS.FWF_NOHEADERINALLVIEWS)
+ /// https://stackoverflow.com/questions/11776266/ishellview-columnheaders-not-hidden-if-autoview-does-not-choose-details + /// - TODO: CustomDraw, when currently no shellView available
+ /// - DONE: Network folder: E_FAIL => DONE: Returning HRESULT.E_NOTIMPL from MessageSFVCB fixes this
+ /// - DONE: Disk Drive (empty): E_CANCELLED_BY_USER
+ /// - DONE: Disable header in Details view when grouping is enabled + /// - DONE: Creating ViewWindow using '.CreateViewWindow()' fails on Zip-Folders; => Fixed again by returning HRESULT.E_NOTIMPL from MessageSFVCB + /// - TODO: internal static readonly bool IsMinVista = Environment.OSVersion.Version.Major >= 6; // TODO: We use one interface, afaik, that only works in vista and above: IFolderView2 + /// - TODO: Windows 10' Quick Access folder has a special type of grouping, can't find out how this works yet. + /// As soon as we would be able to get all the available properties for an particular item, we would be able found out how this grouping works. + /// However, it seems to be a special group, since folders are Tiles, whereas files are shown in Details mode. + /// - NOTE: The grouping is done by 'Group'. Activate it using "Group by->More->Group", and then do the grouping. + /// However, the Icons for 'Recent Files'-Group get lost. + /// - TODO: ViewMode-Property, Thumbnailsize => Set ThumbnailSize for Large, ExtraLarge, etc. + /// - DONE: Keyboard-Handling + /// - DONE: BrowseObject ->Parent -> Relative + /// - TODO: Properties in design editor!!! + /// - TODO: Write History correctly! + /// - TODO: Check getting / losing Focus! again + /// - TODO: Context-Menu -> "Open File Location" doesn't work on folder "Quick Access" + /// - TODO: When columns get reordered in details mode, then navigate to another folder, then back => column content gets messed + /// + /// NOTE: https://stackoverflow.com/questions/7698602/how-to-get-embedded-explorer-ishellview-to-be-browsable-i-e-trigger-browseobje + /// NOTE: https://stackoverflow.com/questions/54390268/getting-the-current-ishellview-user-is-interacting-with + /// NOTE: https://www.codeproject.com/Articles/35197/Undocumented-List-View-Features // IMPORTANT! + /// NOTE: https://answers.microsoft.com/en-us/windows/forum/windows_10-files-winpc/windows-10-quick-access-folders-grouped-separately/ecd4be4a-1847-4327-8c44-5aa96e0120b8 + ///
+ [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [Description("A Shell object that displays a list of Shell Items.")] + [Guid("B8B0F852-9527-4CA8-AB1D-648AE95B618E")] + public class ShellBrowser + : UserControl + , IWin32Window + , IShellBrowser + , Shell32.IServiceProvider + { + private FOLDERSETTINGS folderSettings = new( + FOLDERVIEWMODE.FVM_AUTO, + FOLDERFLAGS.FWF_NOHEADERINALLVIEWS | + FOLDERFLAGS.FWF_NOWEBVIEW | + FOLDERFLAGS.FWF_USESEARCHFOLDER); - /// - public virtual HRESULT IncludeObject(IShellView ppshv, IntPtr pidl) => shellView.IncludeItem(pidl) ? HRESULT.S_OK : HRESULT.S_FALSE; + internal const int defaultThumbnailSize = 32; - /// - public virtual HRESULT InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL; + private readonly StringBuilder processCmdKeyClassName = new(processCmdKeyClassNameMaxLength + 1); + private const int processCmdKeyClassNameMaxLength = 31; + private const string processCmdKeyClassNameEdit = "Edit"; - /// - public virtual HRESULT OnDefaultCommand(IShellView ppshv) - { - var selected = shellView.SelectedItems; + private IStream viewStateStream; + private string viewStateStreamIdentifier; - if (selected.Length > 0 && selected[0].IsFolder) - { - try { shellView.Navigate(selected[0] is ShellFolder f ? f : selected[0].Parent); } - catch { } - } - else - { - shellView.OnDoubleClick(EventArgs.Empty); - } + #region Properties ==================================================================================================== - return HRESULT.S_OK; - } + /// + /// + ///

+ /// Note: I've tried using ComCtl32.ListViewMessage.LVM_SETBKIMAGE, but this doesn't work properly. + /// That's why this property has been hidden. + ///
+ [Bindable(false)] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override Image BackgroundImage { get => base.BackgroundImage; } - /// - public virtual HRESULT OnStateChange(IShellView ppshv, CDBOSC uChange) - { - if (uChange == CDBOSC.CDBOSC_SELCHANGE) - shellView.OnSelectionChanged(); - return HRESULT.S_OK; - } + /// + protected override Size DefaultSize => new(200, 150); - /// - public virtual HRESULT OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL; + private string emptyFolderText = "This folder is empty."; - /// - public virtual HRESULT QueryActiveShellView(out IShellView ppshv) - { - ppshv = null; - return HRESULT.E_NOTIMPL; - } + /// The default text that is displayed when an empty folder is shown + [Category("Appearance"), DefaultValue("This folder is empty."), Description("The default text that is displayed when an empty folder is shown.")] + public string EmptyFolderText + { + get => this.emptyFolderText; + set + { + this.emptyFolderText = value; - /// - public virtual HRESULT QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject) - { - var lriid = riid; - var i = GetType().GetInterfaces().FirstOrDefault(i => i.IsCOMObject && i.GUID == lriid); - if (i is null) - { - ppvObject = IntPtr.Zero; - return HRESULT.E_NOINTERFACE; - } + if (this.ViewHandler.IsValid) + this.ViewHandler.Text = value; + } + } - ppvObject = Marshal.GetComInterfaceForObject(this, i); - return HRESULT.S_OK; - } + /// Contains the navigation history of the ShellBrowser + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ShellNavigationHistory History { get; private set; } - /// - public virtual HRESULT QueryStatus([In, Optional] GuidPtr pguidCmdGroup, uint cCmds, [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] OLECMD[] prgCmds, [In, Out, Optional] IntPtr /* OLECMDTEXT* */ pCmdText) => HRESULT.E_FAIL; + /// The set of ShellItems in the ShellBrowser + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IReadOnlyList Items { get; } - /// - public virtual HRESULT RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL; + /// The set of selected ShellItems in the ShellBrowser + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IReadOnlyList SelectedItems { get; } - /// - public virtual HRESULT SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret) - { - pret = default; - return HRESULT.E_NOTIMPL; - } + /// The size of the thumbnails in pixels. + [Category("Appearance"), DefaultValue(defaultThumbnailSize), Description("The size of the thumbnails in pixels.")] + public int ThumbnailSize + { + get => (this.ViewHandler.IsValid) ? this.ViewHandler.ThumbnailSize : defaultThumbnailSize; + set + { + if (this.ViewHandler.IsValid) + this.ViewHandler.ThumbnailSize = value; + } + } - /// - public virtual HRESULT SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL; + /// The that is currently in use. + protected ShellBrowserViewHandler ViewHandler { get; private set; } - /// - public virtual HRESULT SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL; + /// The viewing mode of the ShellBrowser + /// Internally, this uses LVM_SETVIEW and LVM_GETVIEW messages on the ListView control + [Category("Appearance"), DefaultValue(typeof(ShellBrowserViewMode), "Auto"), Description("The viewing mode of the ShellBrowser.")] + public ShellBrowserViewMode ViewMode + { + get => (ShellBrowserViewMode)this.folderSettings.ViewMode; + set + { + // TODO: Set ThumbnailSize accordingly? + this.folderSettings.ViewMode = (FOLDERVIEWMODE)value; - /// - public virtual HRESULT SetToolbarItems(ComCtl32.TBBUTTON[] lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL; + if (this.ViewHandler.IsValid) + this.ViewHandler.ViewMode = this.folderSettings.ViewMode; + } + } - /// - public virtual HRESULT TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL; - } -} \ No newline at end of file + /// The Registry Key where Browser ViewStates get serialized + /// Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Streams\\ + [Category("Behavior"), Description("The Registry Key where Browser ViewStates get serialized.")] + public string ViewStateRegistryKey { get; set; } = + $"Software\\{ Application.CompanyName }\\{ Application.ProductName }\\ShellBrowser\\ViewStateStreams"; + + #endregion ============================================================================================================ + + #region Published Events ============================================================================================== + + /// Fires when the Items collection changes. + [Category("Action"), Description("Items changed.")] + public event EventHandler ItemsChanged; + + /// Fires when ShellBrowser has navigated to a new folder. + [Category("Action"), Description("ShellBowser has navigated to a new folder.")] + public event EventHandler Navigated; + + /// Fires when the SelectedItems collection changes. + [Category("Behavior"), Description("Selection changed.")] + public event EventHandler SelectionChanged; + + #endregion ============================================================================================================ + + /// Initializes a new instance of the class. + public ShellBrowser() + : base() + { + this.InitializeComponent(); + + this.History = new ShellNavigationHistory(); + this.Items = new ShellItemCollection(this, SVGIO.SVGIO_ALLVIEW); + this.SelectedItems = new ShellItemCollection(this, SVGIO.SVGIO_SELECTION); + + this.Resize += this.ShellBrowser_Resize; + this.HandleDestroyed += this.ShellBrowser_HandleDestroyed; + } + + /// Process known command keys of the ShellBrowser. + /// Windows Message + /// Key codes and modifiers + /// true if character was processed by the control; otherwise, false + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + // We have to take special care when the ShellView is currently renaming an item: + // If message's sender equals class 'Edit', and its parent window is our ViewWindow, + // the ShellView is currently showing an Edit-field to let the User edit an item's name. + // Thus, we have to pass all Key Strokes directly to the ShellView. + + if (this.ViewHandler.Validated() != null) + { + // Note: I tried using the LVM_GETEDITCONTROL message for finding the edit control without luck + if (User32.GetClassName(msg.HWnd, + this.processCmdKeyClassName, + ShellBrowser.processCmdKeyClassNameMaxLength) > 0) + { + if (processCmdKeyClassName.ToString().Equals(ShellBrowser.processCmdKeyClassNameEdit)) + { + // Try to get Edit field's parent 'SysListView32' handle + HWND hSysListView32 = User32.GetParent(msg.HWnd); + + if (hSysListView32 != null) + { + // Try to get SysListView32's parent 'SHELLDLL_DefView' handle + HWND hShellDllDefViewWindow = User32.GetParent(hSysListView32); + + if ((hShellDllDefViewWindow != null) && (hShellDllDefViewWindow == this.ViewHandler.ViewWindow)) + { + this.ViewHandler.ShellView.TranslateAccelerator( + new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam)); + + return true; + } + } + } + } + } + + // + // Process tab key for control focus cycle: + // Tab => Focus next control + // Tab + Shift => Focus previous control + // + if ((keyData & Keys.KeyCode) == Keys.Tab) + { + bool forward = (keyData & Keys.Shift) != Keys.Shift; + + this.Parent.SelectNextControl(ActiveControl, + forward: forward, + tabStopOnly: true, + nested: true, + wrap: true); + + return true; + } + + // + // Process folder navigation shortcuts: + // Alt + Left OR BrowserBack => Navigate back in history + // Alt + Right OR BrowserForward => Navigate forward in history + // Backspace => Navigate to parent folder + // + switch (keyData) + { + case Keys.BrowserBack: + case Keys.Alt | Keys.Left: + this.NavigateBack(); + + return true; + + case Keys.BrowserForward: + case Keys.Alt | Keys.Right: + this.NavigateForward(); + + return true; + + case Keys.Back: + this.NavigateParent(); + + return true; + } + + // + // Let the ShellView process all other keystrokes + // + if (this.ViewHandler.Validated() != null) + { + this.ViewHandler.ShellView.TranslateAccelerator( + new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam)); + + return true; + } + + return base.ProcessCmdKey(ref msg, keyData); + } + + /// + /// 's event handler for event: + /// Save ViewState when ShellBrowser gets closed. + /// + protected internal virtual void ShellBrowser_HandleDestroyed(object sender, EventArgs e) + { + this.ViewHandler.Validated()?.ShellView.SaveViewState(); + } + + /// + /// 's event handler for event: + /// Resize ViewWindow when ShellBrowser gets resized. + /// + protected internal virtual void ShellBrowser_Resize(object sender, EventArgs e) + { + this.ViewHandler?.MoveWindow(0, 0, this.ClientRectangle.Width, this.ClientRectangle.Height, false); + } + + /// Gets the items in the ShellBrowser as an IShellItemArray + /// An instance or if not available. + internal IShellItemArray GetItemsArray(SVGIO opt) + { + try + { + var viewHandler = this.ViewHandler.Validated(); + + if (viewHandler != null) + return viewHandler.FolderView2.Items(opt); + else + return null; + } + catch { return null; } + } + + /// Selects all items in the current view. + public void SelectAll() + { + var viewHandler = this.ViewHandler.Validated(); + + if (viewHandler != null) + { + // NOTE: The for-loop is rather slow, so send (Ctrl+A)-KeyDown-Message instead and let the ShellView do the work + // for (var i = 0; i < viewHandler.FolderView2.ItemCount(SVGIO.SVGIO_ALLVIEW); i++) + // viewHandler.FolderView2.SelectItem(i, SVSIF.SVSI_SELECT); + // + // TODO: Another way would be to use this Windows Message-Pattern (Workaround #2): + // https://stackoverflow.com/questions/9039989/how-to-selectall-in-a-winforms-virtual-listview + + var msg = new Message() + { + HWnd = (IntPtr)this.ViewHandler.ViewWindow, + Msg = (int)User32.WindowMessage.WM_KEYDOWN, + }; + + this.ProcessCmdKey(ref msg, Keys.Control | Keys.A); + } + } + + /// Unselects all items in the current view. + public void UnselectAll() + { + var viewHandler = this.ViewHandler.Validated(); + + if (viewHandler != null) + viewHandler.FolderView2.SelectItem(-1, SVSIF.SVSI_DESELECTOTHERS); + } + + /// Raises the event. + protected internal virtual void OnItemsChanged() => this.ItemsChanged?.Invoke(this, EventArgs.Empty); + + /// Raises the event. + protected internal virtual void OnNavigated(ShellFolder shellFolder) + { + if (this.Navigated != null) + { + ShellBrowserNavigatedEventArgs eventArgs = new(shellFolder); + + this.Navigated.Invoke(this, eventArgs); + } + } + + /// Raises the event. + protected internal virtual void OnSelectionChanged() => this.SelectionChanged?.Invoke(this, EventArgs.Empty); + + /// + /// Navigates to the last 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 NavigateBack() + { + return this.BrowseObject(IntPtr.Zero, Shell32.SBSP.SBSP_NAVIGATEBACK).Succeeded; + } + + /// + /// 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() + { + return this.BrowseObject(IntPtr.Zero, Shell32.SBSP.SBSP_NAVIGATEFORWARD).Succeeded; + } + + /// + /// Navigate within the navigation log in a specific direciton. This does not change the set of locations in the navigation log. + /// + /// The direction to navigate within the navigation logs collection. + /// True if the navigation succeeded, false if it failed for any reason. + public bool NavigateFromHistory(NavigationLogDirection direction) + { + return direction switch + { + NavigationLogDirection.Backward => this.NavigateBack(), + NavigationLogDirection.Forward => this.NavigateForward(), + _ => false, + }; + } + + /// Navigate within the navigation log. This does not change the set of locations in the navigation log. + /// An index into the navigation logs Locations collection. + /// True if the navigation succeeded, false if it failed for any reason. + public bool NavigateToHistoryIndex(int historyIndex) + { + using (ShellItem shellFolder = this.History.Seek(historyIndex, SeekOrigin.Current)) + { + if (shellFolder != null) + return this.BrowseObject((IntPtr)shellFolder.PIDL, + SBSP.SBSP_ABSOLUTE | SBSP.SBSP_WRITENOHISTORY).Succeeded; + } + + return false; + } + + /// + /// Navigates to the parent folder. + /// + /// True if the navigation succeeded, false if it failed for any reason. + public bool NavigateParent() + { + return this.BrowseObject(IntPtr.Zero, Shell32.SBSP.SBSP_PARENT).Succeeded; + } + + #region IShellBrowser interface ======================================================================================= + + /// + public HRESULT GetWindow(out HWND phwnd) + { + phwnd = this.Handle; + return HRESULT.S_OK; + } + + /// + public HRESULT ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL; + + /// + public HRESULT InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL; + + /// + public HRESULT SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL; + + /// + public HRESULT RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL; + + /// + public HRESULT SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL; + + /// + public HRESULT EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL; + + /// + public HRESULT TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL; + + /// + /// + /// + /// + /// + /// HRESULT.STG_E_PATHNOTFOUND if path n found + public HRESULT BrowseObject(IntPtr pidl, Shell32.SBSP wFlags) + { + ShellItem shellObject = null; + + // + // The given PIDL equals Desktop, so ignore the other flags + // + if (ShellFolder.Desktop.PIDL.Equals(pidl)) + { + shellObject = new ShellItem(ShellFolder.Desktop.PIDL); + } + + // + // SBSP_NAVIGATEBACK stands for the last item in the navigation history list (and ignores the pidl) + // + else if (wFlags.HasFlag(Shell32.SBSP.SBSP_NAVIGATEBACK)) + { + if (this.History.CanSeekBackward) + shellObject = this.History.SeekBackward(); + else + return HRESULT.STG_E_PATHNOTFOUND; + } + + // + // SBSP_NAVIGATEFORWARD stands for the next item in the navigation history list (and ignores the pidl) + // + else if (wFlags.HasFlag(Shell32.SBSP.SBSP_NAVIGATEFORWARD)) + { + if (this.History.CanSeekForward) + shellObject = this.History.SeekForward(); + else + return HRESULT.STG_E_PATHNOTFOUND; + } + + // + // SBSP_RELATIVE stands for a pidl relative to the current folder + // + else if (wFlags.HasFlag(Shell32.SBSP.SBSP_RELATIVE)) + { + var currentObject = this.History.Current; + + var targetObject = PIDLUtil.ILCombine((IntPtr)currentObject.PIDL, pidl); + + shellObject = new ShellItem(targetObject); + } + + // + // SBSP_PARENT stands for the parent folder (and ignores the pidl) + // + else if (wFlags.HasFlag(Shell32.SBSP.SBSP_PARENT)) + { + var currentObject = this.History.Current; + var parentObject = currentObject.Parent; + + if ((parentObject != null) && (parentObject.PIDL.IsParentOf(currentObject.PIDL))) + shellObject = parentObject; + else + return HRESULT.STG_E_PATHNOTFOUND; + } + + // + // SBSP_ABSOLUTE as the remaining option stands for an absolute pidl that is given + // + else + { + // Remember we are not the owner of this pidl, so clone it to have our own copy on the heap. + shellObject = new ShellItem(new PIDL(pidl, true)); + } + + if (this.InvokeRequired) + this.BeginInvoke((Action)(() => BrowseShellItemInternal(shellObject))); + else + BrowseShellItemInternal(shellObject); + + return HRESULT.S_OK; + + #region BrowseShellItemInternal + + void BrowseShellItemInternal(ShellItem shellItem) + { + // Save ViewState of current folder + this.ViewHandler.Validated()?.ShellView.SaveViewState(); + + if (this.viewStateStream != null) + Marshal.ReleaseComObject(this.viewStateStream); + + this.viewStateStreamIdentifier = shellItem.ParsingName; + + var viewHandler = new ShellBrowserViewHandler(this, + new ShellFolder(shellItem), + ref this.folderSettings, + ref this.emptyFolderText); + + // Clone the PIDL, to have our own object copy on the heap! + if (!wFlags.HasFlag(SBSP.SBSP_WRITENOHISTORY)) + this.History.Add(new PIDL(viewHandler.ShellFolder.PIDL)); + + var oldViewHandler = this.ViewHandler; + this.ViewHandler = viewHandler; + oldViewHandler?.UIDeactivate(); + viewHandler.UIActivate(); + oldViewHandler?.DestroyView(); + + this.OnNavigated(viewHandler.ShellFolder); + this.OnSelectionChanged(); + } + + #endregion + } + + /// + public HRESULT GetViewStateStream(STGM grfMode, out IStream stream) + { + if (this.viewStateStream != null) + Marshal.ReleaseComObject(this.viewStateStream); + + stream = this.viewStateStream = ShlwApi.SHOpenRegStream2( + hkey: HKEY.HKEY_CURRENT_USER, + pszSubkey: this.ViewStateRegistryKey, + pszValue: this.viewStateStreamIdentifier, + grfMode: grfMode); + + return stream == null ? HRESULT.E_FAIL : HRESULT.S_OK; + } + + /// + public HRESULT GetControlWindow(Shell32.FCW id, out HWND hwnd) + { + hwnd = HWND.NULL; + return HRESULT.E_NOTIMPL; + } + + /// + public HRESULT SendControlMsg(Shell32.FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret) + { + pret = IntPtr.Zero; + return HRESULT.E_NOTIMPL; + } + + /// + public HRESULT QueryActiveShellView(out Shell32.IShellView shellView) + { + if (this.ViewHandler.Validated() != null) + { + Marshal.AddRef(Marshal.GetIUnknownForObject(this.ViewHandler.ShellView)); + shellView = this.ViewHandler.ShellView; + + return HRESULT.S_OK; + } + + shellView = null; + return HRESULT.E_PENDING; + } + + /// + public HRESULT OnViewWindowActive(Shell32.IShellView ppshv) => HRESULT.E_NOTIMPL; + + /// + public HRESULT SetToolbarItems(ComCtl32.TBBUTTON[] lpButtons, uint nButtons, Shell32.FCT uFlags) => HRESULT.E_NOTIMPL; + + #endregion ============================================================================================================ + + #region IServiceProvider interface ==================================================================================== + + /// + /// -Interface Implementation for .
+ ///
+ /// Responds to the following Interfaces:
+ /// -
+ /// -
+ ///
+ /// The service's unique identifier (SID). + /// The IID of the desired service interface. + /// When this method returns, contains the interface pointer requested riid. If successful, + /// the calling application is responsible for calling IUnknown::Release using this value when the service is no + /// longer needed. In the case of failure, this value is NULL. + /// + /// or
+ /// + ///
+ HRESULT Shell32.IServiceProvider.QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject) + { + // IShellBrowser: Guid("000214E2-0000-0000-C000-000000000046") + if (riid.Equals(typeof(Shell32.IShellBrowser).GUID)) + { + ppvObject = Marshal.GetComInterfaceForObject(this, typeof(Shell32.IShellBrowser)); + + return HRESULT.S_OK; + } + + // IShellFolderViewCB: Guid("2047E320-F2A9-11CE-AE65-08002B2E1262") + if (riid.Equals(typeof(Shell32.IShellFolderViewCB).GUID)) + { + ShellBrowserViewHandler shvwHandler = this.ViewHandler.Validated(); + + if (!(shvwHandler is null)) + { + ppvObject = Marshal.GetComInterfaceForObject(shvwHandler, typeof(Shell32.IShellFolderViewCB)); + + return HRESULT.S_OK; + } + } + + ppvObject = IntPtr.Zero; + return HRESULT.E_NOINTERFACE; + } + + #endregion ============================================================================================================ + + + + #region ShellItemCollection =========================================================================================== + + /// + /// Represents a collection of attached to an . + /// + private class ShellItemCollection : IReadOnlyList + { + private readonly ShellBrowser shellBrowser; + private readonly SVGIO option; + + internal ShellItemCollection(ShellBrowser shellBrowser, SVGIO opt) + { + this.shellBrowser = shellBrowser; + this.option = opt; + } + + /// Gets the number of elements in the collection. + /// Returns a value. + public int Count + { + get + { + var viewHandler = this.shellBrowser.ViewHandler.Validated(); + + if (viewHandler != null) + return viewHandler.FolderView2.ItemCount(this.option); + + return 0; + } + } + + private IShellItemArray Array => this.shellBrowser.GetItemsArray(this.option); + + private IEnumerable Items + { + get + { + var array = this.Array; + + if (array is null) + yield break; + try + { + for (uint i = 0; i < array.GetCount(); i++) + yield return array.GetItemAt(i); + } + finally + { + Marshal.ReleaseComObject(array); + } + } + } + + /// Gets the at the specified index. + /// The . + /// The zero-based index of the element to get. + public ShellItem this[int index] + { + get + { + var array = Array; + try + { + return array is null ? null : ShellItem.Open(array.GetItemAt((uint)index)); + } + catch + { + return null; + } + finally + { + if (array != null) + Marshal.ReleaseComObject(array); + } + } + } + + /// Returns an enumerator that iterates through the collection. + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() => Items.Select(ShellItem.Open).GetEnumerator(); + + /// Returns an enumerator that iterates through the collection. + /// An enumerator that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + + #endregion ============================================================================================================ + + + + + #region Component Designer Support ==================================================================================== + + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } + + #endregion ============================================================================================================ + } + + #region SFVMUD: ShellFolderView undocumented Messages ===================================================================== + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Undocumented Flags used by Callback Handler. + /// + public enum SFVMUD + { + SFVM_SELECTIONCHANGED = 8, + SFVM_DRAWMENUITEM = 9, + SFVM_MEASUREMENUITEM = 10, + SFVM_EXITMENULOOP = 11, + SFVM_VIEWRELEASE = 12, + SFVM_GETNAMELENGTH = 13, + + SFVM_WINDOWCLOSING = 16, + SFVM_LISTREFRESHED = 17, + SFVM_WINDOWFOCUSED = 18, + SFVM_REGISTERCOPYHOOK = 20, + SFVM_COPYHOOKCALLBACK = 21, + + SFVM_ADDINGOBJECT = 29, + SFVM_REMOVINGOBJECT = 30, + + SFVM_GETCOMMANDDIR = 33, + SFVM_GETCOLUMNSTREAM = 34, + SFVM_CANSELECTALL = 35, + + SFVM_ISSTRICTREFRESH = 37, + SFVM_ISCHILDOBJECT = 38, + + SFVM_GETEXTVIEWS = 40, + + SFVM_GET_CUSTOMVIEWINFO = 77, + SFVM_ENUMERATEDITEMS = 79, // It seems this msg never gets sent, using Win 10 at least. + SFVM_GET_VIEW_DATA = 80, + SFVM_GET_WEBVIEW_LAYOUT = 82, + SFVM_GET_WEBVIEW_CONTENT = 83, + SFVM_GET_WEBVIEW_TASKS = 84, + SFVM_GET_WEBVIEW_THEME = 86, + SFVM_GETDEFERREDVIEWSETTINGS = 92, + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #endregion ================================================================================================================ + + #region ShellBrowserViewHandler =========================================================================================== + + /// + /// Encapsulates an IShellFolderViewCB-Implementation within an + /// -Object. Beside that it's implemented as a Wrapper-Object that is responsible for + /// creating and disposing the following objects aka Interface-Instances:
+ /// -
+ /// -
+ /// -
+ ///
+ /// While doing that, it also handles some common error cases:
+ /// - When there's no disk in a disk drive
+ ///
+ /// Implements the following Interfaces:
+ /// -
+ ///
+ /// This class make use of some undocumented Messages in its + /// Callback Handler.
+ ///
+ /// For more Information on these see:
+ /// - Google Drive Shell Extension: + /// ShellFolderViewCBHandler.cpp + ///
+ /// - ReactOS: + /// IShellFolderViewCB.cpp File Reference + /// , + /// IShellFolderViewCB.cpp + /// + ///
+ public class ShellBrowserViewHandler + : Shell32.IShellFolderViewCB + { + private string text; + private int thumbnailSize = ShellBrowser.defaultThumbnailSize; + + /// + /// {"The operation was canceled by the user. (Exception from HRESULT: 0x800704C7)"} is the result of a + /// call to on a Shell Item that targets a removable Disk Drive + /// when currently no Media is present. Let's catch these to use our own error handling for this. + /// + internal static readonly HRESULT HRESULT_CANCELLED = new(0x800704C7); + + #region Properties ==================================================================================================== + + /// The owner of this instance of , i.e. the . + public ShellBrowser Owner { get; } + /// The . + public ShellFolder ShellFolder { get; private set; } + /// The . + public Shell32.IShellView ShellView { get; private set; } + /// The . + public Shell32.IFolderView2 FolderView2 { get; private set; } + /// The ViewWindow. + public HWND ViewWindow { get; private set; } + /// Indicates that no error occured while creating this instance, i.e. the View is fully functional. + public bool IsValid { get; private set; } + /// Indicates that an "No Disk In Drive"-error occured while creating this instance. + public bool NoDiskInDriveError { get; } + /// The that occured, if creation of the instance failed. + public COMException ValidationError { get; private set; } + + /// The default text to be used when there are no items in the view. + public string Text + { + get => this.text; + + set + { + this.text = value; + + if (this.IsValid) + this.FolderView2.SetText(Shell32.FVTEXTTYPE.FVST_EMPTYTEXT, value); + } + } + + /// The size of the thumbnails in pixels. + public int ThumbnailSize + { + get + { + if (this.IsValid) + this.FolderView2.GetViewModeAndIconSize(out _, out this.thumbnailSize); + + return this.thumbnailSize; + } + set + { + if (this.IsValid) + { + this.FolderView2.GetViewModeAndIconSize(out var fvm, out _); + this.FolderView2.SetViewModeAndIconSize(fvm, this.thumbnailSize = value); + } + } + } + + /// The viewing mode of the ShellBrowser. + public Shell32.FOLDERVIEWMODE ViewMode + { + get + { + if (this.IsValid) + { + return this.FolderView2.GetCurrentViewMode(); + // TODO: Check ThumbNailSize for new ViewModes with larger sized icons + } + + return Shell32.FOLDERVIEWMODE.FVM_AUTO; // TODO! + } + set + { + if (this.IsValid) + this.FolderView2.SetCurrentViewMode(value); + } + } + + #endregion ============================================================================================================ + + /// + /// Create an instance of to handle Callback messages for the given ShellFolder. + /// + /// The that is owner of this instance. + /// The ShellFolder for the view. + /// The folder settings for the view. + /// Text to display if the folder is empty. + public ShellBrowserViewHandler( + ShellBrowser owner, + ShellFolder shellFolder, + ref Shell32.FOLDERSETTINGS folderSettings, + ref string emptyFolderText) + { + this.Owner = owner ?? throw new ArgumentNullException(nameof(owner)); + this.ShellFolder = shellFolder ?? throw new ArgumentNullException(nameof(shellFolder)); + + // Create ShellView and FolderView2 objects, then its ViewWindow + try + { + var sfvCreate = new Shell32.SFV_CREATE() + { + cbSize = (uint)Marshal.SizeOf(typeof(SFV_CREATE)), + pshf = shellFolder.IShellFolder, + psvOuter = null, + psfvcb = this, + }; + + Shell32.SHCreateShellFolderView(ref sfvCreate, out Shell32.IShellView shellView).ThrowIfFailed(); + + this.ShellView = shellView ?? + throw new InvalidComObjectException(nameof(this.ShellView)); + + this.FolderView2 = (Shell32.IFolderView2)this.ShellView ?? + throw new InvalidComObjectException(nameof(this.FolderView2)); + + // Try to create ViewWindow and take special care of Exception + // {"The operation was canceled by the user. (Exception from HRESULT: 0x800704C7)"} + // cause this happens when there's no disk in a drive. + try + { + this.ViewWindow = this.ShellView.CreateViewWindow(null, folderSettings, owner, owner.ClientRectangle); + + this.IsValid = true; + + this.Text = emptyFolderText; + } + catch (COMException ex) + { + // TODO: Check if the target folder IS actually a drive with removable disks in it! + if (HRESULT_CANCELLED.Equals(ex.ErrorCode)) + this.NoDiskInDriveError = true; + throw; + } + } + catch (COMException ex) + { + // TODO: e.g. C:\Windows\CSC => Permission denied! + // 0x8007 0005 E_ACCESSDENIED + this.ValidationError = ex; + } + } + + /// Destroy the view. + public void DestroyView() + { + this.IsValid = false; + + // TODO: Remove MessageSFVCB here! + + // Destroy ShellView's ViewWindow + this.ViewWindow = HWND.NULL; + this.ShellView.DestroyViewWindow(); + + this.FolderView2 = null; + this.ShellView = null; + //this.ShellFolder = null; // NOTE: I >>think<< this one causes RPC-Errors + } + + /// + /// Allows communication between the system folder view object and a system folder view callback object. + /// + /// One of the SFVM_* notifications. + /// Additional information. See the individual notification pages for specific requirements. + /// Additional information. See the individual notification pages for specific requirements. + /// TODO: @dahall: Where does this come from? + /// S_OK if the notification has been handled. E_NOTIMPL otherwise. + HRESULT Shell32.IShellFolderViewCB.MessageSFVCB(Shell32.SFVM uMsg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult) + { + switch ((SFVMUD)uMsg) + { + case SFVMUD.SFVM_SELECTIONCHANGED: + this.Owner.OnSelectionChanged(); + return HRESULT.S_OK; + + case SFVMUD.SFVM_LISTREFRESHED: + this.Owner.OnItemsChanged(); + return HRESULT.S_OK; + + default: + // + // TODO: What happens when the ViewMode gets changed via Context-Menu? => Msg #33, #18 + // + return HRESULT.E_NOTIMPL; + } + } + + /// Changes the position and dimensions of this . + /// Left + /// Top + /// Width + /// Height + /// Force redraw + /// If the function succeeds, the return value is nonzero. + public bool MoveWindow(int X, int Y, int nWidth, int nHeight, bool bRepaint) => + this.ViewWindow != HWND.NULL && User32.MoveWindow(this.ViewWindow, X, Y, nWidth, nHeight, bRepaint); + + /// Activate the ShellView of this ShellBrowser. + /// The to be set + public void UIActivate(Shell32.SVUIA uState = Shell32.SVUIA.SVUIA_ACTIVATE_NOFOCUS) => this.ShellView?.UIActivate(uState); + + /// Deactivate the ShellView of this ShellBrowser. + public void UIDeactivate() => this.UIActivate(Shell32.SVUIA.SVUIA_DEACTIVATE); + } + + /// + /// Extension methods for . + /// + public static class ShellBrowserViewHandlerExtension + { + /// + /// Returns the reference to the given if it is not null and valid, null otherwise + /// + /// A (possible) reference. + /// + public static ShellBrowserViewHandler Validated(this ShellBrowserViewHandler shellBrowserViewHandler) => + ((!(shellBrowserViewHandler is null)) && shellBrowserViewHandler.IsValid) ? shellBrowserViewHandler : null; + } + + + #endregion ================================================================================================================ + +} diff --git a/Windows.Shell/ShellObjects/ShellView.cs b/Windows.Shell/ShellObjects/ShellView.cs index c8f882ed..09b7788c 100644 --- a/Windows.Shell/ShellObjects/ShellView.cs +++ b/Windows.Shell/ShellObjects/ShellView.cs @@ -191,7 +191,7 @@ namespace Vanara.Windows.Shell /// Gets the default size of the control. protected override Size DefaultSize => new Size(250, 200); - private IShellBrowser Browser => iBrowser ??= new ShellBrowser(this); + private IShellBrowser Browser => iBrowser ??= new ShellBrowser(/* this */); /// Implements the operator !=. /// The left.