Draw shape with transparent background - c#

I have a program with a lot of straight lines which represent pipes (oil pipes).
The lines are user controls where the line is drawn in the Paint event of each control with the following sample code for a vertical line:
e.Graphics.DrawLine(linePen, new Point(0, 0), new Point(0, this.Height));
The issue is that I want to display the flow direction of the oil in the pipes, and therefore need to add an arrow somehow.
StartCap and EndCap don't work here for the following reason:
The user control itself must be exactly the width of the line (pipe) to not have any "dead" area around the line, which will overlap other user controls on my form later on.
If using StartCap or EndCap, and a line width of e.g. 2 pixel, the user control must be wider for the arrow (StartCap or EndCap) to be drawn.
The easy way would be to make the "empty" area transparent, but after googling for a very long time I gave up; there doesn't seem to be a reliable way to achieve this with a user control.
Then I thought I could just make a separate user control that would only draw the arrow, but I then still have the problem with the undrawn area covering the other user controls.
Does anyone have a suggestion how to either:
make the user control area that is not drawn on transparent?
some other approach to achieve the above?
As my "pipes" are only 2 pixel wide there is no possibility to draw anything inside the line/pipe :(
Any suggestions/comments are much appreciated!

There is a way to make a Control's Background transparent in winforms (with overlapping each other). However moving the control at runtime may make it flicker. Another choice is using Region to specify the region for your control so that it has theoretically any shape. This is what I can do for you, just a demo:
public partial class VerticalArrow : UserControl
{
public VerticalArrow()
{
InitializeComponent();
Direction = ArrowDirection.Up;
}
public enum ArrowDirection
{
Up,
Down
}
ArrowDirection dir;
public ArrowDirection Direction
{
get { return dir; }
set
{
if (dir != value)
{
dir = value;
UpdateRegion();
}
}
}
//default values of ArrowWidth and ArrowHeight
int arrowWidth = 14;
int arrowHeight = 18;
public int ArrowWidth
{
get { return arrowWidth; }
set
{
if (arrowWidth != value)
{
arrowWidth = value;
UpdateRegion();
}
}
}
public int ArrowHeight
{
get { return arrowHeight; }
set
{
if (arrowHeight != value)
{
arrowHeight = value;
UpdateRegion();
}
}
}
//This will keep the size of the UserControl fixed at design time.
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
base.SetBoundsCore(x, y, Math.Max(ArrowWidth, 4), height, specified);
}
private void UpdateRegion()
{
GraphicsPath gp = new GraphicsPath();
int dx = ArrowWidth / 2 - 1;
int dy = ArrowHeight / 2;
Point p1 = new Point(dx, Direction == ArrowDirection.Up ? dy : 1);
Point p2 = new Point(ArrowWidth - dx, Direction == ArrowDirection.Up ? dy + 1: 1);
Point p3 = new Point(ArrowWidth - dx, Direction == ArrowDirection.Up ? ClientSize.Height : ClientSize.Height - dy);
Point p4 = new Point(dx, Direction == ArrowDirection.Up ? ClientSize.Height : ClientSize.Height - dy);
Point q1 = Direction == ArrowDirection.Up ? new Point(0, ArrowHeight) : new Point(0, ClientSize.Height - ArrowHeight);
Point q2 = Direction == ArrowDirection.Up ? new Point(dx, 0) : new Point(dx, ClientSize.Height);
Point q3 = Direction == ArrowDirection.Up ? new Point(ArrowWidth, ArrowHeight) : new Point(ArrowWidth, ClientSize.Height - ArrowHeight);
if (Direction == ArrowDirection.Up) gp.AddPolygon(new Point[] { p1, q1, q2, q3, p2, p3, p4 });
else gp.AddPolygon(new Point[] {p1,p2,p3,q3,q2,q1,p4});
Region = new Region(gp);
}
protected override void OnSizeChanged(EventArgs e)
{
UpdateRegion();
base.OnSizeChanged(e);
}
}
And here is the result:
You can use BackColor to change the color of the arrow. If we just need to draw the arrow, the code would be simpler, especially with the help of System.Drawing.Drawing2D.AdjustableArrowCap and deal with properties CustomStartCap and CustomEndCap. However as for your requirement, using Region is almost the best choice in many cases.
UPDATE
If you want the solution using transparent Background in which we use Pen and CustomLineCap instead of clipping Region, the VerticalArrow has to inherit from Control. Here is the code:
public class VerticalArrow : Control
{
public VerticalArrow()
{
Width = 30;
Height = 100;
Direction = ArrowDirection.Up;
ArrowHeight = 4;
ArrowWidth = 4;
TrunkWidth = 2;
SetStyle(ControlStyles.Opaque, true);
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x20;
return cp;
}
}
public ArrowDirection Direction { get; set; }
public int ArrowHeight { get; set; }
public int ArrowWidth { get; set; }
public int TrunkWidth { get; set; }
Point p1, p2;
public enum ArrowDirection
{
Up,
Down,
UpDown
}
protected override void OnSizeChanged(EventArgs e)
{
p1 = new Point(Width / 2, 0);
p2 = new Point(Width / 2, Height);
base.OnSizeChanged(e);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (Pen p = new Pen(ForeColor))
{
using (AdjustableArrowCap cap = new AdjustableArrowCap(ArrowWidth, ArrowHeight))
{
if (Direction == ArrowDirection.Up || Direction == ArrowDirection.UpDown) p.CustomStartCap = cap;
if (Direction == ArrowDirection.Down || Direction == ArrowDirection.UpDown) p.CustomEndCap = cap;
}
p.Width = TrunkWidth;
e.Graphics.DrawLine(p, p1, p2);
}
}
}
Screenshot:
To change Arrow color change the ForeColor.

