Windows Forms Custom Control not painting correctly - c#

So i'm trying to make a nice rounded switch that when clicked it will slide either left or right to basically turn something on or off (it could be used for other things). I have a rectangle version working somewhat ok (i have a few tweaks in mind that I want to make for it) but the problem I'm running into is by using rounded rectangles. I made a few classes to help my self in this. I have one called RoundRectanglePath. Using the Create method I give it a Rectangle (or x,y,w,h) and a radius for the corners and it returns a closed GraphicsPath that I can then use Graphics.[Fill|Draw]Path with. I then have a RoundRectangle class which is a just a control that acts very similar to a Label. I found that if I override the OnPaintBackground and not send the event to the base, but instead paint a rectangle the same color as it's Parent.BackColor than I get the illusion that the control is really round. (as a related side note I allow transparent)
For my RoundMovableSwitch class I use 2 RoundRectanglePaths to split the Control in half. The left is a green Color and the right is Pink (thinking about it now I could have just used a horizontal LinearGradient brush...ooops oh well) I then draw the string On and Off on opposing sides. To that control I add a RoundRectangle. When the user clicks on either the RoundRectangle or the MoveableSwitch the Control then moves the RoundRectangle left or right 1 pixel at a time. The movement works great. The problem I am having is this. The outside Edge of the RoundRectangle is the correct Transparent color. The inside edge is the wrong color. See RoundMovingSwitch 1 and 2 in picture below. Once I get the code working correctly I'll go back and reorganize the code a bit more.
The code is hosted on GitHub: Here

"The problem I am having is this. The outside Edge of the RoundRectangle is the correct Transparent color. The inside edge is the wrong color."
Not sure I understand the problem...
Are you trying to get rid of the blue corners that are outside the rounded edges?
If so, then try this in RoundRectangle:
public RoundRectangle()
{
this.ResizeRedraw = true;
this.VisibleChanged += new EventHandler(RoundRectangle_VisibleChanged);
}
private bool RegionSet = false;
void RoundRectangle_VisibleChanged(object sender, EventArgs e)
{
if (this.Visible && !RegionSet)
{
RegionSet = true;
var r = new RectangleEx(this.ClientRectangle);
var path = RoundRectanglePath.Create(r.ToRectangle(), this.Radius, this.Corners);
this.Region = new Region(path);
}
}
*If the size of the control changes then you should reset the Region() property to the new size.
Edit: To make it reset the Region when the size changes:
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
var r = new RectangleEx(this.ClientRectangle);
var path = RoundRectanglePath.Create(r.ToRectangle(), this.Radius, this.Corners);
this.Region = new Region(path);
}

Related

