Introduction
Recently, I needed to implement drag and drop with the Windows Explorer, but all of the examples I could find were not quite what I wanted. I wanted bi-directional, and in C#. So I wrote one.
This sample project lists a folder full of files, and lets you drag and drop them into Explorer. You can also drag from Explorer into the sample, and you can use the Shift and Ctrl keys to modify the action, just like in Explorer.
You can also use the right click options for cut and paste. I discovered that it is not possible to receive notifications when someone pastes your files into another folder (thereby cutting them out of the folder you are displaying) to allow you to update the view. So, I implemented a simple file watcher to watch the displayed folder and refresh it if it changes.
Background
I found a couple of examples on MSDN for drag and drop, but not using files, but they were still useful (Performing Drag-and-Drop Operations and Control.DoDragDrop Method).
Also, there was a snippet for doing cut & paste, which was useful: How to Cut Files to the Clipboard Programmatically in C#?
Using the code
The sample is fairly self explanatory, I’ve tried to cut out any code that wasn’t showing stuff that was relevant.
To start a drag operation into Explorer, we implement the ItemDrag
event from the Listview
, which gets called after you drag an item more than a few pixels. We simply call DoDragDrop
passing the files to be dragged wrapped in a DataObject
. You don't really need to understand DataObject
- it implements the IDataObject
interface used in the communication.
private void listView1_ItemDrag(object sender,
System.Windows.Forms.ItemDragEventArgs e)
{
string[] files = GetSelection();
if(files != null)
{
DoDragDrop(new DataObject(DataFormats.FileDrop, files),
DragDropEffects.Copy |
DragDropEffects.Move );
RefreshView();
}
}
Then, during the drag operation, if the drag happens over your window, your DragOver
method gets called. You have to tell the caller what would happen if the user drops on this location. You do this by setting e.Effect
to the correct values:
private void listView1_DragOver(object sender,
System.Windows.Forms.DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.None;
return;
}
if ((e.KeyState & SHIFT) == SHIFT &&
(e.AllowedEffect & DragDropEffects.Move) == DragDropEffects.Move)
{
e.Effect = DragDropEffects.Move;
}
else if ((e.KeyState & CTRL) == CTRL &&
(e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy)
{
e.Effect = DragDropEffects.Copy;
}
else if ((e.AllowedEffect & DragDropEffects.Move) == DragDropEffects.Move)
{
e.Effect = DragDropEffects.Move;
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length > 0 && !files[0].ToUpper().StartsWith(homeDisk) &&
(e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy)
e.Effect = DragDropEffects.Copy;
}
else
e.Effect = DragDropEffects.None;
Point pt = listView1.PointToClient(new Point(e.X, e.Y));
ListViewItem itemUnder = listView1.GetItemAt(pt.X, pt.Y);
}
Then, when the user actually releases the mouse, the DragDrop
method gets called. Here, we actually do the cut or copy operation:
private void listView1_DragDrop(object sender,
System.Windows.Forms.DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
{
return;
}
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
foreach (string file in files)
{
string dest = homeFolder + "\\" + Path.GetFileName(file);
bool isFolder = Directory.Exists(file);
bool isFile = File.Exists(file);
if (!isFolder && !isFile)
continue;
try
{
switch(e.Effect)
{
case DragDropEffects.Copy:
if(isFile)
File.Copy(file, dest, false);
break;
case DragDropEffects.Move:
if (isFile)
File.Move(file, dest);
break;
case DragDropEffects.Link:
break;
}
}
catch(IOException ex)
{
MessageBox.Show(this, "Failed to perform the" +
" specified operation:\n\n" + ex.Message,
"File operation failed", MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
}
RefreshView();
}
That's all there is to it! Well, actually, there is a little more. If you want to use custom cursors, you can implement GiveFeedback
, and if you want to know when the drag leaves your window, you can implement QueryContinueDrag
, but I left these out to keep the code clean.
Cut and paste turned out to be quite simple as well. On the cut or copy operation, you just put the filenames on the clipboard using our trusty DataObject
class:
void CopyToClipboard(bool cut)
{
string[] files = GetSelection();
if(files != null)
{
IDataObject data = new DataObject(DataFormats.FileDrop, files);
MemoryStream memo = new MemoryStream(4);
byte[] bytes = new byte[]{(byte)(cut ? 2 : 5), 0, 0, 0};
memo.Write(bytes, 0, bytes.Length);
data.SetData("Preferred DropEffect", memo);
Clipboard.SetDataObject(data);
}
}
And then on the paste (if someone pastes onto us), we do the reverse:
private void pasteMenuItem_Click(object sender, System.EventArgs e)
{
IDataObject data = Clipboard.GetDataObject();
if (!data.GetDataPresent(DataFormats.FileDrop))
return;
string[] files = (string[])
data.GetData(DataFormats.FileDrop);
MemoryStream stream = (MemoryStream)
data.GetData("Preferred DropEffect", true);
int flag = stream.ReadByte();
if (flag != 2 && flag != 5)
return;
bool cut = (flag == 2);
foreach (string file in files)
{
string dest = homeFolder + "\\" +
Path.GetFileName(file);
try
{
if(cut)
File.Move(file, dest);
else
File.Copy(file, dest, false);
}
catch(IOException ex)
{
MessageBox.Show(this, "Failed to perform the" +
" specified operation:\n\n" + ex.Message,
"File operation failed",
MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
RefreshView();
}
Points of interest
The most annoying thing was that if you cut from the sample and paste into Explorer, Explorer will move the files for you and take them out of your folder. However, there is no way to get a notification when this paste happens, so it is impossible to keep your view of the files up to date. I implemented a small file watcher which watches the sample folder, but I imagine this could get much more complicated if you need to watch many folders.