Create custom shape for button - c#

I would like to make a map that shows each state, when hovering over a certain state, the respective shape would change color and some information about it would appear.
Here is a web-based example of something similar
kartograph.org/showcase/usa-projection
Using .NET 4.5, C#, and WinForms is it possible to achieve this with a Button and handling mouse events?

This isn't a complete answer, but might put you on the right path.
WinForms won't let you use the Button object in this way; WinForms buttons are quite limited in their ability to be customized--WPF would likely lend itself to this, if it's an option.
To do this in WinForms it's likely that you'll need to use GDI and load each state into it's own Graphics object and write your own plumbing for click events and such. While I can't offer a specific example it should be feasible, but it's also likely to be a fair amount of work (especially for things such as transparent parts of the image).
However, if you either look into WPF or into interacting with GDI objects you should be able to make progress.

This answer completely ignores your question about creating buttons with funny shapes and instead only deals with building something like the example you showed a link to: Identifiying a state on a map by clicking or hovering with the mouse.
To identify the state is simple:
If you can assign each state a color (even if it is only very slightly different) you could use GetPixel to check on which country/color the mouse has clicked or is hovering..
If you don't want visible colors you can still use the same trick by simply using two overlaid maps and show the top map while using the colored one below as a lookup table.
Of course you don't even need to put the lookup map into a control; you can simply use its Bitmap as long it has the same size as the visible map.
Won't take more than a few lines of code in winforms. Setting up the list of states and filling them with colors will be more work.
To change its color is more tricky. I think I would use a quite difffrent approach here: Coding a floodfill algorithm is pretty simple; wikipedia has a few nice ones, expecially the one without recursion (queue-based) is really simple to implement.
So you could use an overlaid copy of the map and floodfill the state the mouse hovers over.. (For this to work, you need to make sure the states can be floodfilled, i.e. that they have closed outlines. This can be part of the preparation of coloring them.)
When the mouse moves to a different state/color you would restore the original map..
Your example has a nice, if somewhat slow, animation. This would be even trickier. If you need that maybe WPF is really worth considering.. Although a coloring animation is doable in Winforms as as well, maybe with a Color Matrix and a Timer it certainly wasn't built for glitz..
Here is a piece of code that goes at least half the way:
// simple but effective floodfill
void Fill4(Bitmap bmp, Point pt, Color c0, Color c1)
{
Rectangle bmpRect = new Rectangle(Point.Empty, bmp.Size);
Stack<Point> stack = new Stack<Point>();
int x0 = pt.X;
int y0 = pt.Y;
stack.Push(new Point(x0, y0) );
while (stack.Any() )
{
Point p = stack.Pop();
if (!bmpRect.Contains(p)) continue;
Color cx = bmp.GetPixel(p.X, p.Y);
if (cx == Color.Black) return;
if (cx == SeaColor) return;
if (cx == c0)
{
bmp.SetPixel(p.X, p.Y, c1);
stack.Push(new Point(p.X, p.Y + 1));
stack.Push(new Point(p.X, p.Y - 1));
stack.Push(new Point(p.X + 1, p.Y));
stack.Push(new Point(p.X - 1, p.Y));
}
}
}
// create a random color for the test
Random R = new Random();
// current and last mouse location
Point mouseLoc = Point.Empty;
Point lastMouseLoc = Point.Empty;
// recognize that we have move inside the same state
Color lastColor = Color.White;
// recognize the outside parts of the map
Color SeaColor = Color.Aquamarine;
// start a timer since Hover works only once
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
mouseLoc = e.Location;
timer1.Stop();
timer1.Interval = 333;
timer1.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
// I keep the map in the Background image
Bitmap bmp = (Bitmap)pictureBox1.BackgroundImage;
// have we left the image?
if (!new Rectangle(Point.Empty, bmp.Size).Contains(mouseLoc)) return;
// still in the same state: nothing to do
if (lastColor == bmp.GetPixel(mouseLoc.X, mouseLoc.Y)) return;
// a random color
Color nextColor = Color.FromArgb(255, R.Next(255), R.Next(255), R.Next(256));
// we've been in the map before, so we restore the last state to white
if (lastMouseLoc != Point.Empty)
Fill4(bmp, lastMouseLoc,
bmp.GetPixel(lastMouseLoc.X, lastMouseLoc.Y), Color.White );
// now we color the current state
Fill4(bmp, mouseLoc, bmp.GetPixel(mouseLoc.X, mouseLoc.Y), nextColor);
// remember things, show image and stop the timer
lastMouseLoc = mouseLoc;
lastColor = nextColor;
pictureBox1.Image = bmp;
timer1.Stop();
}
All you need to run it is a PictureBox pictureBox1 and a Timer timer1 and a version of the map that has only 3 colors: Black, White and Aquamarine.
What it will do is paint the state you hover over in a random color.
Your next steps would be to create a list of all states with a number, title and info text. Then you create a 2nd version of the map where you color each state with a a color you derive from the state's number.
You can use the code above for the coloring, if you expand it a little.
Finally you code a lookup in the Tick event to get the info to display in a Tooltip..
Of course this is assuming that you are satisfied with working with the map as a Bitmap. The source to link to uses a SVG file, where all data are stored as vector data in an XML format. Parsing this to get Points for a GraphicsPath is also an option, which will then work in the vector realm. But I guess it could take a few days more to build..
My finished, rough version including the code to create the color map and the code for doing lookups comes in a ca. 150 lines, without comments.

