Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Foreach loop to create 100 buttons, painting all buttons at same time as to prevent flicker

In my minesweeper game I need to dynamically create controls in order to shift between easy - medium - hard. Let's say for the sake of the question hard consists of 100 buttons.

This is how I'm creating them:

this.SuspendLayout(); //Creating so many things that I assume it would be faster to suspend and resume.
foreach (string i in playingField)
{

    Button button = new Button();
    button.Name = i;
    button.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
    button.Margin = new Padding(0);
    button.TabIndex = 0;
    button.Location = new System.Drawing.Point(3, 3);
    button.Size = new System.Drawing.Size(25, 25);
    button.BackgroundImage = blockImage; //##//
    button.MouseDown += new System.Windows.Forms.MouseEventHandler(this.GetUnderSide);
    button.UseVisualStyleBackColor = false;
    minesPanel.Controls.Add(button); //Adding the controls to the container
}
this.ResumeLayout(); //Refer to above. Correct me if I'm wrong please.

As you can see I'm creating all of the controls through a for loop, and then adding them. It results in each button being painted once at a time. I also tried name.Controls.AddRange(arrayButtons) and it still resulted in the same problem. Individual painting.

What I need is a way to create all items, and then paint them ALL after, like painting a single bitmap with DoubleBuffered, but alas, that doesn't work either.

Furthermore, I have tried this:

    protected override CreateParams CreateParams
    {
        get
        {
            // Activate double buffering at the form level.  All child controls will be double buffered as well.
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x02000000;   // WS_EX_COMPOSITED
            return cp;
        }
    }

Which kind of works. It only works on application startup. However, given that I will be changing grid sizes and adding more controls at runtime, it is not a viable option.

I've been told it's as easy as using the methods in the Graphics class. Problem is, I don't know where to start.

If anyone could provide some help on getting all my controls to be painted at the same time, that would be great.

like image 915
Anteara Avatar asked Dec 27 '22 15:12

Anteara


1 Answers

Sadly, WinForms does not like to have too many controls, especially when you get into the hundreds. You can never have each control paint at the same time since each control will send its own paint message to windows.

I think the best way to approach a game board like MineSweeper is to just use one control and draw the grid of buttons.

Simple Mine class:

public class Mine {
  public Rectangle Bounds { get; set; }
  public bool IsBomb { get; set; }
  public bool IsRevealed { get; set; }
}

Then create a new control where you inherit from the Panel control and set the DoubleBuffered property to prevent any flicker:

public class MineSweeperControl : Panel {
  private int columns = 16;
  private int rows = 12;
  private Mine[,] mines;

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

    // initialize mine field:
    mines = new Mine[columns, rows];
    for (int y = 0; y < rows; ++y) {
      for (int x = 0; x < columns; ++x) {
        mines[x, y] = new Mine();
      }
    }
  }

  // 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);
        mines[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 (mines[x, y].IsRevealed) {
          e.Graphics.FillRectangle(Brushes.DarkGray, mines[x, y].Bounds);
        } else {
          ControlPaint.DrawButton(e.Graphics, mines[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 (mines[x, y].Bounds.Contains(e.Location)) {
          mines[x, y].IsRevealed = true;
          this.Invalidate();
          MessageBox.Show(
            string.Format("You pressed on button ({0}, {1})",
            x.ToString(), y.ToString())
          );
        }
      }
    }
    base.OnMouseDown(e);
  }
}

enter image description here

like image 178
LarsTech Avatar answered Jan 05 '23 00:01

LarsTech