I want my WPF application to be a drop target, and I want to be able to drag an image from any web page.
When an image is dragged from a web page, apparently it is in the "DragImageBits" format, which can be deserialized to type ShDragImage. (See the bottom of the question for how I've defined it)
How do I convert this to a WPF image?
Here's my current attempt. (If anybody knows the correct way to do the desirialization, I'm all ears)
private void UserControl_Drop(object sender, System.Windows.DragEventArgs e)
{
string[] formats = data.GetFormats();
// DragImageBits
if (formats.Contains("DragImageBits"))
{
MemoryStream imageStream = data.GetData("DragImageBits") as MemoryStream;
// Now I'm deserializing this, the only way I know how
imageStream.Seek(0, SeekOrigin.Begin);
BinaryReader br = new BinaryReader(imageStream);
ShDragImage shDragImage;
shDragImage.sizeDragImage.cx = br.ReadInt32();
shDragImage.sizeDragImage.cy = br.ReadInt32();
shDragImage.ptOffset.x = br.ReadInt32();
shDragImage.ptOffset.y = br.ReadInt32();
shDragImage.hbmpDragImage = new IntPtr(br.ReadInt32());
shDragImage.crColorKey = br.ReadInt32();
var systemDrawingBitmap = System.Drawing.Bitmap.FromHbitmap(shDragImage.hbmpDragImage);
At this point I get an exception of type System.Runtime.InteropServices.ExternalException
, with the message simply being Generic GDI+ error
.
Does anyone know what I should be doing?
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Win32Point
{
public int x;
public int y;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Win32Size
{
public int cx;
public int cy;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ShDragImage
{
public Win32Size sizeDragImage;
public Win32Point ptOffset;
public IntPtr hbmpDragImage;
public int crColorKey;
}
Here's what I've learnt:
"DragImageBits" is provided by the windows shell, and is meant only for the drag cursor, not for the final data. The shell transforms the image to an appropriate drag cursor through resizing and transparency.
For example, if you drag this image:
The SHDRAGIMAGE will render as this:
If you really want to extract the image from the SHDRAGIMAGE, here is the code. (Partially lifted from this answer)
MemoryStream imageStream = data.GetData("DragImageBits") as MemoryStream;
imageStream.Seek(0, SeekOrigin.Begin);
BinaryReader br = new BinaryReader(imageStream);
ShDragImage shDragImage;
shDragImage.sizeDragImage.cx = br.ReadInt32();
shDragImage.sizeDragImage.cy = br.ReadInt32();
shDragImage.ptOffset.x = br.ReadInt32();
shDragImage.ptOffset.y = br.ReadInt32();
shDragImage.hbmpDragImage = new IntPtr(br.ReadInt32()); // I do not know what this is for!
shDragImage.crColorKey = br.ReadInt32();
int stride = shDragImage.sizeDragImage.cx * 4;
var imageData = new byte[stride * shDragImage.sizeDragImage.cy];
// We must read the image data as a loop, so it's in a flipped format
for (int i = (shDragImage.sizeDragImage.cy - 1) * stride; i >= 0; i -= stride)
{
br.Read(imageData, i, stride);
}
var bitmapSource = BitmapSource.Create(shDragImage.sizeDragImage.cx, shDragImage.sizeDragImage.cy,
96, 96,
PixelFormats.Bgra32,
null,
imageData,
stride);
If you want to utilize the DragImageBits for it's intended purpose (as a drag image), see Shell Style Drag and Drop in .NET (WPF and WinForms) (archived here) for a simple, downloadable example.
Dragging an image from a web page gets complicated, because Firefox, Chrome, and IE9 all give you a different set of formats. Also, you want to handle both an image and an image hyperlink, and these are treated differently again.
Google and Firefox provides a "text/html" format, which gives you a single HTML element as an image. Google gives it to you as an ASCII string, and Firefox gives it to you as a unicode string. So here's the code I wrote to handle it:
System.Windows.IDataObject data = e.Data;
string[] formats = data.GetFormats();
if (formats.Contains("text/html"))
{
var obj = data.GetData("text/html");
string html = string.Empty;
if (obj is string)
{
html = (string)obj;
}
else if (obj is MemoryStream)
{
MemoryStream ms = (MemoryStream)obj;
byte[] buffer = new byte[ms.Length];
ms.Read(buffer, 0, (int)ms.Length);
if (buffer[1] == (byte)0) // Detecting unicode
{
html = System.Text.Encoding.Unicode.GetString(buffer);
}
else
{
html = System.Text.Encoding.ASCII.GetString(buffer);
}
}
// Using a regex to parse HTML, but JUST FOR THIS EXAMPLE :-)
var match = new Regex(@"<img[^/]src=""([^""]*)""").Match(html);
if (match.Success)
{
Uri uri = new Uri(match.Groups[1].Value);
SetImageFromUri(uri);
}
}
In this case, the regular expression will handle both a straight image and an image hyperlink.
And my SetImageFromUri
function:
private void SetImageFromUri(Uri uri)
{
string fileName = System.IO.Path.GetTempFileName();
using (WebClient webClient = new WebClient())
{
webClient.DownloadFile(uri, fileName);
}
using (FileStream fs = File.OpenRead(fileName))
{
byte[] imageData = new byte[fs.Length];
fs.Read(imageData, 0, (int)fs.Length);
this.ImageBinary = imageData;
}
File.Delete(fileName);
}
For IE9 you can handle the "FileDrop" format. This works well in IE9. Chrome does not support it. Firefox does support it, but converts the image to a bitmap and converts transparent pixels to black. For this reason, you should only handle the "FileDrop" format if the "text.html" format isn't available.
else if (formats.Contains("FileDrop"))
{
var filePaths = (string[])data.GetData("FileDrop");
using (var fileStream = File.OpenRead(filePaths[0]))
{
var buffer = new byte[fileStream.Length];
fileStream.Read(buffer, 0, (int)fileStream.Length);
this.ImageBinary = buffer;
}
}
The "FileDrop" format is not provided if you drag an image hyperlink from IE9. I haven't figured out how to drag an image from an image hyperlink in IE9 onto my image control.
If you're using this example, but still need to convert this binary data into an image, here's a useful code snippet:
BitmapImage sourceImage = new BitmapImage();
using (MemoryStream ms = new MemoryStream(imageBinary))
{
sourceImage.BeginInit();
sourceImage.CacheOption = BitmapCacheOption.OnLoad;
sourceImage.StreamSource = ms;
sourceImage.EndInit();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With