These are two approaches to this problem:
The first method involves creating controls, e.g PictureBoxes
or Panels
, which have the shape of the image and are only clickable inside that shape.
This is nice, provided you have access to the vector outline that makes up the shape.
Here is an example that restricts the visible&clickable Region
of a Panel
to an irregularly-shaped blob created from a list of trace points:
List<Point> points = new List<Point>();
points.Add(new Point(50,50));points.Add(new Point(60,65));points.Add(new Point(40,70));
points.Add(new Point(50,90));points.Add(new Point(30,95));points.Add(new Point(20,60));
points.Add(new Point(40,55));
using (GraphicsPath gp = new GraphicsPath())
{
gp.AddClosedCurve(points.ToArray());
panel1.Region = new Region(gp);
}
Unfortunately making a Region
from the points contained in it will not work; imagine a Region
as a list of vector shapes, these are made up of points, but only to create containing vectors, not pixels..
You could trace around the shapes but that is a lot of work, and imo not worth it.
So if you don't have vector shapes: go for the second method:
This will assume that you have images (probably PNGs), which are transparent at all spots where no clicks should be accepted.
The simplest and most efficient way will be to put them in a list together with the points where they shall be located; then, whenever they have changed, draw them all into one image, which you can assign to a PictureBox.Image
.
Here is a Mouseclick
event that will search for the topmost Image
in a List of Images to find the one that was clicked. To combine them with their locations I use a Tuple list:
List<Tuple<Image, Point>> imgList = new List<Tuple<Image, Point>>();
We search through this list in each MouseClick
:
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
int found = -1;
// I search backward because I drew forward:
for (int i = imageList1.Images.Count - 1; i >= 0; i--)
{
Bitmap bmp = (Bitmap) imgList[i].Item1;
Point pt = (Point) imgList[i].Item2;
Point pc = new Point(e.X - pt.X, e.Y - pt.Y);
Rectangle bmpRect = new Rectangle(pt.X, pt.Y, bmp.Width, bmp.Height);
// I give a little slack (11) but you could also check for > 0!
if (bmpRect.Contains(e.Location) && bmp.GetPixel(pc.X, pc.Y).A > 11)
{ found = i; break; }
}
// do what you want with the found image..
// I show the image in a 2nd picBox and its name in the form text:
if (found >= 0) {
pictureBox2.Image = imageList1.Images[found];
Text = imageList1.Images.Keys[found];
}
}
Here is how I combined the images into one. Note that for testing I had added them to an ImageList
object. This has serious drawbacks as all images are scaled to a common size. You probably will want to create a proper list of your own!
Bitmap patchImages()
{
Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
imgList.Clear();
Random R = new Random(1);
using (Graphics G = Graphics.FromImage(bmp) )
{
foreach (Image img in imageList1.Images)
{
// for testing: put each at a random spot
Point pt = new Point(R.Next(333), R.Next(222));
G.DrawImage(img, pt);
// also add to the searchable list:
imgList.Add(new Tuple<Image, Point>(img, pt));
}
}
return bmp;
}
I called it at startup :
private void Form1_Load(object sender, EventArgs e)
{
pictureBox1.Image = patchImages();
}
Aside: This way of drawing all the images into one, is also the only one that lets you overlap the images freely. Winforms
does not support real transparency with overlapping Controls.. And testing one Pixel
(at most) for each of your shapes is also very fast.