|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Description: Static internal class implements utility functions for icon
// implementation for the Window class.
//
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using MS.Internal.Interop;
using MS.Win32;
namespace MS.Internal.AppModel
{
internal static class IconHelper
{
private static Size s_smallIconSize;
private static Size s_iconSize;
private static int s_systemBitDepth;
/// Lazy init of static fields. Call this at the beginning of any external entrypoint.
private static void EnsureSystemMetrics()
{
if (s_systemBitDepth == 0)
{
// The values here *may* change, but it's not worthwhile to requery.
// We need to release the DC to correctly track our native handles.
var hdcDesktop = new HandleRef(null, UnsafeNativeMethods.GetDC(new HandleRef()));
try
{
int sysBitDepth = UnsafeNativeMethods.GetDeviceCaps(hdcDesktop, NativeMethods.BITSPIXEL);
sysBitDepth *= UnsafeNativeMethods.GetDeviceCaps(hdcDesktop, NativeMethods.PLANES);
// If the s_systemBitDepth is 8, make it 4. Why? Because windows does not
// choose a 256 color icon if the display is running in 256 color mode
// because of palette flicker.
if (sysBitDepth == 8)
{
sysBitDepth = 4;
}
// We really want to be pixel aware here. Don't use the SystemParameters class.
int cxSmallIcon = UnsafeNativeMethods.GetSystemMetrics(SM.CXSMICON);
int cySmallIcon = UnsafeNativeMethods.GetSystemMetrics(SM.CYSMICON);
int cxIcon = UnsafeNativeMethods.GetSystemMetrics(SM.CXICON);
int cyIcon = UnsafeNativeMethods.GetSystemMetrics(SM.CYICON);
s_smallIconSize = new Size(cxSmallIcon, cySmallIcon);
s_iconSize = new Size(cxIcon, cyIcon);
s_systemBitDepth = sysBitDepth;
}
finally
{
UnsafeNativeMethods.ReleaseDC(new HandleRef(), hdcDesktop);
}
}
}
/// <returns></returns>
public static void GetDefaultIconHandles(out NativeMethods.IconHandle largeIconHandle, out NativeMethods.IconHandle smallIconHandle)
{
largeIconHandle = null;
smallIconHandle = null;
// Get the handle of the module that created the running process.
string iconModuleFile = UnsafeNativeMethods.GetModuleFileName(new HandleRef());
// We don't really care about the return value. Handles will be invalid on error.
int extractedCount = UnsafeNativeMethods.ExtractIconEx(iconModuleFile, 0, out largeIconHandle, out smallIconHandle, 1);
}
public static void GetIconHandlesFromImageSource(ImageSource image, out NativeMethods.IconHandle largeIconHandle, out NativeMethods.IconHandle smallIconHandle)
{
EnsureSystemMetrics();
largeIconHandle = CreateIconHandleFromImageSource(image, s_iconSize);
smallIconHandle = CreateIconHandleFromImageSource(image, s_smallIconSize);
}
/// <returns>A new HICON based on the image source</returns>
public static NativeMethods.IconHandle CreateIconHandleFromImageSource(ImageSource image, Size size)
{
EnsureSystemMetrics();
bool asGoodAsItGets = false;
var bf = image as BitmapFrame;
if (bf?.Decoder?.Frames != null)
{
bf = GetBestMatch(bf.Decoder.Frames, size);
// If this was actually a multi-framed icon then we don't want to do any corrections.
// Let Windows do its thing. We don't want to unnecessarily deviate from the system.
// If this was a jpeg or png, then we're doing something Windows doesn't do,
// and we can be better. (unless it was a perfect match :)
asGoodAsItGets = bf.Decoder is IconBitmapDecoder // i.e. was this a .ico?
|| bf.PixelWidth == size.Width && bf.PixelHeight == size.Height;
image = bf;
}
if (!asGoodAsItGets)
{
// Unless this was a .ico, render it into a new BitmapFrame with the appropriate dimensions
// to preserve the aspect ratio in the HICON and do the appropriate padding.
bf = BitmapFrame.Create(GenerateBitmapSource(image, size));
}
return CreateIconHandleFromBitmapFrame(bf);
}
/// <summary>
/// Creates a BitmapSource from an arbitrary ImageSource.
/// </summary>
private static BitmapSource GenerateBitmapSource(ImageSource img, Size renderSize)
{
// By now we should just assume it's a vector image that we need to rasterize.
// We want to keep the aspect ratio, but one of the dimensions will go the full length.
var drawingDimensions = new Rect(0, 0, renderSize.Width, renderSize.Height);
// There's no reason to assume that the requested image dimensions are square.
double renderRatio = renderSize.Width / renderSize.Height;
double aspectRatio = img.Width / img.Height;
// If it's smaller than the requested size, then place it in the middle and pad the image.
if (img.Width <= renderSize.Width && img.Height <= renderSize.Height)
{
drawingDimensions = new Rect((renderSize.Width - img.Width) / 2, (renderSize.Height - img.Height) / 2, img.Width, img.Height);
}
else if (renderRatio > aspectRatio)
{
double scaledRenderWidth = (img.Width / img.Height) * renderSize.Width;
drawingDimensions = new Rect((renderSize.Width - scaledRenderWidth) / 2, 0, scaledRenderWidth, renderSize.Height);
}
else if (renderRatio < aspectRatio)
{
double scaledRenderHeight = (img.Height / img.Width) * renderSize.Height;
drawingDimensions = new Rect(0, (renderSize.Height - scaledRenderHeight) / 2, renderSize.Width, scaledRenderHeight);
}
var dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();
dc.DrawImage(img, drawingDimensions);
dc.Close();
// Need to use Pbgra32 because that's all that RenderTargetBitmap currently supports.
// 96 is the right DPI to use here because we're being very pixel aware.
var bmp = new RenderTargetBitmap((int)renderSize.Width, (int)renderSize.Height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(dv);
return bmp;
}
/// <returns></returns>
//
// Creates and HICON from a bitmap frame
private static NativeMethods.IconHandle CreateIconHandleFromBitmapFrame(BitmapFrame sourceBitmapFrame)
{
Invariant.Assert(sourceBitmapFrame != null, "sourceBitmapFrame cannot be null here");
BitmapSource bitmapSource = sourceBitmapFrame;
if (bitmapSource.Format != PixelFormats.Bgra32 && bitmapSource.Format != PixelFormats.Pbgra32)
{
bitmapSource = new FormatConvertedBitmap(bitmapSource, PixelFormats.Bgra32, null, 0.0);
}
// data used by CopyPixels
int w = bitmapSource.PixelWidth;
int h = bitmapSource.PixelHeight;
int bpp = bitmapSource.Format.BitsPerPixel;
// ensuring it is in 4 byte increments since we're dealing
// with ARGB fromat
int stride = (bpp * w + 31) / 32 * 4;
int sizeCopyPixels = stride * h;
byte[] xor = new byte[sizeCopyPixels];
bitmapSource.CopyPixels(xor, stride, 0);
return CreateIconCursor(xor, w, h, 0, 0, true);
}
// Also used by PenCursorManager
// Creates a 32 bit per pixel Icon or cursor. This code is moved from framework\ms\internal\ink\pencursormanager.cs
internal static NativeMethods.IconHandle CreateIconCursor(
byte[] colorArray,
int width,
int height,
int xHotspot,
int yHotspot,
bool isIcon)
{
// 1. We are going to generate a WIN32 color bitmap which represents the color cursor.
// 2. Then we need to create a monochrome bitmap which is used as the cursor mask.
// 3. At last we create a WIN32 HICON from the above two bitmaps
NativeMethods.BitmapHandle colorBitmap = null;
NativeMethods.BitmapHandle maskBitmap = null;
try
{
// 1) Create the color bitmap using colorArray
// Fill in the header information
NativeMethods.BITMAPINFO bi = new NativeMethods.BITMAPINFO(
width, // width
-height, // A negative value indicates the bitmap is top-down DIB
32 // biBitCount
);
bi.bmiHeader_biCompression = NativeMethods.BI_RGB;
IntPtr bits = IntPtr.Zero;
colorBitmap = MS.Win32.UnsafeNativeMethods.CreateDIBSection(
new HandleRef(null, IntPtr.Zero), // A device context. Pass null in if no DIB_PAL_COLORS is used.
ref bi, // A BITMAPINFO structure which specifies the dimensions and colors.
NativeMethods.DIB_RGB_COLORS, // Specifies the type of data contained in the bmiColors array member of the BITMAPINFO structure
ref bits, // An out Pointer to a variable that receives a pointer to the location of the DIB bit values
null, // Handle to a file-mapping object that the function will use to create the DIB. This parameter can be null.
0 // dwOffset. This value is ignored if hSection is NULL
);
if (colorBitmap.IsInvalid || bits == IntPtr.Zero)
{
// Note we will release the GDI resources in the finally block.
return NativeMethods.IconHandle.GetInvalidIcon();
}
// Copy the color bits to the win32 bitmap
Marshal.Copy(colorArray, 0, bits, colorArray.Length);
// 2) Now create the mask bitmap which is monochrome
byte[] maskArray = GenerateMaskArray(width, height, colorArray);
Invariant.Assert(maskArray != null);
maskBitmap = UnsafeNativeMethods.CreateBitmap(width, height, 1, 1, maskArray);
if (maskBitmap.IsInvalid)
{
// Note we will release the GDI resources in the finally block.
return NativeMethods.IconHandle.GetInvalidIcon();
}
// Now create HICON from two bitmaps.
NativeMethods.ICONINFO iconInfo = new NativeMethods.ICONINFO();
iconInfo.fIcon = isIcon; // fIcon == ture means creating an Icon, otherwise Cursor
iconInfo.xHotspot = xHotspot;
iconInfo.yHotspot = yHotspot;
iconInfo.hbmMask = maskBitmap;
iconInfo.hbmColor = colorBitmap;
return UnsafeNativeMethods.CreateIconIndirect(iconInfo);
}
finally
{
if (colorBitmap != null)
{
colorBitmap.Dispose();
colorBitmap = null;
}
if (maskBitmap != null)
{
maskBitmap.Dispose();
maskBitmap = null;
}
}
}
// generates the mask array for the input colorArray.
// The mask array is 1 bpp
private static byte[] GenerateMaskArray(int width, int height, byte[] colorArray)
{
int nCount = width * height;
// NOTICE-2005/04/26-WAYNEZEN,
// Check out the notes in CreateBitmap in MSDN. The scan line has to be aliged to WORD.
int bytesPerScanLine = AlignToBytes(width, 2) / 8;
byte[] bitsMask = new byte[bytesPerScanLine * height];
// We are scaning all pixels in color bitmap.
// If the alpha value is 0, we should set the corresponding mask bit to 1. So the pixel will show
// the screen pixel. Otherwise, we should set the mask bit to 0 which causes the cursor to display
// the color bitmap pixel.
for (int i = 0; i < nCount; i++)
{
// Get the i-th pixel position (hPos, vPos)
int hPos = i % width;
int vPos = i / width;
// For each byte in 2-bit color bitmap, the lowest the bit represents the right-most display pixel.
// For example the bollow mask -
// 1 1 1 0 0 0 0 1
// ^ ^
// offsetBit = 0x80 offsetBit = 0x01
int byteIndex = hPos / 8;
byte offsetBit = (byte)(0x80 >> (hPos % 8));
// Now we turn the mask on or off accordingly.
if (colorArray[i * 4 + 3] /* Alpha value since it's in Argb32 Format */ == 0x00)
{
// Set the mask bit to 1.
bitsMask[byteIndex + bytesPerScanLine * vPos] |= (byte)offsetBit;
}
else
{
// Reset the mask bit to 0
bitsMask[byteIndex + bytesPerScanLine * vPos] &= (byte)(~offsetBit);
}
// Since the scan line of the mask bitmap has to be aligned to word. We have set all padding bits to 1.
// So the extra pixel can be seen through.
if (hPos == width - 1 && width == 8)
{
bitsMask[1 + bytesPerScanLine * vPos] = 0xff;
}
}
return bitsMask;
}
// Also used by PenCursorManager
/// <summary>
/// Calculate the bits count aligned to N-Byte based on the input count
/// </summary>
/// <param name="original">The original value</param>
/// <param name="nBytesCount">N-Byte</param>
/// <returns>the nearest bit count which is aligned to N-Byte</returns>
internal static int AlignToBytes(double original, int nBytesCount)
{
Debug.Assert(nBytesCount > 0, "The N-Byte has to be greater than 0!");
int nBitsCount = 8 << (nBytesCount - 1);
return (((int)Math.Ceiling(original) + (nBitsCount - 1)) / nBitsCount) * nBitsCount;
}
///
/// We're copying the algorithm Windows uses to pick icons.
/// The comments and implementation are based on core\ntuser\client\clres.c
///
/// MatchImage
///
/// This function takes LPINTs for width & height in case of "real size".
/// For this option, we use dimensions of 1st icon in resdir as size to
/// load, instead of system metrics.
/// Returns a number that measures how "far away" the given image is
/// from a desired one. The value is 0 for an exact match. Note that our
/// formula has the following properties:
/// (1) Differences in width/height count much more than differences in
/// color format.
/// (2) Bigger images are better than smaller, since shrinking produces
/// better results than stretching.
/// (3) Color matching is done by the difference in bit depth. No
/// preference is given to having a candidate equally different
/// above and below the target.
///
/// The formula is the sum of the following terms:
/// abs(bppCandidate - bppTarget)
/// abs(cxCandidate - cxTarget), times 2 if the image is
/// narrower than what we'd like. This is because we will get a
/// better result when consolidating more information into a smaller
/// space, than when extrapolating from less information to more.
/// abs(cxCandidate - cxTarget), times 2 if the image is
/// shorter than what we'd like. This is for the same reason as
/// the width.
///
/// Let's step through an example. Suppose we want a 4bpp (16 color),
/// 32x32 image. We would choose the various candidates in the following order:
///
/// Candidate Score Formula
///
/// 32x32x4bpp = 0 abs(32-32)*1 + abs(32-32)*1 + 2*abs(4-4)*1
/// 32x32x2bpp = 4
/// 32x32x8bpp = 8
/// 32x32x16bpp = 24
/// 48x48x4bpp = 32
/// 48x48x2bpp = 36
/// 48x48x8bpp = 40
/// 32x32x32bpp = 56
/// 48x48x16bpp = 56 abs(48-32)*1 + abs(48-32)*1 + 2*abs(16-4)*1
/// 16x16x4bpp = 64
/// 16x16x2bpp = 68 abs(16-32)*2 + abs(16-32)*2 + 2*abs(2-4)*1
/// 16x16x8bpp = 72
/// 48x48x32bpp = 88 abs(48-32)*1 + abs(48-32)*1 + 2*abs(32-4)*1
/// 16x16x16bpp = 88
/// 16x16x32bpp = 104
private static int MatchImage(BitmapFrame frame, Size size, int bpp)
{
/*
* Here are the rules for our "match" formula:
* (1) A close size match is much preferable to a color match
* (2) Bigger icons are better than smaller
* (3) The smaller the difference in bit depths the better
*/
int score = 2 * MyAbs(bpp, s_systemBitDepth, false) +
MyAbs(frame.PixelWidth, (int)size.Width, true) +
MyAbs(frame.PixelHeight, (int)size.Height, true);
return score;
}
///
/// MyAbs (also from core\ntuser\client\clres.c)
///
/// Calcules my weighted absolute value of the difference between 2 nums.
/// This of course normalizes values to >= zero. But it can also "punish" the
/// returned value by a factor of two if valueHave < valueWant. This is
/// because you get worse results trying to extrapolate from less info up then
/// interpolating from more info down.
///
private static int MyAbs(int valueHave, int valueWant, bool fPunish)
{
int diff = (valueHave - valueWant);
if (diff < 0)
{
diff = (fPunish ? -2 : -1) * diff;
}
return diff;
}
/// From a list of BitmapFrames find the one that best matches the requested dimensions.
/// The methods used here are copied from Win32 sources. We want to be consistent with
/// system behaviors.
private static BitmapFrame GetBestMatch(ReadOnlyCollection<BitmapFrame> frames, Size size)
{
Invariant.Assert(size.Width != 0, "input param width should not be zero");
Invariant.Assert(size.Height != 0, "input param height should not be zero");
int bestScore = int.MaxValue;
int bestBpp = 0;
int bestIndex = 0;
bool isBitmapIconDecoder = frames[0].Decoder is IconBitmapDecoder;
for (int i = 0; i < frames.Count && bestScore != 0; ++i)
{
// determine the bit-depth (# of colors) in the
// current frame
//
// if the icon is palettized, Format.BitsPerPixel gives
// the # of bits required to index into the palette (thus,
// the # of colors in the palette). If it is a true
// color icon, it gives the # of bits required to support
// true colors.
// For icons, get the Format from the Thumbnail rather than from the
// BitmapFrame directly because the unmanaged icon decoder
// converts every icon to 32-bit. Thumbnail.Format.BitsPerPixel
// will give us the original bit depth.
int currentIconBitDepth = isBitmapIconDecoder ? frames[i].Thumbnail.Format.BitsPerPixel : frames[i].Format.BitsPerPixel;
// If it looks like nothing is specified at this point, assume a bpp of 8.
if (currentIconBitDepth == 0)
{
currentIconBitDepth = 8;
}
int score = MatchImage(frames[i], size, currentIconBitDepth);
if (score < bestScore)
{
bestIndex = i;
bestBpp = currentIconBitDepth;
bestScore = score;
}
else if (score == bestScore)
{
// Tie breaker: choose the higher color depth. If that fails, choose first one.
if (bestBpp < currentIconBitDepth)
{
bestIndex = i;
bestBpp = currentIconBitDepth;
}
}
}
return frames[bestIndex];
}
}
}
|