Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Select a file for renaming in SharpShell context menu

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?)

like image 576
Lynn Avatar asked Jul 16 '15 21:07

Lynn


2 Answers

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.

like image 135
Arturo Menchaca Avatar answered Oct 16 '22 20:10

Arturo Menchaca


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);
        }
    }
}
like image 3
cokeman19 Avatar answered Oct 16 '22 19:10

cokeman19