Related

How to optimize draw area in pixel art editor

I have pixel art creator program, and I have rectangles on canvas that are one field (pixel?). And this is good solution on not huge amount of it (for example 128x128). if i want to create 1024x1024 rectangles on canvas this process is very long, ram usage is about 1-2 gb and after that program runs very slowly. How to optimize this, or create better solution?
Using a Rectangle to represent each pixel is the wrong way to do this. As a FrameworkElement, every rectangle participates in layout and input hit testing. That approach is too heavy weight to be scalable. Abandon it now.
I would recommend drawing directly to a WriteableBitmap and using a custom surface to render the bitmap as the user draws.
Below is a minimum proof of concept that allows simple drawing in a single color. It requires the WriteableBitmapEx library, which is available from NuGet.
public class PixelEditor : FrameworkElement
{
private readonly Surface _surface;
private readonly Visual _gridLines;
public int PixelWidth { get; } = 128;
public int PixelHeight { get; } = 128;
public int Magnification { get; } = 10;
public PixelEditor()
{
_surface = new Surface(this);
_gridLines = CreateGridLines();
Cursor = Cursors.Pen;
AddVisualChild(_surface);
AddVisualChild(_gridLines);
}
protected override int VisualChildrenCount => 2;
protected override Visual GetVisualChild(int index)
{
return index == 0 ? _surface : _gridLines;
}
private void Draw()
{
var p = Mouse.GetPosition(_surface);
var magnification = Magnification;
var surfaceWidth = PixelWidth * magnification;
var surfaceHeight = PixelHeight * magnification;
if (p.X < 0 || p.X >= surfaceWidth || p.Y < 0 || p.Y >= surfaceHeight)
return;
_surface.SetColor(
(int)(p.X / magnification),
(int)(p.Y / magnification),
Colors.DodgerBlue);
_surface.InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && IsMouseCaptured)
Draw();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
CaptureMouse();
Draw();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override Size MeasureOverride(Size availableSize)
{
var magnification = Magnification;
var size = new Size(PixelWidth* magnification, PixelHeight * magnification);
_surface.Measure(size);
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
_surface.Arrange(new Rect(finalSize));
return finalSize;
}
private Visual CreateGridLines()
{
var dv = new DrawingVisual();
var dc = dv.RenderOpen();
var w = PixelWidth;
var h = PixelHeight;
var m = Magnification;
var d = -0.5d; // snap gridlines to device pixels
var pen = new Pen(new SolidColorBrush(Color.FromArgb(63, 63, 63, 63)), 1d);
pen.Freeze();
for (var x = 1; x < w; x++)
dc.DrawLine(pen, new Point(x * m + d, 0), new Point(x * m + d, h * m));
for (var y = 1; y < h; y++)
dc.DrawLine(pen, new Point(0, y * m + d), new Point(w * m, y * m + d));
dc.Close();
return dv;
}
private sealed class Surface : FrameworkElement
{
private readonly PixelEditor _owner;
private readonly WriteableBitmap _bitmap;
public Surface(PixelEditor owner)
{
_owner = owner;
_bitmap = BitmapFactory.New(owner.PixelWidth, owner.PixelHeight);
_bitmap.Clear(Colors.White);
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.NearestNeighbor);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var magnification = _owner.Magnification;
var width = _bitmap.PixelWidth * magnification;
var height = _bitmap.PixelHeight * magnification;
dc.DrawImage(_bitmap, new Rect(0, 0, width, height));
}
internal void SetColor(int x, int y, Color color)
{
_bitmap.SetPixel(x, y, color);
}
}
}
Just import it into your Xaml, preferably inside a ScrollViewer:
<Window x:Class="WpfTest.PixelArtEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfTest"
Title="PixelArtEditor"
Width="640"
Height="480">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<l:PixelEditor />
</ScrollViewer>
</Window>
Obviously, this is a far cry from being a fully-featured pixel art editor, but it's functional, and it's enough to get you on the right track. The difference in memory usage between editing a 128x128 image vs. 1024x1024 is about ~30mb. Fire it up and see it in action:
Hey, that was fun! Thanks for the diversion.
Just to improve Mike Strobel solution to snap gridlines to device pixels.
var d = -0.5d; // snap gridlines to device pixels
using (DrawingContext dc = _dv.RenderOpen())
{
GuidelineSet guidelineSet = new GuidelineSet();
guidelineSet.GuidelinesX.Add(0.5);
guidelineSet.GuidelinesY.Add(0.5);
dc.PushGuidelineSet(guidelineSet);
// Draw grid
}

