c# - Arrastre y suelte entre instancias de la misma aplicación de Windows Forms
winforms drag-and-drop (4)
He creado una pequeña aplicación de prueba de Windows Forms para probar un código de arrastrar y soltar. El formulario consta de tres PictureBoxes. Mi intención era tomar una imagen de un PictureBox, mostrarla como un cursor personalizado durante la operación de arrastre y luego soltarla en otro objetivo de PictureBox.
Esto funciona bien de un PictureBox a otro siempre y cuando estén en el mismo formulario .
Si abro dos instancias de la misma aplicación e intento arrastrar / soltar entre ellas, aparece el siguiente error críptico:
Este proxy remoto no tiene receptor de canales, lo que significa que el servidor no tiene canales de servidor registrados que estén escuchando o que esta aplicación no tiene un canal de cliente adecuado para hablar con el servidor.
Sin embargo, por alguna razón, funciona para arrastrar / soltar en Wordpad (pero no en MS Word o Paintbrush).
Los tres PictureBoxes tienen sus eventos conectados así:
foreach (Control pbx in this.Controls) {
if (pbx is PictureBox) {
pbx.AllowDrop = true;
pbx.MouseDown += new MouseEventHandler(pictureBox_MouseDown);
pbx.GiveFeedback += new GiveFeedbackEventHandler(pictureBox_GiveFeedback);
pbx.DragEnter += new DragEventHandler(pictureBox_DragEnter);
pbx.DragDrop += new DragEventHandler(pictureBox_DragDrop);
}
}
Luego están los cuatro eventos como este:
void pictureBox_MouseDown(object sender, MouseEventArgs e) {
int width = (sender as PictureBox).Image.Width;
int height = (sender as PictureBox).Image.Height;
Bitmap bmp = new Bitmap(width, height);
Graphics g = Graphics.FromImage(bmp);
g.DrawImage((sender as PictureBox).Image, 0, 0, width, height);
g.Dispose();
cursorCreatedFromControlBitmap = CustomCursors.CreateFormCursor(bmp, transparencyType);
bmp.Dispose();
Cursor.Current = this.cursorCreatedFromControlBitmap;
(sender as PictureBox).DoDragDrop((sender as PictureBox).Image, DragDropEffects.All);
}
void pictureBox_GiveFeedback(object sender, GiveFeedbackEventArgs gfea) {
gfea.UseDefaultCursors = false;
}
void pictureBox_DragEnter(object sender, DragEventArgs dea) {
if ((dea.KeyState & 32) == 32) { // ALT is pressed
dea.Effect = DragDropEffects.Link;
}
else if ((dea.KeyState & 8) == 8) { // CTRL is pressed
dea.Effect = DragDropEffects.Copy;
}
else if ((dea.KeyState & 4) == 4) { // SHIFT is pressed
dea.Effect = DragDropEffects.None;
}
else {
dea.Effect = DragDropEffects.Move;
}
}
void pictureBox_DragDrop(object sender, DragEventArgs dea) {
if (((IDataObject)dea.Data).GetDataPresent(DataFormats.Bitmap))
(sender as PictureBox).Image = (Image)((IDataObject)dea.Data).GetData(DataFormats.Bitmap);
}
Cualquier ayuda sería muy apreciada!
Después de horas y horas de frustración con el vapor saliendo de mis oídos, finalmente llegué a una segunda solución a este problema. Exactamente cuál es la solución más elegante probablemente esté en los ojos del espectador. Espero que las soluciones de Michael y mis ayuden a los programadores frustrados y les ahorren tiempo cuando se embarcan en misiones similares.
Primero que nada, una cosa que me sorprendió fue que Wordpad pudo recibir las imágenes de arrastrar y soltar fuera de la caja. Por lo tanto, el empaquetado del archivo probablemente no fue el problema, pero quizás hubo algo sospechoso en el extremo receptor.
Y a pescado había. Resulta que hay varios tipos de IDataObjects que flotan en el marco .Net. Como señaló Michael, el soporte de arrastrar y soltar OLE intenta utilizar el control remoto .Net al interactuar entre aplicaciones. Esto realmente pone un System.Runtime.Remoting.Proxies .__ TransparentProxy donde se supone que debe estar la imagen. Claramente esto no es (totalmente) correcto.
El siguiente artículo me dio algunos consejos en la dirección correcta:
De forma predeterminada, Windows Forms es System.Windows.Forms.IDataObject. Sin embargo, como estamos tratando con diferentes procesos aquí, decidí darle una oportunidad a System.Runtime.InteropServices.ComTypes.IDataObject.
En el evento dragdrop, el siguiente código resuelve el problema:
const int CF_BITMAP = 2;
System.Runtime.InteropServices.ComTypes.FORMATETC formatEtc;
System.Runtime.InteropServices.ComTypes.STGMEDIUM stgMedium;
formatEtc = new System.Runtime.InteropServices.ComTypes.FORMATETC();
formatEtc.cfFormat = CF_BITMAP;
formatEtc.dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT;
formatEtc.lindex = -1;
formatEtc.tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_GDI;
Las dos funciones GetData solo comparten el mismo nombre. Uno devuelve un objeto, el otro se define para devolver un vacío y, en cambio, pasa la información al parámetro de salida stgMedium:
(dea.Data as System.Runtime.InteropServices.ComTypes.IDataObject).GetData(ref formatEtc, out stgMedium);
Bitmap remotingImage = Bitmap.FromHbitmap(stgMedium.unionmember);
(sender as PictureBox).Image = remotingImage;
Finalmente, para evitar pérdidas de memoria, probablemente sea una buena idea llamar a la función OLE ReleaseStgMedium:
ReleaseStgMedium(ref stgMedium);
Esa función se puede incluir de la siguiente manera:
[DllImport("ole32.dll")]
public static extern void ReleaseStgMedium([In, MarshalAs(UnmanagedType.Struct)] ref System.Runtime.InteropServices.ComTypes.STGMEDIUM pmedium);
... y este código parece funcionar perfectamente con las operaciones de arrastrar y soltar (de mapas de bits) entre dos aplicaciones. El código podría extenderse fácilmente a otros formatos de portapapeles válidos y probablemente también a formatos de portapapeles personalizados. Como no se hizo nada con la parte del paquete, aún puede arrastrar una imagen a Wordpad, y como acepta formatos de mapa de bits, también puede arrastrar una imagen de Word a la aplicación.
Como nota al margen, arrastrar y soltar una imagen directamente desde IE ni siquiera genera el evento DragDrop. Extraño.
Después de mucho crujir de dientes y arrancarme el cabello, pude encontrar una solución viable. Parece que hay algo de extrañeza indocumentada bajo las cubiertas con .NET y su soporte para arrastrar y soltar OLE. Parece estar intentando usar el control remoto .NET al realizar arrastrar y soltar entre aplicaciones .NET, pero ¿está documentado en alguna parte? No, no creo que lo sea.
Así que la solución que encontré involucra una clase auxiliar para ayudar a calcular los datos de mapa de bits entre procesos. Primero, aquí está la clase.
[Serializable]
public class BitmapTransfer
{
private byte[] buffer;
private PixelFormat pixelFormat;
private Size size;
private float dpiX;
private float dpiY;
public BitmapTransfer(Bitmap source)
{
this.pixelFormat = source.PixelFormat;
this.size = source.Size;
this.dpiX = source.HorizontalResolution;
this.dpiY = source.VerticalResolution;
BitmapData bitmapData = source.LockBits(
new Rectangle(new Point(0, 0), source.Size),
ImageLockMode.ReadOnly,
source.PixelFormat);
IntPtr ptr = bitmapData.Scan0;
int bufferSize = bitmapData.Stride * bitmapData.Height;
this.buffer = new byte[bufferSize];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, bufferSize);
source.UnlockBits(bitmapData);
}
public Bitmap ToBitmap()
{
Bitmap bitmap = new Bitmap(
this.size.Width,
this.size.Height,
this.pixelFormat);
bitmap.SetResolution(this.dpiX, this.dpiY);
BitmapData bitmapData = bitmap.LockBits(
new Rectangle(new Point(0, 0), bitmap.Size),
ImageLockMode.WriteOnly, bitmap.PixelFormat);
IntPtr ptr = bitmapData.Scan0;
int bufferSize = bitmapData.Stride * bitmapData.Height;
System.Runtime.InteropServices.Marshal.Copy(this.buffer, 0, ptr, bufferSize);
bitmap.UnlockBits(bitmapData);
return bitmap;
}
}
Para usar la clase de una manera que admita .NET y los destinatarios no administrados del mapa de bits, se usa una clase DataObject para la operación de arrastrar y soltar de la siguiente manera.
Para iniciar la operación de arrastre:
DataObject dataObject = new DataObject();
dataObject.SetData(typeof(BitmapTransfer),
new BitmapTransfer((sender as PictureBox).Image as Bitmap));
dataObject.SetData(DataFormats.Bitmap,
(sender as PictureBox).Image as Bitmap);
(sender as PictureBox).DoDragDrop(dataObject, DragDropEffects.All);
Para completar la operación:
if (dea.Data.GetDataPresent(typeof(BitmapTransfer)))
{
BitmapTransfer bitmapTransfer =
(BitmapTransfer)dea.Data.GetData(typeof(BitmapTransfer));
(sender as PictureBox).Image = bitmapTransfer.ToBitmap();
}
else if(dea.Data.GetDataPresent(DataFormats.Bitmap))
{
Bitmap b = (Bitmap)dea.Data.GetData(DataFormats.Bitmap);
(sender as PictureBox).Image = b;
}
La verificación del cliente BitmapTransfer se realiza primero, por lo que tiene prioridad sobre la existencia de un Bitmap regular en el objeto de datos. La clase BitmapTransfer podría ubicarse en una biblioteca compartida para su uso con múltiples aplicaciones. Debe marcarse como serializable como se muestra para arrastrar y soltar entre aplicaciones. Lo probé con arrastrar y soltar mapas de bits dentro de una aplicación, entre aplicaciones y desde una aplicación .NET a Wordpad.
Espero que esto te ayude.
Hace poco me encontré con este problema y estaba usando un formato personalizado en el portapapeles, lo que dificulta un poco más la Interoperabilidad. De todos modos, con un poco de reflexión de la luz pude llegar al System.Windows.Forms.DataObject original, y luego llamar al GetData y sacar mi artículo personalizado como de costumbre.
var oleConverterType = Type.GetType("System.Windows.DataObject+OleConverter, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
var oleConverter = typeof(System.Windows.DataObject).GetField("_innerData", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(e.Data);
var dataObject = (System.Windows.Forms.DataObject)oleConverterType.GetProperty("OleDataObject").GetValue(oleConverter, null);
var item = dataObject.GetData(this.Format);
Solo por curiosidad, en el método DragDrop, ¿has intentado probar si puedes obtener la imagen de mapa de bits de DragEventArgs? ¿Sin hacer el elenco emitido? Me pregunto si el objeto picturebox no es serializable, lo que causa el problema cuando intenta usar el remitente en un dominio de aplicación diferente ...