I need to create a WinForm application that allows the user to drag from a set of images, and position them on a grid, a tile designer of sorts.
I've tried using the TableLayoutPanel, however It seems to get rather slow when you have a grid 100x100, I was just wondering if there are any alternative methods out there that would allow the user to drag a tile onto the grid layout, and expand the size of the grid if they required.
Thank you.
If tiles are contiguous, you can get the first "empty cell"
private readonly Size tileSize = new Size(200, 200);
private void button1_Click(object sender, EventArgs e)
{
var pb = new PictureBox();
var pt = GetEmptyCell(tileSize);
pb.Location = pt;
pb.Size = tileSize;
Controls.Add(pb);
}
private Point GetEmptyCell(Size TileSize)
{
var ctrl = GetChildAtPoint(new Point(0, 0));
if (ctrl == null && TileSize.Width < Width && TileSize.Height < Height)
return new Point(0, 0);
for (int x = 0; x < Width; x++)
{
for (int y = 0; y < Height; y++)
{
ctrl = GetChildAtPoint(new Point(x, y));
if (ctrl == null && x + TileSize.Width < Width && y + TileSize.Height < Height)
return new Point(x, y);
}
}
throw new Exception("No Empty cells was found");
}
Related
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);
}
}
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:
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.
I have created application where user can drag and drop the controls during run-time. I use my ss function to align the controls in a table like fashion using 2D-array. I want the application to show/highlight the position where the control will be placed when its brought near certain coordinate.
struct IconPanel
{
public int left;
public int top;
}
static void ss(Control control)
{
int row, col, nearestCol = 0, nearestRow = 0, rowDist = 100, diff, colDist = 100;
for (row = 0; row < iconPanels.GetLength(0); row++)
{
for (col = 0; col < iconPanels.GetLength(1); col++)
{
diff = Math.Abs(control.Left - iconPanels[row, col].left);
if (diff < colDist)
{
colDist = diff;
nearestCol = col;
}
diff = Math.Abs(control.Top - iconPanels[row, col].top);
if (diff < rowDist)
{
rowDist = diff;
nearestRow = row;
}
}
}
control.Left = iconPanels[nearestRow, nearestCol].left;
control.Top = iconPanels[nearestRow, nearestCol].top;
}
You probably don't want these position markers to persist, so this is one of the rare cases where I would use
using ( Graphics G = theParentContainer.CreateGraphics())
foreach(Rectangle rect in yourPositions) G.DrawRectangle(Pens.Red, rect);
Clean them up after the drop or after leaving the control etc.. by calling theParentContainer.Invalidate();
This assumes that you know the possible position and can create a
List<Rectangle> yourPositions = new List<Rectangle>();
holding them.
As we all know ;-) graphics created with a Graphics object that was instantiated with Control.CeateGraphics won't persist and will dissappear with the next Paint event. But for such an interactive helper graphics this is perfect. Other examples are rubberband-drawing or cursor tracking..
On control that you would like to be highlited subscribe to Paint event.
private void highlightControl_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
if(mouse is over or other control is over) {
Graphics g = e.Graphics;
g.FillRectangle(Brushes.Red, 0, 0, this.Width, this.Height);
}
}
I am now drawing to a panel some dots to indicate a sort of dotted grid with 1% of margin of total panel width.
This is what I am doing now:
private void panel1_Paint(object sender, PaintEventArgs e)
{
Pen my_pen = new Pen(Color.Gray);
int x,y;
int k = 1 ,t = 1;
int onePercentWidth = panel1.Width / 100;
for (y = onePercentWidth; y < panel1.Height-1; y += onePercentWidth)
{
for (x = onePercentWidth; x < panel1.Width-1; x += onePercentWidth)
{
e.Graphics.DrawEllipse(my_pen, x, y, 1, 1);
}
}
}
What is bothering me is that when the app starts I can see the dots being drawn on the panel. Even if it is very quick it still bothers me a lot.
Is it possible to draw the dots on the panel and load it directly drawn?
Thank you for the help
You could create a bitmap and draw it instead.
But before you do that: DrawEllipse is a little expensive. Use DrawLine with a Pen that has a dotted linestyle instead:
int onePercentWidth = panel1.ClientSize.Width / 100;
using (Pen my_pen = new Pen(Color.Gray, 1f))
{
my_pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
my_pen.DashPattern = new float[] { 1F, onePercentWidth -1 };
for (int y = onePercentWidth; y < panel1.ClientSize.Height - 1; y += onePercentWidth)
e.Graphics.DrawLine(my_pen, 0, y, panel1.ClientSize.Width, y);
}
Note that I am using using so I don't leak the Pen and ClientSize so I use only the inner width. Also note the exaplanation about the custom DashPattern on MSDN
I need to display a persistent grid in a TabPage. My problems would be instantly solved if I could draw to the entire non-visible portion of the TabPage and prevent graphics from being erased when scrolling.
The only other solution I can think of is tracking the scroll position in the tab and basing the grid drawn from that.
To get this to draw in the first place, I had to create an EventHandler for TabPage.Paint.
//Code removed
This method draws vertical and horizontal lines to create a grid within the visible tab, but it continues to draw whenever a Paint event occurs (i.e. scrolling), so it creates overlapping lines and aren't aligned to anything but the size of the current visible area of the tab.
Maybe something like this will work for you:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
const int gridSpacing = 20;
const int lineThickness = 1;
Bitmap bmp = new Bitmap(gridSpacing, gridSpacing);
using (System.Drawing.Pen pen = new System.Drawing.Pen(Color.Blue, lineThickness))
{
using (Graphics G = Graphics.FromImage(bmp))
{
G.Clear(this.BackColor);
G.DrawLine(pen, 0, bmp.Height - pen.Width, bmp.Width, bmp.Height - pen.Width); // horizontal
G.DrawLine(pen, bmp.Width - pen.Width, 0, bmp.Width - pen.Width, bmp.Height); // vertical
}
}
foreach (TabPage TP in tabControl1.TabPages)
{
TP.BackgroundImage = bmp;
TP.BackgroundImageLayout = ImageLayout.Tile;
}
}
}
Keep in mind that this solution is just pseudo. You also have to respond to scrolling.
void form_draw()
{
spacingX = offsetX % scale * -1;
spacingY = offsetY % scale * -1;
if (form.HorizontalPosition != lastXPosition && form.VerticalPosition == lastYPosition)
lastStartX += spacingX;
else if (tab.HorizontalScroll.Value == lastXPosition && form.VerticalPosition != lastYPosition)
lastStartY += spacingY;
lastYPosition = form.VerticalPosition;
lastXPosition = form.HorizontalPosition;
for (int i = lastStartY; i < formHeight; i += scale)
form.draw(0, i, formWidth, i);
for (int i = lastStartX; i < formWidth; i += scale)
form.draw(i, 0, i, formWidth);
}
I'm trying to create 81 picture boxes and have them automatically positioned a certain distance apart from one another but they don't seem to be placing in any logical order. I have to initialize the X point to -1700 for them to even appear on the screen. The following code gets the first 15 where I want them but then they start stacking on top of one another instead of continuing the pattern. This is the result of about an hour of tinkering but initially the logic looked fine. I even had a message box that would display the current X,Y that was being set and it was correct it just would not place them at those coordinates.
int X = -1700;
int Y = 0;
for (int i = 0; i < 81; i++)
{
this.Controls.Add(championThumbNailsArray[i]);
championThumbNailsArray[i].Height = 80;
championThumbNailsArray[i].Width = 80;
championThumbNailsArray[i].Location = new Point(X, Y);
// MessageBox.Show(Convert.ToString(X) + "," + Convert.ToString(Y));
championThumbNailsArray[i].ImageLocation = akali.grabPicture();
//championThumbNailsArray[i].ImageLocation = championsArray[i].grabPicture();
if (X <= 425)
X = X + 85;
else
{
X = -1700;
Y = Y + 85;
}
}
Instead of manually placing elements use a FlowLayoutPanel. Add the controls to the panel and let it do the arrangement for you.
This code works as you are expecting
private void Form1_Load(object sender, EventArgs e)
{
int x = 0;
int y = 0;
for (int i = 0; i < 81; i++)
{
PictureBox p = new PictureBox();
p.BorderStyle = BorderStyle.Fixed3D;
p.Height = 80;
p.Width = 80;
p.Location = new Point(x, y);
x += 85;
if (x > 425)
{
x = 0;
y += 85;
}
this.Controls.Add(p);
}
}
But I would go with something like #Ed said, a FlowLayout control