Related

Make a Map of Buttons?

How do I do the following I'm not asking for specific code but I need some direction as I have been racking my brains on this for weeks. Simply I want to make a map of the eg. united states and each state is a different picture or area that can be moused over and clicked. I tried playing with png and transparencies but I'm at a dead end. More ambitiously I'd like to drag labels with state capitals over each state and drop them there then have a process where if the label/capital matches the state it correct else it's not.
I've tried GIS(?) I want to do this C# but I can't get traction how to do it so far. Can anyone help? Is this too difficult in C#? SHould i be using another approach? Please what is the approach?
The good news first: The programming part is not so hard and even with good old Winforms you can do the core checks in a few lines. The clickable areas can't be Buttons in Winforms, however.
Here are two solutions:
Solution 1 : You can define a list of areas, called regions and test if one got clicked.
Here is a start: I define a very simple Rectangle Region and a not so simple Polygon Region and test on each click if one was hit. If it was hit I output its data into the Form's title text:
//..
using System.Drawing.Drawing2D;
//..
public Form1()
{
InitializeComponent();
// most real states don't look like rectangles
Region r = new Region(new Rectangle(0, 0, 99, 99));
regions.Add(r);
List<int> coords = new List<int>() {23,137, 76,151, 61,203,
117,283, 115,289, 124,303, 112,329, 76,325, 34,279, 11,162};
List<Point> points = new List<Point>();
for (int i = 0; i < coords.Count ; i += 2)
points.Add(new Point(coords[i], coords[i+1]));
byte[] fillmodes = new byte[points.Count];
for (int i = 0 ; i < fillmodes.Length; i++) fillmodes[i] = 1;
GraphicsPath GP = new GraphicsPath(points.ToArray(), fillmodes);
regions.Add(r);
}
List<Region> regions = new List<Region>();
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
using (Graphics G = pictureBox1.CreateGraphics() )
foreach(Region r in regions)
{
Region ri = r.Clone();
ri.Intersect(new Rectangle(e.X, e.Y, 1, 1));
if (!ri.IsEmpty(G)) // hurray, we got a hit
{ this.Text = r.GetBounds(G).ToString(); break; }
}
}
The regions in the test programm are not visible. For testing you may want to add something like this:
private void button1_Click(object sender, EventArgs e)
{ // paint a non-persistent filling
using (Graphics G = pictureBox1.CreateGraphics())
foreach (Region r in regions)
{ G.FillRegion(Brushes.Red, r); }
}
But now for the bad news: You can define regions to be complicated polygons that look like real states, however that is a lot of work! There are ready-made image map editors out there in the web for making clickable maps on websites. Your best course might be to use one of these to create all those polygons and then convert the html output to a list of points you can read into your program. You may even get lucky and find ready image maps for the USA, but I haven't looked..
Update: There are many image maps of the USA out there. One is even on wikipedia. Converting these coordinates to fit your map will be a task but lot easier than creating them from scratch, imo. I have changed the code to include one list of cordinates from the wikipedia source. Guess the state!
I have implmented a program that lets you click on a state and displays the name in 78 lines of code, excluding the text file with the 50 states.
Solution 2:
Instead of a list of polygons you can prepare a colored map and use the colors as keys into a Dictionary<Color, string> or Dictionary<Color, StateInfo>. You must make sure that each state has one unique color and that the image is not compressed as jpeg, which would introduce artifacts and mess up the key mapping.
Next you map each color to the related info; this is the real work because you must know those states to create the Dictionary ;-)
Finally you can look up the clicked color in the Dictionary you have created:
Color c = ((Bitmap) pictureBox1.Image).GetPixel(e.X, e.Y)
string StateName = stateDictionary[c];
If you are using a class or struct as your value in the dictionary you can include the capital etc..
You can zoom but must scale the click location accordingly; you can even display a geographical Image if you look up the key color not in the PictureBox but in an invisible color code bitmap.
All in all an even simpler solution imo.. your pick!
Once you got that working, you can play with drag&drop. Here you'll want to test on DragDrop or maybe on Mouseup to see where you have dropped the Label.
This is a nice project and well worth of creating a few classes to make things easier and more expandable..!
For real Buttons you may get lucky with WPF.

