I use the TextOut function to do this for ASCII rendering in my hexeditor.
Pros:
- You can print all characters, even ones unprintable in the Console.
- You can use any colors you want.
- Ridiculously fast.
Cons:
- Requires a lot of P/Invoke and therefore will likely only run on Windows.
public class ASCIIPalette
{
uint[] fg; // foreground color table
uint[] bg; // background color table
byte[] xl; // character translation table
public uint[] FG { get { return fg;} }
public uint[] BG { get { return bg;} }
public byte[] XL { get { return xl;} }
public ASCIIPalette()
{
fg = new uint[256];
bg = new uint[256];
xl = new byte[256];
uint black = GDI.RGB(0,0,0);
uint lgrey = GDI.RGB(160,160,160);
uint dgrey = GDI.RGB(112,112,112);
uint yellow = GDI.RGB(255,255,0);
// Set default colors
for (int i=0; i<256; ++i)
{
bg[i] = black;
switch (i)
{
case 0:
fg[i] = dgrey;
xl[i] = 177;
break;
case 255:
fg[i] = yellow;
xl[i] = 177;
break;
default:
fg[i] = lgrey;
xl[i] = (byte)i;
break;
}
}
}
}
[SuppressUnmanagedCodeSecurity]
public class ASCIIUtil
{
static byte[] buffer = new byte[256];
public static unsafe void Paint(IntPtr hdc, ASCIIPalette palette, byte[] data, int x, int y, int count)
{
// 1 = some random value that shouldn't be used.
uint fgOld=1, fgNew=1;
uint bgOld=1, bgNew=1;
GDI.TextMetric textmetric = new GDI.TextMetric();
GDI.SelectObject(hdc, GDI.GetStockObject(GDI.StockObject.OemFixedFont));
GDI.GetTextMetrics(hdc, ref textmetric);
int fontwidth = textmetric.AveCharWidth;
int i=0; // buffer insertion index
int bX=x; // batch X coordinate
fixed (byte *fixedData = data)
fixed (byte *fixedBuffer = buffer)
fixed (uint *paletteFG = palette.FG)
fixed (uint *paletteBG = palette.BG)
fixed (byte *paletteXL = palette.XL)
{
for (int a=0; a<count; ++a)
{
bool flush = false;
byte current = fixedData[a];
fgNew = paletteFG[current];
bgNew = paletteBG[current];
// Flush conditions
if (i > 255)
{
GDI.SetTextColor(hdc, fgNew);
GDI.SetBkColor (hdc, bgNew);
flush = true;
}
else if (fgNew != fgOld || bgNew != bgOld)
{
GDI.SetTextColor(hdc, fgOld);
GDI.SetBkColor (hdc, bgOld);
fgOld = fgNew;
bgOld = bgNew;
flush = true;
}
if (flush && i > 0)
{
GDI.TextOutA(hdc, bX, y, fixedBuffer, i);
i=0;
bX = x + a*fontwidth;
}
fixedBuffer[i++] = paletteXL[current];
}
// Write out the final batch
GDI.SetTextColor(hdc, fgNew);
GDI.SetBkColor (hdc, bgNew);
GDI.TextOutA(hdc, bX, y, fixedBuffer, i);
}
}
public static int GetTextWidth(IntPtr hdc)
{
GDI.TextMetric tm = new GDI.TextMetric();
GDI.SelectObject(hdc, GDI.GetStockObject(GDI.StockObject.OemFixedFont));
GDI.GetTextMetrics(hdc, ref tm);
return tm.AveCharWidth;
}
public static int GetTextHeight(IntPtr hdc)
{
GDI.TextMetric tm = new GDI.TextMetric();
GDI.SelectObject(hdc, GDI.GetStockObject(GDI.StockObject.OemFixedFont));
GDI.GetTextMetrics(hdc, ref tm);
return tm.Height;
}
}
public static class GDI
{
public enum StockObject : int
{
OemFixedFont = 10
}
public enum BmpUsage : uint
{
RGB = 0,
PAL = 1
}
public enum RasterOp : uint
{
// Binary raster ops
Black = 1, // 0
NotMergePen = 2, // DPon
MaskNotPen = 3, // DPna
NotCopyPen = 4, // PN
MaskPenNot = 5, // PDna
Not = 6, // Dn
XorPen = 7, // DPx
NotMaskPen = 8, // DPan
MaskPen = 9, // DPa
NotXorPen = 10, // DPxn
Nop = 11, // D
MergeNotPen = 12, // DPno
CopyPen = 13, // P
MergePenNot = 14, // PDno
MergePen = 15, // DPo
White = 16, // 1
Last = 16,
/// <summary>
/// Dest = Source
/// </summary>
SrcCopy = 0x00CC0020,
/// <summary>
/// Dest = Source | Dest
/// </summary>
SrcPaint = 0x00EE0086,
/// <summary>
/// Dest = Source & Dest
/// </summary>
SrcAnd = 0x008800C6,
/// <summary>
/// Dest = Source ^ Dest
/// </summary>
SrcInvert = 0x00660046,
/// <summary>
/// Dest = Source & ~Dest
/// </summary>
SrcErase = 0x00440328,
/// <summary>
/// Dest = ~Source
/// </summary>
NotSrcCopy = 0x00330008,
/// <summary>
/// Dest = ~Source & ~Dest
/// </summary>
NotSrcErase = 0x001100A6,
/// <summary>
/// Dest = Source & Pattern
/// </summary>
MergeCopy = 0x00C000CA,
/// <summary>
/// Dest = ~Source | Dest
/// </summary>
MergePaint = 0x00BB0226,
/// <summary>
/// Dest = Pattern
/// </summary>
PatCopy = 0x00F00021,
/// <summary>
/// Dest = DPSnoo
/// </summary>
PatPaint = 0x00FB0A09,
/// <summary>
/// Dest = Pattern ^ Dest
/// </summary>
PatInvert = 0x005A0049,
/// <summary>
/// Dest = ~Dest
/// </summary>
DstInvert = 0x00550009,
/// <summary>
/// Dest = Black
/// </summary>
Blackness = 0x00000042,
/// <summary>
/// Dest = White
/// </summary>
Whiteness = 0x00FF0062,
/// <summary>
/// Do not mirror the bitmap in this call
/// </summary>
NoMirrorBitmap = 0x80000000,
/// <summary>
/// Include layered windows
/// </summary>
CaptureBlt = 0x40000000
}
/// <summary>
/// Edge Types for DrawEdge.
/// </summary>
public enum Edge : int
{
RaisedOuter = 0x0001,
SunkenOuter = 0x0002,
RaisedInner = 0x0004,
SunkenInner = 0x0008,
Raised = RaisedOuter | RaisedInner,
Sunken = SunkenOuter | SunkenInner,
Etched = SunkenOuter | RaisedInner,
Bump = RaisedOuter | SunkenInner,
}
/// <summary>
/// Border Flags for DrawEdge.
/// </summary>
public enum BorderFlags : int
{
Left = 0x0001,
Top = 0x0002,
Right = 0x0004,
Bottom = 0x0008,
TopLeft = (Top | Left),
TopRight = (Top | Right),
BottomLeft = (Bottom | Left),
BottomRight = (Bottom | Right),
EntireRect = (Top | Bottom | Left | Right),
Diagonal = 0x0010,
DiagonalEndTopRight = (Diagonal | Top | Right),
DiagonalEndTopLeft = (Diagonal | Top | Left),
DiagonalEndBottomLeft = (Diagonal | Bottom | Left),
DiagonalEndBottomRight = (Diagonal | Bottom | Right),
Middle = 0x0800, // Fill in the middle
Soft = 0x1000, // For softer buttons
Adjust = 0x2000, // Calculate the space left over
Flat = 0x4000, // For flat rather than 3D borders
Mono = 0x8000, // For monochrome borders
}
[StructLayout(LayoutKind.Sequential)]
public struct TextMetric
{
public int Height;
public int Ascent;
public int Descent;
public int InternalLeading;
public int ExternalLeading;
public int AveCharWidth;
public int MaxCharWidth;
public int Weight;
public int Overhang;
public int DigitizedAspectX;
public int DigitizedAspectY;
public char FirstChar;
public char LastChar;
public char DefaultChar;
public char BreakChar;
public byte Italic;
public byte Underlined;
public byte StruckOut;
public byte PitchAndFamily;
public byte CharSet;
}
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public Rect(int left, int top, int width, int height)
{
this.left = left;
this.top = top;
this.right = left+width-1;
this.bottom = top+height-1;
}
public int left;
public int top;
public int right;
public int bottom;
}
public enum BitmapInfoCompression : uint
{
RGB = 0,
RLE8 = 1,
RLE4 = 2,
Bitfields = 3,
JPEG = 4,
PNG = 5
}
[StructLayout(LayoutKind.Sequential)]
public struct BitmapInfo
{
public BitmapInfo(int width, int height)
{
Size=40;
Width=width;
Height=height;
Planes=1;
BitCount=32;
Compression=0;
SizeImage=0;
XPelsPerMeter=0;
YPelsPerMeter=0;
ClrUsed=0;
ClrImportant=0;
}
public uint Size;
/// <summary>
/// Width in pixels.
/// </summary>
public int Width;
/// <summary>
/// Height in pixels.
/// </summary>
public int Height;
/// <summary>
/// Planes. Must be set to one.
/// </summary>
public ushort Planes;
/// <summary>
/// Bits per pixel.
/// </summary>
public ushort BitCount;
/// <summary>
/// Compression type.
/// </summary>
public BitmapInfoCompression Compression;
/// <summary>
/// Size, in bytes of the image. If Compression is set to BitmapInfoCompression.RGB, use 0.
/// </summary>
public uint SizeImage;
/// <summary>
/// Horizontal pixels-per-meter
/// </summary>
public int XPelsPerMeter;
/// <summary>
/// Vertical pixels-per-meter
/// </summary>
public int YPelsPerMeter;
/// <summary>
/// Amount of colors used in the color table (use zero).
/// </summary>
public uint ClrUsed;
/// <summary>
/// Amount of colors requires (use zero).
/// </summary>
public uint ClrImportant;
}
/// <summary>
/// Select a GDI Object into the specified DC
/// </summary>
/// <param name="hdc">Handle to the DC to select into</param>
/// <param name="obj">Handle to the object to select</param>
/// <returns>Handle to the old selected object</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj);
/// <summary>
/// Gets an object handle for a stock (system-provided) object.
/// </summary>
/// <param name="stockobject">The stock object identifier</param>
/// <returns>Handle to the specified object</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern IntPtr GetStockObject(StockObject stockobject);
/// <summary>
/// Get the text metrics for the currently selected font object on the specified DC
/// </summary>
/// <param name="hdc">Handle to the DC to use</param>
/// <param name="tm">The TextMetric struct to fill with info</param>
/// <returns>True if the function succeeds, False if it fails</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern bool GetTextMetrics(IntPtr hdc, ref TextMetric tm);
/// <summary>
/// Writes text to the specified DC using the pre-selected font
/// </summary>
/// <param name="hdc">Handle to the DC to write on</param>
/// <param name="X">Initial X position</param>
/// <param name="Y">Initial Y Position</param>
/// <param name="chars">A byte ASCII string pointer</param>
/// <param name="length">The number of ASCII chars to write</param>
/// <returns>True on success, False otherwise</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static unsafe extern bool TextOutA(IntPtr hdc, int X, int Y, byte *chars, int length);
/// <summary>
/// Sets the text foreground color for the specified DC
/// </summary>
/// <param name="hdc">Handle to the DC to use</param>
/// <param name="color">The new color to use</param>
/// <returns>The previous selected foreground color</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern uint SetTextColor(IntPtr hdc, uint color);
/// <summary>
/// Sets the text background color for the specified DC
/// </summary>
/// <param name="hdc">Handle to the DC to use</param>
/// <param name="color">The new color to use</param>
/// <returns>The previous selected background color</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern uint SetBkColor(IntPtr hdc, uint color);
/// <summary>
/// Gets the system dialog base units.
/// </summary>
/// <returns>Loword: Horizontal, Hiword: Vertical</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll")]
public static extern uint GetDialogBaseUnits();
/// <summary>
/// Converts dialog units to screen units.
/// </summary>
/// <param name="hwnd">Handle to a dialog box (other windows are invalid).</param>
/// <param name="rect">The Rect to convert.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll")]
public static extern bool MapDialogRect(IntPtr hwnd, ref Rect rect);
/// <summary>
/// Creates a DC that is compatible with an existing DC.
/// </summary>
/// <param name="hdc">The existing DC to use.</param>
/// <returns>The compatible DC.</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
#if UNSAFE
// TODO: finalize DIB Section help
/// <summary>
/// Creates a DeviceIndependateBitmap Section.
/// </summary>
/// <param name="hdc">DC to use.</param>
/// <param name="bitmapinfo">Information used to create the DIB.</param>
/// <param name="usage">Specifies whether to use RGB or palette info.</param>
/// <param name="data">Receives a pointer to the actual bitmap data.</param>
/// <param name="mappingsection">Handle to a mapped file to use to create the DIB (or null).</param>
/// <param name="offset">Offset in the mapped file to start (ignored if mappingsection is null).</param>
/// <returns>The DIB Section.</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static unsafe extern IntPtr CreateDIBSection(IntPtr hdc, BitmapInfo bitmapinfo, BmpUsage usage, ref byte* data, IntPtr mappingsection, uint offset);
#endif
/// <summary>
/// Copies bitmap data from one DC to another.
/// </summary>
/// <param name="hdcdest">Destination DC.</param>
/// <param name="xdest">X coordinate of the upper left corner of the destination.</param>
/// <param name="ydest">Y coordinate of the upper left corner of the destination.</param>
/// <param name="width">Width of data to copy.</param>
/// <param name="height">Height of data to copy.</param>
/// <param name="hdcsrc">Source DC.</param>
/// <param name="xsrc">X coordinate of the upper left corner of the source.</param>
/// <param name="ysrc">Y coordinate of the upper left cornet of the source.</param>
/// <param name="ROP">Raster operation used when writing to the destination.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr hdcdest, int xdest, int ydest, int width, int height, IntPtr hdcsrc, int xsrc, int ysrc, RasterOp ROP);
/// <summary>
/// Gets the DC for a window.
/// </summary>
/// <param name="hwnd">Window handle.</param>
/// <returns>The DC.</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hwnd);
/// <summary>
/// Releases a DC previously retrieved from GetDC.
/// </summary>
/// <param name="hwnd">The Window that owns the DC.</param>
/// <param name="hdc">The DC to release.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll")]
public static extern bool ReleaseDC(IntPtr hwnd, IntPtr hdc);
/// <summary>
/// Deletes a DC.
/// </summary>
/// <param name="hdc">The DC to delete.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hdc);
/// <summary>
/// Deletes a GDI Object.
/// </summary>
/// <param name="hobj">The Object to delete.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hobj);
/// <summary>
/// Draws one or more edges of a rectangle.
/// </summary>
/// <param name="hdc">The DC to draw to.</param>
/// <param name="qrc">Rectangle defining the box to draw.</param>
/// <param name="edge">Type of edge.</param>
/// <param name="grfFlags">Type of border.</param>
/// <returns>True: Success, False: Failure</returns>
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll")]
public static extern bool DrawEdge(IntPtr hdc, ref Rect qrc, Edge edge, BorderFlags grfFlags);
/// <summary>
/// Create a uint (COLORREF compatible) from red, green, blue components
/// </summary>
/// <param name="r">Red amount (0-255)</param>
/// <param name="g">Green amount (0-255)</param>
/// <param name="b">Blue amount (0-255)</param>
/// <returns>The packed color value</returns>
public static uint RGB(int r, int g, int b)
{
return (uint)(r + (g<<8) + (b<<16));
}
}
IntPtr hdc comes from this in a WinForms-style OnPaint method:
protected override void OnPaint(PaintEventArgs e)
{
IntPtr hdc = e.Graphics.GetHdc();
// Calls to ASCIIUtil.Paint here
}
In my ASCIIPalette above, characters 0 and 0xFF are altered slightly to make them render differently from 0x20 (the space character). Normally characters 0, 0x20 and 0xFF all render as an empty glyph (in the OEM font anyway), and in my hexeditor it's important to be able to visually differentiate all bytes.
Also keep in mind that my code is not thread-safe due to the use of that static buffer.