1

In a winforms application is there any way to draw directly to the pixel buffer/byte array for the window?

I have a bytearray with an image in the format byte[] myimg = new byte[width x height x 4] for an ARGB bitmap, now i want to display it in the form, the only way i know of is first to make a bitmap, then use lockbits to write my pixels into the bitmap, then i set a picturebox.image to my bitmap instance. But i want to skip this step and write directly to the form, if possible without even a picturebox, is this possible?

Update

To clarify, i specifically want to avoid the overhead and slowness of windows handling scaling/stretching. I just want the most efficient way of getting my own pixels onto the form's canvas area, my own byte array being prepared and scaled accordingly in advance by myself.

Update 2

Turns out the lower windows api's does provide a way using bitblt , as jtxkopt has shown, this killed my overhead almost completely

Lasse
  • 83
  • 1
  • 5
  • 1
    [Bitmaps, Device Contexts, and Drawing Surfaces](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps--device-contexts--and-drawing-surfaces) -> [Memory Device Contexts](https://learn.microsoft.com/en-us/windows/win32/gdi/memory-device-contexts) <- won't solve any *speed* issues. You can simply assign a Bitmap to a double-buffered Control, then fill that Bitmap with different data and `Invalidate()` the Control (~as creating a compatible Device Context and *selecting* a Bitmap object into that DC) – Jimi Sep 22 '22 at 01:17
  • You can create `MemoryStream` from `myimg` byte array and pass the stream to a new `Bitmap` ctor to create it. See the Bitmap class ctor [Overloads](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.bitmap.-ctor?view=dotnet-plat-ext-6.0). Then you can set it to the Form's `.BackgroundImage` or override the Form's `OnPaint` (or handle its `Paint` event) to draw it and draw anything else you want over it. Keep it simple. – dr.null Sep 22 '22 at 01:17

1 Answers1

1

You cannot draw a raw image data directly onto the Form. There is no function like DrawImageData(byte[] data, int width, int height, PixelFormat format);. There are two option I am aware of. You can try one of them that suits to your needs most.

Option 1

You can create a Bitmap class from an image data using the constructor, fill the data as you want and draw the bitmap onto the screen. However, as others stated, doing so you don't gain too much performance increase as long as you stay in managed code.

For a simple implementation, you can see the example code.

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace CustomRendering
{
    public unsafe partial class MainForm : Form
    {
        public MainForm()
        {
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
            InitializeComponent();
        }

        private Bitmap m_surfaceBitmap;
        private byte* m_surfaceData;
        private int m_stride;
        private int m_width, m_height;
        protected override void OnPaint(PaintEventArgs e)
        {
            Graphics g = e.Graphics; 
            g.SmoothingMode = SmoothingMode.HighSpeed;
            g.InterpolationMode = InterpolationMode.NearestNeighbor;
            g.PixelOffsetMode = PixelOffsetMode.Half;
            g.DrawImage(m_surfaceBitmap, Point.Empty);
        }
        protected override void OnHandleCreated(EventArgs e)
        {
            this.FormBorderStyle = FormBorderStyle.FixedSingle;
            m_width = ClientSize.Width;
            m_height = ClientSize.Height;
            m_stride = (32 * m_width + 31) / 32 * 4; // Calculate the stride.
            m_surfaceData = (byte*)Marshal.AllocHGlobal(m_stride * m_height);
            m_surfaceBitmap = new Bitmap(m_width, m_height, m_stride, PixelFormat.Format32bppArgb, (IntPtr)m_surfaceData);
        }

        protected unsafe override void OnMouseMove(MouseEventArgs e)
        {
            Clear(Color.White);
            FillRectangle(e.X, e.Y, 100, 100, Color.Black);
            Invalidate();
            base.OnMouseMove(e);
        }
        private void Clear(Color color)
        {
            int argb = color.ToArgb();
            for (int i = 0; i < m_stride * m_height; i += 4) 
                *(int*)(m_surfaceData + i) = argb; 
        }
        private void FillRectangle(int x0, int y0, int width, int height, Color color)
        {
            int argb = color.ToArgb();
            for (int y = y0; y < y0 + height; y++) 
                for (int x = x0; x < x0 + width; x++) 
                    SetPixel(x, y, argb); 
        }

        private void SetPixel(int x, int y, int argb)
        {
            if (x >= m_width || x < 0 || y >= m_height || y < 0)
                return;
            m_surfaceData[y * m_stride + 4 * x + 0] = (byte)((argb >> 0) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 1] = (byte)((argb >> 8) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 2] = (byte)((argb >> 16) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 3] = (byte)((argb >> 24) & 0x000000FF);
        }
    }

}
Option 2

This is a bit lower-level solution that use Win32 API but I think this one is faster than previous one since it handles the WM_PAINT message itself and uses the bitblt function to display a DIB(Device Independent Bitmap) instead of drawing a Gdiplus Bitmap. I don't explain how a DIB can be created and other Win32 related code works. The example code is below. Compare them and choose one of them.

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace CustomRendering
{ 
    public unsafe partial class MainForm : Form
    {
        public MainForm()
        {
             InitializeComponent();
        }
         
        private byte* m_surfaceData;
        private int m_stride;
        private int m_width, m_height;
        private IntPtr m_hdcWindow;
        private IntPtr m_surfaceDC;
        private IntPtr m_surfaceBitmap;
        private IntPtr m_oldObject;
        private Point m_mouseLocation;
         
        protected override void WndProc(ref Message m)
        {
            // Process WM_PAINT ourself.
            if(m.Msg == 0x000F)
            {
                Clear(Color.White);
                FillRectangle(m_mouseLocation.X, m_mouseLocation.Y, 100, 100, Color.Black);
                PAINTSTRUCT ps;
                IntPtr hdc = BeginPaint(Handle, out ps);
                
                BitBlt(m_hdcWindow, 0, 0, m_width, m_height, m_surfaceDC, 0, 0, RasterOperations.SRCCOPY);
                EndPaint(Handle, ref ps);base.WndProc(ref m);
            }
            // Process WM_ERASEBKGND to prevent flickering.
            else if(m.Msg == 0x0014) 
                m.Result = (IntPtr)1; 
            else
                base.WndProc(ref m);
        }
        protected override void OnHandleCreated(EventArgs e)
        { 
            m_width = Screen.PrimaryScreen.WorkingArea.Width;
            m_height = Screen.PrimaryScreen.WorkingArea.Height;
            m_stride = (32 * m_width + 31) / 32 * 4; // Calculate the stride.
            CreateSurface(m_width, m_height);
         }

        protected unsafe override void OnMouseMove(MouseEventArgs e)
        {
            m_mouseLocation = e.Location;
            Invalidate(ClientRectangle); // Invalidate the only visible area.
        }
        private void CreateSurface(int width, int height)
        {
            BITMAPINFO bi = new BITMAPINFO();

            m_hdcWindow = GetDC(Handle);
            m_surfaceDC = CreateCompatibleDC(m_hdcWindow);

            bi.bmiHeader.biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>();
            bi.bmiHeader.biWidth = width;
            bi.bmiHeader.biHeight = -height;
            bi.bmiHeader.biPlanes = 1;
            bi.bmiHeader.biBitCount = 32;
            bi.bmiHeader.biCompression = BitmapCompressionMode.BI_RGB; // No compression
            bi.bmiHeader.biSizeImage = (uint)(width * height * 4);
            bi.bmiHeader.biXPelsPerMeter = 0;
            bi.bmiHeader.biYPelsPerMeter = 0;
            bi.bmiHeader.biClrUsed = 0;
            bi.bmiHeader.biClrImportant = 0; 

            IntPtr ppvBits;
            m_surfaceBitmap = CreateDIBSection(m_surfaceDC, ref bi, DIB_RGB_COLORS, out ppvBits, IntPtr.Zero, 0);

            m_surfaceData = (byte*)ppvBits.ToPointer();

            m_oldObject = SelectObject(m_surfaceDC, m_surfaceBitmap);
        }
        private void Clear(Color color)
        {
            int argb = color.ToArgb();
            for (int i = 0; i < m_stride * m_height; i += 4)
                *(int*)(m_surfaceData + i) = argb;
        }
        private void FillRectangle(int x0, int y0, int width, int height, Color color)
        {
            int argb = color.ToArgb();
            for (int y = y0; y < y0 + height; y++)
                for (int x = x0; x < x0 + width; x++)
                    SetPixel(x, y, argb);
        }

        private void SetPixel(int x, int y, int argb)
        { 
            m_surfaceData[y * m_stride + 4 * x + 0] = (byte)((argb >> 0) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 1] = (byte)((argb >> 8) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 2] = (byte)((argb >> 16) & 0x000000FF);
            m_surfaceData[y * m_stride + 4 * x + 3] = (byte)((argb >> 24) & 0x000000FF);
        }
        private enum RasterOperations : uint
        {
            SRCCOPY = 0x00CC0020,
            SRCPAINT = 0x00EE0086,
            SRCAND = 0x008800C6,
            SRCINVERT = 0x00660046,
            SRCERASE = 0x00440328,
            NOTSRCCOPY = 0x00330008,
            NOTSRCERASE = 0x001100A6,
            MERGECOPY = 0x00C000CA,
            MERGEPAINT = 0x00BB0226,
            PATCOPY = 0x00F00021,
            PATPAINT = 0x00FB0A09,
            PATINVERT = 0x005A0049,
            DSTINVERT = 0x00550009,
            BLACKNESS = 0x00000042,
            WHITENESS = 0x00FF0062,
            CAPTUREBLT = 0x40000000
        }
        private enum BitmapCompressionMode : uint
        {
            BI_RGB = 0,
            BI_RLE8 = 1,
            BI_RLE4 = 2,
            BI_BITFIELDS = 3,
            BI_JPEG = 4,
            BI_PNG = 5
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct BITMAPINFOHEADER
        {
            public uint biSize;
            public int biWidth;
            public int biHeight;
            public ushort biPlanes;
            public ushort biBitCount;
            public BitmapCompressionMode biCompression;
            public uint biSizeImage;
            public int biXPelsPerMeter;
            public int biYPelsPerMeter;
            public uint biClrUsed;
            public uint biClrImportant; 
        }
        [StructLayoutAttribute(LayoutKind.Sequential)]
        private struct BITMAPINFO
        { 
            public BITMAPINFOHEADER bmiHeader; 
            [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.Struct)]
            public int[] bmiColors;
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int Left, Top, Right, Bottom; 
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct PAINTSTRUCT
        {
            public IntPtr hdc;
            public bool fErase;
            public RECT rcPaint;
            public bool fRestore;
            public bool fIncUpdate;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public byte[] rgbReserved;
        }
        private const int DIB_RGB_COLORS = 0;
        private const int DIB_PAL_COLORS = 1;

        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr hwnd);

        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO pbmi, uint usage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

        [DllImport("gdi32.dll")]
        private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, RasterOperations dwRop);

        [DllImport("gdi32.dll")]
        private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hNewObj);

        [DllImport("user32.dll")]
        static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint);
         
        [DllImport("user32.dll")]
        static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint);
    }

}
jtxkopt
  • 916
  • 1
  • 8
  • 21
  • When i run this code with 1500*840 resolution and move the mouse frantically across the window i get like 10% cpu usage on my machine, then i comment out the g.drawImage in the unpaint event and now cpu usage is around 4-5%, i've also tried setting smoothing and interpolationmode to lowest, changes nothing. In other words windows is using ALOT of cpu just putting the image onto the display. I can even optimize the clear function using marshal.copy and loop unrolling, not to mention FillRectangle which could be optimized alot with unsafe code. – Lasse Sep 27 '22 at 05:51
  • I didn't write the functions `Clear` and `FillRectangle` for you to use them in a production quality application (They are demo :) ). I didn't even optimized anything. There was only one purpose: it is to show you how to display a pixel data onto the Form. Optimizing a rasterizer wasn't the topic of your question. Finally, as I already mentioned it, *you don't gain too much performance increase as long as you stay in managed code*. – jtxkopt Sep 27 '22 at 07:36
  • We talk past eachother. What i wanted to say was that using g.drawimage is slow and uses alot of cpu, is there no faster way? – Lasse Sep 27 '22 at 10:01
  • Yeah, there is one more option that uses `Win32` API doesn't involve Gdiplus API. I will update my answer. – jtxkopt Sep 27 '22 at 10:24
  • Thank you very much, the overhead is almost completely gone – Lasse Sep 27 '22 at 15:26