Drawing interactively lines over a Bitmap

I need to allow to the user to draw lines over an bitmap. Lines should be drawn interactively, I mean something performed using typical code giving to the user a visual feedback about what is drawn:
private void MainPictureBox_MouseDown( object sender, MouseEventArgs e)
{
DrawingInProgress = true ;
Origin = new Point (e.X, e.Y);
}
private void MainPictureBox_MouseUp(object sender, MouseEventArgs e)
{
DrawingInProgress = false ;
}
private void MainPictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (!DrawingInProgress) return ;
End = new Point (e.X, e.Y);
using( Pen OverlayPen = new Pen( Color .Red, 1.0f))
using (Graphics g = MainPictureBox.CreateGraphics())
{
g.DrawLine(OverlayPen, Origin, End);
}
}
Of course I keep track of the points using List.Add within MainPictureBox_MouseUp in order to draw lines in the Paint event (code not shown for the sake of simplicity)
Without the background image things could be done nicely simply overwriting the previous line with the background color, something like:
g.DrawLine(BackgroundColorPen, Origin, PreviousEnd);
g.DrawLine(OverlayPen, Origin, End);
but this is not possible with a not uniform background.
Invalidating the rectangle defined by the points: Origin, PreviousEnd then using Update() makes the rendering quite messy. I am wondering how to perform this task and those are possible ways to do so i am considering:
Draw the lines over a transparent bitmap then draw the bitmap over the Picturebox. I guess that with big images this is simply unfeasible for performances reason.
Using the Picture.BackgroundImage for the bitmap then drawing on the Picture.Image but I unable to figure out how this could really saave the day
Using double buffering? How?
Stacking a different control (a panel?) over the pictureBox, making it transparent (is it possible?) then drawing over it.
Could someone give a hint in the best direction? I am really getting lost.
The solutions working for me has been the following:
Create a transparent panel;
Put it over the bitmap having them overlap completely;
Draw on the panel using proper mouse events;
There is no need to cancel the previous shape, of course: it was a misleading question. It is sufficient to distinguish permanent shapes recorded in proper lists fed to the Paint event from the transient shape previously drawn that will be not drawn again in the next Paint event;
Make absolutely sure that all drawings are performed in the Paint event using the Graphics provided by the PaintEventArgs. Thanks to #HansPassant to have stressed this in a different post.

Render Large Canvases in UserControl