Create a custom control with the form of a pie without tip?

I want to create a own Control with the form of a pie without the tip of it like in the picture afterwards. I just dont get how to get this working.
http://www.directupload.net/file/d/3563/a3hvpodw_png.htm
//EDIT:
Ok I forgot to mention that i want to fill it afterwards. So if I'm right I need a region for that but i don't know how to do this. And to be honest i didn't think about your idea so far. I just used a Pie so far, like this:
Graphics gfx = pe.Graphics;
Pen p = new Pen(Color.Red);
gfx.DrawPie(p, 0, 0, 200, 200, 0, 45);
base.OnPaint(pe);
It's my first time with custom controls, so sorry if it is a little bit goofy what I'm asking.
Try this:
class ShapedControl : Control
{
private float startAngle;
private float sweepAngle;
private float innerRadius;
private float outerRadius;
public ShapedControl()
{
InnerRadius = 30;
OuterRadius = 60;
StartAngle = 0;
SweepAngle = 360;
}
[DefaultValue(0)]
[Description("The starting angle for the pie section, measured in degrees clockwise from the X-axis.")]
public float StartAngle
{
get { return startAngle; }
set
{
startAngle = value;
Invalidate();
}
}
[DefaultValue(360)]
[Description("The angle between StartAngle and the end of the pie section, measured in degrees clockwise from the X-axis.")]
public float SweepAngle
{
get { return sweepAngle; }
set
{
sweepAngle = value;
Invalidate();
}
}
[DefaultValue(20)]
[Description("Inner radius of the excluded inner area of the pie")]
public float InnerRadius
{
get { return innerRadius; }
set
{
innerRadius = value;
Invalidate();
}
}
[DefaultValue(30)]
[Description("Outer radius of the pie")]
public float OuterRadius
{
get { return outerRadius; }
set
{
outerRadius = value;
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
g.Clear(this.BackColor);
GraphicsPath gp1 = new GraphicsPath();
GraphicsPath gp2 = new GraphicsPath();
float xInnerPos = -innerRadius / 2f + this.Width / 2f;
float yInnerPos = -innerRadius / 2f + this.Height / 2f;
float xOuterPos = -outerRadius / 2f + this.Width / 2f;
float yOuterPos = -outerRadius / 2f + this.Height / 2f;
if (innerRadius != 0.0)
gp1.AddPie(xInnerPos, yInnerPos, innerRadius, innerRadius, startAngle, sweepAngle);
gp2.AddPie(xOuterPos, yOuterPos, outerRadius, outerRadius, startAngle, sweepAngle);
Region rg1 = new System.Drawing.Region(gp1);
Region rg2 = new System.Drawing.Region(gp2);
g.DrawPath(Pens.Transparent, gp1);
g.DrawPath(Pens.Transparent, gp2);
rg1.Xor(rg2);
g.FillRegion(Brushes.Black, rg1);
this.Region = rg1;
}
//Just for testing purpose. Place a breakpoint
//in here and you'll see it will only get called when
//you click inside the "pie" shape
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
}
}
EDIT: made the code better by centering the shape and adding properties for the VS Designer , stolen from another answer ;-)
MORE EDITS: taking care of the case where Inner Radius == 0
Try this code sample of a complex shaped control.
You can control its shape using StartAngle, SweepAngle and InnerPercent properties.
public partial class PathUserControl : UserControl
{
private readonly GraphicsPath outerPath = new GraphicsPath();
private readonly GraphicsPath innerPath = new GraphicsPath();
private float startAngle;
private float sweepAngle = 60;
private float innerPercent = 30;
public PathUserControl()
{
base.BackColor = SystemColors.ControlDark;
}
[DefaultValue(0)]
[Description("The starting angle for the pie section, measured in degrees clockwise from the X-axis.")]
public float StartAngle
{
get { return startAngle; }
set
{
startAngle = value;
SetRegion();
}
}
[DefaultValue(60)]
[Description("The angle between StartAngle and the end of the pie section, measured in degrees clockwise from the X-axis.")]
public float SweepAngle
{
get { return sweepAngle; }
set
{
sweepAngle = value;
SetRegion();
}
}
[DefaultValue(30)]
[Description("Percent of the radius of the excluded inner area of the pie, measured from 0 to 100.")]
public float InnerPercent
{
get { return innerPercent; }
set
{
if (value < 0 || value > 100f)
throw new ArgumentOutOfRangeException("value", "Percent must be in the range 0 .. 100");
innerPercent = value;
SetRegion();
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
SetRegion();
}
private void SetRegion()
{
if (Region != null)
{
Region.Dispose();
Region = null;
}
if (ClientSize.IsEmpty)
return;
float innerCoef = 0.01f * InnerPercent;
outerPath.Reset();
innerPath.Reset();
outerPath.AddPie(0, 0, ClientSize.Width, ClientSize.Height, StartAngle, SweepAngle);
innerPath.AddPie(ClientSize.Width * (1 - innerCoef) / 2, ClientSize.Height * (1 - innerCoef) / 2, ClientSize.Width * innerCoef, ClientSize.Height * innerCoef, StartAngle, SweepAngle);
Region region = new Region(outerPath);
region.Xor(innerPath);
Region = region;
}
}
EDIT The #LucMorin idea with XOR is great, I've stolen it.

Rotating drawn objects

so i have drawn a few objects , circles squares or even lines. This is the code i use to draw the images:
Graphics surface = this.splitContainer1.Panel2.CreateGraphics();
Pen pen1 = new Pen(ColorR.BackColor, float.Parse(boxWidth.Text));
switch (currentObject)
{
case "line":
if (step == 1)
{
splitContainer1.Panel2.Focus();
one.X = e.X;
one.Y = e.Y;
boxX.Text = one.X.ToString();
boxY.Text = one.Y.ToString();
step = 2;
}
else
{
two.X = e.X;
two.Y = e.Y;
boxX2.Text = two.X.ToString();
boxY2.Text = two.Y.ToString();
surface.DrawLine(pen1, one, two);
step = 1;
}
break;
case "circle":
if (step == 1)
{
boxX.Text = e.X.ToString();
boxY.Text = e.Y.ToString();
step = 2;
}
else
{
int tempX = int.Parse(boxX.Text);
int tempY = int.Parse(boxY.Text);
int tempX2 = e.X;
int tempY2 = e.Y;
int sideX, sideY;
if (tempX > tempX2)
{
sideX = tempX - tempX2;
}
else
{
sideX = tempX2 - tempX;
}
if (tempY > tempY2)
{
sideY = tempY - tempY2;
}
else
{
sideY = tempY2 - tempY;
}
double tempRadius;
tempRadius = Math.Sqrt(sideX * sideX + sideY * sideY);
tempRadius *= 2;
bWidth.Text = bHeight.Text = Convert.ToInt32(tempRadius).ToString();
surface.DrawEllipse(
pen1,
int.Parse(boxX.Text) - int.Parse(bWidth.Text) / 2,
int.Parse(boxY.Text) - int.Parse(bHeight.Text) / 2,
float.Parse(bWidth.Text), float.Parse(bHeight.Text));
step = 1;
}
break;
case "square":
if (step == 1)
{
boxX.Text = e.X.ToString();
boxY.Text = e.Y.ToString();
step = 2;
}
else if (step == 2)
{
int tempX = e.X;
if (tempX > int.Parse(boxX.Text))
{
bWidth.Text = (tempX - int.Parse(boxX.Text)).ToString();
}
else
{
bWidth.Text = (int.Parse(boxX.Text) - tempX).ToString();
}
step = 3;
}
else
{
int tempY = e.Y;
if (tempY > int.Parse(boxY.Text))
{
bHeight.Text = (tempY - int.Parse(boxY.Text)).ToString();
}
else
{
bHeight.Text = (int.Parse(boxY.Text) - tempY).ToString();
}
surface.DrawRectangle(
pen1,
int.Parse(boxX.Text),
int.Parse(boxY.Text),
int.Parse(bWidth.Text),
int.Parse(bHeight.Text));
step = 1;
}
break;
}
So after I draw the images, I want to be able to select a figure and--for example--change the color or rotate it. But I cant seem to figure it out how to do it.
I suggest defining a base abstract shape class that has methods all shapes should provide, such as a method to draw itself on a graphics object, a method that says whether a point is within it / should could as selecting it, a method to rotate it by a given amount and a method to change the color.
Once you've got your shape class then you've got to work out how to fill in the methods for each derived shape. For drawing you've already got the code. For selecting it, that will be dependent on the shape. For something like a circle it's fairly easy, just calculate the distance between the center of the circle, and the point clicked, for something like a line it's harder as you don't want the user to have to click it exactly.
That leaves rotating and changing the colour. Changing the colour is easy, just have a Color property on the Shape class, then when you draw your shapes, use that colour to create a brush or pen.
As for rotation, take a look at Graphics.RotateTransform.
public abstract class Shape
{
public Color Color { get; set; }
public float Rotation { get; set; }
public Point Position { get; set; }
public Shape(Color color, float rotation, Point position)
{
Color = color;
Rotation = rotation;
Position = position;
}
public void ChangeRotation(float amount)
{
Rotation += amount;
}
public abstract void Draw(Graphics graphics);
public abstract bool WithinBounds(Point point);
}
public class Circle : Shape
{
public float Radius { get; set; }
public Circle(Color color, float rotation, Point position)
:base(color, rotation, position)
{
}
public override void Draw(Graphics graphics)
{
}
public override bool WithinBounds(Point point)
{
if (Math.Sqrt(Math.Pow(point.X - Position.X, 2) + Math.Pow(point.Y - Position.Y, 2)) <= Radius)
return true;
else
return false;
// Note, if statement could be removed to become the below:
//return Math.Sqrt(Math.Pow(point.X - Position.X, 2) + Math.Pow(point.Y - Position.Y, 2)) <= Radius;
}
}
Have a look at the RotateTransform method of the Graphics object. There is a TranslateTransform method too.

WPF custom panel scaling to fit window

I have a WPF custom panel which arranges its child elements in a spiral shape as per my requirements. The problem I am having is to do with scaling of the items when the window is resized - at the moment it does not scale. Can anyone provide a solution? Thanks - Ben
Custom panel
public class TagPanel : Panel
{
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
Size resultSize = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Measure(availableSize);
resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ?
resultSize.Width : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ?
resultSize.Height : availableSize.Height;
return resultSize;
}
protected class InnerPos
{
public UIElement Element { get; set; }
public Size Size { get; set; }
}
private Point GetSpiralPosition(double theta, Size windowSize)
{
double a = 5.0;
double n = 1.0;
double r = a * (Math.Pow(theta, 1.0 / n));
double x = r * Math.Cos(theta);
double y = r * Math.Sin(theta);
x += windowSize.Width / 2.0;
y += windowSize.Height / 2.0;
return new Point(x,y);
}
private Rect CreateRectangleCenteredAtPoint(Point pt, double width, double height)
{
return new Rect(new Point(pt.X - (width / 2.0), pt.Y - (height / 2.0)),
new Size(width, height));
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
//double startPos = 0.0;
List<InnerPos> positions = new List<InnerPos>();
foreach (UIElement ch in Children)
{
// If this is the first time
// we've seen this child, add our transforms
//if (ch.RenderTransform as TransformGroup == null)
//{
// ch.RenderTransformOrigin = new Point(0, 0.5);
// TransformGroup group = new TransformGroup();
// ch.RenderTransform = group;
// group.Children.Add(new ScaleTransform());
// group.Children.Add(new TranslateTransform());
//}
positions.Add(new InnerPos()
{
Element = ch,
Size = ch.DesiredSize
});
}
//double currentTopMax
List<Rect> alreadyUsedPositions = new List<Rect>();
foreach (InnerPos child in positions.OrderByDescending(i => i.Size.Width))
{
for (double theta = 0.0; theta < 100.0; theta += 0.1)
{
Point spiralPos = GetSpiralPosition(theta, finalSize);
Rect centeredRect = CreateRectangleCenteredAtPoint(spiralPos,
child.Element.DesiredSize.Width,
child.Element.DesiredSize.Height);
bool posIsOk = true;
foreach (Rect existing in alreadyUsedPositions)
{
bool positionClashes = existing.IntersectsWith(centeredRect);
if (positionClashes == true)
{
posIsOk = false;
break;
}
}
if (posIsOk)
{
alreadyUsedPositions.Add(centeredRect);
child.Element.Arrange(centeredRect);
break;
}
}
}
return finalSize;
}
}
Is the HorizontalAlignment and VerticalAlignment of the Panel set to Stretch and the Width/Height to Auto (double.NaN)?
It looks like you are passing the finalSize into GetSpiralPosition(), which is the function where all the true work happens. Without seeing the code to this function it is difficult to say more, but I assume you are calculating correctly off of the finalSize.
Have you debugged to see if Arrange is getting called with the updated size when the window is resized? There would be two ways to test this: First, put a trace point instead of a breakpoint. Dump finalSize to the output window and see if it changes when the window is resized. Second, add a handler in the window to SizeChanged. Put a breakpoint in the handler. When the breakpoint is hit, put a breakpoint in the arrange method of the custom panel and then run.

