What's the most efficient way of extracting thumbnails from any file, not just just images at varying sizes?
I've looked all over, most promising of which was Windows API ShellFile yet this didn't appear to install properly. I'm using windows 7.
Some time ago I wrote a ThumbnailProvider that loads a thumbnail from the Win API. Supports transparent images. This is the implementation:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.IO;
namespace ThumbnailGenerator
{
[Flags]
public enum ThumbnailOptions
{
None = 0x00,
BiggerSizeOk = 0x01,
InMemoryOnly = 0x02,
IconOnly = 0x04,
ThumbnailOnly = 0x08,
InCacheOnly = 0x10,
}
public class WindowsThumbnailProvider
{
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string path,
// The following parameter is not used - binding context.
IntPtr pbc,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteObject(IntPtr hObject);
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
internal interface IShellItem
{
void BindToHandler(IntPtr pbc,
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
out IntPtr ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
};
internal enum SIGDN : uint
{
NORMALDISPLAY = 0,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000
}
internal enum HResult
{
Ok = 0x0000,
False = 0x0001,
InvalidArguments = unchecked((int)0x80070057),
OutOfMemory = unchecked((int)0x8007000E),
NoInterface = unchecked((int)0x80004002),
Fail = unchecked((int)0x80004005),
ElementNotFound = unchecked((int)0x80070490),
TypeElementNotFound = unchecked((int)0x8002802B),
NoObject = unchecked((int)0x800401E5),
Win32ErrorCanceled = 1223,
Canceled = unchecked((int)0x800704C7),
ResourceInUse = unchecked((int)0x800700AA),
AccessDenied = unchecked((int)0x80030005)
}
[ComImportAttribute()]
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItemImageFactory
{
[PreserveSig]
HResult GetImage(
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
[In] ThumbnailOptions flags,
[Out] out IntPtr phbm);
}
[StructLayout(LayoutKind.Sequential)]
internal struct NativeSize
{
private int width;
private int height;
public int Width { set { width = value; } }
public int Height { set { height = value; } }
};
[StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD
{
public byte rgbBlue;
public byte rgbGreen;
public byte rgbRed;
public byte rgbReserved;
}
public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
try
{
// return a System.Drawing.Bitmap from the hBitmap
return GetBitmapFromHBitmap(hBitmap);
}
finally
{
// delete HBitmap to avoid memory leaks
DeleteObject(hBitmap);
}
}
public static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap)
{
Bitmap bmp = Bitmap.FromHbitmap(nativeHBitmap);
if (Bitmap.GetPixelFormatSize(bmp.PixelFormat) < 32)
return bmp;
return CreateAlphaBitmap(bmp, PixelFormat.Format32bppArgb);
}
public static Bitmap CreateAlphaBitmap(Bitmap srcBitmap, PixelFormat targetPixelFormat)
{
Bitmap result = new Bitmap(srcBitmap.Width, srcBitmap.Height, targetPixelFormat);
Rectangle bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);
BitmapData srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
bool isAlplaBitmap = false;
try
{
for (int y = 0; y <= srcData.Height - 1; y++)
{
for (int x = 0; x <= srcData.Width - 1; x++)
{
Color pixelColor = Color.FromArgb(
Marshal.ReadInt32(srcData.Scan0, (srcData.Stride * y) + (4 * x)));
if (pixelColor.A > 0 & pixelColor.A < 255)
{
isAlplaBitmap = true;
}
result.SetPixel(x, y, pixelColor);
}
}
}
finally
{
srcBitmap.UnlockBits(srcData);
}
if (isAlplaBitmap)
{
return result;
}
else
{
return srcBitmap;
}
}
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
IShellItem nativeShellItem;
Guid shellItem2Guid = new Guid(IShellItem2Guid);
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
if (retCode != 0)
throw Marshal.GetExceptionForHR(retCode);
NativeSize nativeSize = new NativeSize();
nativeSize.Width = width;
nativeSize.Height = height;
IntPtr hBitmap;
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
Marshal.ReleaseComObject(nativeShellItem);
if (hr == HResult.Ok) return hBitmap;
throw Marshal.GetExceptionForHR((int)hr);
}
}
}
Then you can use it in the following way:
int THUMB_SIZE = 256;
Bitmap thumbnail = WindowsThumbnailProvider.GetThumbnail(
fileName, THUMB_SIZE, THUMB_SIZE, ThumbnailOptions.None);
Remember that you need to Dispose() the bitmap after using it.
This is an improved version of the (awesome) answer that was accepted. The following was changed:
Use unsafe code instead of SetPixel() -> that improves performance drastically
remove the whole isAlphaBitmap check. I have no idea why that was implemented in the first place, but I see no reason to determine between two images which contain the same content (I just return the Bitmap with transparency, regardless if that changed something)
[Flags]
public enum ThumbnailOptions
{
None = 0x00,
BiggerSizeOk = 0x01,
InMemoryOnly = 0x02,
IconOnly = 0x04,
ThumbnailOnly = 0x08,
InCacheOnly = 0x10
}
public class WindowsThumbnailProvider
{
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string path,
// The following parameter is not used - binding context.
IntPtr pbc,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteObject(IntPtr hObject);
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern unsafe IntPtr memcpy(void* dst, void* src, UIntPtr count);
public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
var hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
try
{
// return a System.Drawing.Bitmap from the hBitmap
return GetBitmapFromHBitmap(hBitmap);
}
finally
{
// delete HBitmap to avoid memory leaks
DeleteObject(hBitmap);
}
}
public static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap)
{
Bitmap bmp = Image.FromHbitmap(nativeHBitmap);
if (Image.GetPixelFormatSize(bmp.PixelFormat) < 32)
return bmp;
using (bmp)
return CreateAlphaBitmap(bmp, PixelFormat.Format32bppArgb);
}
public static unsafe Bitmap CreateAlphaBitmap(Bitmap srcBitmap, PixelFormat targetPixelFormat)
{
var result = new Bitmap(srcBitmap.Width, srcBitmap.Height, targetPixelFormat);
var bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);
var srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
var destData = result.LockBits(bmpBounds, ImageLockMode.ReadOnly, targetPixelFormat);
var srcDataPtr = (byte*) srcData.Scan0;
var destDataPtr = (byte*) destData.Scan0;
try
{
for (int y = 0; y <= srcData.Height - 1; y++)
{
for (int x = 0; x <= srcData.Width - 1; x++)
{
//this is really important because one stride may be positive and the other negative
var position = srcData.Stride * y + 4 * x;
var position2 = destData.Stride * y + 4 * x;
memcpy(destDataPtr + position2, srcDataPtr + position, (UIntPtr) 4);
}
}
}
finally
{
srcBitmap.UnlockBits(srcData);
result.UnlockBits(destData);
}
return result;
}
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
IShellItem nativeShellItem;
Guid shellItem2Guid = new Guid(IShellItem2Guid);
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
if (retCode != 0)
throw Marshal.GetExceptionForHR(retCode);
NativeSize nativeSize = new NativeSize();
nativeSize.Width = width;
nativeSize.Height = height;
IntPtr hBitmap;
HResult hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, options, out hBitmap);
Marshal.ReleaseComObject(nativeShellItem);
if (hr == HResult.Ok) return hBitmap;
throw Marshal.GetExceptionForHR((int) hr);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
internal interface IShellItem
{
void BindToHandler(IntPtr pbc,
[MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
out IntPtr ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
internal enum SIGDN : uint
{
NORMALDISPLAY = 0,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000
}
internal enum HResult
{
Ok = 0x0000,
False = 0x0001,
InvalidArguments = unchecked((int) 0x80070057),
OutOfMemory = unchecked((int) 0x8007000E),
NoInterface = unchecked((int) 0x80004002),
Fail = unchecked((int) 0x80004005),
ElementNotFound = unchecked((int) 0x80070490),
TypeElementNotFound = unchecked((int) 0x8002802B),
NoObject = unchecked((int) 0x800401E5),
Win32ErrorCanceled = 1223,
Canceled = unchecked((int) 0x800704C7),
ResourceInUse = unchecked((int) 0x800700AA),
AccessDenied = unchecked((int) 0x80030005)
}
[ComImport]
[Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItemImageFactory
{
[PreserveSig]
HResult GetImage(
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
[In] ThumbnailOptions flags,
[Out] out IntPtr phbm);
}
[StructLayout(LayoutKind.Sequential)]
internal struct NativeSize
{
private int width;
private int height;
public int Width
{
set { width = value; }
}
public int Height
{
set { height = value; }
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD
{
public byte rgbBlue;
public byte rgbGreen;
public byte rgbRed;
public byte rgbReserved;
}
}
I am writing an application that requires scanning in .net (c# version 4.0, Visual Studio 2010). I'm using the TWAIN API in order to do the scanning and I have a problem with the layout feature. The following code works perfectly fine on a Microtek i800, a CanoScan 9000F and a Microtek Artix Scan F2 but when I run it against an Epson Perfection V700 something really strange occurs.
Even though I am setting the left margin of the layout to 0 the left edge of the image is cut off. I tried setting it to negative values but that made no difference. It seems like there is some strangeness and it is forcing it to be a film size (perhaps because I am turning the light on). If I use the tool that comes with the scanner it allows me to select a region that includes both edges (and have the light on) so it must be possible. Also, the top and bottom coordinates work perfectly fine.
So my question is...
Does anyone know of any way I can make it scan the entire width? Is there some other setting in TWAIN that I can set first to get it to forget its paper sizes perhaps? (I tried setting PaperDetectable to false too but it made no difference).
One other thing: If I do not set the layout, it still cuts off the picture on the edges (just not top and bottom) but if I also do not set the light on (or I do not set the light on but I do set the size) it does what I would expect: specifically scans the entire picture from the left most edge (but the problem is, I really need the light and the entire width of the scan - surely that isn't too much to ask...).
Here is the code (it is the code behind for a 1 form windows form application with a single button on it):
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Forms;
namespace TwainLayoutWindowsFormsApplication
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
TwainIdentity appid = null;
TwainIdentity scanner = null;
bool enabled = false;
try
{
appid = InitializeTwain(Handle);
scanner = GetSource(appid, "EPSON Perfection V700/V750");
Open(appid, scanner);
SetLightOn(appid, scanner);
SetLayout(appid, scanner);
Enable(appid, scanner, Handle);
enabled = true;
var bmps = Scan(appid, scanner);
Disable(appid, scanner);
enabled = false;
bmps.First().Save(#"c:\users\public\scan.bmp", ImageFormat.Bmp);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
if (appid != null && scanner != null)
{
if (enabled)
{
try
{
Disable(appid, scanner);
}
catch(InvalidOperationException)
{
}
}
Close(appid, scanner);
}
}
}
private static void SetLayout(TwainIdentity appid,
TwainIdentity scanner)
{
TwainImageLayout layout = new TwainImageLayout();
var rc = NativeMethods.DSilayout(appid,
scanner,
TwainDataGroups.Image,
TwainDataArgumentType.ImageLayout,
TwainMessage.Get,
layout);
// 1 inch from the top and 0 from the left
layout.Frame.Top = new TwainFix32();
layout.Frame.Top.Whole = 1;
layout.Frame.Left = new TwainFix32();
layout.Frame.Left.Whole = 0;
layout.Frame.Right = new TwainFix32();
layout.Frame.Right.Whole = 6;
layout.Frame.Bottom = new TwainFix32();
layout.Frame.Bottom.Whole = 3;
layout.FrameNumber = 1;
layout.PageNumber = 1;
layout.DocumentNumber = 1;
rc = NativeMethods.DSilayout(appid,
scanner,
TwainDataGroups.Image,
TwainDataArgumentType.ImageLayout,
TwainMessage.Set,
layout);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Failed to set layout");
var s = new TwainStatus();
rc = NativeMethods.DSstatus(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.Status,
TwainMessage.Get, s);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Failed to get layout");
}
private IEnumerable<Bitmap> Scan(TwainIdentity appid,
TwainIdentity scanner)
{
var pictures = new List<Bitmap>();
TwainReturnCode rc;
IntPtr hbitmap;
var pxfr = new TwainPendingXfers();
do
{
pxfr.Count = 10;
hbitmap = IntPtr.Zero;
var iinf = new TwainImageInfo();
rc = NativeMethods.DSiinf(appid,
scanner,
TwainDataGroups.Image,
TwainDataArgumentType.ImageInfo,
TwainMessage.Get,
iinf);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Could not INF");
rc = NativeMethods.DSixfer(appid,
scanner,
TwainDataGroups.Image,
TwainDataArgumentType.ImageNativeXfer,
TwainMessage.Get,
ref hbitmap);
if (rc != TwainReturnCode.XferDone)
throw new InvalidOperationException("Could DSI XFER");
rc = NativeMethods.DSpxfer(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.PendingXfers,
TwainMessage.EndXfer,
pxfr);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Could DSP XFER");
var bmp = TwainBitmapConvertor.ToBitmap(hbitmap);
pictures.Add(bmp);
}
while (pxfr.Count != 0);
NativeMethods.DSpxfer(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.PendingXfers,
TwainMessage.StopFeeder,
pxfr);
return pictures;
}
private static void Enable(TwainIdentity appid,
TwainIdentity scanner,
IntPtr hwnd)
{
var guif = new TwainUserInterface();
guif.ShowUI = 0;
guif.ModalUI = 1;
guif.ParentHand = hwnd;
var rc = NativeMethods.DSuserif(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.UserInterface,
TwainMessage.EnableDS,
guif);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Could not enable");
}
private static void Disable(TwainIdentity appid, TwainIdentity scanner)
{
var guif = new TwainUserInterface();
var rc = NativeMethods.DSuserif(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.UserInterface,
TwainMessage.DisableDS,
guif);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Could not disable");
}
private static void SetLightOn(TwainIdentity appid, TwainIdentity scanner)
{
using (var capability = new TwainCapability(TwainCapabilityType.Lightpath, 1))
{
var rc = NativeMethods.DScap(appid,
scanner,
TwainDataGroups.Control,
TwainDataArgumentType.Capability,
TwainMessage.Set,
capability);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Failed to set light");
}
}
private static void Close(TwainIdentity appid, TwainIdentity scanner)
{
NativeMethods.DSMident(appid,
IntPtr.Zero,
TwainDataGroups.Control,
TwainDataArgumentType.Identity,
TwainMessage.CloseDS,
scanner);
}
private static void Open(TwainIdentity appid, TwainIdentity scanner)
{
var rc = NativeMethods.DSMident(appid,
IntPtr.Zero,
TwainDataGroups.Control,
TwainDataArgumentType.Identity,
TwainMessage.OpenDS,
scanner);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Failed to open");
}
private static TwainIdentity InitializeTwain(IntPtr hwndp)
{
var appid = new TwainIdentity();
appid.Version.MajorNum = 1;
appid.Version.MinorNum = 0;
appid.Version.Language = 13;
appid.Version.Country = 1;
appid.Version.Info = "Test";
appid.Id = IntPtr.Zero;
appid.ProtocolMajor = 1;
appid.ProtocolMinor = 9;
appid.SupportedGroups = (int)(TwainDataGroups.Image | TwainDataGroups.Control);
appid.Manufacturer = "Test Manufacturer";
appid.ProductFamily = "Test Family";
appid.ProductName = "Test Product";
var rc = NativeMethods.DSMparent(appid,
IntPtr.Zero,
TwainDataGroups.Control,
TwainDataArgumentType.Parent,
TwainMessage.OpenDSM,
ref hwndp);
if (rc != TwainReturnCode.Success)
throw new InvalidOperationException("Could Not DSMParent");
return appid;
}
private static TwainIdentity GetSource(TwainIdentity appid, string name)
{
var device = new TwainIdentity { Id = IntPtr.Zero };
var rc = NativeMethods.DSMentry(appid,
IntPtr.Zero,
TwainDataGroups.Control,
TwainDataArgumentType.Identity,
TwainMessage.GetFirst,
device);
if (rc != TwainReturnCode.EndOfList &&
device.ProductName.Equals(name,
StringComparison.OrdinalIgnoreCase))
{
return device;
}
while (rc != TwainReturnCode.EndOfList)
{
device = new TwainIdentity { Id = IntPtr.Zero };
rc = NativeMethods.DSMentry(appid,
IntPtr.Zero,
TwainDataGroups.Control,
TwainDataArgumentType.Identity,
TwainMessage.GetNext,
device);
if (rc != TwainReturnCode.EndOfList &&
device.ProductName.Equals(name,
StringComparison.OrdinalIgnoreCase))
{
return device;
}
}
throw new InvalidOperationException("Could not find device");
}
}
}
For completeness here is the code I use to convert the HBITMAP to a System.Drawing.Bitmap:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Globalization;
namespace TwainLayoutWindowsFormsApplication
{
internal static class TwainBitmapConvertor
{
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private class BitmapInfoHeader
{
public int Size;
public int Width;
public int Height;
public short Planes;
public short BitCount;
public int Compression;
public int SizeImage;
public int XPelsPerMeter;
public int YPelsPerMeter;
public int ClrUsed;
public int ClrImportant;
}
internal static Bitmap ToBitmap(IntPtr dibHandle)
{
var bitmapPointer = NativeMethods.GlobalLock(dibHandle);
var bitmapInfo = new BitmapInfoHeader();
Marshal.PtrToStructure(bitmapPointer, bitmapInfo);
var rectangle = new Rectangle();
rectangle.X = rectangle.Y = 0;
rectangle.Width = bitmapInfo.Width;
rectangle.Height = bitmapInfo.Height;
if (bitmapInfo.SizeImage == 0)
{
bitmapInfo.SizeImage =
((((bitmapInfo.Width * bitmapInfo.BitCount) + 31) & ~31) >> 3)
* bitmapInfo.Height;
}
// The following code only works on x86
if (Marshal.SizeOf(typeof(IntPtr)) != 4)
throw new NotSupportedException("Only x86 is supported");
int pixelInfoPointer = bitmapInfo.ClrUsed;
if ((pixelInfoPointer == 0) && (bitmapInfo.BitCount <= 8))
{
pixelInfoPointer = 1 << bitmapInfo.BitCount;
}
pixelInfoPointer = (pixelInfoPointer * 4) + bitmapInfo.Size
+ bitmapPointer.ToInt32();
IntPtr pixelInfoIntPointer = new IntPtr(pixelInfoPointer);
var bitmap = new Bitmap(rectangle.Width, rectangle.Height);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
IntPtr hdc = graphics.GetHdc();
try
{
NativeMethods.SetDIBitsToDevice(hdc,
0, 0, rectangle.Width, rectangle.Height, 0, 0, 0,
rectangle.Height, pixelInfoIntPointer, bitmapPointer, 0);
}
finally
{
graphics.ReleaseHdc(hdc);
}
}
bitmap.SetResolution(PpmToDpi(bitmapInfo.XPelsPerMeter),
PpmToDpi(bitmapInfo.YPelsPerMeter));
NativeMethods.GlobalUnlock(dibHandle);
NativeMethods.GlobalFree(dibHandle);
return bitmap;
}
private static float PpmToDpi(double pixelsPerMeter)
{
double pixelsPerMillimeter = (double)pixelsPerMeter / 1000.0;
double dotsPerInch = pixelsPerMillimeter * 25.4;
return (float)Math.Round(dotsPerInch, 2);
}
}
}
and here are the p/invokes:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace TwainLayoutWindowsFormsApplication
{
internal static class NativeMethods
{
[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern IntPtr GlobalAlloc(int flags, int size);
[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern IntPtr GlobalLock(IntPtr handle);
[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern bool GlobalUnlock(IntPtr handle);
[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern IntPtr GlobalFree(IntPtr handle);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern int SetDIBitsToDevice(IntPtr hdc, int xdst, int ydst, int width, int height,
int xsrc, int ysrc, int start, int lines, IntPtr bitsptr, IntPtr bmiptr, int color);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMparent([In, Out] TwainIdentity origin, IntPtr zeroptr, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, ref IntPtr refptr);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMident([In, Out] TwainIdentity origin, IntPtr zeroptr, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainIdentity idds);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMentry([In, Out] TwainIdentity origin, IntPtr zeroptr, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainIdentity idds);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMstatus([In, Out] TwainIdentity origin, IntPtr zeroptr, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainStatus dsmstat);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSuserif([In, Out] TwainIdentity origin, [In, Out] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, TwainUserInterface guif);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSevent([In, Out] TwainIdentity origin, [In, Out] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, ref TwainEvent evt);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSstatus([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainStatus dsmstat);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DScap([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainCapability capa);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSiinf([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainImageInfo imginf);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSixfer([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, ref IntPtr hbitmap);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMemixfer([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainImageMemXfer memxfr);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSpxfer([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainPendingXfers pxfr);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSilayout([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainImageLayout layout);
[DllImport("twain_32.dll", EntryPoint = "#1")]
internal static extern TwainReturnCode DSMEntry([In, Out] TwainIdentity origin, [In] TwainIdentity dest, TwainDataGroups dg, TwainDataArgumentType dat, TwainMessage msg, [In, Out] TwainSetupFileXfer fileXf);
}
}
and finally, the rest of the stuff (structures and whatnot) that that needs to work:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace TwainLayoutWindowsFormsApplication
{
[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)]
internal class TwainIdentity
{
public IntPtr Id;
public TwainVersion Version;
public short ProtocolMajor;
public short ProtocolMinor;
public int SupportedGroups;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string Manufacturer;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string ProductFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string ProductName;
}
internal enum TwainReturnCode : short
{
Success = 0x0000,
Failure = 0x0001,
CheckStatus = 0x0002,
Cancel = 0x0003,
DSEvent = 0x0004,
NotDSEvent = 0x0005,
XferDone = 0x0006,
EndOfList = 0x0007,
InfoNotSupported = 0x0008,
DataNotAvailable = 0x0009
}
[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)]
internal struct TwainVersion
{
public short MajorNum;
public short MinorNum;
public short Language;
public short Country;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string Info;
}
[Flags]
internal enum TwainDataGroups : short
{
Control = 0x0001,
Image = 0x0002,
Audio = 0x0004
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal struct TwainEvent
{
public IntPtr EventPtr;
public short Message;
}
internal enum TwainDataArgumentType : short
{
Null = 0x0000,
Capability = 0x0001,
Event = 0x0002,
Identity = 0x0003,
Parent = 0x0004,
PendingXfers = 0x0005,
SetupMemXfer = 0x0006,
SetupFileXfer = 0x0007,
Status = 0x0008,
UserInterface = 0x0009,
XferGroup = 0x000a,
TwunkIdentity = 0x000b,
CustomDSData = 0x000c,
DeviceEvent = 0x000d,
FileSystem = 0x000e,
PassThru = 0x000f,
ImageInfo = 0x0101,
ImageLayout = 0x0102,
ImageMemXfer = 0x0103,
ImageNativeXfer = 0x0104,
ImageFileXfer = 0x0105,
CieColor = 0x0106,
GrayResponse = 0x0107,
RGBResponse = 0x0108,
JpegCompression = 0x0109,
Palette8 = 0x010a,
ExtImageInfo = 0x010b,
SetupFileXfer2 = 0x0301
}
internal enum TwainMessage : short
{
Null = 0x0000,
Get = 0x0001,
GetCurrent = 0x0002,
GetDefault = 0x0003,
GetFirst = 0x0004,
GetNext = 0x0005,
Set = 0x0006,
Reset = 0x0007,
QuerySupport = 0x0008,
XFerReady = 0x0101,
CloseDSReq = 0x0102,
CloseDSOK = 0x0103,
DeviceEvent = 0x0104,
CheckStatus = 0x0201,
OpenDSM = 0x0301,
CloseDSM = 0x0302,
OpenDS = 0x0401,
CloseDS = 0x0402,
UserSelect = 0x0403,
DisableDS = 0x0501,
EnableDS = 0x0502,
EnableDSUIOnly = 0x0503,
ProcessEvent = 0x0601,
EndXfer = 0x0701,
StopFeeder = 0x0702,
ChangeDirectory = 0x0801,
CreateDirectory = 0x0802,
Delete = 0x0803,
FormatMedia = 0x0804,
GetClose = 0x0805,
GetFirstFile = 0x0806,
GetInfo = 0x0807,
GetNextFile = 0x0808,
Rename = 0x0809,
Copy = 0x080A,
AutoCaptureDir = 0x080B,
PassThru = 0x0901
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct TwainWindowMessage
{
public IntPtr hwnd;
public int message;
public IntPtr wParam;
public IntPtr lParam;
public int time;
public int x;
public int y;
}
internal enum TwainOn : short
{
Array = 0x0003,
Enum = 0x0004,
One = 0x0005,
Range = 0x0006,
DontCare = -1
}
internal enum TwainCapabilityType : short
{
XferCount = 0x0001,
ICompression = 0x0100,
IPixelType = 0x0101,
IUnits = 0x0102,
IXferMech = 0x0103,
Author = 0x1000,
Caption = 0x1001,
FeederEnabled = 0x1002,
FeederLoaded = 0x1003,
Timedate = 0x1004,
SupportedCapabilities = 0x1005,
Extendedcaps = 0x1006,
AutoFeed = 0x1007,
ClearPage = 0x1008,
FeedPage = 0x1009,
RewindPage = 0x100a,
Indicators = 0x100b,
SupportedCapsExt = 0x100c,
PaperDetectable = 0x100d,
UIControllable = 0x100e,
DeviceOnline = 0x100f,
AutoScan = 0x1010,
ThumbnailsEnabled = 0x1011,
Duplex = 0x1012,
DuplexEnabled = 0x1013,
Enabledsuionly = 0x1014,
CustomdsData = 0x1015,
Endorser = 0x1016,
JobControl = 0x1017,
Alarms = 0x1018,
AlarmVolume = 0x1019,
AutomaticCapture = 0x101a,
TimeBeforeFirstCapture = 0x101b,
TimeBetweenCaptures = 0x101c,
ClearBuffers = 0x101d,
MaxBatchBuffers = 0x101e,
DeviceTimeDate = 0x101f,
PowerSupply = 0x1020,
CameraPreviewUI = 0x1021,
DeviceEvent = 0x1022,
SerialNumber = 0x1024,
Printer = 0x1026,
PrinterEnabled = 0x1027,
PrinterIndex = 0x1028,
PrinterMode = 0x1029,
PrinterString = 0x102a,
PrinterSuffix = 0x102b,
Language = 0x102c,
FeederAlignment = 0x102d,
FeederOrder = 0x102e,
ReAcquireAllowed = 0x1030,
BatteryMinutes = 0x1032,
BatteryPercentage = 0x1033,
CameraSide = 0x1034,
Segmented = 0x1035,
CameraEnabled = 0x1036,
CameraOrder = 0x1037,
MicrEnabled = 0x1038,
FeederPrep = 0x1039,
Feederpocket = 0x103a,
Autobright = 0x1100,
Brightness = 0x1101,
Contrast = 0x1103,
CustHalftone = 0x1104,
ExposureTime = 0x1105,
Filter = 0x1106,
Flashused = 0x1107,
Gamma = 0x1108,
Halftones = 0x1109,
Highlight = 0x110a,
ImageFileFormat = 0x110c,
LampState = 0x110d,
LightSource = 0x110e,
Orientation = 0x1110,
PhysicalWidth = 0x1111,
PhysicalHeight = 0x1112,
Shadow = 0x1113,
Frames = 0x1114,
XNativeResolution = 0x1116,
YNativeResolution = 0x1117,
XResolution = 0x1118,
YResolution = 0x1119,
MaxFrames = 0x111a,
Tiles = 0x111b,
Bitorder = 0x111c,
Ccittkfactor = 0x111d,
Lightpath = 0x111e,
Pixelflavor = 0x111f,
Planarchunky = 0x1120,
Rotation = 0x1121,
Supportedsizes = 0x1122,
Threshold = 0x1123,
Xscaling = 0x1124,
Yscaling = 0x1125,
Bitordercodes = 0x1126,
Pixelflavorcodes = 0x1127,
Jpegpixeltype = 0x1128,
Timefill = 0x112a,
BitDepth = 0x112b,
Bitdepthreduction = 0x112c,
Undefinedimagesize = 0x112d,
Imagedataset = 0x112e,
Extimageinfo = 0x112f,
Minimumheight = 0x1130,
Minimumwidth = 0x1131,
Fliprotation = 0x1136,
Barcodedetectionenabled = 0x1137,
Supportedbarcodetypes = 0x1138,
Barcodemaxsearchpriorities = 0x1139,
Barcodesearchpriorities = 0x113a,
Barcodesearchmode = 0x113b,
Barcodemaxretries = 0x113c,
Barcodetimeout = 0x113d,
Zoomfactor = 0x113e,
Patchcodedetectionenabled = 0x113f,
Supportedpatchcodetypes = 0x1140,
Patchcodemaxsearchpriorities = 0x1141,
Patchcodesearchpriorities = 0x1142,
Patchcodesearchmode = 0x1143,
Patchcodemaxretries = 0x1144,
Patchcodetimeout = 0x1145,
Flashused2 = 0x1146,
Imagefilter = 0x1147,
Noisefilter = 0x1148,
Overscan = 0x1149,
Automaticborderdetection = 0x1150,
Automaticdeskew = 0x1151,
Automaticrotate = 0x1152,
Jpegquality = 0x1153,
Feedertype = 0x1154,
Iccprofile = 0x1155,
Autosize = 0x1156,
AutomaticCropUsesFrame = 0x1157,
AutomaticLengthDetection = 0x1158,
AutomaticColorEnabled = 0x1159,
AutomaticColorNonColorPixelType = 0x115a,
ColorManagementEnabled = 0x115b,
ImageMerge = 0x115c,
ImageMergeHeightThreshold = 0x115d,
SupoortedExtImageInfo = 0x115e,
Audiofileformat = 0x1201,
Xfermech = 0x1202
}
internal enum TwainType : short
{
Int8 = 0x0000,
Int16 = 0x0001,
Int32 = 0x0002,
UInt8 = 0x0003,
UInt16 = 0x0004,
UInt32 = 0x0005,
Bool = 0x0006,
Fix32 = 0x0007,
Frame = 0x0008,
Str32 = 0x0009,
Str64 = 0x000a,
Str128 = 0x000b,
Str255 = 0x000c,
Str1024 = 0x000d,
Str512 = 0x000e
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainStatus
{
public short ConditionCode;
public short Reserved;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainUserInterface
{
public short ShowUI;
public short ModalUI;
public IntPtr ParentHand;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainImageInfo
{
public int XResolution;
public int YResolution;
public int ImageWidth;
public int ImageLength;
public short SamplesPerPixel;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public short[] BitsPerSample;
public short BitsPerPixel;
public short Planar;
public short PixelType;
public short Compression;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwMemory
{
public uint Flags;
public uint Length;
IntPtr TheMem;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainImageMemXfer
{
public ushort Compression;
public uint BytesPerRow;
public uint Columns;
public uint Rows;
public uint XOffset;
public uint YOffset;
public uint BytesWritten;
[MarshalAs(UnmanagedType.Struct)]
TwMemory Memory;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal struct TwainFix32
{
public short Whole;
public ushort Frac;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainFrame
{
public TwainFix32 Left;
public TwainFix32 Top;
public TwainFix32 Right;
public TwainFix32 Bottom;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainImageLayout
{
public TwainFrame Frame;
public int DocumentNumber;
public int PageNumber;
public int FrameNumber;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainPendingXfers
{
public short Count;
public int EOJ;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainSetupFileXfer
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
public string FileName;
public ushort Format;
public short VRefNum;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwainCapability : IDisposable
{
internal TwainCapability(TwainCapabilityType cap)
{
Cap = (short)cap;
ConType = -1;
}
internal TwainCapability(TwainCapabilityType cap, short sval)
{
Cap = (short)cap;
ConType = (short)TwainOn.One;
Handle = NativeMethods.GlobalAlloc(0x42, 6);
IntPtr pv = NativeMethods.GlobalLock(Handle);
Marshal.WriteInt16(pv, 0, (short)TwainType.Int16);
Marshal.WriteInt32(pv, 2, (int)sval);
NativeMethods.GlobalUnlock(Handle);
}
~TwainCapability()
{
Dispose(false);
}
public short Cap;
public short ConType;
public IntPtr Handle;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (Handle != IntPtr.Zero)
NativeMethods.GlobalFree(Handle);
}
}
}
So, in case anyone is interested I ended up giving up on TWAIN entirely - I think it is simply not possible.
What I did instead was install:
Cygwin (basic install plus gcc-core, make and libusb-win32 packages)
libusb-win32
SANE.
After a whole heap of configuring, making and installing I finally was able to issue this command from the Cygwin prompt:
scanimage -t 30 -y 30 --mode Color --depth 8 --resolution 1200 --sharpness 2 --format=tiff --source TPU8x10 > out.tiff
and would you believe it out.tiff was wide and sharp and a whole lot better than I could achieve with TWAIN.
So, it is a horrible kludge I know, but I don't have a whole heap of options (it must be a windows, thick client application that runs on a machine with a scanner connected), so I just launch this:
c:\cygwin\bin\bash.exe --login -c "scanimage -t 30 -y 30 --mode Color --depth 8 --resolution 1200 --sharpness 2 --format=tiff --source TPU8x10 > ~/out.tiff"
via a Process in the .NET application and when it returns I load up the file into a Bitmap and carry on as if nothing freaky just happened (means the application has now a whole heap of extra prerequisites and fiddly configuration steps, but, well, meh, sue me).
I want to customize DrawNode in OwnerDrawText mode in a TreeView. I found it very slow even with this handler:
void RegistryTreeDrawNode(object sender, DrawTreeNodeEventArgs e)
{
e.DrawDefault = true;
}
Am I doing something wrong?
Thanks.
I think you may need to show a bit more code for what you're trying to do. There shouldn't be anything noticeably different drawing like that, versus not owner drawing at all; you're basically overriding the default draw and then undoing it in what you posted. It's ugly for no gain... but shouldn't be a perf hit.
So switching away from the lack of code and going after your core desire of a custom drawn tree, let me tell you that there is NOT a lot of good information out in the wild right now.
I've been doing my own custom treeview work over the past few days and will probably end up writing a tutorial on all that I've learned. In the meantime feel free to take a look at my code and see if it helps you out.
Mine was simply a custom drawn explorer treeview. The code that populates the treeview is separate from the TreeView drawing code. You'll probably need to add your own +/- images if you wanted to run my code.
Utilities\IconReader.cs
using System;
using System.Runtime.InteropServices;
namespace TreeViewTestProject.Utilities
{
public class IconReader
{
public enum IconSize
{
Large = 0,
Small = 1
};
public enum FolderType
{
Open = 0,
Closed = 1
};
/// <summary>
/// Returns an icon for a given file - indicated by the name parameter.
/// </summary>
/// <param name="name">Pathname for file.</param>
/// <param name="size">Large or small</param>
/// <param name="linkOverlay">Whether to include the link icon</param>
/// <returns>System.Drawing.Icon</returns>
public static System.Drawing.Icon GetFileIcon(string name, IconSize size, bool linkOverlay)
{
Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;
if(true == linkOverlay) flags |= Shell32.SHGFI_LINKOVERLAY;
/* Check the size specified for return. */
if(IconSize.Small == size)
{
flags |= Shell32.SHGFI_SMALLICON;
}
else
{
flags |= Shell32.SHGFI_LARGEICON;
}
Shell32.SHGetFileInfo(name,
Shell32.FILE_ATTRIBUTE_NORMAL,
ref shfi,
(uint)System.Runtime.InteropServices.Marshal.SizeOf(shfi),
flags);
// Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly
System.Drawing.Icon icon = (System.Drawing.Icon)System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();
User32.DestroyIcon(shfi.hIcon); // Cleanup
return icon;
}
/// <summary>
/// Used to access system folder icons.
/// </summary>
/// <param name="size">Specify large or small icons.</param>
/// <param name="folderType">Specify open or closed FolderType.</param>
/// <returns>System.Drawing.Icon</returns>
public static System.Drawing.Icon GetFolderIcon(string Foldername, IconSize size, FolderType folderType)
{
// Need to add size check, although errors generated at present!
uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;
if(FolderType.Open == folderType)
{
flags |= Shell32.SHGFI_OPENICON;
}
if(IconSize.Small == size)
{
flags |= Shell32.SHGFI_SMALLICON;
}
else
{
flags |= Shell32.SHGFI_LARGEICON;
}
// Get the folder icon
Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
Shell32.SHGetFileInfo(Foldername,
Shell32.FILE_ATTRIBUTE_DIRECTORY,
ref shfi,
(uint)System.Runtime.InteropServices.Marshal.SizeOf(shfi),
flags);
System.Drawing.Icon.FromHandle(shfi.hIcon); // Load the icon from an HICON handle
// Now clone the icon, so that it can be successfully stored in an ImageList
System.Drawing.Icon icon = (System.Drawing.Icon)System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();
User32.DestroyIcon(shfi.hIcon); // Cleanup
return icon;
}
}
public class Shell32
{
public const int MAX_PATH = 256;
[StructLayout(LayoutKind.Sequential)]
public struct SHITEMID
{
public ushort cb;
[MarshalAs(UnmanagedType.LPArray)]
public byte[] abID;
}
[StructLayout(LayoutKind.Sequential)]
public struct ITEMIDLIST
{
public SHITEMID mkid;
}
[StructLayout(LayoutKind.Sequential)]
public struct BROWSEINFO
{
public IntPtr hwndOwner;
public IntPtr pidlRoot;
public IntPtr pszDisplayName;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpszTitle;
public uint ulFlags;
public IntPtr lpfn;
public int lParam;
public IntPtr iImage;
}
// Browsing for directory.
public const uint BIF_RETURNONLYFSDIRS = 0x0001;
public const uint BIF_DONTGOBELOWDOMAIN = 0x0002;
public const uint BIF_STATUSTEXT = 0x0004;
public const uint BIF_RETURNFSANCESTORS = 0x0008;
public const uint BIF_EDITBOX = 0x0010;
public const uint BIF_VALIDATE = 0x0020;
public const uint BIF_NEWDIALOGSTYLE = 0x0040;
public const uint BIF_USENEWUI = (BIF_NEWDIALOGSTYLE | BIF_EDITBOX);
public const uint BIF_BROWSEINCLUDEURLS = 0x0080;
public const uint BIF_BROWSEFORCOMPUTER = 0x1000;
public const uint BIF_BROWSEFORPRINTER = 0x2000;
public const uint BIF_BROWSEINCLUDEFILES = 0x4000;
public const uint BIF_SHAREABLE = 0x8000;
[StructLayout(LayoutKind.Sequential)]
public struct SHFILEINFO
{
public const int NAMESIZE = 80;
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NAMESIZE)]
public string szTypeName;
};
public const uint SHGFI_ICON = 0x000000100; // get icon
public const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name
public const uint SHGFI_TYPENAME = 0x000000400; // get type name
public const uint SHGFI_ATTRIBUTES = 0x000000800; // get attributes
public const uint SHGFI_ICONLOCATION = 0x000001000; // get icon location
public const uint SHGFI_EXETYPE = 0x000002000; // return exe type
public const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index
public const uint SHGFI_LINKOVERLAY = 0x000008000; // put a link overlay on icon
public const uint SHGFI_SELECTED = 0x000010000; // show icon in selected state
public const uint SHGFI_ATTR_SPECIFIED = 0x000020000; // get only specified attributes
public const uint SHGFI_LARGEICON = 0x000000000; // get large icon
public const uint SHGFI_SMALLICON = 0x000000001; // get small icon
public const uint SHGFI_OPENICON = 0x000000002; // get open icon
public const uint SHGFI_SHELLICONSIZE = 0x000000004; // get shell size icon
public const uint SHGFI_PIDL = 0x000000008; // pszPath is a pidl
public const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; // use passed dwFileAttribute
public const uint SHGFI_ADDOVERLAYS = 0x000000020; // apply the appropriate overlays
public const uint SHGFI_OVERLAYINDEX = 0x000000040; // Get the index of the overlay
public const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
public const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
[DllImport("Shell32.dll")]
public static extern IntPtr SHGetFileInfo(
string pszPath,
uint dwFileAttributes,
ref SHFILEINFO psfi,
uint cbFileInfo,
uint uFlags
);
}
public class User32
{
/// <summary>
/// Provides access to function required to delete handle. This method is used internally
/// and is not required to be called separately.
/// </summary>
/// <param name="hIcon">Pointer to icon handle.</param>
/// <returns>N/A</returns>
[DllImport("User32.dll")]
public static extern int DestroyIcon(IntPtr hIcon);
}
}
ExplorerTreeView.cs :
using System;
using System.IO;
using System.Windows.Forms;
using TreeViewTestProject.Utilities;
using System.Collections;
namespace TreeViewTestProject
{
public partial class ExplorerTreeView : TreeViewEx
{
#region ExplorerNodeSorter Class
private class ExplorerNodeSorter : IComparer
{
public int Compare(object x, object y)
{
TreeNode nx = x as TreeNode;
TreeNode ny = y as TreeNode;
bool nxDir = (nx.ImageKey == kDirectoryImageKey);
bool nyDir = (ny.ImageKey == kDirectoryImageKey);
if(nxDir && !nyDir)
{
return -1;
}
else if(nyDir && !nxDir)
{
return 1;
}
else
{
return string.Compare(nx.Text, ny.Text);
}
}
}
#endregion
private const string kDirectoryImageKey = "directory";
private const string kReplacementText = "C43C65D1-D40F-46F0-BC5E-57265322DDFC";
public ExplorerTreeView()
{
InitializeComponent();
this.BeforeExpand += new TreeViewCancelEventHandler(ExplorerTreeView_BeforeExpand);
this.ImageList = m_FileIcons;
this.TreeViewNodeSorter = new ExplorerNodeSorter();
this.LabelEdit = true;
// Create the root of the tree and populate it
PopulateTreeView(#"C:\");
}
private void PopulateTreeView(string DirectoryName)
{
this.BeginUpdate();
string rootDir = DirectoryName;
TreeNode rootNode = CreateTreeNode(rootDir);
rootNode.Text = rootDir;
this.Nodes.Add(rootNode);
PopulateDirectory(rootNode);
this.EndUpdate();
}
private bool PathIsDirectory(string FullPath)
{
FileAttributes attr = File.GetAttributes(FullPath);
return ((attr & FileAttributes.Directory) == FileAttributes.Directory);
}
private TreeNode CreateTreeNode(string FullPath)
{
string key = FullPath.ToLower();
string name = "";
object tag = null;
if(PathIsDirectory(key))
{
DirectoryInfo info = new DirectoryInfo(FullPath);
key = kDirectoryImageKey;
name = info.Name;
tag = info;
}
else
{
FileInfo info = new FileInfo(FullPath);
name = info.Name;
tag = info;
}
if(!m_FileIcons.Images.ContainsKey(key))
{
if(key == "directory")
{
m_FileIcons.Images.Add(key, IconReader.GetFolderIcon(Environment.CurrentDirectory, IconReader.IconSize.Small, IconReader.FolderType.Open).ToBitmap());
}
else
{
m_FileIcons.Images.Add(key, IconReader.GetFileIcon(FullPath, IconReader.IconSize.Small, false));
}
}
TreeNode node = new TreeNode(name);
node.Tag = tag;
node.ImageKey = key;
node.SelectedImageKey = key;
return node;
}
private void PopulateDirectory(TreeNode ParentNode)
{
DirectoryInfo parentInfo = ParentNode.Tag as DirectoryInfo;
foreach(DirectoryInfo subDir in parentInfo.GetDirectories())
{
TreeNode child = CreateTreeNode(subDir.FullName);
PopulateForExpansion(child);
ParentNode.Nodes.Add(child);
}
foreach(FileInfo file in parentInfo.GetFiles())
{
ParentNode.Nodes.Add(CreateTreeNode(file.FullName));
}
}
private void PopulateForExpansion(TreeNode ParentNode)
{
// We need the +/- to show up if this directory isn't empty... but only want to populate the node on demand
DirectoryInfo parentInfo = ParentNode.Tag as DirectoryInfo;
try
{
if((parentInfo.GetDirectories().Length > 0) || (parentInfo.GetFiles().Length > 0))
{
ParentNode.Nodes.Add(kReplacementText);
}
}
catch { }
}
private void ReplacePlaceholderDirectoryNode(TreeNode ParentNode)
{
if((ParentNode.Nodes.Count == 1) && (ParentNode.Nodes[0].Text == kReplacementText))
{
ParentNode.Nodes.Clear();
PopulateDirectory(ParentNode);
}
}
private void ExplorerTreeView_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
this.BeginUpdate();
ReplacePlaceholderDirectoryNode(e.Node);
this.EndUpdate();
}
}
}
TreeViewEx.cs:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TreeViewTestProject
{
public partial class TreeViewEx : TreeView
{
// Notes: TextRenderer uses GDI to render the text, whereas Graphics uses GDI+. "TreeView" has existed for a long long time
// and thus uses GDI under the covers. For User Drawing TreeNode's, we need to make sure we use the TextRenderer version
// of text rendering functions.
#region Properties
private DashStyle m_SelectionDashStyle = DashStyle.Dot;
public DashStyle SelectionDashStyle
{
get { return m_SelectionDashStyle; }
set { m_SelectionDashStyle = value; }
}
private DashStyle m_LineStyle = DashStyle.Solid;
public DashStyle LineStyle
{
get { return m_LineStyle; }
set { m_LineStyle = value; }
}
private bool m_ShowLines = true;
public new bool ShowLines // marked as 'new' to replace base functionality fixing ShowLines/FullRowSelect issues in base.
{
get { return m_ShowLines; }
set { m_ShowLines = value; }
}
protected override CreateParams CreateParams
{
get
{
// Removes all the flickering of repainting node's
// This is the only thing I found that works properly for doublebuffering a treeview.
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_CLIPCHILDREN
return cp;
}
}
#endregion
[DllImport("user32.dll", ExactSpelling = false, CharSet = CharSet.Auto)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
private static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, HandleRef lParam);
private const int GWL_STYLE = -16;
private const int WS_VSCROLL = 0x00200000;
private const uint TV_FIRST = 0x1100;
private const uint TVM_EDITLABELA = (TV_FIRST + 14);
private const uint TVM_EDITLABELW = (TV_FIRST + 65);
private bool m_SelectionChanged = false;
private bool m_DoubleClicked = false;
private bool m_HierarchyChanged = false;
public TreeViewEx()
{
InitializeComponent();
// ShowLines must be "false" for FullRowSelect to work - so we're overriding the variable to correct for that.
base.ShowLines = false;
this.FullRowSelect = true;
this.ItemHeight = 21;
this.DrawMode = TreeViewDrawMode.OwnerDrawAll;
this.DrawNode += OnDrawNode;
}
private void OnDrawNode(object sender, DrawTreeNodeEventArgs e)
{
e.DrawDefault = false;
if(e.Node.Bounds.IsEmpty) return;
// Clear out the previous contents for the node. If we don't do this, when you mousehover the font will get slightly more bold
Rectangle bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - 1, e.Node.Bounds.Height - 1);
e.Graphics.FillRectangle(SystemBrushes.Window, bounds);
// Draw everything
DrawNodeFocusedHighlight(e);
DrawNodeLines(e);
DrawPlusMinus(e);
DrawNodeIcon(e);
DrawNodeText(e);
}
private void DrawNodeFocusedHighlight(DrawTreeNodeEventArgs e)
{
if(SelectedNode != e.Node) return;
int scrollWidth = 0;
if(VScrollVisible())
{
scrollWidth = SystemInformation.VerticalScrollBarWidth;
}
Rectangle bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - scrollWidth, e.Node.Bounds.Height - 1);
if(!e.Node.IsEditing)
{
e.Graphics.FillRectangle(SystemBrushes.Highlight, bounds);
}
using(Pen focusPen = new Pen(Color.Black))
{
focusPen.DashStyle = SelectionDashStyle;
bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - scrollWidth - 5, e.Node.Bounds.Height - 2);
e.Graphics.DrawRectangle(focusPen, bounds);
}
}
private void DrawNodeText(DrawTreeNodeEventArgs e)
{
if(e.Node.Bounds.IsEmpty) return;
if(e.Node.IsEditing) return;
Rectangle bounds = e.Node.Bounds;
using(Font font = e.Node.NodeFont)
{
bounds.Width = TextRenderer.MeasureText(e.Node.Text, font).Width;
bounds.Y -= 1;
bounds.X += 1;
if(IsRootNode(e.Node))
{
bounds = new Rectangle(this.Margin.Size.Width + Properties.Resources.minus.Width + 9, 0, bounds.Width, bounds.Height);
}
Color fontColor = SystemColors.InactiveCaptionText;
if(this.Focused)
{
fontColor = e.Node.IsSelected?SystemColors.HighlightText:this.ForeColor;
}
TextRenderer.DrawText(e.Graphics, e.Node.Text, font, bounds, fontColor);
}
}
private bool IsRootNode(TreeNode Node)
{
return (Node.Level == 0 && Node.PrevNode == null);
}
private void DrawNodeLines(DrawTreeNodeEventArgs e)
{
DrawNodeLineVertical(e);
DrawNodeLineHorizontal(e);
}
private void DrawNodeLineVertical(DrawTreeNodeEventArgs e)
{
if(IsRootNode(e.Node)) return;
if(!ShowLines) return;
Pen linePen = new Pen(Color.Black);
linePen.DashStyle = LineStyle;
for(int x = 0; x < e.Node.Level; x++)
{
int xLoc = this.Indent + (x * this.Indent) + (Properties.Resources.minus.Width / 2);
int height = e.Bounds.Height;
if(ShouldDrawVerticalLineForLevel(e.Node, x))
{
e.Graphics.DrawLine(linePen, xLoc, e.Bounds.Top, xLoc, e.Bounds.Top + height);
}
}
// Draw the half line for the last node
if(e.Node.Parent.LastNode == e.Node)
{
int halfLoc = (e.Node.Level * this.Indent) + (Properties.Resources.minus.Width / 2);
e.Graphics.DrawLine(linePen, halfLoc, e.Bounds.Top, halfLoc, (e.Bounds.Top + e.Bounds.Height / 2) - 1);
}
}
private bool ShouldDrawVerticalLineForLevel(TreeNode Current, int Level)
{
TreeNode node = Current;
TreeNode c = Current;
while(node.Level != Level)
{
c = node;
node = node.Parent;
}
return !(node.LastNode == c);
}
private void DrawNodeLineHorizontal(DrawTreeNodeEventArgs e)
{
if(IsRootNode(e.Node)) return;
if(!ShowLines) return;
Pen linePen = new Pen(Color.Black);
int xLoc = (e.Node.Level * this.Indent) + (Properties.Resources.minus.Width / 2);
int midY = (e.Bounds.Top + e.Bounds.Bottom) / 2 - 1;
e.Graphics.DrawLine(linePen, xLoc, midY, xLoc + 7, midY);
}
private void DrawNodeIcon(DrawTreeNodeEventArgs e)
{
if(this.ImageList == null) return;
int indent = (e.Node.Level * this.Indent) + this.Margin.Size.Width;
int iconLeft = indent + this.Indent;
int imgIndex = this.ImageList.Images.IndexOfKey(e.Node.ImageKey);
if(!IsRootNode(e.Node))
{
if(imgIndex >= 0)
{
Image img = this.ImageList.Images[imgIndex];
int y = (e.Bounds.Y + e.Bounds.Height / 2) - (img.Height / 2) - 1;
e.Graphics.DrawImage(img, new Rectangle(iconLeft, y, img.Width, img.Height), new Rectangle(0, 0, img.Width, img.Height), GraphicsUnit.Pixel);
}
}
}
private void DrawPlusMinus(DrawTreeNodeEventArgs e)
{
if(e.Node.Nodes.Count == 0) return;
int indent = (e.Node.Level * this.Indent) + this.Margin.Size.Width;
int iconLeft = indent + this.Indent;
Image img = Properties.Resources.plus;
if(e.Node.IsExpanded) img = Properties.Resources.minus;
e.Graphics.DrawImage(img, iconLeft - img.Width - 2, (e.Bounds.Y + e.Bounds.Height / 2) - (img.Height / 2) - 1);
}
private bool VScrollVisible()
{
int style = GetWindowLong(this.Handle, GWL_STYLE);
return ((style & WS_VSCROLL) != 0);
}
private void BeginEditNode()
{
if(this.SelectedNode == null) return;
if(!this.LabelEdit) throw new Exception("This TreeView is not configured with LabelEdit=true");
IntPtr result = SendMessage(new HandleRef(this, this.Handle), TVM_EDITLABELA, IntPtr.Zero, new HandleRef(this.SelectedNode, this.SelectedNode.Handle));
if(result == IntPtr.Zero)
{
throw new Exception("Failed to send EDITLABEL message to TreeView control.");
}
}
private void TreeViewEx_BeforeLabelEdit(object sender, NodeLabelEditEventArgs e)
{
if(m_DoubleClicked)
{
m_DoubleClicked = false;
return;
}
if(m_SelectionChanged)
{
e.CancelEdit = true;
m_SelectionChanged = false;
}
}
private void TreeViewEx_MouseDoubleClick(object sender, MouseEventArgs e)
{
if(m_HierarchyChanged)
{
m_HierarchyChanged = false;
return;
}
if((e.Button & MouseButtons.Left) > 0)
{
if(this.LabelEdit && (this.SelectedNode != null))
{
m_DoubleClicked = true;
BeginInvoke(new MethodInvoker(delegate() { this.SelectedNode.BeginEdit(); }));
}
}
}
private void TreeViewEx_AfterCollapse(object sender, TreeViewEventArgs e)
{
m_HierarchyChanged = true;
}
private void TreeViewEx_AfterExpand(object sender, TreeViewEventArgs e)
{
m_HierarchyChanged = true;
}
}
}