I have an irregularly-shaped picture like a heart or any random shape. I can make it transparent visually, but I need to make it clickable only on the shape area. I heard that I should use "Region" for this, but I can't figure out how.
I tried to search all pixels that aren't null, transparent, or empty and create a point array with them, but I can't create/reshape the current control region. I'm trying to make a custom control that you can select a button or picture, and they are irregularly-shaped and close to one another.
Here is what I'm dealing with:
As you can see in the picture, there are 8 different parts (assuming the right and left sides are combined). As you can see, they are close to each other, and some of them are even fit in the empty space between others.
My goal is, for example, if I clicked on Pectorals (red zone in the figure), it will change to the colored version of it, and a bunch of other code will run.
The problem is, by default when we add any Picture with a PictureBox
, it will create a Rectangle
around that picture starting from the boundaries of it. So if I place two pictures (as in the figure) close together, one's empty zone prevents me from clicking the other's.
Also it raises the wrong object's ClickEvent
because of this problem.
I'm trying to set "Raise Event Region" that I assume we called it Graphic Region
just where the picture exists. I can collect positions of pixels with a loop which determines which coordinates of that picture has "Color" (meaning it is part of the picture, the area that I want clickable) but I can't limit that area with that data.
An example of what I'm trying to achieve: https://www.youtube.com/watch?v=K_JzL4kzCoE
What is the best way to do this?
These are two approaches to this problem:
Work with Regions
.
Work with transparent Images
.
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.
Here is a Winforms example of handling an image mask. When the user clicks on the mask image it pops up a message box. This basic technique can obviously be modified to suit.
public partial class Form1 : Form {
readonly Color mask = Color.Black;
public Form1() {
InitializeComponent();
}
private void pictureBox1_Click(object sender, EventArgs e) {
var me = e as MouseEventArgs;
using (var bmp = new Bitmap(pictureBox1.Image)) {
if (me.X < pictureBox1.Image.Width && me.Y < pictureBox1.Image.Height) {
var colorAtMouse = bmp.GetPixel(me.X, me.Y);
if (colorAtMouse.ToArgb() == mask.ToArgb()) {
MessageBox.Show("Mask clicked!");
}
}
}
}
}
pictureBox1
has an Image
loaded from a resource of a heart shape that I free-handed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With