12

I'm using SharpShell to write a tiny new shell context menu item that copies the currently selected files to a new subfolder, then prompts the user for the directory's new name.

Searching StackOverflow, I found this answer. However, I'd like to do the same in SharpShell.

I will somehow have to fire SVSI_EDIT at it, which I can find buried deep in SharpShell.Interop, but I'm not sure how any of this works. I can't find any documentation or code samples whatsoever.

(Edit: I think finding out how to get a Pidl from a file name would be a good start, but maybe I don't really need that at all?)

Community
  • 1
  • 1
Lynn
  • 10,425
  • 43
  • 75
  • 1
    When you create a new folder using Explorer, it names the folder "New Folder", and then it enters rename mode. When you are typing the folders name and then pressing enter, you are actually renaming the folder. What happens if the user renames the folder, then presses enter before the files finish copying? The rename will probably fail because the files are still being copied to the folder. I think this approach is not good and you are better off showing a form with a text box to the user. – Ove Mar 20 '16 at 17:05

2 Answers2

4

You can start creating a project with SharpShell to register a new shell context menu like in this tutorial.

Here, we have to define a class implementing SharpContextMenu. For simplicity we will create the menu for any filetype and always show it:

[ComVisible(true)]
[COMServerAssociation(AssociationType.AllFiles)]
public class CopyFilesExtension : SharpContextMenu
{
    protected override bool CanShowMenu()
    {
        return true;
    }

    protected override ContextMenuStrip CreateMenu()
    {
        var menu = new ContextMenuStrip();

        var copyFiles = new ToolStripMenuItem { Text = "Copy Files To Folder..." };

        copyFiles.Click += (sender, args) => CopyFiles();
        menu.Items.Add(copyFiles);

        return menu;
    }

    private void CopyFiles()
    {
        ...
    }
} 

But I'm sure you've done all this, the problem here is to implement the CopyFiles() method.

One way to do this is showing a dialog asking for the name of the folder, something like this:

CopyFilesDialog

Then, implement CopyFiles() like so:

private void CopyFiles()
{
    using (var dialog = new CopyFileDialog())
    {
        if (dialog.ShowDialog() == DialogResult.OK)
        {
            var folder = Path.GetDirectoryName(SelectedItemPaths.First());
            var newFolder = Path.Combine(folder, dialog.FolderName);

            Directory.CreateDirectory(newFolder);

            foreach (var path in SelectedItemPaths)
            {
                var newPath = Path.Combine(newFolder, Path.GetFileName(path));
                File.Move(path, newPath);
            }
        }
    }
}

In above code, we asked for the name of the folder, then create the folder and finally move selected files to that folder.

However, if you want to do it using Rename command in Windows Explorer we can start by importing some needed Win32 functions:

class Win32
{
    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr ILCreateFromPath([In, MarshalAs(UnmanagedType.LPWStr)] string pszPath);

    [DllImport("shell32.dll")]
    public static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, int dwFlags);

    [DllImport("shell32.dll")]
    public static extern void ILFree(IntPtr pidl);
}
  • ILCreateFromPath allows us get the PIDL from a filename.
  • SHOpenFolderAndSelectItems allow us select the file and send rename command.
  • ILFree frees unmanaged PIDL created.

With these Win32 functions we can defines CopyFiles() as follows:

private void CopyFiles()
{
    var folder = Path.GetDirectoryName(SelectedItemPaths.First());
    var newFolder = Path.Combine(folder, "New Folder");
    Directory.CreateDirectory(newFolder);

    foreach (var path in SelectedItemPaths)
    {
        var newPath = Path.Combine(newFolder, Path.GetFileName(path));
        File.Move(path, newPath);
    }

    RenameInExplorer(newFolder);
}


private static void RenameInExplorer(string itemPath)
{
    IntPtr folder = Win32.ILCreateFromPath(Path.GetDirectoryName(itemPath));
    IntPtr file = Win32.ILCreateFromPath(itemPath);

    try
    {
        Win32.SHOpenFolderAndSelectItems(folder, 1, new[] { file }, 1);
    }
    finally
    {
        Win32.ILFree(folder);
        Win32.ILFree(file);
    }
}

