Reputation: 1311
I want to use C# code to simulate a user dragging and dropping a file onto a control in a separate process. As a stepping stone to this goal, I am trying to send a WM_DROPFILES message to my own TextBox and verifying that the DragDrop event is triggered.
With the code below in a Form containing a single TextBox and two Buttons, clicking on button1 successfully sets the text of textBox1 to "Hello world". So, it seems that I'm using SendMessage correctly and am able to supply arguments via pointers. Dragging and dropping a file from Windows Explorer onto textBox1 displays the MessageBox, so textBox1 is set up to receive drag-dropped files correctly. However, when I click button2, nothing happens. Why don't I see a MessageBox when I click button2?
using System;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace StackOverflow
{
public partial class BadDragDrop : Form
{
#region WINAPI
[Serializable]
[StructLayout(LayoutKind.Sequential)]
struct POINT
{
public Int32 X;
public Int32 Y;
}
[Serializable]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
class DROPFILES
{
public Int32 size;
public POINT pt;
public Int32 fND;
public Int32 WIDE;
}
const uint WM_DROPFILES = 0x0233;
const uint WM_SETTEXT = 0x000C;
[DllImport("Kernel32.dll", SetLastError = true)]
static extern int GlobalLock(IntPtr Handle);
[DllImport("Kernel32.dll", SetLastError = true)]
static extern int GlobalUnlock(IntPtr Handle);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
#endregion
public BadDragDrop()
{
InitializeComponent();
textBox1.AllowDrop = true;
}
private void button1_Click(object sender, EventArgs e)
{
string textToSet = "Hello world\0";
IntPtr p = Marshal.AllocHGlobal(textToSet.Length);
Marshal.Copy(textToSet.Select(c => (byte)c).ToArray(), 0, p, textToSet.Length);
int success = GlobalUnlock(p);
SendMessage(textBox1.Handle, WM_SETTEXT, IntPtr.Zero, p);
Marshal.FreeHGlobal(p);
}
private void button2_Click(object sender, EventArgs e)
{
string filePath = @"C:\Windows\win.ini" + "\0\0";
DROPFILES s = new DROPFILES()
{
size = Marshal.SizeOf<DROPFILES>(),
pt = new POINT() { X = 10, Y = 10 },
fND = 0,
WIDE = 0,
};
int wparamLen = s.size + filePath.Length;
IntPtr p = Marshal.AllocHGlobal(wparamLen);
int iSuccess = GlobalLock(p);
Marshal.StructureToPtr(s, p, false);
Marshal.Copy(filePath.Select(c => (byte)c).ToArray(), 0, p + s.size, filePath.Length);
iSuccess = GlobalUnlock(p);
var verify = new byte[wparamLen];
Marshal.Copy(p, verify, 0, wparamLen);
var ipSuccess = SendMessage(textBox1.Handle, WM_DROPFILES, p, IntPtr.Zero);
Marshal.FreeHGlobal(p);
}
private void textBox1_DragDrop(object sender, DragEventArgs e)
{
MessageBox.Show(this, "Drag drop!");
}
private void textBox1_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Copy;
}
}
}
Upvotes: 0
Views: 2156
Reputation: 595367
The reason you don't see your MessageBox
appear is likely because the TextBox
doesn't handle WM_DROPFILES
messages to begin with. It implements its drop support using OLE Drag&Drop instead by implementing the IDropTarget
interface (see Drag and Drop Overview in the WPF documentation).
WM_DROPFILES
has been deprecated ever since DoDragDrop()
was introduced way back in Windows 95. OLE Drag&Drop has been the preferred way to implement drag&drop on Windows for a very long time. WM_DROPFILES
is still supported by Windows (but not .NET) but only for backwards compatibility with legacy apps.
Dragging items from Windows Explorer to other apps uses OLE Drag&Drop under the hood, even if the receiver doesn't implement OLE Drag&Drop.
If you drag&drop an IDataObject
onto a window that has had RegisterDragDrop()
called on it (like your TextBox
has), the IDataObject
will be passed to the window's IDropTarget
interface for handling.
If you drag&drop an IDataObject
onto a window that doesn't implement IDropTarget
, but has had DragAcceptFiles()
called on it, or at least has the WS_EX_ACCEPTFILES
window style, Windows will generate a WM_DROPFILES
message if the IDataObject
contains CF_HDROP
data in it.
So, to do what you are attempting with your TextBox
, you need to implement the IDropSource
and IDataObject
interfaces, and then call DoDragDrop()
with them. Let Windows handle the actual dragging and dropping for you. See Shell Clipboard Formats and Handling Shell Data Transfer Scenarios.
Now, with that said, if you are still determined to send a WM_DROPFILES
message to another process (which your question says is your ultimate goal), you can do that, and Windows will marshal your DROPFILES
struct across process boundaries for you, but you need to use PostMessage()
instead of SendMessage()
(not sure why, only that it is required), and be sure to free the DROPFILES
only if PostMessage()
fails. Windows will free the DROPFILES
for you if PostMessage()
is successful.
But even then, there is no guarantee that the receiving process actually handles WM_DROPFILES
messages (and even if it does, your manual WM_DROPFILES
message might get blocked by UIPI, unless the receiver calls ChangeWindowMessageFilter/Ex()
on itself to allow WM_DROPFILES
and WM_COPYGLOBALDATA
messages). The receiver might be expecting IDataObject
instead. If the receiver even supports drag&drop at all.
Upvotes: 3