Search code examples
c#.netgraphicsdrawingcontrols

C# Create Grid For Painting Pixels and Rendering Text


I'm trying to find the best path to go about creating a pixel "grid" that would allow basic paint functions such as coloring the pixels by clicking and moving the mouse, selecting an area for copying, pasting, or moving, or using other graphics functions in order to render text or shapes to the pixels. I've looked at a few samples, such as this example which overrides the panel control and has a similar look to what I'm trying to achieve, but the painting is slow and it seems like it wouldn't perform well for drawing with the mouse. Is there a control, or one that I could override, that would allow for the functionality that I'm looking for?

Here's a sample of what the above example looks like: Sample pixel grid

And the code I adapted from the above example:

public class Pixel
{
    public Rectangle Bounds { get; set; }
    public bool IsOn { get; set; }
    public bool IsSelected { get; set; }
}


public class PixelGridControl : Panel
{
    public int Columns = 99;
    public int Rows = 63;
    private readonly Pixel[,] pixels;

    public PixelGridControl()
    {
        this.DoubleBuffered = true;
        this.ResizeRedraw = true;

        // initialize pixel grid:
        pixels = new Pixel[Columns, Rows];
        for (int y = 0; y < Rows; ++y)
        {
            for (int x = 0; x < Columns; ++x)
            {
                pixels[x, y] = new Pixel();
            }
        }
    }

    // adjust each column and row to fit entire client area:
    protected override void OnResize(EventArgs e)
    {
        int top = 0;
        for (int y = 0; y < Rows; ++y)
        {
            int left = 0;
            int height = (this.ClientSize.Height - top) / (Rows - y);
            for (int x = 0; x < Columns; ++x)
            {
                int width = (this.ClientSize.Width - left) / (Columns - x);
                pixels[x, y].Bounds = new Rectangle(left, top, width, height);
                left += width;
            }
            top += height;
        }
        base.OnResize(e);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        for (int y = 0; y < Rows; ++y)
        {
            for (int x = 0; x < Columns; ++x)
            {
                if (pixels[x, y].IsOn)
                {
                    e.Graphics.FillRectangle(Brushes.Gold, pixels[x, y].Bounds);
                }
                else
                {
                    ControlPaint.DrawButton(e.Graphics, pixels[x, y].Bounds,
                                            ButtonState.Normal);
                }
            }
        }
        base.OnPaint(e);
    }

    // determine which button the user pressed:
    protected override void OnMouseDown(MouseEventArgs e)
    {
        for (int y = 0; y < Rows; ++y)
        {
            for (int x = 0; x < Columns; ++x)
            {
                if (pixels[x, y].Bounds.Contains(e.Location))
                {
                    pixels[x, y].IsOn = true;
                    this.Invalidate();
                    MessageBox.Show(
                      string.Format("You pressed on button ({0}, {1})",
                      x.ToString(), y.ToString())
                    );
                }
            }
        }
        base.OnMouseDown(e);
    }
}

Solution

  • Here is an minimal example that uses a bitmap as the pixel storage and displays the enlarged pixels in a panel for editing.

    You can use it by setting it up to edit the pixels in an Picturebox pBox_Target.Image, maybe like this:

    public Form1()
    {
        InitializeComponent();
        ..
        ..
        pixelEditor1.APBox = pBox_Target;
        pixelEditor1.TgtBitmap = (Bitmap)pixelEditor1.APBox.Image;
        ..
    
    }
    

    The editor exposes the pixel size and the color to draw with among a minimum of properties..

    all in all it is slightly over 100 lines - of course it is meant to be expanded!

    Here it is at work: enter image description here

    class PixelEditor : Panel
    {
        public Color DrawColor { get; set; }
        public Color GridColor { get; set; }
        int pixelSize = 8;
        public int PixelSize
        {
            get { return pixelSize; }
            set
            {
                pixelSize = value;
                Invalidate();
            }
        }
    
    
        public Bitmap TgtBitmap { get; set; }
        public Point TgtMousePos { get; set; }
    
        Point lastPoint = Point.Empty;
    
        PictureBox aPBox = null;
        public PictureBox APBox {
            get { return aPBox; }
            set
            {
                if (value == null) return;
                aPBox = value;
                aPBox.MouseClick -=APBox_MouseClick;
                aPBox.MouseClick +=APBox_MouseClick;
            }
        }
    
        private void APBox_MouseClick(object sender, MouseEventArgs e)
        {
            TgtMousePos = e.Location;
            Invalidate();
        }
    
    
        public PixelEditor()
        {
            DoubleBuffered = true;
            BackColor = Color.White;
            GridColor = Color.DimGray;
            DrawColor = Color.Red;
            PixelSize = 10;
            TgtMousePos = Point.Empty;
    
            if (APBox != null && APBox.Image != null)
                TgtBitmap = (Bitmap)APBox.Image;
    
            MouseClick +=PixelEditor_MouseClick;
            MouseMove +=PixelEditor_MouseMove;
            Paint +=PixelEditor_Paint;
        }
    
        private void PixelEditor_Paint(object sender, PaintEventArgs e)
        {
            if (DesignMode) return;
    
            Graphics g = e.Graphics;
    
            int cols = ClientSize.Width / PixelSize;
            int rows = ClientSize.Height / PixelSize;
    
            if (TgtMousePos.X < 0 || TgtMousePos.Y < 0) return;
    
            for (int x = 0; x < cols; x++)
                for (int y = 0; y < rows; y++)
                {
                    int sx = TgtMousePos.X + x;
                    int sy = TgtMousePos.Y + y;
    
                    if (sx > TgtBitmap.Width  || sy > TgtBitmap.Height) continue;
    
                    Color col = TgtBitmap.GetPixel(sx, sy);
    
                    using (SolidBrush b = new SolidBrush(col))
                    using (Pen p = new Pen(GridColor))
                    {
                        Rectangle rect = new Rectangle(x * PixelSize, y * PixelSize, 
                                                           PixelSize, PixelSize);
                        g.FillRectangle(b, rect);
                        g.DrawRectangle(p, rect);
                    }
                }
        }
    
        private void PixelEditor_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) return;
    
            int x = TgtMousePos.X + e.X / PixelSize;
            int y = TgtMousePos.Y + e.Y / PixelSize;
    
            if (new Point(x, y) == lastPoint) return;
    
            Bitmap bmp = (Bitmap)APBox.Image;
            bmp.SetPixel(x,y, DrawColor);
            APBox.Image = bmp;
            Invalidate();
            lastPoint = new Point(x, y);
        }
    
        private void PixelEditor_MouseClick(object sender, MouseEventArgs e)
        {
            int x = TgtMousePos.X + e.X / PixelSize;
            int y = TgtMousePos.Y + e.Y / PixelSize;
            Bitmap bmp = (Bitmap)APBox.Image;
            bmp.SetPixel(x,y, DrawColor);
            APBox.Image = bmp;
            Invalidate();
        }
    
    
    }
    

    Note that this solution neither uses a special pixel class nor makes any provisions for selecting sets of pixels. To implement most selection tools you can use a GraphicsPath; you can the e.g. fill the path or loop over its bounds using IsVisible to enumerate the pixels in the path..


    Update: Instead of a Panel, which is a Container control and not really meant to draw onto you can use a Picturebox or a Label (with Autosize=false); both have the DoubleBuffered property turned on out of the box and support drawing better than Panels do.