How should I buffer drawn rectangles to improve performance (C#/.NET/WinForms/GDI+)

What I'm doing
I am working on a C#/.NET 4.7.2/WinForms app that draws a significant number of filled rectangles on a form using Graphics.FillRectangle.
Currently, the rectangles are drawn in the form's Paint event. After all the rectangles are drawn, a crosshair is drawn based on mouse position.
Whenever the mouse moves, Invalidate is called on the form to force a repaint so that the crosshair appears in its new position.
The problem
This is inefficient because the rectangles don't change, only the crosshair position, yet the rectangles are being redrawn every time. The CPU usage during mouse move is significant.
What next
I believe that the solution to this problem is to draw the rectangles to a buffer first (outside of the Paint event). Then, the Paint event only needs to render the buffer plus draw a crosshair on top.
Since I am new to GDI+ and manual buffering, I am wary of going down the wrong track. Google searches reveal plenty of articles on manual buffering, but each article seems to take a different approach which adds to my confusion.
I would be grateful for suggested approaches that favour simplicity and efficiency. If there is an idiomatic .NET way of doing this — the way it's meant to be done — I'd love to know.
Here's a quick and easy solution that doesn't require any buffering. To replicate this, start with a fresh Windows Forms project. I only draw two rectangles, but you can have as many as you want.
If you create a new WinForms project with these two member variables and these two handlers, you will get a working sample.
First, a couple of member variables for your form:
private bool _started = false;
private Point _lastPoint;
The started flag will turn to true after the first mouse move. The _lastPoint field will track the point at which the last cross-hairs was drawn (that's mostly why _started exists).
The Paint handler will draw the cross hairs every time it's called (you'll see why this is ok with the MouseMove handler):
private void Form1_Paint(object sender, PaintEventArgs e)
{
var graphics = e.Graphics;
var clientRectangle = this.ClientRectangle;
//draw a couple of rectangles
var firstRectangle = clientRectangle;
firstRectangle.Inflate(-20, -40);
graphics.FillRectangle(Brushes.Aqua, firstRectangle);
var secondRectangle = clientRectangle;
secondRectangle.Inflate(-100, -4);
graphics.FillRectangle(Brushes.Red, secondRectangle);
//draw Cross-Hairs
if (_started)
{
//horizontal
graphics.DrawLine(Pens.LightGray, new Point(clientRectangle.X, _lastPoint.Y),
new Point(ClientRectangle.Width + clientRectangle.X, _lastPoint.Y));
//vertical
graphics.DrawLine(Pens.LightGray, new Point(_lastPoint.X, clientRectangle.Y),
new Point(_lastPoint.X, ClientRectangle.Height + clientRectangle.Y));
}
}
Now comes the MouseMove handler. It's where the magic happens.
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
var clientRectangle = this.ClientRectangle;
var position = e.Location;
if (clientRectangle.Contains(position))
{
Rectangle horizontalInvalidationRect;
Rectangle verticalInvalidationRect;
if (_started)
{
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X - 1, clientRectangle.X),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
_started = true;
_lastPoint = position;
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X, clientRectangle.X - 1),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
}
If the cursor is within the form, I do a bunch of work. First I declare two rectangles that I will be using for invalidate. The horizontal one will be a rectangle that fills the width of the client rectangle, but is only 3 pixels high, centered on the Y coordinate of the area that I want to invalidate. The vertical one is as high as the client rectangle, but only 3 pixels wide. It's centered on the X coordinate of the area that I want to invalidate.
When the Paint handler runs, it virtually paints the entire client area, but only the pixels in the total invalidated area actually get drawn on the screen. Anything outside the invalidate area is left alone.
So, when the mouse moves, I create two rectangles (one vertical, one horizontal) that surround where the last set of cross-hairs were (so that when the pixels in those rectangles are drawn (including the background), the old cross-hairs are effectively erased) and then I create two new rectangles surrounding where the current cross-hairs should go (causing the background and the new cross-hairs to be drawn).
You are going to want to learn about invalidation rectangles if you have a complicated drawing app. For example, when the form is resized, what you want to do is invalidate only the newly unveiled rectangle(s), so that the whole drawing doesn't need to be rendered.
This works, but picking a color (or a brush) for the cross-hairs so that they always show can be difficult. Using my other suggestion (that you draw the lines twice (one to erase, one to draw) using an INVERT (i.e. XOR) brush is faster, and it always shows.

WinForms Control Alpha Blending [duplicate]

TL;DR: Look at the picture below
So I'm trying to make a little picture, and I and people around me are kinda out of ideas.
I have a table (the sitting+eating one) in the middle (seen from above), and people sitting around it. Those people are round, as isthe table.
Every person has their own picturebox, I just use one picture, rotate it, and set it as image in the next box.
Thep roblem now is: The PictureBoxes of people on corners overlap the table with empty corner, in the image there is transparency there. It should show the table below it, but instead it shows the background of the Form :(
Edit: All backgrounds are set to transparent, the Form has the marble as background and white ("Window") as background colour.
I put one person in the back and one in the front, so it's easy to see:
Edit 2 (same as ocmment):
In the last two days I read this question about 10 times, and not one that described this exact problem has had an actual answer. When trying to push one of those, I was told I should post a new question.
Example: How to make picturebox transparent?
Transparency in winforms is kind of misleading, since it's not really transparency.
Winforms controls mimic transparency by painting the part of their parent control they would hide instead of their own background.
However, they will not paint the other controls that might be partially covered by them.
This is the reason your top most picture boxes hides your big picture box.
You can overcome this by creating a custom control that inherits from PictureBox and override its OnPaintBackground method (taken, with slight adjustments, from this code project article):
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
Graphics g = e.Graphics;
if (this.Parent != null)
{
var index = Parent.Controls.GetChildIndex(this);
for (var i = Parent.Controls.Count - 1; i > index; i--)
{
var c = Parent.Controls[i];
if (c.Bounds.IntersectsWith(Bounds) && c.Visible)
{
using (var bmp = new Bitmap(c.Width, c.Height, g))
{
c.DrawToBitmap(bmp, c.ClientRectangle);
g.TranslateTransform(c.Left - Left, c.Top - Top);
g.DrawImageUnscaled(bmp, Point.Empty);
g.TranslateTransform(Left - c.Left, Top - c.Top);
}
}
}
}
}
Microsoft have published a Knowledge base article to solve this problem a long time ago, however it's a bit out-dated and it's code sample is in VB.Net.
Another option is to paint the images yourself, without picture boxes to hold them, by using Graphics.DrawImage method.
The best place to do it is probably in the OnPaint method of the form.

Make Picture boxes transparent, each overlapping the other with a corner?

TL;DR: Look at the picture below
So I'm trying to make a little picture, and I and people around me are kinda out of ideas.
I have a table (the sitting+eating one) in the middle (seen from above), and people sitting around it. Those people are round, as isthe table.
Every person has their own picturebox, I just use one picture, rotate it, and set it as image in the next box.
Thep roblem now is: The PictureBoxes of people on corners overlap the table with empty corner, in the image there is transparency there. It should show the table below it, but instead it shows the background of the Form :(
Edit: All backgrounds are set to transparent, the Form has the marble as background and white ("Window") as background colour.
I put one person in the back and one in the front, so it's easy to see:
Edit 2 (same as ocmment):
In the last two days I read this question about 10 times, and not one that described this exact problem has had an actual answer. When trying to push one of those, I was told I should post a new question.
Example: How to make picturebox transparent?
Transparency in winforms is kind of misleading, since it's not really transparency.
Winforms controls mimic transparency by painting the part of their parent control they would hide instead of their own background.
However, they will not paint the other controls that might be partially covered by them.
This is the reason your top most picture boxes hides your big picture box.
You can overcome this by creating a custom control that inherits from PictureBox and override its OnPaintBackground method (taken, with slight adjustments, from this code project article):
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
Graphics g = e.Graphics;
if (this.Parent != null)
{
var index = Parent.Controls.GetChildIndex(this);
for (var i = Parent.Controls.Count - 1; i > index; i--)
{
var c = Parent.Controls[i];
if (c.Bounds.IntersectsWith(Bounds) && c.Visible)
{
using (var bmp = new Bitmap(c.Width, c.Height, g))
{
c.DrawToBitmap(bmp, c.ClientRectangle);
g.TranslateTransform(c.Left - Left, c.Top - Top);
g.DrawImageUnscaled(bmp, Point.Empty);
g.TranslateTransform(Left - c.Left, Top - c.Top);
}
}
}
}
}
Microsoft have published a Knowledge base article to solve this problem a long time ago, however it's a bit out-dated and it's code sample is in VB.Net.
Another option is to paint the images yourself, without picture boxes to hold them, by using Graphics.DrawImage method.
The best place to do it is probably in the OnPaint method of the form.

ToolStrip Rounded Corners

I am working on a Windows Form app (C#, .NET 4.0, VS 2010), where I have a pretty standard MainForm with a ToolStrip (GripStyle: Hidden, Dock: Top, RenderMode: ManagerRenderMode). The toolstrip contains a few basic items (ToolStripLabel, ToolStripSeparator, ToolStripSplitButton).
This is rendered as follows:
At first I simply wanted to add a 'bottom' border below the toolstrip, but I also noticed that this toolstrip is rendered with 'rounded corners' (you can see the right-hand-side top and bottom ones in the image), and a vertical gradient line.
How can I make these corners NOT rounded?
I tried:
public class MainFormToolStripRenderer : ToolStripProfessionalRenderer
{
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
{
base.OnRenderToolStripBorder(e);
var y = e.ToolStrip.Height-1;
e.Graphics.DrawLine(new Pen(SystemColors.ControlDark, 1), new Point(0, y), new Point(e.ToolStrip.Width, y));
}
And wired it up via this.toolStrip_Actions.Renderer=new MainFormToolStripRenderer(); in my form initialization.
This gave me the bottom border, but didn't do anything for the rounded corners. Also, with the added bottom border, the rounded corners are more noticeable:
Next I tried drawing a rectangle during the same event handler above, to try (at least) to hide the rounded corners and vertical gradient behind a solid rectangular border. That didn't work because the available drawing area (e.AffectedBounds) is within the rounded borders.
I also tried to set the ToolStrip's RenderMode to System (and not use my renderer). In this case the toolstrip corners seem to fit snugly (rectangular), BUT the splitbutton within the toolbar seems to be broken (clicking the down arrow does not display the dropdown), for as-yet-unknown reasons, and the overall look-n-feel is a bit underwhelming (quite flat, until you hover on some buttons in the toolstrip).
I guess in the end I'd rather stick with the ManageeRenderedMode, or a custom renderer inheriting from the Professional one - but I need to get rid of the rounded corners.
Among others, I found this SO Q which seems to point close to what I'm looking at but didn't give me an answer for my case.
Thanks in advance
Try this in your renderer class:
public class MainFormToolStripRenderer : ToolStripProfessionalRenderer {
public MainFormToolStripRenderer() {
this.RoundedEdges = false;
}
}
Building on the accepted answer by LarsTech, you don't necessarily need to implement a new Renderer class, unless there are compelling reasons to do so.
You can do this as a one liner as follows:
toolStrip_Actions.Renderer = new ToolStripProfessionalRenderer() { RoundedEdges = false };
or since the default renderer for a ToolStrip with RenderMode set to ManagerRenderMode is already a ToolStripProfessionalRenderer, you may cast it as such and access the RoundedEdges property directly as follows:
((ToolStripProfessionalRenderer)toolStrip_Actions.Renderer).RoundedEdges = false;
As am05mhz mentioned, just select RenderMode > System and the rounded corners disappear:

MouseLeftButtonDown on canvas requires too much precision

I am responding to MouseLeftButtonDown events on elements added to a WPF canvas. It all works fine when clicked (i.e. the eventhandler fires off correctly), but it requires too much precision from the mouse pointer. You have to be perfectly on top of the circle to make it work. I need it to be a little more forgiving; maybe at least 1 or 2 pixles forgiving. The elements on the canvas are nice big circles (about the size of a quarter on the screen), so the circles themselves are not too small, but the StrokeWidth of each one is 1, so it is a thin line.
You can see a screenshot here: http://twitpic.com/1f2ci/full
Most graphics app aren't this picky about the mouse picking, so I want to give the user a familiar experience.
How can I make it a little more forgiving.
You can hook up to the MouseLeftButtonDown event of your root layout object instead, and check which elements is in range of a click by doing this:
List<UIElement> hits = System.Windows.Media.VisualTreeHelper.FindElementsInHostCoordinates(Point, yourLayoutRootElement) as List<UIElement>;
http://msdn.microsoft.com/en-us/library/cc838402(VS.95).aspx
For the Point parameter, you can use the MouseEventArgs parameter e, and call its GetPosition method like this:
Point p = e.GetPosition(null)
I can't remember whether to use HitTest instead of the FindElementsInHostCoordinates. Try both.
http://msdn.microsoft.com/en-us/library/ms608752.aspx
You could create 4 Point objects from the mouse position to create a fake tolerence effect, and call either FindElementsInHostCoordinates or HitTest for all 4 points.
You might want to try to fill the circle with the Transparent colour to make the whole circle clickable...
If that fails, you can also draw helper circles on the same location as the other circles. Make the circle foreground colour Transparent, and make the thickness of the brush a few pixels wider for a more acceptable clickable region around the circle..
Hope this helps!
I think I've done it (with you help to get me started)...
First, I've moved the move event handling to the Canvas instead of each Ellipse. That's good and bad, from an OOP standpoint. At least when the mouse event handling is a responsibility of the HolePattern to set it on up each Hole (the ellipse that is the visual of the Hole), it is abstracted away so that any consumer of my HolePattern will get this functioanality automactically. However, by moving it to the main UI code, I now am dealing with my canvas mouse event at a higher level. But that's not all bad either. We could discuss this part for days.
The point is, I have designed a way to create a "margin of error" when picking something on a canvas with a mouse, and then reading the Hole that the selected Ellipse belongs to, and then I can read the HolePattern that the Hole belongs to, and my entire UI (ListView, textboxes, gridview fo coordinates) are ALL updated by the existing XAML binding, and the Canvas is updated with one call to an existing method to regenerate the canvas.
To be honest, I can't believe I've figured all this out (with your help and others too, of course). It is such a cool feeling to have the vision of this this and see it come to be.
Check out the main code here:
void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int ClickMargin = 2;
Point ClickedPoint = e.GetPosition(canvas1);
Point p1 = new Point(ClickedPoint.X - ClickMargin, ClickedPoint.Y - ClickMargin);
Point p2 = new Point(ClickedPoint.X - ClickMargin, ClickedPoint.Y + ClickMargin);
Point p3 = new Point(ClickedPoint.X + ClickMargin, ClickedPoint.Y + ClickMargin);
Point p4 = new Point(ClickedPoint.X + ClickMargin, ClickedPoint.Y - ClickMargin);
var PointPickList = new Collection<Point>();
PointPickList.Add(ClickedPoint);
PointPickList.Add(p1);
PointPickList.Add(p2);
PointPickList.Add(p3);
PointPickList.Add(p4);
foreach (Point p in PointPickList)
{
HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
if (SelectedCanvasItem.VisualHit.GetType() == typeof(Ellipse))
{
var SelectedEllipseTag = SelectedCanvasItem.VisualHit.GetValue(Ellipse.TagProperty);
if (SelectedEllipseTag!=null && SelectedEllipseTag.GetType().BaseType == typeof(Hole))
{
Hole SelectedHole = (Hole)SelectedEllipseTag;
SetActivePattern(SelectedHole.ParentPattern);
SelectedHole.ParentPattern.CurrentHole = SelectedHole;
}
}
}
}
Just Increase Stroke ThickNess of the Ellipse so that it is adjustable
thus the MouseLeftButtonDown event works
Example:
In Ellipse tag:
Ellipse
Canvas.Left="10" Canvas.Top="133" Height="24" Name="ellipse1" Width="23" Stroke="Red" MouseLeftButtonDown="ellipse1_MouseLeftButtonDown" ToolTip="Temp Close" StrokeEndLineCap="Flat" StrokeThickness="12"
private void ellipse1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Application curApp = Application.Current;
curApp.Shutdown();
}

Categories

Resources