Dynamically created PictureBox rendering problem - c#

The end goal is a somewhat playable memory game. Currently, I'm stuck on a rendering problem. I have the following classes:
Field, which is an abstract UserControl:
public abstract class Field : UserControl
{
protected PictureBox _pictureBox;
public Field()
{
_pictureBox = new PictureBox();
_pictureBox.Image = Properties.Resources.empty;
_pictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
_pictureBox.BorderStyle = BorderStyle.FixedSingle;
this.Controls.Add(_pictureBox);
}
// ...
// some abstract methods, not currently important
}
MemoryField, which derives from Field:
public class MemoryField : Field
{
public MemoryField(Form parent, int xPos, int yPos, int xSize, int ySize)
{
_pictureBox.ClientSize = new Size(xSize, ySize);
_pictureBox.Location = new Point(xPos, yPos);
_pictureBox.Parent = parent;
}
// ...
}
And finally, MainForm which is an entry point for my application:
public partial class MainForm : Form
{
private readonly int fieldWidth = 100; // 150 no rendering problems at all
private readonly int fieldHeight = 100;
public MainForm() { InitializeComponent(); }
private void MainForm_Load(object sender, EventArgs e)
{
for (int y = 0; y < 6; y++) // 6 rows
{
for (int x = 0; x < 10; x++) // 10 columns
{
Field field = new MemoryField(this,
x * (fieldWidth + 3), // xPos, 3 is for a small space between fields
labelTimer.Location.Y + labelTimer.Height + y * (fieldHeight + 3), // yPos
fieldWidth,
fieldHeight);
this.Controls.Add(field);
}
}
}
}
Here's where my problem lies:
In those for loops I'm trying to generate a 6x10 grid of Fields (with each containing a PictureBox 100x100 px in size). I do that almost successfully, as my second field is not rendered correctly:
Only thing I found that works (fixes the problem completely) is making field bigger (i.e. 150px). On the other hand, making it smaller (i.e. 50px) makes the problem even bigger:
Maybe useful information and things I've tried:
My MainForm is AutoSize = true; with AutoSizeMode = GrowAndShrink;
My MainForm (initially) doesn't contain any components except menuStrip and label
I tried changing PictureBox.Image property, that didn't work.
I tried creating the grid with just PictureBox controls (not using Field as a PictureBox wrapper), that did work.
I tried placing labelTimer in that "problematic area" which does fix the problem depending on where exactly I put it. (because field positioning depends on labelTimer 's position and height)
I tried relaunching visual studio 2017, didn't work.
Of course, I could just change the size to 150px and move on, but I'm really curious to see what's the root of this problem. Thanks!

The easiest thing to do to fix the problem is something you've already tried - using directly a PictureBox instead of a Field. Now, considering that you only use Field to wrap a PictureBox, you could inherit from PictureBox instead of just wrapping it.
Changing your classes to these will fix the issue as you've noticed:
public abstract class Field : PictureBox {
public Field() {
Image = Image.FromFile(#"Bomb01.jpg");
SizeMode = PictureBoxSizeMode.StretchImage;
BorderStyle = BorderStyle.FixedSingle;
Size = new Size(100, 100);
}
// ...
// some abstract methods, not currently important
}
public class MemoryField : Field {
public MemoryField(Form parent, int xPos, int yPos, int xSize, int ySize) {
ClientSize = new Size(xSize, ySize);
Location = new Point(xPos, yPos);
}
// ...
}
The real reason it was not working has to do with both sizing and positioning of each Field and their subcomponents. You should not set the Location of each _pictureBox relatively to its parent MemoryField, but rather change the Location of the MemoryField relatively to its parent Form.
You should also set the size of your MemoryField to the size of its child _pictureBox otherwise it won't size correctly to fit its content.
public class MemoryField : Field {
public MemoryField(Form parent, int xSize, int ySize) {
_pictureBox.ClientSize = new Size(xSize, ySize);
// I removed the setting of Location for the _pictureBox.
this.Size = _pictureBox.ClientSize; // size container to its wrapped PictureBox
this.Parent = parent; // not needed
}
// ...
}
and change your creation inner loop to
for (int x = 0; x < 10; x++) // 10 columns
{
Field field = new MemoryField(this,
fieldWidth,
fieldHeight);
field.Location = new Point(x * (fieldWidth + 3), 0 + 0 + y * (fieldHeight + 3)); // Set the Location here instead!
this.Controls.Add(field);
}

Related

Cannot modify the return value of a collection of rectangles

Here's what I used to solve this problem:
public class Snake {
List<Rectangle> bodyParts = new List<Rectangle>();
}
Snake snk = new Snake();
snk.bodyParts.Add(new Rectangle(760,25,8,8))
//the Exception Occurs here
//snk.bodyParts[0].Y = snk.bodyPArts[0].Y-10;
//Here's my approach
snk.bodyParts[0] = new Rectangle(bodyParts[0].X,bodyParts[0].Y-10,8,8);
Exception: Cannot modify the return value of 'System.Collections.Generic.List.this[int]'
My question is: Are they any alternative/better ways to manage this exception ?
Thanks.
EDIT: I got my answer, can you please close this question.
Rather than working directly with Rectangles, how about adding a BodyPart class, with some manipulation methods:
public class BodyPart
{
private Rectangle rectangle;
public BodyPart(Rectangle rectangle)
{
this.rectangle = rectangle;
}
public void MoveY(int value)
{
rectangle.Y += value;
}
public void MoveX(int value)
{
rectangle.X += value;
}
}
public class Snake
{
public List<BodyPart> Parts = new List<BodyPart>();
}
public class AppSnake
{
public void Run()
{
var snake = new Snake();
snake.Parts.Add(new BodyPart(new Rectangle(760, 25, 8, 8)));
snake.Parts[0].MoveY(-10);
}
}
Rectangle, RectangleF, Point, PointF, Size and SizeF are value types (struct type). This means that you can not/should not change an individual part of the structure.
The reason is that unlike classes each variable keeps its own copy of the structure. When you type list[0].X = 10, the indexer list[0] returns a copy of the rectangle and not the original value. The correct way is to assign a new rectangle list[0] = A which copies all the values from A into the array.
Please read more on value types and structs before attempting to write code that uses them.
The quickest way to fix your code without completely changing it around is by adding methods that manipulate all the body parts in predefined ways:
public class Snake
{
public List<Rectangle> Parts { get; } = new List<Rectangle>();
public void MoveX(int delta)
{
for(int i = 0; i < Parts.Count; i++)
{
// Read the current rectangle from the list
var rect = Parts[i];
// Change the coordinate.
rect.X += delta;
// Write the modified rectangle back into the list
Parts[i] = rect;
}
}
public void MoveY(int delta)
{
for(int i = 0; i < Parts.Count; i++)
{
// Read the current rectangle from the list
var rect = Parts[i];
// Change the coordinate.
rect.Y += delta;
// Write the modified rectangle back into the list
Parts[i] = rect;
}
}
}
Use Rectangle.Location and Rectanagle.Size properties e.g.:
snk.bodyParts[0].Location = new Point(newX, newY);
snk.bodyParts[0].Size = new Size(newWidth, newHeight);

Select drawn figure within panel box

I am working on an 'use-case-diagram-form' where an user can select an element and a modus
Just a simple form. It al works fine, made a class for each actor element and each use-case element. Both are added in a list after beeing created.
But somehow I just can't figure out how to select a created element and after do something with it.
classes i made:
class Actor
{
private static int _id;
private Panel _panel;
public string Name { get; private set; }
public int X { get; private set; }
public int Y { get; private set; }
public Actor(Panel panel, string name, int x, int y)
{
_id++;
_panel = panel;
Name = name;
X = x;
Y = y;
}
public void DrawActor()
{
// draw Actor
var graphics = _panel.CreateGraphics();
var pen = new Pen(Color.Black, 2);
graphics.DrawEllipse(pen, X - 10, Y - 30, 20, 20);
graphics.DrawLine(pen, X, Y - 10, X, Y + 20);
graphics.DrawLine(pen, X - 15, Y, X + 15, Y);
graphics.DrawLine(pen, X, Y + 20, X - 15, Y + 35);
graphics.DrawLine(pen, X, Y + 20, X + 15, Y + 35);
// rectangle around actor
graphics.DrawRectangle(pen, (X - 20), (Y - 30), 40, 80);
// setup font
var stringFont = new Font("Arial", 10);
// measure string
var textWith = graphics.MeasureString(Name, stringFont).Width;
// label
var label = new Label();
var actorText = (_id < 10 ? "0" : "") + _id.ToString() + "-" + Name;
label.Text = actorText;
label.Location = new Point(X - (Convert.ToInt32(textWith)/2), Y + 40);
label.AutoSize = true;
label.BorderStyle = BorderStyle.FixedSingle;
_panel.Controls.Add(label);
}
class UseCase
{
private static int _id;
private Panel _panel;
private string _name;
private int _x;
private int _y;
public UseCase(Panel panel, string name, int x, int y)
{
_id++;
_panel = panel;
_name = name;
_x = x;
_y = y;
}
public void DrawUseCase()
{
var graphics = _panel.CreateGraphics();
var pen = new Pen(Color.Black, 2);
graphics.DrawEllipse(pen, _x , _y , 120, 50);
// setup font
var stringFont = new Font("Arial", 10);
// measure string
var textWith = graphics.MeasureString(_name, stringFont).Width;
// label
var label = new Label();
var useCaseText = (_id < 10 ? "0" : "") + _id.ToString() + "-" + _name;
label.Text = useCaseText;
label.Location = new Point(_x - (Convert.ToInt32(textWith) / 2) + 60, _y + 20);
label.AutoSize = true;
label.BorderStyle = BorderStyle.FixedSingle;
_panel.Controls.Add(label);
}
}
Github repository:
https://github.com/JimVercoelen/use-case-helper
Thanks
Your code has several issues, all of which will go away once you learn how to draw properly in winforms!
There are many posts describing it but what you need to understand that you really have these two options:
Draw onto the surface of the control. This is what you do, but you do it all wrong.
Draw into a Bitmap which is displayed in the control, like the Picturbox's Image or a Panel's BackgroundImage.
Option two is best for graphics that slowly add up and won't need to be corrected all the time.
Option one is best for interactive graphics, where the user will move things around a lot or change or delete them.
You can also mix the options by caching drawn graphics in a Bitmap when they get too numerous.
Since you started with drawing onto the surface let's see how you should do it correctly:
The Golden Rule: All drawing needs to be done in the control's Paint event or be triggered from there always using only the Paint event's e.Graphics object!
Instead you have created a Graphics object by using control.CreateGraphics. This is almost always wrong.
One consequence of the above rule is that the Paint event needs to be able to draw all objects the user has created so far. So you will need to have class level lists to hold the necessary data: List<ActorClass> and List<UseCaseClass>. Then it can do maybe a
foreach(ActorClass actor in ActorList) actor.drawActor(e.Graphics)
etc.
Yes this fully repainting everything looks like a waste but it won't be a problem until you need to draw several hundreds of object.
But if you don't do it this way, nothing you draw persists.
Test it by running your present code and doing a Minimize/Maximize sequence. Poof, all drawings are gone..
Now back to your original question: How to select an e.g. Actor?
This really gets simple as you can can iterate over the ActorList in the MouseClick event (do not use the Click event, as it lacks the necessary parameters):
foreach (ActorClass actor in ActorList)
if (actor.rectangle.Contains e.Location)
{
// do stuff
break;
}
This is a simple implementation; you may want to refine it for the case of overlapping objects..
Now you could do things like maybe change the color of the rectangle or add a reference to the object in a currentActor variable.
Whenever you have made any changes to your lists of things to draw, like adding or deleting a object or moving it or changing any (visible) properties you should trigger an update via the Paint event by calling Invalidate.
Btw: You asked about a PictureBox in the title but use only a Panel in the code. Using a PictureBoxis recommended as it is doublebuffered and also combines two Bitmaps to let you use both a caching Image and a BackgroundImage with maybe a nice paper..
As far as I can see your code so far lacks the necessary classes. When you write them add a Draw routine and either a reference to the Label you add or simply use DrawString to draw the text yourself..
Update:
After looking at your project, here a the minimal changes to make the drawing work:
// private Graphics graphics; // delete!!
Never try to cache a Graphics object!
private void pl_Diagram_Paint(object sender, PaintEventArgs e)
{
pen = new Pen(Color.Black, 1);
DrawElements(e.Graphics); // pass out the 'good' object
//graphics = pl_Diagram.CreateGraphics(); // delete!
}
The same; pass the real Graphics object into the drawing routine instead!
// actor
if (rb_Actor.Checked)
{
if (e.X <= 150)
{
var actor = new Actor(name, e.X, e.Y);
_actors.Add(actor);
pl_Diagram.Invalidate(); // trigger the paint event
//DrawElements();
}
}
// use case
if (rb_Use_Cases.Checked)
{
var useCase = new UseCase(name, e.X, e.Y);
_useCases.Add(useCase);
pl_Diagram.Invalidate(); // trigger the paint event
//DrawElements();
}
Instead of calling the routine directly we trigger the Paint event, which then can pass a good Graphics object to it.
public void DrawElements(Graphics graphics)
{
foreach (var actor in _actors)
{
DrawActor(graphics, actor);
}
foreach (var useCase in _useCases)
{
DrawUseCase(graphics, useCase);
}
}
We receive the Graphics object and pass it on..
private void DrawActor(Graphics graphics, Actor actor)
and
graphics.DrawEllipse(pen, (useCase.X - 60), (useCase.Y - 30), 120, 60);
After these few changes the drawing persists.
Replacing the Panel by a Picturebox is still recommended to avoid flicker during the redraw. (Or replace by a double-buffered Panel subclass..)

Why is DrawRectangle drawing a cross inside my PictureBox

I'm trying to draw 10 rectangles, but when I use g.DrawRectangle() it is drawing a cross as shown below:
I'm creating Vertex objects that contain a getRectangle() function which returns a Rectangle object for that vertex.
I was hoping to create these objects and show them as Rectangles on the pictureBox.
Here's my code
private System.Drawing.Graphics g;
private System.Drawing.Pen pen1 = new System.Drawing.Pen(Color.Blue, 2F);
public Form1()
{
InitializeComponent();
pictureBox.Dock = DockStyle.Fill;
pictureBox.BackColor = Color.White;
}
private void paintPictureBox(object sender, PaintEventArgs e)
{
// Draw the vertex on the screen
g = e.Graphics;
// Create new graph object
Graph newGraph = new Graph();
for (int i = 0; i <= 10; i++)
{
// Tried this code too, but it still shows the cross
//g.DrawRectangle(pen1, Rectangle(10,10,10,10);
g.DrawRectangle(pen1, newGraph.verteces[0,i].getRectangle());
}
}
Code for Vertex class
class Vertex
{
public int locationX;
public int locationY;
public int height = 10;
public int width = 10;
// Empty overload constructor
public Vertex()
{
}
// Constructor for Vertex
public Vertex(int locX, int locY)
{
// Set the variables
this.locationX = locX;
this.locationY = locY;
}
public Rectangle getRectangle()
{
// Create a rectangle out of the vertex information
return new Rectangle(locationX, locationY, width, height);
}
}
Code for Graph class
class Graph
{
//verteces;
public Vertex[,] verteces = new Vertex[10, 10];
public Graph()
{
// Generate the graph, create the vertexs
for (int i = 0; i <= 10; i++)
{
// Create 10 Vertexes with different coordinates
verteces[0, i] = new Vertex(0, i);
}
}
}
Looks like an exception in your draw loop
last call to:
newGraph.verteces[0,i]
fails with OutOfRangeException
you shoul iterate not to i <= 10, but to i < 10
Red Cross Indicates that an Exception has been thrown, you are not seeing it because it's being handled. Configure Visual Studio to break on exception throw to catch it.
An exception has been thrown. At first look your code:
for (int i = 0; i <= 10; i++)
will generate an IndexOutOfRangeException because verteces has 10 items but it will cycle from 0 to 10 (included so it'll search for 11 elements). It depends on what you want to do but you have to change the cycle to (removing the = from <=):
for (int i = 0; i < 10; i++)
or to increment the size of verteces to 11.

Performance issue when adding controls to a panel in C#

I am creating a panel and then adding some labels/buttons to it to form a grid. The issue is that if I add more than say 25x25 items to the panel, there is a terrible performance hit. I can resize the form ok but when I scroll the panel to see all the labels the program lags, the labels/buttons tear or flicker, and sometimes it can make the program unresponsive. I have tried adding the controls to a "DoubleBufferedPanel" that I created. This seems to have no effect. What else could I do? Sorry for such a large code listing. I didn't want to waste anyone's time.
namespace GridTest
{
public partial class Form1 : Form
{
private const int COUNT = 50;
private const int SIZE = 50;
private Button[,] buttons = new Button[COUNT, COUNT];
private GridPanel pnlGrid;
public Form1()
{
InitializeComponent();
pnlGrid = new GridPanel();
pnlGrid.AutoScroll = true;
pnlGrid.Dock = DockStyle.Fill;
pnlGrid.BackColor = Color.Black;
this.Controls.Add(pnlGrid);
}
private void Form1_Load(object sender, EventArgs e)
{
int x = 0;
int y = 0;
int offset = 1;
for (int i = 0; i < COUNT; i++)
{
for (int j = 0; j < COUNT; j++)
{
buttons[i, j] = new Button();
buttons[i, j].Size = new Size(SIZE, SIZE);
buttons[i, j].Location = new Point(x, y);
buttons[i, j].BackColor = Color.White;
pnlGrid.Controls.Add(buttons[i, j]);
x = x + SIZE + offset;
}
x = 0;
y = y + SIZE + offset;
}
}
}
}
Also, the GridPanel class:
namespace GridTest
{
public class GridPanel : Panel
{
public GridPanel()
: base()
{
this.DoubleBuffered = true;
this.ResizeRedraw = false;
}
}
}
If you must add controls at run time, based on some dynamic or changing value, you might want to consider creating an image on the fly, and capturing mouse click events on its picturebox. This would be much quicker and only have one control to draw rather than hundreds. You would lose some button functionality such as the click animation and other automatic properties and events; but you could recreate most of those in the generation of the image.
This is a technique I use to offer users the ability to turn on and off individual devices among a pool of thousands, when the location in a 2-dimensional space matters. If the arrangement of the buttons is unimportant, you might be better offering a list of items in a listview or combobox, or as other answers suggest, a datagridview with button columns.
EDIT:
An example showing how to add a graphic with virtual buttons. Very basic implementation, but hopefully you will get the idea:
First, some initial variables as preferences:
int GraphicWidth = 300;
int GraphicHeight = 100;
int ButtonWidth = 60;
int ButtonHeight = 20;
Font ButtonFont = new Font("Arial", 10F);
Pen ButtonBorderColor = new Pen(Color.Black);
Brush ButtonTextColor = new SolidBrush(Color.Black);
Generating the image:
Bitmap ControlImage = new Bitmap(GraphicWidth, GraphicHeight);
using (Graphics g = Graphics.FromImage(ControlImage))
{
g.Clear(Color.White);
for (int x = 0; x < GraphicWidth; x += ButtonWidth)
for (int y = 0; y < GraphicHeight; y += ButtonHeight)
{
g.DrawRectangle(ButtonBorderColor, x, y, ButtonWidth, ButtonHeight);
string ButtonLabel = ((GraphicWidth / ButtonWidth) * (y / ButtonHeight) + x / ButtonWidth).ToString();
SizeF ButtonLabelSize = g.MeasureString(ButtonLabel, ButtonFont);
g.DrawString(ButtonLabel, ButtonFont, ButtonTextColor, x + (ButtonWidth/2) - (ButtonLabelSize.Width / 2), y + (ButtonHeight/2)-(ButtonLabelSize.Height / 2));
}
}
pictureBox1.Image = ControlImage;
And responding to the Click event of the pictureBox:
// Determine which "button" was clicked
MouseEventArgs em = (MouseEventArgs)e;
Point ClickLocation = new Point(em.X, em.Y);
int ButtonNumber = (GraphicWidth / ButtonWidth) * (ClickLocation.Y / ButtonHeight) + (ClickLocation.X / ButtonWidth);
MessageBox.Show(ButtonNumber.ToString());
I think you won't be able to prevent flickering having 625 buttons (btw, windows) on the panel. If such layout is mandatory for you, try to use the DataGridView, bind it to a fake data source containing the required number of columns and rows and create DataGridViewButtonColumns in it. This should work much more better then your current result ...

WPF: what is the right way to display a 2D array in a user-drawn control

I want to make a user-drawn control for the only purpose of displaying a Color[,] array. The control itself should draw an NxM grid of rectangles of corresponding colors.
I'm trying to inherit from a FrameworkElement and to override OnRender method:
public class CustomControl1 : FrameworkElement
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public Color[,] ColorCollection
{
get { return (Color[,])GetValue(ColorGridProperty); }
set { SetValue(ColorGridProperty, value); }
}
public static readonly DependencyProperty ColorGridProperty =
DependencyProperty.Register("ColorCollection", typeof(Color[,]), typeof(CustomControl1),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
protected override void OnRender(DrawingContext drawingContext)
{
if (ColorCollection != null)
{
int dimx = this.ColorCollection.GetLength(0);
int dimy = this.ColorCollection.GetLength(1);
double w = this.ActualWidth / dimx;
double h = this.ActualWidth / dimy;
for (int x = 0; x < dimx; x++)
{
for (int y = 0; y < dimy; y++)
{
SolidColorBrush brush = new SolidColorBrush(ColorCollection[x, y]);
drawingContext.DrawRectangle(brush, null, new Rect(x * w, 0, w, this.ActualHeight));
}
}
}
}
}
The problem is that my control doesn't redraw itself when i change elements in the underlying array. It works fine when i assign a whole new array or resize the control though.
Obviously I need another class which somehow notifies control about internal changes in the collection. I was looking at INotifyCollectionChange and ObservableCollection but the only articles I found were about binding collections to existing controls, not custom user-drawn ones. So I got confused and stuck at this point.
You probably can create a custom collection for yourself that works like your 2D array, but you need to also implement INotifyCollectionChange interface, which is not so hard to implement. That way WPF will listen to your collection changes and update the control if necessary.
I think the sample may be useful.

Categories

Resources