I've been having trouble trying to implement this for a couple of days now. I've searched extensively on similar questions in regards to what I'm trying to do but I haven't come across a question that helps my issues directly.
Basically I'm rendering tiles onto a grid on my UserControl class. This is for my Tile Engine based world editor I'm developing. Here is a screenshot of an open world document and some tiles brushed on.
Initially, I was going to use a Bitmap in my control that would be the world's preview canvas. Using a brush tool for example, when you move your mouse and have the left button down, it sets the nearest tile beneath your cursor to the brush's tile, and paints it on the layer bitmap. The control's OnPaint method is overridden to where the layer bitmap is draw with respect to the paint event's clipping rectangle.
The issue with this method is that when dealing with large worlds, the bitmap will be extremely large. I need this application to be versatile with world sizes, and it's quite obvious there are performance issues when rendering large bitmaps onto the control each time it's invalidated.
Currently, I'm drawing the tiles onto the control directly in my control's overridden OnPaint event. This is great because it doesn't require a lot of memory. For example, a (1000, 1000) world at (20, 20) per tile (total canvas size is (20000, 20000)) runs at about 18mb of memory for the whole application. While not memory intensive, it's pretty processor intensive because every time the control is invalidated it iterates through every tile in the viewport. This produces a very annoying flicker.
What I want to accomplish is a way to meet in the middle as far as memory usage and performance. Essentially double buffer the world so that there isn't flickering when the control is redrawn (form resize, focus and blur, scrolling, etc). Take Photoshop for example - how does it render the open document when it overflows the container viewport?
For reference, here's my control's OnPaint override that is using the direct draw method mentioned above.
getRenderBounds returns a rectangle relative to PaintEventArgs.ClipRectangle that is used to render visible tiles, instead of looping through all the tiles in the world and checking if it's visible.
protected override void OnPaint(PaintEventArgs e)
{
WorldSettings settings = worldSettings();
Rectangle bounds = getRenderBounds(e.ClipRectangle),
drawLocation = new Rectangle(Point.Empty, settings.TileSize);
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.None;
e.Graphics.PixelOffsetMode =
System.Drawing.Drawing2D.PixelOffsetMode.None;
e.Graphics.CompositingQuality =
System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
for (int x = bounds.X; x < bounds.Width; x++)
{
for (int y = bounds.Y; y < bounds.Height; y++)
{
if (!inWorld(x, y))
continue;
Tile tile = getTile(x, y);
if (tile == null)
continue;
drawLocation.X = x * settings.TileSize.Width;
drawLocation.Y = y * settings.TileSize.Height;
e.Graphics.DrawImage(img,
drawLocation,
tileRectangle,
GraphicsUnit.Pixel);
}
}
}
Just comment if you need some more context from my code.
The trick is to not use a big bitmap for this at all. You only need a bitmap covering the visible area. Then you draw whatever is visible.
To achieve this you will need to maintain the data separately from the bitmap. This can be a simple array or an array/list with a simple class holding information for each block such as world position.
When your block is within the visible area then you draw it. You may or may not have to iterate through the whole array, but that isn't really a problem (you can also calculate the visible array on a separate thread). You can also make the function more intelligent by creating region indexes so you don't iterate all blocks.
To add a new block to the array, calculate it's canvas position to world coordinates, add it and then render the array again (or the area where the block is drawn).
This is how controls with scrollable areas are drawn by the system too.
Enable double-buffering will keep it clear and flicker-less.
In this case I would also use a panel with separate scroll bars and calculate the scroll-bars' relative position.

Change Border of ToolStripComboBox with Flat Style

