Windows 在不同的应用程序之间拖放

Windows drag drop between different applications

我有两个 WinForms 应用程序,希望能够将对象从一个拖到另一个。

我的数据对象代码很简单:

// the data object
[ComVisible(true)]
[Serializable]
public class MyData : ISerializable {
    public int Value1 { get; set; }
    public int Value2 { get; set; } 

    public MyData() { }

    public MyData(int value1, int value2) {
        Value1 = value1;
        Value2 = value2;
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context) {
        info.AddValue(nameof(Value1), Value1);
        info.AddValue(nameof(Value2), Value2);
    }
}

该对象是 dll 的一部分,我的两个 WinForms 应用程序都引用了它。

我正在使用以下方法初始化拖放:

// inside some control
MyData toBeTransmitted = new MyData(0, 0);
IDataObject dataObject = new DataObject(DataFormats.Serializable, toBeTransmitted);
this.DoDragDrop(dataObject, DragDropEffects.All);

并使用以下方式处理它:

// inside some drag over handler
IDataObject dataObject = dragEvent.DataObject;
if (dataObject.GetDataPresent(DataFormats.Serializable)) {
    object obj = e.DataObject.GetData(DataFormats.Serializable);
}

只要我在单个应用程序中拖放数据,所有这些都可以正常工作。 但是,一旦我将数据从一个进程拖到另一个进程,检索拖动的数据 returns 类型的对象 System.__ComObject 而不是 MyData.

如何检索 IDataObject 中包含的实际数据?

(注意:我也尝试使用自定义格式而不是 DataFormats.Serializable,但运气不佳。)

问题:
当指定 DataFormats.Serializable 时,DataObject class 使用 BinaryFormatter 序列化实现 ISerializable 的 class 对象(顺便说一句,您应该添加一个 public MyData(SerializationInfo info, StreamingContext context) 构造函数)。

BinaryFormatter 对象是使用带有默认 Binder 的标准形式创建的,这意味着序列化对象包含 strict 程序集信息。
因此,您可以 Drag/Drop 您的对象 to/from 代表同一程序集实例的进程没有问题,但如果程序集不同或它们的版本不匹配,则 BinaryFormatter 反序列化失败结果你得到一个展开的 IComDataObject

您可以自己编组此 COM 对象,这意味着您必须构建一个兼容的 FORMATETC struct object (System.Runtime.InteropServices.ComTypes.FORMATETC), get the STGMEDIUM from IDataObject.GetData([FORMATETC], out [STGMEDIUM]), get the IStream object using Marshal.GetObjectForIUnknown(),传递 [STGMEDIUM].unionmember 指针。
然后创建一个指定 less restrictive / custom Binder 的 BinaryFormatter 并反序列化流,忽略或替换程序集名称。

在你问之前,你不能直接在你的 ISerializable class 中设置 [SerializationInfo].AssemblyName(即使它不是 read-only),这是行不通的。

可能的解决方案:
一种简单的方法是用不同的序列化程序替换 BinaryFormatter 并创建 IDataObject 设置自定义格式(或与生成的数据兼容的预定义 DataFormat)。


使用 XmlSerializer 作为序列化程序和 MemoryStream 的示例:

可以简化 class 对象,删除 ISerializable 实现:

[Serializable]
public class MyData {
    public int Value1 { get; set; }
    public int Value2 { get; set; }

    public MyData() { }

    public MyData(int value1, int value2)
    {
        Value1 = value1;
        Value2 = value2;
    }

    public override string ToString()
    {
        return $"Value1: {Value1}, Value2: {Value2}";
    }
}

两个静态方法用于在 Source 端生成 IDataObject 并在 Target 端提取其内容。 XmlSerializer 用于序列化/反序列化 class 个对象。

private static IDataObject SetObjectData<T>(object value, string format) where T : class
{
    using (var ms = new MemoryStream())
    using (var sw = new StreamWriter(ms)) {
        var serializer = new XmlSerializer(typeof(T), "");
        serializer.Serialize(sw, value);
        sw.Flush();
        var data = new DataObject(format, ms.ToArray());
        // Failsafe custom data type - could be a GUID, anything else, or removed entirely
        data.SetData("MyApp_DataObjectType", format);
        return data;
    };
}

private static T GetObjectData<T>(IDataObject data, string format) where T : class
{
    // Throws if the byte[] cast fails
    using (var ms = new MemoryStream(data.GetData(format) as byte[])) {
        var serializer = new XmlSerializer(typeof(T));
        var obj = serializer.Deserialize(ms);
        return (T)obj;
    }
}

在示例中,我使用 Dictionary<string, Action> 来调用基于从 DragDrop 操作接收到的 IDataObject 中包含的类型的方法。
这是因为我想你可以转移不同的类型。当然你可以使用其他任何东西。
您也可以使用通用接口,并将其用作 <T>。它将大大简化整个实现(以及未来的扩展,如果可以定义足够通用的方法和属性的话)。

Dictionary<string, Action<IDataObject>> dataActions = new Dictionary<string, Action<IDataObject>>() {
    ["MyData"] = (data) => {
        // The Action delegate deserialzies the IDataObject...
        var myData = GetObjectData<MyData>(data, "MyData");
        // ...and calls a method passing the class object
        MessageBox.Show(myData.ToString());
    },
    ["MyOtherData"] = (data) => {
        var otherData = GetObjectData<MyOtherData>(data, "MyOtherData");
        MessageBox.Show(otherData.ToString());
    }
};

源端(Drag/Drop 发起者):

Point mouseDownPos = Point.Empty;

private void SomeSourceControl_MouseDown(object sender, MouseEventArgs e)
{
    mouseDownPos = e.Location;
}

private void SomeSourceControl_MouseMove(object sender, MouseEventArgs e)
{
    MyData toBeTransmitted = new MyData(100, 100);

    if (e.Button == MouseButtons.Left &&
        ((Math.Abs(e.X - mouseDownPos.X) > SystemInformation.DragSize.Width) ||
         (Math.Abs(e.Y - mouseDownPos.Y) > SystemInformation.DragSize.Height))) {

        var data = SetObjectData<MyData>(toBeTransmitted, "MyData");
        DoDragDrop(data, DragDropEffects.All);
    }
}

在目标端(Drag/Drop 目标)

private void SomeTargetControl_DragEnter(object sender, DragEventArgs e)
{
    var formats = e.Data.GetFormats();
    // Verify that a Data Type is defined in the Dictionary
    if (formats.Any(f => dataActions.ContainsKey(f))) {
        e.Effect = DragDropEffects.All;
    }
}

private void SomeTargetControl_DragDrop(object sender, DragEventArgs e)
{
    // Double check: the fail-safe Data Type is present
    string dataType = (string)e.Data.GetData("MyApp_DataObjectType");
    // If the Data Type is in the Dictionary, deserialize and call the Action
    if (dataActions.ContainsKey(dataType)) {
        dataActions[dataType](e.Data);
    }
}