BizHawk/BizHawk.Client.EmuHawk/CustomControls/GDIRenderer.cs

760 lines
22 KiB
C#

using System;
using System.Text;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
namespace BizHawk.Client.EmuHawk.CustomControls
{
/// <summary>
/// Wrapper for GDI rendering functions
/// This class is not thread-safe as GDI functions should be called from the UI thread
/// </summary>
public sealed class GDIRenderer : IDisposable
{
/// <summary>
/// used for <see cref="MeasureString(string, System.Drawing.Font, float, out int, out int)"/> calculation.
/// </summary>
private static readonly int[] CharFit = new int[1];
/// <summary>
/// used for <see cref="MeasureString(string, System.Drawing.Font,float, out int, out int)"/> calculation
/// </summary>
private static readonly int[] CharFitWidth = new int[1000];
/// <summary>
/// Cache of all the HFONTs used, rather than create them again and again
/// </summary>
private readonly Dictionary<Font, FontCacheEntry> FontsCache = new Dictionary<Font, FontCacheEntry>();
class FontCacheEntry
{
public IntPtr HFont;
}
/// <summary>
/// Cache of all the brushes used, rather than create them again and again
/// </summary>
private readonly Dictionary<Color, IntPtr> BrushCache = new Dictionary<Color, IntPtr>();
private Graphics _g;
private IntPtr _hdc;
private IntPtr _currentBrush = IntPtr.Zero;
#region Construct and Destroy
public GDIRenderer()
{
SetBkMode(_hdc, BkModes.OPAQUE);
}
public void Dispose()
{
foreach (var brush in BrushCache)
{
if (brush.Value != IntPtr.Zero)
{
DeleteObject(brush.Value);
}
}
foreach (var fc in FontsCache)
DeleteObject(fc.Value.HFont);
EndOffScreenBitmap();
System.Diagnostics.Debug.Assert(_hdc == IntPtr.Zero, "Disposed a GDIRenderer while it held an HDC");
System.Diagnostics.Debug.Assert(_g == null, "Disposed a GDIRenderer while it held a Graphics");
}
#endregion
#region Api
/// <summary>
/// Draw a bitmap object at the given position
/// </summary>
public void DrawBitmap(Bitmap bitmap, Point point, bool blend = false)
{
IntPtr hbmp = bitmap.GetHbitmap();
var bitHDC = CreateCompatibleDC(CurrentHDC);
IntPtr old = SelectObject(bitHDC, hbmp);
if (blend)
{
AlphaBlend(CurrentHDC, point.X, point.Y, bitmap.Width, bitmap.Height, bitHDC, 0, 0, bitmap.Width, bitmap.Height, new BLENDFUNCTION(AC_SRC_OVER, 0, 0xff, AC_SRC_ALPHA));
}
else
{
BitBlt(CurrentHDC, point.X, point.Y, bitmap.Width, bitmap.Height, bitHDC, 0, 0, 0xCC0020);
}
SelectObject(bitHDC, old);
DeleteDC(bitHDC);
DeleteObject(hbmp);
}
/// <summary>
/// Required to use before calling drawing methods
/// </summary>
public GdiGraphicsLock LockGraphics(Graphics g)
{
_g = g;
_hdc = g.GetHdc();
SetBkMode(_hdc, BkModes.TRANSPARENT);
return new GdiGraphicsLock(this);
}
/// <summary>
/// Measure the width and height of string <paramref name="str"/> when drawn on device context HDC
/// using the given font <paramref name="font"/>
/// </summary>
public Size MeasureString(string str, Font font)
{
SetFont(font);
var size = new Size();
GetTextExtentPoint32(CurrentHDC, str, str.Length, ref size);
return size;
}
/// <summary>
/// Measure the width and height of string <paramref name="str"/> when drawn on device context HDC
/// using the given font <paramref name="font"/>
/// Restrict the width of the string and get the number of characters able to fit in the restriction and
/// the width those characters take
/// </summary>
/// <param name="maxWidth">the max width to render the string in</param>
/// <param name="charFit">the number of characters that will fit under <see cref="maxWidth"/> restriction</param>
/// <param name="charFitWidth"></param>
public Size MeasureString(string str, Font font, float maxWidth, out int charFit, out int charFitWidth)
{
SetFont(font);
var size = new Size();
GetTextExtentExPoint(CurrentHDC, str, str.Length, (int)Math.Round(maxWidth), CharFit, CharFitWidth, ref size);
charFit = CharFit[0];
charFitWidth = charFit > 0 ? CharFitWidth[charFit - 1] : 0;
return size;
}
public void DrawString(string str, Point point)
{
TextOut(CurrentHDC, point.X, point.Y, str, str.Length);
}
//this returns an IntPtr HFONT because .net's Font class will erase the relevant properties when using its Font.FromLogFont()
//note that whether this is rotated clockwise or CCW might affect how you have to position the text (right-aligned sometimes?, up or down by the height of the font?)
public static IntPtr CreateRotatedHFont(Font font, bool CW)
{
LOGFONT logf = new LOGFONT();
//font.ToLogFont(logf);
//logf.lfEscapement = CW ? 2700 : 900;
logf.lfFaceName = "System";
logf.lfEscapement = 3600 - 450;
logf.lfOrientation = logf.lfEscapement;
logf.lfOutPrecision = (byte)FontPrecision.OUT_TT_ONLY_PRECIS;
//this doesnt work! .net erases the relevant propreties.. it seems?
//return Font.FromLogFont(logf);
var ret = CreateFontIndirect(ref logf);
return ret;
}
public static void DestroyHFont(IntPtr hfont)
{
DeleteObject(hfont);
}
public void PrepDrawString(IntPtr hfont, Color color)
{
SetGraphicsMode(CurrentHDC, 2); //shouldnt be necessary.. cant hurt
SelectObject(_hdc, hfont);
SetTextColor(color);
}
public void PrepDrawString(Font font, Color color)
{
SetFont(font);
SetTextColor(color);
}
/// <summary>
/// Draw the given string using the given font and foreground color at given location
/// See [http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx][15]
/// </summary>
public void DrawString(string str, Font font, Color color, Rectangle rect, TextFormatFlags flags)
{
SetFont(font);
SetTextColor(color);
var rect2 = new Rect(rect);
DrawText(CurrentHDC, str, str.Length, ref rect2, (uint)flags);
}
/// <summary>
/// Set the text color of the device context
/// </summary>
public void SetTextColor(Color color)
{
int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
SetTextColor(CurrentHDC, rgb);
}
public void SetBackgroundColor(Color color)
{
int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
SetBkColor(CurrentHDC, rgb);
}
public void DrawRectangle(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect)
{
Rectangle(CurrentHDC, nLeftRect, nTopRect, nRightRect, nBottomRect);
}
public void SetBrush(Color color)
{
if (BrushCache.ContainsKey(color))
{
_currentBrush = BrushCache[color];
}
else
{
int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
var newBrush = CreateSolidBrush(rgb);
BrushCache.Add(color, newBrush);
_currentBrush = newBrush;
}
}
public void FillRectangle(int x, int y, int w, int h)
{
var r = new GDIRect(new Rectangle(x, y, w, h));
FillRect(CurrentHDC, ref r, _currentBrush);
}
public void SetPenPosition(int x, int y)
{
MoveToEx(CurrentHDC, x, y, IntPtr.Zero);
}
public void SetSolidPen(Color color)
{
int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
SelectObject(CurrentHDC, GetStockObject((int)PaintObjects.DC_PEN));
SetDCPenColor(CurrentHDC, rgb);
}
public void Line(int x1, int y1, int x2, int y2)
{
MoveToEx(CurrentHDC, x1, y1, IntPtr.Zero);
LineTo(CurrentHDC, x2, y2);
}
private IntPtr CurrentHDC
{
get { return _bitHDC != IntPtr.Zero ? _bitHDC : _hdc; }
}
private IntPtr _bitMap = IntPtr.Zero;
private IntPtr _bitHDC = IntPtr.Zero;
private int _bitW;
private int _bitH;
public void StartOffScreenBitmap(int width, int height)
{
_bitW = width;
_bitH = height;
_bitHDC = CreateCompatibleDC(_hdc);
_bitMap = CreateCompatibleBitmap(_hdc, width, height);
SelectObject(_bitHDC, _bitMap);
SetBkMode(_bitHDC, BkModes.TRANSPARENT);
}
public void EndOffScreenBitmap()
{
_bitW = 0;
_bitH = 0;
DeleteObject(_bitMap);
DeleteObject(_bitHDC);
_bitHDC = IntPtr.Zero;
_bitMap = IntPtr.Zero;
}
public void CopyToScreen()
{
BitBlt(_hdc, 0, 0, _bitW, _bitH, _bitHDC, 0, 0, 0x00CC0020);
}
#endregion
#region Helpers
/// <summary>
/// Set a resource (e.g. a font) for the specified device context.
/// </summary>
private void SetFont(Font font)
{
SelectObject(_hdc, GetCachedHFont(font));
}
private IntPtr GetCachedHFont(Font font)
{
//the original code struck me as bad. attempting to ID fonts by picking a subset of their fields is not gonna work.
//don't call this.Font in InputRoll.cs, it is probably slow.
//consider Fonts to be a jealously guarded resource (they need to be disposed, after all) and manage them carefully.
//this cache maintains the HFONTs only.
FontCacheEntry ce;
if (!FontsCache.TryGetValue(font, out ce))
{
FontsCache[font] = ce = new FontCacheEntry();
ce.HFont = font.ToHfont();
}
return ce.HFont;
}
#endregion
#region Imports
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr BeginPaint(IntPtr hWnd, ref IntPtr lpPaint);
[DllImport("user32.dll")]
private static extern IntPtr EndPaint(IntPtr hWnd, IntPtr lpPaint);
[DllImport("gdi32.dll")]
static extern IntPtr CreateFontIndirect([In] ref LOGFONT lplf);
[DllImport("gdi32.dll")]
private static extern int Rectangle(IntPtr hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
[DllImport("user32.dll")]
private static extern int FillRect(IntPtr hdc, [In] ref GDIRect lprc, IntPtr hbr);
[DllImport("gdi32.dll")]
private static extern int SetBkMode(IntPtr hdc, BkModes mode);
[DllImport("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiObj);
[DllImport("gdi32.dll")]
private static extern int SetTextColor(IntPtr hdc, int color);
[DllImport("gdi32.dll")]
private static extern int SetBkColor(IntPtr hdc, int color);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W")]
private static extern int GetTextExtentPoint32(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Size size);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentExPointW")]
private static extern bool GetTextExtentExPoint(IntPtr hDc, [MarshalAs(UnmanagedType.LPWStr)]string str, int nLength, int nMaxExtent, int[] lpnFit, int[] alpDx, ref Size size);
[DllImport("gdi32.dll", EntryPoint = "TextOutW")]
private static extern bool TextOut(IntPtr hdc, int x, int y, [MarshalAs(UnmanagedType.LPWStr)] string str, int len);
[DllImport("gdi32.dll")]
public static extern int SetGraphicsMode(IntPtr hdc, int iMode);
[DllImport("user32.dll", EntryPoint = "DrawTextW")]
private static extern int DrawText(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Rect rect, uint uFormat);
[DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")]
private static extern bool ExtTextOut(IntPtr hdc, int X, int Y, uint fuOptions, uint cbCount, [In] IntPtr lpDx);
[DllImport("gdi32.dll")]
static extern bool SetWorldTransform(IntPtr hdc, [In] ref XFORM lpXform);
[DllImport("gdi32.dll")]
private static extern int SelectClipRgn(IntPtr hdc, IntPtr hrgn);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateSolidBrush(int color);
[DllImport("gdi32.dll")]
private static extern IntPtr CreatePen(int fnPenStyle, int nWidth, int color);
[DllImport("gdi32.dll")]
private static extern IntPtr MoveToEx(IntPtr hdc, int x, int y, IntPtr point);
[DllImport("gdi32.dll")]
private static extern IntPtr LineTo(IntPtr hdc, int nXEnd, int nYEnd);
[DllImport("gdi32.dll")]
private static extern IntPtr GetStockObject(int fnObject);
[DllImport("gdi32.dll")]
private static extern IntPtr SetDCPenColor(IntPtr hdc, int crColor);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll", EntryPoint = "DeleteDC")]
public static extern bool DeleteDC([In] IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int height);
[DllImport("gdi32.dll", EntryPoint = "BitBlt", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool BitBlt([In] IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, [In] IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")]
static extern bool AlphaBlend(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, BLENDFUNCTION blendFunction);
public enum FontWeight : int
{
FW_DONTCARE = 0,
FW_THIN = 100,
FW_EXTRALIGHT = 200,
FW_LIGHT = 300,
FW_NORMAL = 400,
FW_MEDIUM = 500,
FW_SEMIBOLD = 600,
FW_BOLD = 700,
FW_EXTRABOLD = 800,
FW_HEAVY = 900,
}
public enum FontCharSet : byte
{
ANSI_CHARSET = 0,
DEFAULT_CHARSET = 1,
SYMBOL_CHARSET = 2,
SHIFTJIS_CHARSET = 128,
HANGEUL_CHARSET = 129,
HANGUL_CHARSET = 129,
GB2312_CHARSET = 134,
CHINESEBIG5_CHARSET = 136,
OEM_CHARSET = 255,
JOHAB_CHARSET = 130,
HEBREW_CHARSET = 177,
ARABIC_CHARSET = 178,
GREEK_CHARSET = 161,
TURKISH_CHARSET = 162,
VIETNAMESE_CHARSET = 163,
THAI_CHARSET = 222,
EASTEUROPE_CHARSET = 238,
RUSSIAN_CHARSET = 204,
MAC_CHARSET = 77,
BALTIC_CHARSET = 186,
}
public enum FontPrecision : byte
{
OUT_DEFAULT_PRECIS = 0,
OUT_STRING_PRECIS = 1,
OUT_CHARACTER_PRECIS = 2,
OUT_STROKE_PRECIS = 3,
OUT_TT_PRECIS = 4,
OUT_DEVICE_PRECIS = 5,
OUT_RASTER_PRECIS = 6,
OUT_TT_ONLY_PRECIS = 7,
OUT_OUTLINE_PRECIS = 8,
OUT_SCREEN_OUTLINE_PRECIS = 9,
OUT_PS_ONLY_PRECIS = 10,
}
public enum FontClipPrecision : byte
{
CLIP_DEFAULT_PRECIS = 0,
CLIP_CHARACTER_PRECIS = 1,
CLIP_STROKE_PRECIS = 2,
CLIP_MASK = 0xf,
CLIP_LH_ANGLES = (1 << 4),
CLIP_TT_ALWAYS = (2 << 4),
CLIP_DFA_DISABLE = (4 << 4),
CLIP_EMBEDDED = (8 << 4),
}
public enum FontQuality : byte
{
DEFAULT_QUALITY = 0,
DRAFT_QUALITY = 1,
PROOF_QUALITY = 2,
NONANTIALIASED_QUALITY = 3,
ANTIALIASED_QUALITY = 4,
CLEARTYPE_QUALITY = 5,
CLEARTYPE_NATURAL_QUALITY = 6,
}
[Flags]
public enum FontPitchAndFamily : byte
{
DEFAULT_PITCH = 0,
FIXED_PITCH = 1,
VARIABLE_PITCH = 2,
FF_DONTCARE = (0 << 4),
FF_ROMAN = (1 << 4),
FF_SWISS = (2 << 4),
FF_MODERN = (3 << 4),
FF_SCRIPT = (4 << 4),
FF_DECORATIVE = (5 << 4),
}
//it is important for this to be the right declaration
//see more here http://www.tech-archive.net/Archive/DotNet/microsoft.public.dotnet.framework.drawing/2004-04/0319.html
//if it's wrong (I had a wrong one from pinvoke.net) then ToLogFont will fail mysteriously
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
class LOGFONT
{
public int lfHeight = 0;
public int lfWidth = 0;
public int lfEscapement = 0;
public int lfOrientation = 0;
public int lfWeight = 0;
public byte lfItalic = 0;
public byte lfUnderline = 0;
public byte lfStrikeOut = 0;
public byte lfCharSet = 0;
public byte lfOutPrecision = 0;
public byte lfClipPrecision = 0;
public byte lfQuality = 0;
public byte lfPitchAndFamily = 0;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string lfFaceName = null;
}
/// <summary>
/// The graphics mode that can be set by SetGraphicsMode.
/// </summary>
public enum GraphicsMode : int
{
/// <summary>
/// Sets the graphics mode that is compatible with 16-bit Windows. This is the default mode. If
/// this value is specified, the application can only modify the world-to-device transform by
/// calling functions that set window and viewport extents and origins, but not by using
/// SetWorldTransform or ModifyWorldTransform; calls to those functions will fail.
/// Examples of functions that set window and viewport extents and origins are SetViewportExtEx
/// and SetWindowExtEx.
/// </summary>
GM_COMPATIBLE = 1,
/// <summary>
/// Sets the advanced graphics mode that allows world transformations. This value must be
/// specified if the application will set or modify the world transformation for the specified
/// device context. In this mode all graphics, including text output, fully conform to the
/// world-to-device transformation specified in the device context.
/// </summary>
GM_ADVANCED = 2,
}
/// <summary>
/// The XFORM structure specifies a world-space to page-space transformation.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct XFORM
{
public float eM11;
public float eM12;
public float eM21;
public float eM22;
public float eDx;
public float eDy;
public XFORM(float eM11, float eM12, float eM21, float eM22, float eDx, float eDy)
{
this.eM11 = eM11;
this.eM12 = eM12;
this.eM21 = eM21;
this.eM22 = eM22;
this.eDx = eDx;
this.eDy = eDy;
}
/// <summary>
/// Allows implicit converstion to a managed transformation matrix.
/// </summary>
public static implicit operator System.Drawing.Drawing2D.Matrix(XFORM xf)
{
return new System.Drawing.Drawing2D.Matrix(xf.eM11, xf.eM12, xf.eM21, xf.eM22, xf.eDx, xf.eDy);
}
/// <summary>
/// Allows implicit converstion from a managed transformation matrix.
/// </summary>
public static implicit operator XFORM(System.Drawing.Drawing2D.Matrix m)
{
float[] elems = m.Elements;
return new XFORM(elems[0], elems[1], elems[2], elems[3], elems[4], elems[5]);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct BLENDFUNCTION
{
byte BlendOp;
byte BlendFlags;
byte SourceConstantAlpha;
byte AlphaFormat;
public BLENDFUNCTION(byte op, byte flags, byte alpha, byte format)
{
BlendOp = op;
BlendFlags = flags;
SourceConstantAlpha = alpha;
AlphaFormat = format;
}
}
const byte AC_SRC_OVER = 0x00;
const byte AC_SRC_ALPHA = 0x01;
[DllImport("gdi32.dll")]
static extern int SetBitmapBits(IntPtr hbmp, uint cBytes, byte[] lpBits);
#endregion
#region Classes, Structs, and Enums
public class GdiGraphicsLock : IDisposable
{
private readonly GDIRenderer Gdi;
public GdiGraphicsLock(GDIRenderer gdi)
{
this.Gdi = gdi;
}
public void Dispose()
{
Gdi._g.ReleaseHdc(Gdi._hdc);
Gdi._hdc = IntPtr.Zero;
Gdi._g = null;
}
}
private struct Rect
{
private int _left;
private int _top;
private int _right;
private int _bottom;
public Rect(Rectangle r)
{
_left = r.Left;
_top = r.Top;
_bottom = r.Bottom;
_right = r.Right;
}
}
private struct GDIRect
{
private int left;
private int top;
private int right;
private int bottom;
public GDIRect(Rectangle r)
{
left = r.Left;
top = r.Top;
bottom = r.Bottom;
right = r.Right;
}
}
private struct GDIPoint
{
private int x;
private int y;
private GDIPoint(int x, int y)
{
this.x = x;
this.y = y;
}
}
[Flags]
public enum ETOOptions : uint
{
CLIPPED = 0x4,
GLYPH_INDEX = 0x10,
IGNORELANGUAGE = 0x1000,
NUMERICSLATIN = 0x800,
NUMERICSLOCAL = 0x400,
OPAQUE = 0x2,
PDY = 0x2000,
RTLREADING = 0x800,
}
/// <summary>
/// See [http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx][15]
/// </summary>
[Flags]
public enum TextFormatFlags : uint
{
Default = 0x00000000,
Center = 0x00000001,
Right = 0x00000002,
VCenter = 0x00000004,
Bottom = 0x00000008,
WordBreak = 0x00000010,
SingleLine = 0x00000020,
ExpandTabs = 0x00000040,
TabStop = 0x00000080,
NoClip = 0x00000100,
ExternalLeading = 0x00000200,
CalcRect = 0x00000400,
NoPrefix = 0x00000800,
Internal = 0x00001000,
EditControl = 0x00002000,
PathEllipsis = 0x00004000,
EndEllipsis = 0x00008000,
ModifyString = 0x00010000,
RtlReading = 0x00020000,
WordEllipsis = 0x00040000,
NoFullWidthCharBreak = 0x00080000,
HidePrefix = 0x00100000,
ProfixOnly = 0x00200000,
}
[Flags]
public enum PenStyles
{
PS_SOLID = 0x00000000
// TODO
}
public enum PaintObjects
{
WHITE_BRUSH = 0,
LTGRAY_BRUSH = 1,
GRAY_BRUSH = 2,
DKGRAY_BRUSH = 3,
BLACK_BRUSH = 4,
NULL_BRUSH = 5,
WHITE_PEN = 6,
BLACK_PEN = 7,
NULL_PEN = 8,
OEM_FIXED_FONT = 10,
ANSI_FIXED_FONT = 11,
ANSI_VAR_FONT = 12,
SYSTEM_FONT = 13,
DEVICE_DEFAULT_FONT = 14,
DEFAULT_PALETTE = 15,
SYSTEM_FIXED_FONT = 16,
DC_BRUSH = 18,
DC_PEN = 19,
}
public enum BkModes : int
{
TRANSPARENT = 1,
OPAQUE = 2
}
#endregion
}
}