I would like to be able to change the border color of ToolStripComboBox controls in some of my toolstrips, since the default border color of ComboBoxes when used with flat styling is SystemColors.Window, which is basically invisible against the default control color of the toolstrip. After a lot of digging around in Reflector, I don't see any obvious way to do this, since all the infrastructure behind ComboBox rendering is highly protected behind internal and private interfaces.
Outside of ToolStrips, a common solution I've seen proposed for fixing border color on ComboBoxes is to subclass ComboBox, override WndProc, and manually paint the border. This can't work for ToolStripComboBox controls since the internal ComboBox control is its own private subclass of ComboBox, with no way that I can see to replace the instance of the control.
An alternative solution I'm considering is putting one of the extended ComboBox objects into a ToolStripControlHost, which allows me to draw a border, but then I have to give up some of the professional renderer tweaks. A secondary drawback I've noticed is that I get occasional flicker during mouseover.
Switching my design to WPF is not an acceptable solution. Wrapping controls in parent controls for drawing borders is also not acceptable, as this gains nothing over the ToolStripControlHost alternative.
Does anyone have a clever solution to defeat this problem, or is there an existing (permissively-licensed) re-implementation of the ComboBox flat-style rendering stack out in the wild, which fixes some of the shortcomings in the existing implementation?
Here's a way to make it work ... sort of :)
Create an event handler for the Paint event of the ToolStrip. Then loop through all of the ToolStripComboBoxes and paint a rectangle around them.
private Color cbBorderColor = Color.Gray;
private Pen cbBorderPen = new Pen(SystemColors.Window);
private void toolStrip1_Paint(object sender, PaintEventArgs e)
{
foreach (ToolStripComboBox cb in toolStrip1.Items)
{
Rectangle r = new Rectangle(
cb.ComboBox.Location.X - 1,
cb.ComboBox.Location.Y - 1,
cb.ComboBox.Size.Width + 1,
cb.ComboBox.Size.Height + 1);
cbBorderPen.Color = cbBorderColor;
e.Graphics.DrawRectangle(cbBorderPen, r);
}
}
Here's what it looks like (note that you may need to adjust the Height of the ToolStrip to prevent the painted border from being cut off):
improvement:
check the type of the toolstrip item,
so the program will not crush if it is toolstipLabel for example.
foreach (var item in toolStrip1.Items)
{
var asComboBox = item as ToolStripComboBox;
if (asComboBox != null)
{
var location = asComboBox.ComboBox.Location;
var size = asComboBox.ComboBox.Size;
Pen cbBorderPen = new Pen(Color.Gray);
Rectangle rect = new Rectangle(
location.X - 1,
location.Y - 1,
size.Width + 1,
size.Height + 1);
e.Graphics.DrawRectangle(cbBorderPen, rect);
}
}
toolStrip1.ComboBox.FlatStyle = FlatStyle.System;
This sets the default, OS-styled, border around the combo box. It is a light grey and thin border on Windows 10. Although, depending on the background, this may not show. In which case, you could try the other options like FlatStyle.Popup.
If the presets aren't what you are looking for, the other answers allow you to draw a custom border. However, since the rectangle is drawn with +1 pixel size around the combo box, the border is 1 pixel larger than the combo box. Removing the +1s and -1s doesn't work either.

Can I use graphics.RotateTransform() without these artifacts?

I'm implementing a colour picker component as described in this seminal article.
As you can see, I've got the basics sorted:
One of the requirements however, is the ability to have the colour wheel rotated by an arbitrary amount. Thinking this would be easy, I some arithmetic to the mouse location -> colour value code and the following code to the bit that actually paints the wheel:
newGraphics.TranslateTransform((float)this.Radius, (float)this.Radius);
newGraphics.RotateTransform((float)this.offset);
newGraphics.TranslateTransform((float)this.Radius * -1, (float)this.Radius * -1);
Unfortunately, rotating the bitmap like this actually produces this:
Note the artefacts that appear either side of the centre.
Am I using the wrong approach? Or is there a way to get rid of these nasty rips?
Looking at the source code from that Microsoft example, I made the following change to the UpdateDisplay method by adding a matrix and setting the RotateAt method.
private void UpdateDisplay() {
// Update the gradients, and place the
// pointers correctly based on colors and
// brightness.
using (Brush selectedBrush = new SolidBrush(selectedColor)) {
using (Matrix m = new Matrix()) {
m.RotateAt(35f, centerPoint);
g.Transform = m;
// Draw the saved color wheel image.
g.DrawImage(colorImage, colorRectangle);
g.ResetTransform();
}
// Draw the "selected color" rectangle.
g.FillRectangle(selectedBrush, selectedColorRectangle);
// Draw the "brightness" rectangle.
DrawLinearGradient(fullColor);
// Draw the two pointers.
DrawColorPointer(colorPoint);
DrawBrightnessPointer(brightnessPoint);
}
}
It rotated the wheel 35 degrees (although the color selection was off now by, well, 35 degrees since I didn't mess with all the code) and it didn't produce any tearing.
Not 100% sure this is the answer (but too long for a comment), so maybe this is helpful.

Categories