How to customise rendering of a ToolStripTextBox?

I like the ToolStripProfessionalRenderer style quite a lot, but I do not like the way it renders a ToolStripTextBox. Here, ToolStripSystemRenderer does a better job IMO. Now is there a way to combine both renderers' behaviour to use system style for text boxes and pro style for everything else? I have successfully managed to use pro style for buttons and system style for the rest (by deriving both classes). But text boxes in a ToolStrip don't seem to be handled by the renderer. Using .NET Reflector, those text boxes don't even seem to have a Paint event handler, although it's called by the ToolStrip.OnPaint method. I'm wondering where's the code to paint such a text box at all and how it can be configured to draw a text box like all other text boxes.
If you just want system rendering, the easiest approach is to use ToolStripControlHost instead:
class ToolStripSystemTextBox : ToolStripControlHost
{
public ToolStripSystemTextBox : base(new TextBox()) { }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[TypeConverter(typeof(ExpandableObjectConverter))]
public TextBox TextBox { get { return Control as TextBox; } }
}
I've taken the easy way out here and exposed the underlying TextBox directly to the form designer, instead of delegating all its properties. Obviously you can write all the property delgation code if you want.
On the other hand, if anyone wants to do truly custom rendering, I'll tell you what ToolStripTextBox does. Instead of hosting a TextBox directly, it hosts a private derived class called ToolStripTextBoxControl. This class overrides its WndProc in order to directly handle WM_NCPAINT. And then instead of delegating the actual drawing to the Renderer, it checks the Renderer's Type, and then branches to different rendering code inside of ToolStripTextBoxControl. It's pretty ugly.
It may not be necessary to dive into "WndProc" either. This was done without it:
The Question really is how do you make a "nice looking" TextBox, because as described by j__m, you can just use ToolStripControlHost, to host a custom control in your tool strip.
More here:
http://msdn.microsoft.com/en-us/library/system.windows.forms.toolstripcontrolhost.aspx
And as documented, the control you use can be a Custom Control.
Firstly, It's insanely tricky to make a custom TextBox Control. If you want to go:
public partial class TextBoxOwnerDraw : TextBox
You are in for HUGE trouble! But it doesn't have to be. Here is a little trick:
If you make a custom control as a Panel, then add the TextBox to the Panel, then set the Textbox borders to None... you can achieve the result as above, and best of all, its just a regular old TextBox, so cut copy paste all works, right click works!
Ok, here is the code for a nice looking textbox:
public partial class TextBoxOwnerDraw : Panel
{
private TextBox MyTextBox;
private int cornerRadius = 1;
private Color borderColor = Color.Black;
private int borderSize = 1;
private Size preferredSize = new Size(120, 25); // Use 25 for height, so it sits in the middle
/// <summary>
/// Access the textbox
/// </summary>
public TextBox TextBox
{
get { return MyTextBox; }
}
public int CornerRadius
{
get { return cornerRadius; }
set
{
cornerRadius = value;
RestyleTextBox();
this.Invalidate();
}
}
public Color BorderColor
{
get { return borderColor; }
set
{
borderColor = value;
RestyleTextBox();
this.Invalidate();
}
}
public int BorderSize
{
get { return borderSize; }
set
{
borderSize = value;
RestyleTextBox();
this.Invalidate();
}
}
public Size PrefSize
{
get { return preferredSize; }
set
{
preferredSize = value;
RestyleTextBox();
this.Invalidate();
}
}
public TextBoxOwnerDraw()
{
MyTextBox = new TextBox();
this.Controls.Add(MyTextBox);
RestyleTextBox();
}
private void RestyleTextBox()
{
double TopPos = Math.Floor(((double)this.preferredSize.Height / 2) - ((double)MyTextBox.Height / 2));
MyTextBox.BackColor = Color.White;
MyTextBox.BorderStyle = BorderStyle.None;
MyTextBox.Multiline = false;
MyTextBox.Top = (int)TopPos;
MyTextBox.Left = this.BorderSize;
MyTextBox.Width = preferredSize.Width - (this.BorderSize * 2);
this.Height = MyTextBox.Height + (this.BorderSize * 2); // Will be ignored, but if you use elsewhere
this.Width = preferredSize.Width;
}
protected override void OnPaint(PaintEventArgs e)
{
if (cornerRadius > 0 && borderSize > 0)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Rectangle cRect = this.ClientRectangle;
Rectangle safeRect = new Rectangle(cRect.X, cRect.Y, cRect.Width - this.BorderSize, cRect.Height - this.BorderSize);
// Background color
using (Brush bgBrush = new SolidBrush(MyTextBox.BackColor))
{
DrawRoundRect(g, bgBrush, safeRect, (float)this.CornerRadius);
}
// Border
using (Pen borderPen = new Pen(this.BorderColor, (float)this.BorderSize))
{
DrawRoundRect(g, borderPen, safeRect, (float)this.CornerRadius);
}
}
base.OnPaint(e);
}
#region Private Methods
private GraphicsPath getRoundRect(int x, int y, int width, int height, float radius)
{
GraphicsPath gp = new GraphicsPath();
gp.AddLine(x + radius, y, x + width - (radius * 2), y); // Line
gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90); // Corner (Top Right)
gp.AddLine(x + width, y + radius, x + width, y + height - (radius * 2)); // Line
gp.AddArc(x + width - (radius * 2), y + height - (radius * 2), radius * 2, radius * 2, 0, 90); // Corner (Bottom Right)
gp.AddLine(x + width - (radius * 2), y + height, x + radius, y + height); // Line
gp.AddArc(x, y + height - (radius * 2), radius * 2, radius * 2, 90, 90); // Corner (Bottom Left)
gp.AddLine(x, y + height - (radius * 2), x, y + radius); // Line
gp.AddArc(x, y, radius * 2, radius * 2, 180, 90); // Corner (Top Left)
gp.CloseFigure();
return gp;
}
private void DrawRoundRect(Graphics g, Pen p, Rectangle rect, float radius)
{
GraphicsPath gp = getRoundRect(rect.X, rect.Y, rect.Width, rect.Height, radius);
g.DrawPath(p, gp);
gp.Dispose();
}
private void DrawRoundRect(Graphics g, Pen p, int x, int y, int width, int height, float radius)
{
GraphicsPath gp = getRoundRect(x, y, width, height, radius);
g.DrawPath(p, gp);
gp.Dispose();
}
private void DrawRoundRect(Graphics g, Brush b, int x, int y, int width, int height, float radius)
{
GraphicsPath gp = getRoundRect(x, y, width, height, radius);
g.FillPath(b, gp);
gp.Dispose();
}
private void DrawRoundRect(Graphics g, Brush b, Rectangle rect, float radius)
{
GraphicsPath gp = getRoundRect(rect.X, rect.Y, rect.Width, rect.Height, radius);
g.FillPath(b, gp);
gp.Dispose();
}
#endregion
}
Now for the ToolStripControlHost
public partial class ToolStripTextBoxOwnerDraw : ToolStripControlHost
{
private TextBoxOwnerDraw InnerTextBox
{
get { return Control as TextBoxOwnerDraw; }
}
public ToolStripTextBoxOwnerDraw() : base(new TextBoxOwnerDraw()) { }
public TextBox ToolStripTextBox
{
get { return InnerTextBox.TextBox; }
}
public int CornerRadius
{
get { return InnerTextBox.CornerRadius; }
set
{
InnerTextBox.CornerRadius = value;
InnerTextBox.Invalidate();
}
}
public Color BorderColor
{
get { return InnerTextBox.BorderColor; }
set
{
InnerTextBox.BorderColor = value;
InnerTextBox.Invalidate();
}
}
public int BorderSize
{
get { return InnerTextBox.BorderSize; }
set
{
InnerTextBox.BorderSize = value;
InnerTextBox.Invalidate();
}
}
public override Size GetPreferredSize(Size constrainingSize)
{
return InnerTextBox.PrefSize;
}
}
Then When you want to use it, just add it to the tool bar:
ToolStripTextBoxOwnerDraw tBox = new ToolStripTextBoxOwnerDraw();
this.toolStripMain.Items.Add(tBox);
or however you want to add it. If you are in Visual Studio, the preview window supports rendering this Control.
There is only one thing to remember, when accessing the TextBox with the actual text in it, its:
tBox.ToolStripTextBox.Text;

Categories