I'm trying to build a map editor like this one: http://blog.rpgmakerweb.com/wp-content/uploads/2012/04/T2EditorLabelled.png in WPF.
How should it work:
By selecting a specific tile on the "tile list" - B section - is possible to draw that tile on the canvas - section A.
In the end the final result is a full game level drawn on the canvas.
First approach:
In my first approach each tile is drawn by creating a new image control and adding it to the canvas (a WPF canvas control).
Steps:
Select the tile from the tileset
Catch the click event on the canvas
Create a new image, cropping the tile from the tileset
Adding the image in the right spot on the canvas (as a child)
This method is quite naive and it imply two big issues:
All the pixels are already buffered on the tileset that contains all the tiles, but each time I draw a tile on the canvas a new image is create and by doing so I am obliged to replicate part of the tileset pixel data as a source for the new
Too many controls on the canvas:
A map game can reach the size of 1000 tiles x 1000 tiles, WPF start to have sensible decay of performance on a 100x100 tile map.
So create a control image for each tile is an unfeasible solution.
Second approach:
My second approach contemplate the use of a single big WriteableBitmap as canvas background.
As in the previous approach the tile is selected on the tileset and the draw event is a click on the canvas.
In this case though no new image is created ex-novo but the background WriteableBitmap is modified accordingly.
So in the number of controls is reduced drastically since all the draw mechanism is performed on the WriteableBitmap.
The main problem with this approach is that if I want to create a large map with 1k x 1k tiles with a tile of 32x32, the size of the background image would be astronomical.
I wonder if there is a way to develop a good solution of this problem in WPF.
How would you address this development problem?
There a variety of different ways you could approach this problem to increase performance.
In terms of the rendering of images, WPF by default isn't amazing, so you could;
Use GDI's BitBlt to quickly render images to a WinForms control which can be hosted. The benefit of this is that GDI is software and hence does not require a graphics card or anything. (WPF fast method to draw image in UI)
You can use D3DImage as an image source. This would mean that you can use the D3DImage as the canvas to which to draw. Doing this would mean you would have to render all the tiles to the D3DImage image source using Direct3D, which would be much faster as it is hardware accelerated. (https://www.codeproject.com/Articles/28526/Introduction-to-D3DImage)
You may be able to host XNA through a WinForms control and rendering using that, I have no experience with this, so I test to any performance. (WPF vs XNA to render thousands of sprites)
Personally, for rendering, I would use the GDI method as it is software based, relatively easy to set up and I have had experience with it, and seen it's performance.
Additionally, when rendering the tiles to a control, you can use the scrollbar positions and control size to determine the area of your map which is actually visible. From this, you can simply select those few tiles and only render them, saving a lot of time.
Furthermore, when you manage it yourself, you can simply load the different sprites into memory and then use that same memory to draw it in different locations onto a buffer image. This would decrease that memory issue you mentioned.
Below is my example code for the GDI method, I render 2500 32x32 pixel sprites (all of which are the same green color, however you'd set this memory to an actual sprite - src memory). The sprites are bitblit to a buffer image (srcb memory) which is then bitblit to the window, in your case, you'd bitblit the buffer image to a winforms canvas or something. With this, I get between 30 and 40 fps on my base model Surface Pro 3. This should be enough performance for a level editor's rendering. Please note this code is very crude and just roughly outlines the process, it can almost certainly be improved upon.
//
// GDI DLL IMPORT
//
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr hDC);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool BitBlt(IntPtr hDC, int x, int y, int width, int height, IntPtr hDCSource, int sourceX, int sourceY, uint type);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern bool FillRgn(IntPtr hdc, IntPtr hrgn, IntPtr hbr);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateSolidBrush(uint crColor);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
public const uint SRCCOPY = 0x00CC0020; // dest = source
public const uint SRCPAINT = 0x00EE0086; // dest = source OR dest
public const uint SRCAND = 0x008800C6; // dest = source AND dest
public const uint SRCINVERT = 0x00660046; // dest = source XOR dest
public const uint SRCERASE = 0x00440328; // dest = source AND (NOT dest )
public const uint NOTSRCCOPY = 0x00330008; // dest = (NOT source)
public const uint NOTSRCERASE = 0x001100A6; // dest = (NOT src) AND (NOT dest)
public const uint MERGECOPY = 0x00C000CA; // dest = (source AND pattern)
public const uint MERGEPAINT = 0x00BB0226; // dest = (NOT source) OR dest
public const uint PATCOPY = 0x00F00021; // dest = pattern
public const uint PATPAINT = 0x00FB0A09; // dest = DPSnoo
public const uint PATINVERT = 0x005A0049; // dest = pattern XOR dest
public const uint DSTINVERT = 0x00550009; // dest = (NOT dest)
public const uint BLACKNESS = 0x00000042; // dest = BLACK
public const uint WHITENESS = 0x00FF0062; // dest = WHITE
//
// END DLL IMPORT
//
//GDI Graphics
private Graphics g;
//Colors
private const int BACKGROUND_COLOR = 0xffffff;
private const int GRAPH_COLOR_ONE = 0x00FF00;
//Pointers
IntPtr hdc;
IntPtr srcb;
IntPtr dchb;
IntPtr origb;
IntPtr src;
IntPtr dch;
IntPtr orig;
//Brushes
IntPtr brush_one;
IntPtr brush_back;
public Form1()
{
InitializeComponent();
//Create a graphics engine from the window
g = Graphics.FromHwnd(this.Handle);
//Get the handle of the Window's graphics and then create a compatible source handle
hdc = g.GetHdc();
srcb = CreateCompatibleDC(hdc);
src = CreateCompatibleDC(hdc);
//Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
dchb = CreateCompatibleBitmap(hdc, ClientRectangle.Width, ClientRectangle.Height);
origb = SelectObject(srcb, dchb);
//Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
dch = CreateCompatibleBitmap(hdc, 32, 32);
orig = SelectObject(src, dch);
//Create the burshes
brush_one = CreateSolidBrush(GRAPH_COLOR_ONE);
brush_back = CreateSolidBrush(BACKGROUND_COLOR);
//Create Image
FillRectangle(brush_one, src, 0, 0, 32, 32);
//Fill Background
FillRectangle(brush_back, hdc, 0, 0, ClientRectangle.Width, ClientRectangle.Height);
this.Show();
Render();
}
private void Render()
{
Stopwatch s = new Stopwatch();
s.Start();
int frames = 0;
while(frames <= 30)
{
frames++;
FillRectangle(brush_back, srcb, 0, 0, ClientRectangle.Width, ClientRectangle.Height);
for (int i = 0; i < 50; i++)
for (int j = 0; j < 50; j++)
BlitBitmap(i * 5, j * 5, 32, 32, srcb, src);
BlitBitmap(0, 0, ClientRectangle.Width, ClientRectangle.Height, hdc, srcb);
}
s.Stop();
float fps = (float)frames / ((float)s.ElapsedMilliseconds / 1000.0f);
MessageBox.Show(Math.Round(fps, 2).ToString(), "FPS");
}
private void FillRectangle(IntPtr b, IntPtr hdc, int x, int y, int w, int h)
{
//Create the region
IntPtr r = CreateRectRgn(x, y, x + w, y + h);
//Fill the region using the specified brush
FillRgn(hdc, r, b);
//Delete the region object
DeleteObject(r);
}
private void BlitBitmap(int x, int y, int w, int h, IntPtr to, IntPtr from)
{
//Blit the bits of the actual source object to the window, using its handle
BitBlt(to, x, y, w, h, from, 0, 0, SRCCOPY);
}
Related
I want to calculate average pixels color from a specific area of the screen. I'm making a LED backlight for my TV so it needs to be very fast. At least 30fps. Bitmap.GetPixel() is too slow for it. I found an OpenGL method Gl.ReadPixels() but i don't know how it works. It recieving int[] as a data to return. But after calling this method i only get an array full of 0. How can i use this method? Or maybe there is some other way for this task?
By far the fastest way to process images in C# is through pinvoke using Win32 Gdi32 and User32 functionality.
Here is some C# code which captures the current desktop. Optionally you can pass the filepath of a 'mask' image which can be used to isolate a region(s) of the desktop capture. In the mask, texel which is black (with alpha 0) will be discarded from the screen grab. It's quite rudimental since it only supports 32-bit images and requires the mask and screen capture to be the same size. You welcome to improve on it in your own way, or you can see how image processing is done on a texel by texel basis by hooking directly into the Win32 functions with pinvoke (see constructor).
public class ScreenCapture
{
private System.Drawing.Bitmap capture;
/// <summary>
/// Constructor
/// </summary>
/// <param name="maskFile">Optional mask file, use "" for no mask</param>
public ScreenCapture( string maskFile )
{
// capture the screen
capture = CaptureWindow(User32.GetDesktopWindow());
// if there is a mask file, load it and apply it to the capture
if (maskFile != "")
{
string maskfilename = maskFile + ".png";
System.Drawing.Bitmap maskImage = System.Drawing.Image.FromFile(maskfilename) as Bitmap;
// ensure mask is same dim as capture...
if ((capture.Width != maskImage.Width) || (capture.Height != maskImage.Height))
throw new System.Exception("Mask image is required to be " + capture.Width + " x " + capture.Height + " RGBA for this screen capture");
Rectangle rectCapture = new Rectangle(0, 0, capture.Width, capture.Height);
Rectangle rectMask = new Rectangle(0, 0, maskImage.Width, maskImage.Height);
System.Drawing.Imaging.BitmapData dataCapture = capture.LockBits(rectCapture, System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
System.Drawing.Imaging.BitmapData dataMask = maskImage.LockBits(rectCapture, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
IntPtr ptrCapture = dataCapture.Scan0;
IntPtr ptrMask = dataMask.Scan0;
int size = Math.Abs(dataCapture.Stride) * capture.Height;
byte[] bitsCapture = new byte[size];
byte[] bitsMask = new byte[size];
System.Runtime.InteropServices.Marshal.Copy(ptrCapture, bitsCapture, 0, size);
System.Runtime.InteropServices.Marshal.Copy(ptrMask, bitsMask, 0, size);
// check each pixel of the image... if the mask is 0 for each channel, set the capture pixel to 0.
for (int n = 0; n < size; n += 4)
{
bool clearPixel = true;
for ( int c = 0; c < 4; ++c )
{
// if any pixel of the mask is not black, do not clear the capture pixel
if (bitsMask[n + c] != 0)
{
clearPixel = false;
break;
}
}
if ( clearPixel )
{
bitsCapture[n] = 0;
bitsCapture[n + 1] = 0;
bitsCapture[n + 2] = 0;
bitsCapture[n + 3] = 0;
}
}
System.Runtime.InteropServices.Marshal.Copy(bitsCapture, 0, ptrCapture, size);
System.Runtime.InteropServices.Marshal.Copy(bitsMask, 0, ptrMask, size);
capture_.UnlockBits(dataCapture);
maskImage.UnlockBits(dataMask);
}
}
/// <summary>
/// Creates an Image object containing a screen shot of a specific window
/// </summary>
/// <param name="handle">The handle to the window.
/// (In windows forms, this is obtained by the Handle property)</param>
/// <returns>Screen grab image</returns>
private System.Drawing.Bitmap CaptureWindow(IntPtr handle)
{
// get te hDC of the target window
IntPtr hdcSrc = User32.GetWindowDC(handle);
// get the size
User32.RECT windowRect = new User32.RECT();
User32.GetWindowRect(handle, ref windowRect);
int width = windowRect.right - windowRect.left;
int height = windowRect.bottom - windowRect.top;
// create a device context we can copy to
IntPtr hdcDest = GDI32.CreateCompatibleDC(hdcSrc);
// create a bitmap we can copy it to,
// using GetDeviceCaps to get the width/height
IntPtr hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height);
// select the bitmap object
IntPtr hOld = GDI32.SelectObject(hdcDest, hBitmap);
// bitblt over
GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, GDI32.SRCCOPY);
// restore selection
GDI32.SelectObject(hdcDest, hOld);
// clean up
GDI32.DeleteDC(hdcDest);
User32.ReleaseDC(handle, hdcSrc);
// get a .NET image object for it
System.Drawing.Bitmap img = System.Drawing.Image.FromHbitmap(hBitmap);
// free up the Bitmap object
GDI32.DeleteObject(hBitmap);
return img;
}
/// <summary>
/// Save this capture as a Png image.
/// </summary>
/// <param name="filename">File path and name not including extension</param>
public void SaveCapture( string filename )
{
capture.Save(filename + ".png", System.Drawing.Imaging.ImageFormat.Png);
}
/// <summary>
/// Helper class containing Gdi32 API functions
/// </summary>
private class GDI32
{
public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter
[DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest,
int nWidth, int nHeight, IntPtr hObjectSource,
int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth,
int nHeight);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
}
/// <summary>
/// Helper class containing User32 API functions
/// </summary>
private class User32
{
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetActiveWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect);
}
}
It throws exceptions, so be sure to catch them. To use...
ScreenCapture grab = new ScreenCapture( "screenGrabMask.png" );
grab.SaveCapture( "screenGrab.png" );
Take a look at Bitmap.LockBits
https://msdn.microsoft.com/de-de/library/5ey6h79d(v=vs.110).aspx
to provide some basic idea
private unsafe void demo(Bitmap bm)
{
System.Drawing.Imaging.BitmapData D = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
int stride = Math.Abs(D.Stride);
byte* pImg = (byte*)D.Scan0.ToPointer();
for(int y = 0; y < bm.Height; y++)
{
byte* pRow = (byte*)(pImg + y * stride);
for(int x = 0; x < bm.Width; x++)
{
uint b = *pRow++;
uint g = *pRow++;
uint r = *pRow++;
pRow++; // skip alpha
// do something with r g b
}
}
bm.UnlockBits(D);
}
Allow unsafe code (be careful) to avoid extra memory copies.
Using Directshow.NET I have developed an application which will record the desktop screen, to get mouse pointer we need to paint mouse pointer by our own. So I added SampleGrabber adn in BufferCB I have written below code:
public const Int32 CURSOR_SHOWING = 0x00000001;
[StructLayout(LayoutKind.Sequential)]
public struct ICONINFO
{
public bool fIcon;
public Int32 xHotspot;
public Int32 yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public Int32 x;
public Int32 y;
}
[StructLayout(LayoutKind.Sequential)]
public struct CURSORINFO
{
public Int32 cbSize;
public Int32 flags;
public IntPtr hCursor;
public POINT ptScreenPos;
}
[DllImport("user32.dll")]
public static extern bool GetCursorInfo(out CURSORINFO pci);
[DllImport("user32.dll")]
public static extern IntPtr CopyIcon(IntPtr hIcon);
[DllImport("user32.dll")]
public static extern bool DrawIcon(IntPtr hdc, int x, int y, IntPtr hIcon);
[DllImport("user32.dll")]
public static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);
public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
Graphics g;
Bitmap v;
v = new Bitmap(m_videoWidth, m_videoHeight, m_stride, System.Drawing.Imaging.PixelFormat.Format24bppRgb, pBuffer);
g = Graphics.FromImage(v);
CURSORINFO cursorInfo;
cursorInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO));
if (GetCursorInfo(out cursorInfo))
{
if (cursorInfo.flags == CURSOR_SHOWING)
{
var iconPointer = CopyIcon(cursorInfo.hCursor);
ICONINFO iconInfo;
int iconX, iconY;
if (GetIconInfo(iconPointer, out iconInfo))
{
iconX = cursorInfo.ptScreenPos.x - ((int)iconInfo.xHotspot);
iconY = cursorInfo.ptScreenPos.y - ((int)iconInfo.yHotspot);
DrawIcon(g.GetHdc(), iconX, iconY, cursorInfo.hCursor);
g.ReleaseHdc();
g.Dispose();
v.Dispose();
}
}
}
return 0;
}
This code is painting the mouse cursor but cursor is flipped on Y axis.
It's may be because in BufferCB if we convert pBuffer in Bitmap then that frame is flipped on Y axis. To solve this I flipped current frame on Y axis by adding v.RotateFlip(RotateFlipType.RotateNoneFlipY); inside BufferCB after this change mouse pointer is not visible in desktop recording video.
How I can flip mouse pointer?
Update #1
I converted icon pointer to Icon then into Bitmap using Icon.ToBitmap() and then flipped on Y axis, here is code (Thanks to #Roman R.):
...
iconX = cursorInfo.ptScreenPos.x - ((int)iconInfo.xHotspot);
iconY = cursorInfo.ptScreenPos.y - ((int)iconInfo.yHotspot);
Icon ic = Icon.FromHandle(iconPointer);
Bitmap icon = ic.ToBitmap();
icon.RotateFlip(RotateFlipType.RotateNoneFlipY);
g.DrawImage(icon, iconX, iconY);
g.Dispose();
v.Dispose();
icon.Dispose();
ic.Dispose();
...
Only one issue I am facing in above modification sometimes I get ArgumentException at line Bitmap icon = ic.ToBitmap();
System.ArgumentException occurred
HResult=-2147024809
Message=Parameter is not valid.
Source=System.Drawing
StackTrace:
at System.Drawing.Bitmap.FromHicon(IntPtr hicon)
InnerException:
I disposed all the bitmaps used, still i get this exception.
The problem you have here is your BufferCB implementation. You create a temporary Graphics/Bitmap object, so that it could be updated with the cursor overlay. You have this bitmap upside down and normal draw of the icon applies it flipped.
You need to take into account that normal order of lines of 24-bit RGB buffer you get with BufferCB callback is bottom-to-top; order of lines Bitmap constructor expects is the opposite. You need to transfer lines into Bitmap in reversed order, then get them back respectively. I am not sure if negative stride works out there well, presumably it will not. Or simply overlay the cursor pre-flipped, to compensate for flipped background and overlay coordinates too.
You have another option of painting correct mouse pointer. Just take a mouse pointer's PNG image and in BufferCB:
public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
Graphics g;
Bitmap v;
v = new Bitmap(m_videoWidth, m_videoHeight, m_stride, System.Drawing.Imaging.PixelFormat.Format24bppRgb, pBuffer);
g = Graphics.FromImage(v);
CURSORINFO cursorInfo;
cursorInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO));
if (GetCursorInfo(out cursorInfo))
{
if (cursorInfo.flags == CURSOR_SHOWING)
{
var iconPointer = CopyIcon(cursorInfo.hCursor);
ICONINFO iconInfo;
int iconX, iconY;
if (GetIconInfo(iconPointer, out iconInfo))
{
iconX = cursorInfo.ptScreenPos.x - ((int)iconInfo.xHotspot);
iconY = cursorInfo.ptScreenPos.y - ((int)iconInfo.yHotspot);
//DRAW STATIC POINTER IMAGE
Bitmap pointerImage = new Bitmap('pointer.png');
g.DrawImage(pointerImage,iconX,iconY);
g.Dispose();
v.Dispose();
}
}
}
return 0;
}
I am trying to draw some text using TextRenderer (since this is favorable to using Graphics.DrawString) to a Bitmap, however it is having some very undesirable effects.
Example Code
using (Bitmap buffer = new Bitmap(this.ClientRectangle.Width, this.ClientRectangle.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
using (Graphics graphics = Graphics.FromImage(buffer))
{
// Produces the result below
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
// Produces clean text, but I'd really like ClearType!
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black);
}
e.Graphics.DrawImageUnscaled(buffer, this.ClientRectangle);
}
Result
I'm not sure exactly how to fix this...help!
I do NOT want to use Graphics.DrawString as I want correct GDI (as opposed to GDI+) rendering.
NOTE: I've just realized, I've left a gaping hole in this question. Some people have pointed out that rendering ClearType text is working fine on their machines...
I'm trying to render the text to a transparent (Color.Transparent) bitmap. If I do it with a solid color, everything works fine! (but it is imperative that I render to a transparent Bitmap).
Specify a BackColor in your call to DrawText():
TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black, this.BackColor);
You can try setting TextRenderingHint for your Image Graphics:
using (Graphics graphics = Graphics.FromImage(buffer))
{
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black);
}
The issue is that TextRenderer uses GDI rendering that uses ClearType to render text, clear-type uses special anti-aliasing algorithm to smooth the text, unfortunately it doesn't work when you try to draw on bitmap device.
To make it work you have to use a trick, draw into in-memory and then copy to bitmap:
Create in-memory bitmap buffer that is compatible with display device context (IntPtr.Zero handle)
Fill the buffer background with solid color or image
Render the text into the memory bitmap
Copy from in-memory bitmap to image device context (BitBlt)
See this blog for details: GDI text rendering to image.
Sample code, sorry its a bit long:
public static class Test
{
public static Image Render()
{
// create the final image to render into
var image = new Bitmap(190, 30, PixelFormat.Format32bppArgb);
// create memory buffer from desktop handle that supports alpha channel
IntPtr dib;
var memoryHdc = CreateMemoryHdc(IntPtr.Zero, image.Width, image.Height, out dib);
try
{
// create memory buffer graphics to use for HTML rendering
using (var memoryGraphics = Graphics.FromHdc(memoryHdc))
{
// must not be transparent background
memoryGraphics.Clear(Color.White);
// execute GDI text rendering
TextRenderer.DrawText(memoryGraphics, "Test string 1", new Font("Arial", 12), new Point(5, 5), Color.Red, Color.Wheat);
TextRenderer.DrawText(memoryGraphics, "Test string 2", new Font("Arial", 12), new Point(100, 5), Color.Red);
}
// copy from memory buffer to image
using (var imageGraphics = Graphics.FromImage(image))
{
var imgHdc = imageGraphics.GetHdc();
BitBlt(imgHdc, 0, 0, image.Width, image.Height, memoryHdc, 0, 0, 0x00CC0020);
imageGraphics.ReleaseHdc(imgHdc);
}
}
finally
{
// release memory buffer
DeleteObject(dib);
DeleteDC(memoryHdc);
}
return image;
}
private static IntPtr CreateMemoryHdc(IntPtr hdc, int width, int height, out IntPtr dib)
{
// Create a memory DC so we can work off-screen
IntPtr memoryHdc = CreateCompatibleDC(hdc);
SetBkMode(memoryHdc, 1);
// Create a device-independent bitmap and select it into our DC
var info = new BitMapInfo();
info.biSize = Marshal.SizeOf(info);
info.biWidth = width;
info.biHeight = -height;
info.biPlanes = 1;
info.biBitCount = 32;
info.biCompression = 0; // BI_RGB
IntPtr ppvBits;
dib = CreateDIBSection(hdc, ref info, 0, out ppvBits, IntPtr.Zero, 0);
SelectObject(memoryHdc, dib);
return memoryHdc;
}
[DllImport("gdi32.dll")]
public static extern int SetBkMode(IntPtr hdc, int mode);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BitMapInfo pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);
[DllImport("gdi32.dll")]
public static extern int SelectObject(IntPtr hdc, IntPtr hgdiObj);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
public static extern bool DeleteDC(IntPtr hdc);
[StructLayout(LayoutKind.Sequential)]
internal struct BitMapInfo
{
public int biSize;
public int biWidth;
public int biHeight;
public short biPlanes;
public short biBitCount;
public int biCompression;
public int biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public int biClrUsed;
public int biClrImportant;
public byte bmiColors_rgbBlue;
public byte bmiColors_rgbGreen;
public byte bmiColors_rgbRed;
public byte bmiColors_rgbReserved;
}
}
I am writing a program that displays a map, and on top of it another layer where position of cameras and their viewing direction is shown. The map itself can be zoomed and panned. The problem is that the map files are of significant size and zooming does not go smoothly.
I created class ZoomablePictureBox : PictureBox to add zooming and panning ability. I tried different methods, from this and other forums, for zooming and panning and ended up with the following, firing on the OnPaint event of ZoomablePictureBox:
private void DrawImgZoomed(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (imgZoomed != null)
e.Graphics.DrawImage(imgZoomed, new Rectangle(-ShiftX, -ShiftY, imgZoomed.Width, imgZoomed.Height), 0, 0, imgZoomed.Width, imgZoomed.Height, GraphicsUnit.Pixel);
}
Where ShiftX and ShiftY provide proper map panning (calculation irrelevant for this problem).
imgZoomed is zoomed version of original map calculated in BackgroundWorker everytime the zoom changes:
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
Bitmap workerImage = e.Argument as Bitmap;
Bitmap result;
result = new Bitmap(workerImage, new Size((int)(workerImage.Width * Zoom), (int)(workerImage.Height * Zoom)));
e.Result = result;
}
So current approach is, that everytime user scrolls mousewheel, the new imgZoomed is calculated based on current Zoom. With map size of ~30 MB this can take up to 0,5 second which is annoying, but panning runs smoothly.
I realize that this may not be the best idea. In previous approach i did not create zoomed image copy everytime mouse is scrolled but did this instead:
e.Graphics.DrawImage(Image, new Rectangle(-ShiftX, -ShiftY, (int)(this._image.Width * Zoom), (int)(this._image.Height * Zoom)), 0, 0, Image.Width, Image.Height, GraphicsUnit.Pixel);
Zoom was much smoother, because from what i understand, it just stretched original image. On the other hand panning was skipping heavily.
I wast thinking of:
creating copies of orignial map for each zoom in memory/on hard drive - it will take up too much memory/hdd space
creating copies of orignial map for next/actual/previous zooms so i
have more time to calculate next step - it will not help if user
scrolls more than one step at a time
I also tried matrix transformations - no real performance gain, and calculating pan was a real pain in the arse.
I'm running in circles here and don't know how to do it. If i open the map in default Windows picture viewer zooming and panning is smooth. How do they do that?
In what way do I achieve smooth zooming and panning at the same time?
Sorry this is so long, I left in only the essential parts.
Most of the p/invoke stuff is taken from pinvoke.net
My solution for smooth panning across large images involves using the StretchBlt gdi method instead of Graphics.DrawImage. I've created a static class that groups all the native blitting operations.
Another thing that helps greatly is caching the HBitmap and Graphics objects of the Bitmap.
public class ZoomPanWindow
{
private Bitmap map;
private Graphics bmpGfx;
private IntPtr hBitmap;
public Bitmap Map
{
get { return map; }
set
{
if (map != value)
{
map = value;
//dispose/delete any previous caches
if (bmpGfx != null) bmpGfx.Dispose();
if (hBitmap != null) StretchBltHelper.DeleteObject(hBitmap);
if (value == null) return;
//cache the new HBitmap and Graphics.
bmpGfx = Graphics.FromImage(map);
hBitmap = map.GetHbitmap();
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
if (map == null) return;
//finally, the actual painting!
Rectangle mapRect = //whatever zoom/pan logic you implemented.
Rectangle thisRect = new Rectangle(0, 0, this.Width, this.Height);
StretchBltHelper.DrawStretch(
hBitmap,
bmpGfx,
e.Graphics,
mapRect,
thisRect);
}
}
public static class StretchBltHelper
{
public static void DrawStretch(IntPtr hBitmap, Graphics srcGfx, Graphics destGfx,
Rectangle srcRect, Rectangle destRect)
{
IntPtr pTarget = destGfx.GetHdc();
IntPtr pSource = CreateCompatibleDC(pTarget);
IntPtr pOrig = SelectObject(pSource, hBitmap);
if (!StretchBlt(pTarget, destRect.X, destRect.Y, destRect.Width, destRect.Height,
pSource, srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
TernaryRasterOperations.SRCCOPY))
throw new Win32Exception(Marshal.GetLastWin32Error());
IntPtr pNew = SelectObject(pSource, pOrig);
DeleteDC(pSource);
destGfx.ReleaseHdc(pTarget);
}
[DllImport("gdi32.dll", EntryPoint = "SelectObject")]
public static extern System.IntPtr SelectObject(
[In()] System.IntPtr hdc,
[In()] System.IntPtr h);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(
[In()] System.IntPtr ho);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest,
int nWidthDest, int nHeightDest,
IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc,
TernaryRasterOperations dwRop);
public enum TernaryRasterOperations : uint
{
SRCCOPY = 0x00CC0020
//there are many others but we don't need them for this purpose, omitted for brevity
}
}
I have one Windows Form with
this.BackColor = Color.Red
and
this.TransparencyKey = Color.Red
and there is one PictureBox (png image with transparency corners) on this form,
PictureBox.SizeMode = Normal.
Then I set SizeMode of PictureBox to StretchImage and get other result:you can see it here
(sorry, I can put one hyperlink only)
You can see red points/dots, but it is not Color.Red because it is transparency key of form.
I tried to implement transparent form, transparent control to remove these "red" points.
Anyways, I would like to ask about my last point - I tried to override "OnPaintBackground" method and when I implemented some like code below:
e.Graphics.FillRectangle(Brushes.Red, ClientRectangle);
TextureBrush brush = ImageHelper.ScaleImage(BackgroundImage, ClientRectangle.Width, ClientRectangle.Height);
e.Graphics.FillRectangle(brush, ClientRectangle);
I saved scaled BitMap to file before putting it to TextureBrush - this png scaled image doesn't contain "red" points, but they were painted on form.
Does somebody have any idea why it happens and tell me some way to solve it.
Best regards.
This happens because GDI+, which is drawing the image, doesn't know that red is becoming transparent.
Therefore, it blends the borders of the image with the red background, creating reddish (but not completely red) pixels which do not become transparent.
To solve this, you'll need to make a layered window.
EDIT:
Use the following native methods:
static class NativeMethods {
public const int LayeredWindow = 0x80000;//WS_EX_LAYERED
#region Drawing
[DllImport("User32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UpdateLayeredWindow(IntPtr handle, IntPtr screenDc, ref Point windowLocation, ref Size windowSize, IntPtr imageDc, ref Point dcLocation, int colorKey, ref BlendFunction blendInfo, UlwType type);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("User32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("User32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(IntPtr hObject);
#endregion
}
struct BlendFunction {
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
}
enum UlwType : int {
None = 0,
ColorKey = 0x00000001,
Alpha = 0x00000002,
Opaque = 0x00000004
}
Override the form's CreateParams:
protected override CreateParams CreateParams {
get {
CreateParams createParams = base.CreateParams;
createParams.ExStyle |= NativeMethods.LayeredWindow;
return createParams;
}
}
Call the following function in OnShown:
static Point Zero;
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults")]
void UpdateWindow() {
IntPtr screenDC = NativeMethods.GetDC(IntPtr.Zero);
IntPtr imageDC = NativeMethods.CreateCompatibleDC(screenDC);
IntPtr gdiBitmap = IntPtr.Zero;
IntPtr oldBitmap = IntPtr.Zero;
try {
gdiBitmap = image.GetHbitmap(Color.FromArgb(0)); //Get a GDI handle to the image.
oldBitmap = NativeMethods.SelectObject(imageDC, gdiBitmap); //Select the image into the DC, and cache the old bitmap.
Size size = image.Size; //Get the size and location of the form, as integers.
Point location = this.Location;
BlendFunction alphaInfo = new BlendFunction { SourceConstantAlpha = 255, AlphaFormat = 1 }; //This struct provides information about the opacity of the form.
NativeMethods.UpdateLayeredWindow(Handle, screenDC, ref location, ref size, imageDC, ref Zero, 0, ref alphaInfo, UlwType.Alpha);
} finally {
NativeMethods.ReleaseDC(IntPtr.Zero, screenDC); //Release the Screen's DC.
if (gdiBitmap != IntPtr.Zero) { //If we got a GDI bitmap,
NativeMethods.SelectObject(imageDC, oldBitmap); //Select the old bitmap into the DC
NativeMethods.DeleteObject(gdiBitmap); //Delete the GDI bitmap,
}
NativeMethods.DeleteDC(imageDC); //And delete the DC.
}
Invalidate();
}