We can't use SharpShell.Interop.Shell32 since the only method available in this class is ShellExecuteEx() which is used to launch new processes.

Arturo Menchaca
  • 15,783
  • 1
  • 29
  • 53
3

Using the SelectItemInExplorer functionality from the cited example in the question, a very basic implementation would look like the following. Where possible any P/Invoke functionality was rewritten to use as much of SharpShell's existing declarations as possible.

using SharpShell.Attributes;
using SharpShell.Interop;
using SharpShell.SharpContextMenu;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace SendToFolderRename
{
    [ComVisible(true)]
    [COMServerAssociation(AssociationType.AllFiles)]
    public class SendToFolderRename : SharpContextMenu
    {
        [DllImport("ole32.dll")]
        private static extern int CreateBindCtx(int reserved, out IntPtr ppbc);

        [DllImport("shell32.dll")]
        private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, int dwFlags);

        protected override bool CanShowMenu()
        {
            return true;
        }

        protected override ContextMenuStrip CreateMenu()
        {
            var menu = new ContextMenuStrip();
            var itemCountLines = new ToolStripMenuItem
            {
                Text = "Copy files to subfolder"
            };

            itemCountLines.Click += CopyFilesToSubfolder;
            menu.Items.Add(itemCountLines);
            return menu;
        }

        private void CopyFilesToSubfolder(object sender, EventArgs e)
        {
            //System.Diagnostics.Debugger.Break();
            string firstSelectedFile = SelectedItemPaths.FirstOrDefault();

            if (string.IsNullOrEmpty(firstSelectedFile))
                return;

            string currentDirPath = (new FileInfo(firstSelectedFile)).DirectoryName;
            string newDirName = Path.GetRandomFileName();
            string newDirPath = Path.Combine(currentDirPath, newDirName);
            DirectoryInfo newDir = Directory.CreateDirectory(newDirPath);
            foreach (string filePath in SelectedItemPaths)
            {
                FileInfo fileInfo = new FileInfo(filePath);
                string newFilePath = Path.Combine(fileInfo.DirectoryName, newDirName, fileInfo.Name);
                File.Copy(filePath, newFilePath);
            }

            SelectItemInExplorer(IntPtr.Zero, newDirPath, true);
        }

        public static void SelectItemInExplorer(IntPtr hwnd, string itemPath, bool edit)
        {
            if (itemPath == null)
                throw new ArgumentNullException("itemPath");

            IntPtr folder = PathToAbsolutePIDL(hwnd, Path.GetDirectoryName(itemPath));
            IntPtr file = PathToAbsolutePIDL(hwnd, itemPath);
            try
            {
                SHOpenFolderAndSelectItems(folder, 1, new[] { file }, edit ? 1 : 0);
            }
            finally
            {
                Shell32.ILFree(folder);
                Shell32.ILFree(file);
            }
        }

        private static IntPtr GetShellFolderChildrenRelativePIDL(IntPtr hwnd, IShellFolder parentFolder, string displayName)
        {
            IntPtr bindCtx;
            CreateBindCtx(0, out bindCtx);

            uint pchEaten = 0;
            SFGAO pdwAttributes = 0;
            IntPtr ppidl;
            parentFolder.ParseDisplayName(hwnd, bindCtx, displayName, ref pchEaten, out ppidl, ref pdwAttributes);
            return ppidl;
        }

        private static IntPtr PathToAbsolutePIDL(IntPtr hwnd, string path)
        {
            IShellFolder desktopFolder;
            Shell32.SHGetDesktopFolder(out desktopFolder);
            return GetShellFolderChildrenRelativePIDL(hwnd, desktopFolder, path);
        }
    }
}
Community
  • 1
  • 1
cokeman19
  • 2,405
  • 1
  • 25
  • 40
  • Hi, thanks a lot for your answer. I will try it tomorrow (I’ve been busy) and report back if it works. It’d be a bit sad if there isn’t a more SharpShell-y way to do it, but it’s better than nothing :) – Lynn Mar 25 '16 at 09:36