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);
}
}
我有两个 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);
}
}