Перетаскивание между экземплярами одного приложения Windows Forms
Я создал небольшое тестовое приложение Windows Forms, чтобы опробовать код перетаскивания. Форма состоит из трех PictureBox. Мое намерение состояло в том, чтобы взять изображение из одного PictureBox, отобразить его как пользовательский курсор во время операции перетаскивания, а затем поместить его в другую цель PictureBox.
Это прекрасно работает от одного PictureBox к другому, если они находятся в одной форме.
Если я открываю два экземпляра одного и того же приложения и пытаюсь перетаскивать их между собой, я получаю следующую загадочную ошибку:
Этот удаленный прокси-сервер не имеет приемника каналов, что означает, что либо у сервера нет зарегистрированных каналов сервера, которые прослушивают, либо у этого приложения нет подходящего клиентского канала для связи с сервером.
По некоторым причинам, однако, он работает для перетаскивания в Wordpad (но не MS Word или Paintbrush).
Три PictureBox-а подключают свои события следующим образом:
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);
}
}
Тогда есть четыре события как это:
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);
}
Любая помощь будет принята с благодарностью!
4 ответа
После сильного скрежета зубов и выпадения волос я смог найти подходящее решение. Кажется, что под покровом.NET и поддержки OLE drag and drop происходит какая-то недокументированная странность. Кажется, он пытается использовать удаленное взаимодействие.NET при перетаскивании между приложениями.NET, но документировано ли это где-нибудь? Нет, я так не думаю.
Поэтому решение, которое я придумала, включает вспомогательный класс, который помогает распределить растровые данные между процессами. Во-первых, вот класс.
[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;
}
}
Чтобы использовать класс способом, который будет поддерживать как.NET, так и неуправляемых получателей растрового изображения, класс DataObject используется для операции перетаскивания следующим образом.
Чтобы начать операцию перетаскивания:
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);
Для завершения операции:
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;
}
Проверка клиента BitmapTransfer выполняется в первую очередь, поэтому она имеет приоритет над существованием обычного Bitmap в объекте данных. Класс BitmapTransfer может быть помещен в общую библиотеку для использования с несколькими приложениями. Он должен быть помечен как сериализуемый, как показано для перетаскивания между приложениями. Я протестировал его с помощью перетаскивания растровых изображений в приложении, между приложениями и из приложения.NET в Wordpad.
Надеюсь, что это помогает вам.
Недавно я столкнулся с этой проблемой и использовал собственный формат в буфере обмена, что усложняло Interop. В любом случае, с небольшим отражением света я смог добраться до оригинального System.Windows.Forms.DataObject, а затем вызвать GetData и вытащить из него свой пользовательский элемент, как обычно.
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);
После многих часов разочарования от пара, выходящего из моих ушей, я наконец нашел второе решение этой проблемы. Точно, какое решение является самым изящным, вероятно, в глазах смотрящего. Я надеюсь, что решения Майкла и мои помогут и разочарованным программистам, и сэкономят им время, когда они приступят к подобным квестам.
Прежде всего, меня поразило то, что Wordpad мог получать изображения перетаскивания из коробки. Таким образом, упаковка файла, вероятно, не была проблемой, но, возможно, на приемной стороне происходило что-то подозрительное.
И рыбный там был. Оказывается, есть несколько типов IDataObjects, плавающих в.Net Framework. Как отметил Майкл, поддержка OLE drag and drop пытается использовать.Net remoting при взаимодействии между приложениями. Это на самом деле помещает System.Runtime.Remoting.Proxies.__TransparentProxy туда, где должно быть изображение. Ясно, что это не совсем верно.
Следующая статья дала мне несколько советов в правильном направлении:
По умолчанию в Windows Forms используется System.Windows.Forms.IDataObject. Однако, поскольку здесь мы имеем дело с различными процессами, я решил вместо этого попробовать System.Runtime.InteropServices.ComTypes.IDataObject.
В событии dragdrop следующий код решает проблему:
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;
Две функции GetData имеют только одно и то же имя. Один возвращает объект, другой определен для возврата void и вместо этого передает информацию в параметр stgMedium out:
(dea.Data as System.Runtime.InteropServices.ComTypes.IDataObject).GetData(ref formatEtc, out stgMedium);
Bitmap remotingImage = Bitmap.FromHbitmap(stgMedium.unionmember);
(sender as PictureBox).Image = remotingImage;
Наконец, чтобы избежать утечек памяти, возможно, хорошей идеей будет вызвать функцию OLE ReleaseStgMedium:
ReleaseStgMedium(ref stgMedium);
Эта функция может быть включена следующим образом:
[DllImport("ole32.dll")]
public static extern void ReleaseStgMedium([In, MarshalAs(UnmanagedType.Struct)] ref System.Runtime.InteropServices.ComTypes.STGMEDIUM pmedium);
... и этот код прекрасно работает с операциями перетаскивания (растровых изображений) между двумя приложениями. Код можно легко распространить на другие допустимые форматы буфера обмена и, возможно, также на собственные форматы буфера обмена. Поскольку с упаковочной частью ничего не было сделано, вы все равно можете перетащить изображение в Wordpad, а поскольку оно принимает растровые форматы, вы также можете перетащить изображение из Word в приложение.
Как примечание, перетаскивание изображения непосредственно из IE даже не вызывает событие DragDrop. Странный.
Просто из любопытства, в методе DragDrop, вы пытались проверить, можно ли вообще получить растровое изображение из DragEventArgs? Не делая отправителя? Мне интересно, не является ли объект picturebox сериализуемым, что вызывает проблему, когда вы пытаетесь использовать отправителя в другом домене приложения...