如何从 C# 发送 WM_DROPFILES?

How can I send WM_DROPFILES from C#?

我想使用 C# 代码在单独的进程中模拟用户将文件拖放到控件上。作为实现此目标的垫脚石,我正在尝试向我自己的 TextBox 发送 WM_DROPFILES 消息并验证是否触发了 DragDrop 事件。

在包含一个文本框和两个按钮的表单中使用以下代码,单击按钮 1 成功将文本框 1 的文本设置为 "Hello world"。因此,我似乎正确地使用了 SendMessage 并且能够通过指针提供参数。将文件从 Windows Explorer 拖放到 textBox1 上会显示 MessageBox,因此 textBox1 已设置为可以正确接收拖放的文件。但是,当我单击 button2 时,没有任何反应。为什么我在单击 button2 时看不到 MessageBox?

using System;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Whosebug
{
    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[=11=]";
            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" + "[=11=][=11=]";

            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;
        }
    }
}

您看不到 MessageBox 的原因可能是因为 TextBox 不处理 WM_DROPFILES 消息。它通过在 WPF 文档中实现 IDropTarget interface (see Drag and Drop Overview 而不是使用 OLE 拖放来实现拖放支持。

WM_DROPFILES 自从 Windows 95 引入 DoDragDrop() 以来就已被弃用。OLE 拖放一直是 首选 方式在 Windows 上实现拖放很长时间了。 WM_DROPFILES 仍受 Windows 支持(但不支持 .NET),但只是为了与旧版应用程序向后兼容。

将项目从 Windows Explorer 拖到其他应用程序在后台使用 OLE 拖放,即使接收方没有实现 OLE 拖放也是如此。

如果您将 IDataObject 拖放到 RegisterDragDrop() 调用过的 window 上(就像您的 TextBox 一样),IDataObject 将传递给window的IDropTarget接口进行处理。

如果您将 IDataObject 拖放到未实现 IDropTarget 但已调用 DragAcceptFiles() 的 window 上,或者至少具有 WS_EX_ACCEPTFILES window 样式,如果 IDataObject 包含 CF_HDROP 数据,Windows 将生成一条 WM_DROPFILES 消息。

因此,要使用 TextBox 执行您尝试的操作,您需要实施 IDropSourceIDataObject接口,然后用它们调用DoDragDrop()。让 Windows 为您处理实际的拖放操作。请参阅Shell 剪贴板格式 处理Shell 数据传输方案

现在,话虽如此,如果您仍然决定向另一个进程发送 WM_DROPFILES 消息(您的问题说这是您的最终目标),您可以这样做,并且 Windows将为您跨进程边界编组您的 DROPFILES 结构,但您需要使用 PostMessage() 而不是 SendMessage() (不知道为什么,只是它是必需的),并确保释放DROPFILES 仅当 PostMessage() 失败时。如果 PostMessage() 成功,Windows 将为您释放 DROPFILES

但即便如此,也无法保证接收进程实际处理 WM_DROPFILES 消息(即使处理了,您的手动 WM_DROPFILES 消息也可能会被 UIPI 阻止,除非接收方调用ChangeWindowMessageFilter/Ex() 本身允许 WM_DROPFILESWM_COPYGLOBALDATA 消息)。接收者可能期望 IDataObject 相反。如果接收器甚至完全支持拖放。