CSS3-like box shadow implementation / algorithm - c#

I am looking for or trying to implement an algorithm to draw box shadows (as in the CSS 3 specifiction) which accepts the following parameters:
Horizontal Offset
Vertical Offset
Inset
Spread
Blur
Color
(Optional: Opacity).
Where to start.
I have looked for Firefox / Chrome source code to see if I can pull an implementation from there, no such luck!
I have looked into linear gradient algorithms, drawing them with a box, which kind of works, except with rounded rectangles it leaves empty pixels in the shadow, presumably due to the radius of the edge.
I am doing this in .NET with GDI+. My aim is NOT to create drop shadows for images. I have already seen articles on this. I want to create drop shadows for shapes drawn with GDI+.
Any help appreciated!

I coded for you a DropShadowPanel that handles controls inside it and adds the shadows (outer or inner) as required by the Tag of the control.
As you can see in the image controls get their shadows as defined:
tags:
textbox: DropShadow:5,5,5,10,#000000,noinset
calendar: DropShadow:10,10,80,30,#0000FF,noinset
picturebox top left: DropShadow:-50,20,50,10,#888888,noinset
picturebox bottom left: DropShadow:10,10,20,20,#442200,inset
picturebox lower right: DropShadow:0,0,50,50,#442200,noinset
Here is the code for the Panel:
(it uses intermediate drawings into an image before drawing to the control gdi object, to not make the form crawl - this actually works pretty fast)
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication4
{
public class DropShadowPanel : Panel
{
protected override void OnControlAdded(ControlEventArgs e)
{
e.Control.Paint += new PaintEventHandler(Control_Paint);
base.OnControlAdded(e);
}
void Control_Paint(object sender, PaintEventArgs e)
{
CheckDrawInnerShadow(sender as Control, e.Graphics);
}
private void CheckDrawInnerShadow(Control sender, Graphics g)
{
var dropShadowStruct = GetDropShadowStruct(sender);
if (dropShadowStruct == null || !dropShadowStruct.Inset)
{
return;
}
DrawInsetShadow(sender as Control, g);
}
protected override void OnControlRemoved(ControlEventArgs e)
{
e.Control.Paint -= new PaintEventHandler(Control_Paint);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawShadow(Controls.OfType<Control>().Where(c => c.Tag != null && c.Tag.ToString().StartsWith("DropShadow")), e.Graphics);
}
void DrawInsetShadow(Control control, Graphics g)
{
var dropShadowStruct = GetDropShadowStruct(control);
var rInner = new Rectangle(Point.Empty, control.Size);
var img = new Bitmap(rInner.Width, rInner.Height, g);
var g2 = Graphics.FromImage(img);
g2.CompositingMode = CompositingMode.SourceCopy;
g2.FillRectangle(new SolidBrush(dropShadowStruct.Color), 0, 0, control.Width, control.Height);
rInner.Offset(dropShadowStruct.HShadow, dropShadowStruct.VShadow);
rInner.Inflate(dropShadowStruct.Blur, dropShadowStruct.Blur);
rInner.Inflate(-dropShadowStruct.Spread, -dropShadowStruct.Spread);
double blurSize = dropShadowStruct.Blur;
double blurStartSize = blurSize;
do
{
var transparency = blurSize/blurStartSize;
var color = Color.FromArgb(((int)(255 * (transparency * transparency))), dropShadowStruct.Color);
rInner.Inflate(-1,-1);
DrawRoundedRectangle(g2, rInner, (int)blurSize, Pens.Transparent, color);
blurSize--;
} while (blurSize > 0);
g.DrawImage(img, 0, 0);
g.Flush();
g2.Dispose();
img.Dispose();
}
void DrawShadow(IEnumerable<Control> controls, Graphics g)
{
foreach (var control in controls)
{
var dropShadowStruct = GetDropShadowStruct(control);
if (dropShadowStruct.Inset)
{
continue; // must be handled by the control itself
}
DrawOutsetShadow(g, dropShadowStruct, control);
}
}
// drawing the loop on an image because of speed
private void DrawOutsetShadow(Graphics g, dynamic dropShadowStruct, Control control)
{
var rOuter = control.Bounds;
var rInner = control.Bounds;
rInner.Offset(dropShadowStruct.HShadow, dropShadowStruct.VShadow);
rInner.Inflate(-dropShadowStruct.Blur, -dropShadowStruct.Blur);
rOuter.Inflate(dropShadowStruct.Spread, dropShadowStruct.Spread);
rOuter.Offset(dropShadowStruct.HShadow, dropShadowStruct.VShadow);
var originalOuter = rOuter;
var img = new Bitmap(originalOuter.Width, originalOuter.Height, g);
var g2 = Graphics.FromImage(img);
var currentBlur = 0;
do
{
var transparency = (rOuter.Height - rInner.Height)/(double) (dropShadowStruct.Blur*2 + dropShadowStruct.Spread*2);
var color = Color.FromArgb(((int)(255 * (transparency * transparency))), dropShadowStruct.Color);
var rOutput = rInner;
rOutput.Offset(-originalOuter.Left, -originalOuter.Top);
DrawRoundedRectangle(g2, rOutput, currentBlur, Pens.Transparent, color);
rInner.Inflate(1, 1);
currentBlur = (int) ((double) dropShadowStruct.Blur*(1 - (transparency*transparency)));
} while (rOuter.Contains(rInner));
g2.Flush();
g2.Dispose();
g.DrawImage(img, originalOuter);
img.Dispose();
}
private static dynamic GetDropShadowStruct(Control control)
{
if (control.Tag == null || !(control.Tag is string) || !control.Tag.ToString().StartsWith("DropShadow"))
return null;
string[] dropShadowParams = control.Tag.ToString().Split(':')[1].Split(',');
var dropShadowStruct = new
{
HShadow = Convert.ToInt32(dropShadowParams[0]),
VShadow = Convert.ToInt32(dropShadowParams[1]),
Blur = Convert.ToInt32(dropShadowParams[2]),
Spread = Convert.ToInt32(dropShadowParams[3]),
Color = ColorTranslator.FromHtml(dropShadowParams[4]),
Inset = dropShadowParams[5].ToLowerInvariant() == "inset"
};
return dropShadowStruct;
}
private void DrawRoundedRectangle(Graphics gfx, Rectangle bounds, int cornerRadius, Pen drawPen, Color fillColor)
{
int strokeOffset = Convert.ToInt32(Math.Ceiling(drawPen.Width));
bounds = Rectangle.Inflate(bounds, -strokeOffset, -strokeOffset);
var gfxPath = new GraphicsPath();
if (cornerRadius > 0)
{
gfxPath.AddArc(bounds.X, bounds.Y, cornerRadius, cornerRadius, 180, 90);
gfxPath.AddArc(bounds.X + bounds.Width - cornerRadius, bounds.Y, cornerRadius, cornerRadius, 270, 90);
gfxPath.AddArc(bounds.X + bounds.Width - cornerRadius, bounds.Y + bounds.Height - cornerRadius, cornerRadius,
cornerRadius, 0, 90);
gfxPath.AddArc(bounds.X, bounds.Y + bounds.Height - cornerRadius, cornerRadius, cornerRadius, 90, 90);
}
else
{
gfxPath.AddRectangle(bounds);
}
gfxPath.CloseAllFigures();
gfx.FillPath(new SolidBrush(fillColor), gfxPath);
if (drawPen != Pens.Transparent)
{
var pen = new Pen(drawPen.Color);
pen.EndCap = pen.StartCap = LineCap.Round;
gfx.DrawPath(pen, gfxPath);
}
}
}
}
Code is written fast without much review so there may be bugs especially if you set wring tags on controls).
PS. You may notice that inner shadow does not work for some controls. This is because they are wrappers around windows system controls. The panel cannot overcome this by itself, but you may do it like here: http://www.codeproject.com/Articles/4548/Generating-missing-Paint-event-for-TreeView-and-Li

Related

Smooth curves in button

I want to create a toggle button similar to this or to this. Animation does not matter to me. I tried to create a toggle button with the code below. However I cannot get it to be smooth curves and edges. I am using windows forms application and a total beginner in C# UI design.
My question is that is it possible to make the curves smooth, or it will still stay like this? I imported the code in the second link, but still the button did not appear smooth.
I also used -
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
Here is full code for the button -
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace MyWorkspace
{
public class ToggleButton : Control
{
#region variables
public enum ButtonState { ON, OFF };
private ButtonState toggleState = ButtonState.OFF;
private Rectangle contentRectangle = Rectangle.Empty;
#endregion
#region properties
public ButtonState ToggleState
{
get
{
return toggleState;
}
set
{
if (toggleState != value)
{
toggleState = value;
Invalidate();
this.Refresh();
}
}
}
#endregion
public ToggleButton() : base()
{
this.MinimumSize = new Size(50, 25);
this.MaximumSize = new Size(50, 25);
contentRectangle = new Rectangle(0, 0, this.Width, this.Height);
this.BackColor = Application.RenderWithVisualStyles ? Color.Azure : this.Parent.BackColor;
}
// Draw the large or small button, depending on the current state.
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
Rectangle rect = new Rectangle(0, contentRectangle.Y, contentRectangle.Height, contentRectangle.Height);
GraphicsPath gp = new GraphicsPath();
int d = this.Height;
gp.AddArc(contentRectangle.X, contentRectangle.Y, d, d, 180, 90);
gp.AddArc(contentRectangle.X + contentRectangle.Width - d, contentRectangle.Y, d, d, 270, 90);
gp.AddArc(contentRectangle.X + contentRectangle.Width - d, contentRectangle.Y + contentRectangle.Height - d, d, d, 0, 90);
gp.AddArc(contentRectangle.X, contentRectangle.Y + contentRectangle.Height - d, d, d, 90, 90);
this.Region = new Region(gp);
Rectangle ar2 = new Rectangle(rect.X, contentRectangle.Y, (rect.X + rect.Width / 2) + contentRectangle.Right, contentRectangle.Height);
LinearGradientBrush br;
Rectangle ellipse_rect;
if (toggleState == ButtonState.ON)
{
br = new LinearGradientBrush(ar2, Color.FromArgb(0, 127, 234), Color.FromArgb(96, 174, 241), LinearGradientMode.Vertical);
ellipse_rect = new Rectangle(contentRectangle.Right - (contentRectangle.Height -2),
contentRectangle.Y, contentRectangle.Height - 4, contentRectangle.Height);
}
else
{
br = new LinearGradientBrush(ar2, Color.FromArgb(120, 120, 120), Color.Silver, LinearGradientMode.Vertical);
ellipse_rect = rect;
}
e.Graphics.FillRectangle(br, ar2);
e.Graphics.DrawEllipse(new Pen(Color.Gray, 2f), ellipse_rect);
LinearGradientBrush br2 = new LinearGradientBrush(rect, Color.White, Color.Silver, LinearGradientMode.Vertical);
e.Graphics.FillEllipse(br2, ellipse_rect);
Color c = this.Parent != null ? this.Parent.BackColor : Color.White;
e.Graphics.DrawPath(new Pen(c, 2f), gp);
}
protected override void OnClick(EventArgs e)
{
if (toggleState == ButtonState.ON)
toggleState = ButtonState.OFF;
else
toggleState = ButtonState.ON;
Invalidate();
}
}
}
Please post an image of what your solution looks like, but from what you have described I would guess you do not have anti-aliasing enabled. Here is a MSDN article about how to enable anti-aliasing. If you are going to be changing the look of many controls in your application, you may want to look into Windows Presentation Foundation or the new Universal Windows Platform.
In contrast to Windows Forms, both WPF and UWP are vector based interface layers that are designed using XAML. Because of the way WPF and UWP implement their designing system, it is very easy to completely customize the look and feel of your application. In addition both platforms have built in functionality for animations.

Toggle switch control in Windows Forms

I am designing a toggle switch control using CheckBox, but currently my control only draws a circle. How can I draw round shapes like the below image and how can I change the location of the circle based on the value of the control to represent checked and unchecked states like the below image?
Here is my code:
public class MyCheckBox:CheckBox
{
public MyCheckBox()
{
this.Appearance = System.Windows.Forms.Appearance.Button;
this.BackColor = Color.Transparent;
this.TextAlign = ContentAlignment.MiddleCenter;
this.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.FlatAppearance.BorderColor = Color.RoyalBlue;
this.FlatAppearance.BorderSize = 2;
}
protected override void OnPaint(PaintEventArgs e)
{
this.OnPaintBackground(e);
using (var path = new GraphicsPath())
{
var c = e.Graphics.ClipBounds;
var r = this.ClientRectangle;
r.Inflate(-FlatAppearance.BorderSize, -FlatAppearance.BorderSize);
path.AddEllipse(r);
e.Graphics.SetClip(path);
base.OnPaint(e);
e.Graphics.SetClip(c);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
if (this.Checked)
{
using (var p = new Pen(FlatAppearance.BorderColor,
FlatAppearance.BorderSize))
{
e.Graphics.DrawEllipse(p, r);
}
}
}
}
}
I know this is a Windows Forms question. But you may want to take a look at Toggle Switches or read more about Universal Windows App Components.
Anyway, here is an answer for Windows Forms developers. It shows how we can customize rendering of a checkbox to have such appearance.
Currently you are drawing only an ellipse, and it's quite a toggle button. But if you want to show it like the below image, you should first draw a round shape for background, and then based on the Checked value, draw the check circle. Using the code in Example part of the answer you can have a CheckBox with such a UI:
Example
The important thing about this sample is it's completely a CheckBox control and supports check using mouse and keyboard. It also supports data-binding and all other standard features of CheckBox. The code is not perfect, but it is a good start point to have a yes/no toggle switch:
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public class MyCheckBox : CheckBox
{
public MyCheckBox()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
Padding = new Padding(6);
}
protected override void OnPaint(PaintEventArgs e)
{
this.OnPaintBackground(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (var path = new GraphicsPath())
{
var d = Padding.All;
var r = this.Height - 2 * d;
path.AddArc(d, d, r, r, 90, 180);
path.AddArc(this.Width - r - d, d, r, r, -90, 180);
path.CloseFigure();
e.Graphics.FillPath(Checked ? Brushes.DarkGray : Brushes.LightGray, path);
r = Height - 1;
var rect = Checked ? new Rectangle(Width - r - 1, 0, r, r)
: new Rectangle(0, 0, r, r);
e.Graphics.FillEllipse(Checked ? Brushes.Green : Brushes.WhiteSmoke, rect);
}
}
}

Enhanced crosshair cursor possible?

I have found this question (as a few others), but this is the one I have implemented so far:
Crosshair cursor with additional lines in C#
As it states, I can use a stock cursor "cross" directly in the IDE. This is a really good way to do things. The answer specified in the answer above draws a cross on the screen at the given width / height. Eg:
private Cursor crossCursor(Pen pen, Brush brush, int x, int y)
{
var pic = new Bitmap(x, y);
Graphics gr = Graphics.FromImage(pic);
var pathX = new GraphicsPath();
var pathY = new GraphicsPath();
pathX.AddLine(0, y / 2, x, y / 2);
pathY.AddLine(x / 2, 0, x / 2, y);
gr.DrawPath(pen, pathX);
gr.DrawPath(pen, pathY);
IntPtr ptr = pic.GetHicon();
var c = new Cursor(ptr);
return c;
}
My issue is that I want my cross hairs to extend to the Bounds of the viewing area. To provide context here, I have:
//Form
//TableLayoutPanel
//UserControl (fills the TableLayoutPanel visible area)
So how can I adjust my cursor so that the lines extend (much like in CAD pacakages)?
Thanks.
Update: I have tried calling the method from here:
protected override void OnLoad(System.EventArgs e)
{
Cursor = crossCursor(Pens.WhiteSmoke, Brushes.WhiteSmoke, Bounds.Width, Bounds.Height);
}
But it is not Ok because at this point in time Bounds is returning a dimension of 150 by 150 which is not the size of the TableLayoutPanel.
Update: I have adjuted it to use the Resize handler instead and it does improve things:
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Cursor = crossCursor(Pens.WhiteSmoke, Brushes.WhiteSmoke, Bounds.Width, Bounds.Height);
}
The only problem now (and it kind of makes sense I suppose) is that the cursor will only take the full width and height of the view when it is central to the view. As soon as I move about in the view that cursor does not adjust. I always want a horizontal/vertical line through the mouse position (not just the initial cross).
See:
The crosshairs need extending (the thicker red lines). Either I need to constantly create the cursor as the mouse moves or construct the two lines another way. What to do?
I came across this:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/7bdbad6d-1f65-461b-8f0c-6ef4f243fa6b/crosshair-cursor-using-c?forum=csharpgeneral
So, instead of changing the cursor object I now draw lines in the controls MouseMove handler:
Region r = new Region();
r.Union(new Rectangle(0, lastY, this.Width, 1));
r.Union(new Rectangle(lastX, 0, 1, this.Height));
this.Invalidate(r);
this.Update();
Graphics g = Graphics.FromHwnd(this.Handle);
g.DrawLine(Pens.White, 0, e.Y, this.Width, e.Y);
g.DrawLine(Pens.White, e.X, 0, e.X, this.Height);
int intDiameter = 20;//the diameter of this circle
g.DrawEllipse(Pens.White, e.X - intDiameter / 2, e.Y - intDiameter / 2, 20, 20);
//to draw the circle
lastX = e.X;
lastY = e.Y;
It works, but I get noticiable screen flicker doing it this way.
You don't need to create a cursor. You can create a double buffered control and draw cross over control.
using System;
using System.Drawing;
using System.Windows.Forms;
public class DrawingSurface : Control
{
Pen crossPen;
Pen rectanglePen;
Brush rectangleBrush;
public DrawingSurface()
{
this.DoubleBuffered = true;
this.ResizeRedraw = true;
crossPen = new Pen(Color.Red, 2);
rectangleBrush = new SolidBrush(Color.FromArgb(50, Color.Blue));
rectanglePen = new Pen(Color.Blue, 1);
}
bool mouseDown = false;
Point startPoint = Point.Empty;
Point endPoint = Point.Empty;
protected override void OnMouseDown(MouseEventArgs e)
{
startPoint = e.Location;
mouseDown = true;
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
mouseDown = false;
base.OnMouseUp(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
endPoint = e.Location;
this.Invalidate();
base.OnMouseMove(e);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
if (this.ClientRectangle.Contains(endPoint))
DrawCross(e.Graphics, endPoint);
if (mouseDown)
DrawRectangle(e.Graphics, startPoint, endPoint);
}
void DrawCross(Graphics g, Point point)
{
g.DrawLine(crossPen, new Point(0, point.Y), new Point(Width, point.Y));
g.DrawLine(crossPen, new Point(point.X, 0), new Point(point.X, Height));
}
void DrawRectangle(Graphics g, Point point1, Point point2)
{
var rectangle = new Rectangle(
Math.Min(point1.X, point2.X), Math.Min(point1.Y, point2.Y),
Math.Abs(point1.X - point2.X), Math.Abs(point1.Y - point2.Y));
g.FillRectangle(rectangleBrush, rectangle);
g.DrawRectangle(rectanglePen, rectangle);
}
protected override void Dispose(bool disposing)
{
crossPen.Dispose();
rectanglePen.Dispose();
rectangleBrush.Dispose();
base.Dispose(disposing);
}
}

Auto Resize Font to fit rectangle

How can I create in .NET 4.5 / C# a font with a adaptive size to fit a specified rectangle ?
I have a resize based on the string length, the longer the string, the smaller the fontsize, but it does not work very well, if the string is too long the text gets very small. The problem with this method is that if I change the rectangle size all the font sizes are not good again.
It's been a while but I came across this issue.
MSDN offers a sample with a GetAdjustedFont method to help with this process:
(shamelessly stolen from https://msdn.microsoft.com/en-us/library/bb986765.aspx)
public Font GetAdjustedFont(Graphics g, string graphicString, Font originalFont, int containerWidth, int maxFontSize, int minFontSize, bool smallestOnFail)
{
Font testFont = null;
// We utilize MeasureString which we get via a control instance
for (int adjustedSize = maxFontSize; adjustedSize >= minFontSize; adjustedSize--)
{
testFont = new Font(originalFont.Name, adjustedSize, originalFont.Style);
// Test the string with the new size
SizeF adjustedSizeNew = g.MeasureString(graphicString, testFont);
if (containerWidth > Convert.ToInt32(adjustedSizeNew.Width))
{
// Good font, return it
return testFont;
}
}
// If you get here there was no fontsize that worked
// return minimumSize or original?
if (smallestOnFail)
{
return testFont;
}
else
{
return originalFont;
}
}
With Graphics.MeasureString you can measure the size of a string, so you can calculate what you need.
Sample from MSDN
private void MeasureStringMin(PaintEventArgs e)
{
// Set up string.
string measureString = "Measure String";
Font stringFont = new Font("Arial", 16);
// Measure string.
SizeF stringSize = new SizeF();
stringSize = e.Graphics.MeasureString(measureString, stringFont);
// Draw rectangle representing size of string.
e.Graphics.DrawRectangle(new Pen(Color.Red, 1), 0.0F, 0.0F, stringSize.Width, stringSize.Height);
// Draw string to screen.
e.Graphics.DrawString(measureString, stringFont, Brushes.Black, new PointF(0, 0));
}
You can measure the string width with whatever font size (e.g. 16), and then you can calculate your desired font size like this:
fontSize = (previouslyMeasuredFontSize * targetWidthOfString) / previouslyMeasuredStringWidth;
I also needed something similar. I created some code to answer the need of drawing text inside a defined rectangle with several auto font size options (see DrawMethod enum). It also supports auto warp with and without auto font size. My solution was inspired by the answers above.
Be sure to add WindowsBase and PresentationCore assemblies.
DrawTextToBitmap.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ReportPrepare
{
class TextDrawing
{
public enum DrawMethod
{
AutosizeAccordingToText, // create the smallest bitmap needed to draw the text without word warp
AutoFitInConstantRectangleWithoutWarp, // draw text with the biggest font possible while not exceeding rectangle dimensions, without word warp
AutoWarpInConstantRectangle, // draw text in rectangle while performing word warp. font size is a constant input. drawing may exceed bitmap rectangle.
AutoFitInConstantRectangleWithWarp // draw text with the biggest font possible while not exceeding rectangle dimensions, with word warp
}
private static void SetGraphicsHighQualityForTextRendering(Graphics g)
{
// The smoothing mode specifies whether lines, curves, and the edges of filled areas use smoothing (also called antialiasing). One exception is that path gradient brushes do not obey the smoothing mode. Areas filled using a PathGradientBrush are rendered the same way (aliased) regardless of the SmoothingMode property.
g.SmoothingMode = SmoothingMode.AntiAlias;
// The interpolation mode determines how intermediate values between two endpoints are calculated.
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
// Use this property to specify either higher quality, slower rendering, or lower quality, faster rendering of the contents of this Graphics object.
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
// This one is important
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
}
public static Size MeasureDrawTextBitmapSize(string text, Font font)
{
Bitmap bmp = new Bitmap(1, 1);
using (Graphics g = Graphics.FromImage(bmp))
{
SizeF size = g.MeasureString(text, font);
return new Size((int)(Math.Ceiling(size.Width)), (int)(Math.Ceiling(size.Height)));
}
}
public static int GetMaximumFontSizeFitInRectangle(string text, Font font, RectangleF rectanglef, bool isWarp, int MinumumFontSize=6, int MaximumFontSize=1000)
{
Font newFont;
Rectangle rect = Rectangle.Ceiling(rectanglef);
for (int newFontSize = MinumumFontSize; ; newFontSize++)
{
newFont = new Font(font.FontFamily, newFontSize, font.Style);
List<string> ls = WarpText(text, newFont, rect.Width);
StringBuilder sb = new StringBuilder();
if (isWarp)
{
for (int i = 0; i < ls.Count; ++i)
{
sb.Append(ls[i] + Environment.NewLine);
}
}
else
{
sb.Append(text);
}
Size size = MeasureDrawTextBitmapSize(sb.ToString(), newFont);
if (size.Width > rectanglef.Width || size.Height > rectanglef.Height)
{
return (newFontSize - 1);
}
if (newFontSize >= MaximumFontSize)
{
return (newFontSize - 1);
}
}
}
public static List<string> WarpText(string text, Font font, int lineWidthInPixels)
{
string[] originalLines = text.Split(new string[] { " " }, StringSplitOptions.None);
List<string> wrappedLines = new List<string>();
StringBuilder actualLine = new StringBuilder();
double actualWidthInPixels = 0;
foreach (string str in originalLines)
{
Size size = MeasureDrawTextBitmapSize(str, font);
actualLine.Append(str + " ");
actualWidthInPixels += size.Width;
if (actualWidthInPixels > lineWidthInPixels)
{
actualLine = actualLine.Remove(actualLine.ToString().Length - str.Length - 1, str.Length);
wrappedLines.Add(actualLine.ToString());
actualLine.Clear();
actualLine.Append(str + " ");
actualWidthInPixels = size.Width;
}
}
if (actualLine.Length > 0)
{
wrappedLines.Add(actualLine.ToString());
}
return wrappedLines;
}
public static Bitmap DrawTextToBitmap(string text, Font font, Color color, DrawMethod mode, RectangleF rectanglef)
{
StringFormat drawFormat = new StringFormat();
Bitmap bmp;
switch (mode)
{
case DrawMethod.AutosizeAccordingToText:
{
Size size = MeasureDrawTextBitmapSize(text, font);
if (size.Width == 0 || size.Height == 0)
{
bmp = new Bitmap(1, 1);
}
else
{
bmp = new Bitmap(size.Width, size.Height);
}
using (Graphics g = Graphics.FromImage(bmp))
{
SetGraphicsHighQualityForTextRendering(g);
g.DrawString(text, font, new SolidBrush(color), 0, 0);
return bmp;
}
}
case DrawMethod.AutoWarpInConstantRectangle:
{
Rectangle rect = Rectangle.Ceiling(rectanglef);
bmp = new Bitmap(rect.Width,rect.Height);
if (rect.Width == 0 || rect.Height == 0)
{
bmp = new Bitmap(1, 1);
}
else
{
bmp = new Bitmap(rect.Width, rect.Height);
}
using (Graphics g = Graphics.FromImage(bmp))
{
SetGraphicsHighQualityForTextRendering(g);
g.DrawString(text, font, new SolidBrush(color), rectanglef, drawFormat);
return bmp;
}
}
case DrawMethod.AutoFitInConstantRectangleWithoutWarp:
{
Rectangle rect = Rectangle.Ceiling(rectanglef);
bmp = new Bitmap(rect.Width, rect.Height);
if (rect.Width == 0 || rect.Height == 0)
{
bmp = new Bitmap(1, 1);
}
else
{
bmp = new Bitmap(rect.Width, rect.Height);
}
using (Graphics g = Graphics.FromImage(bmp))
{
int fontSize = GetMaximumFontSizeFitInRectangle(text, font, rectanglef, false);
SetGraphicsHighQualityForTextRendering(g);
g.DrawString(text, new Font(font.FontFamily, fontSize,font.Style, GraphicsUnit.Point), new SolidBrush(color), rectanglef, drawFormat);
return bmp;
}
}
case DrawMethod.AutoFitInConstantRectangleWithWarp:
{
Rectangle rect = Rectangle.Ceiling(rectanglef);
if (rect.Width == 0 || rect.Height == 0)
{
bmp = new Bitmap(1, 1);
}
else
{
bmp = new Bitmap(rect.Width, rect.Height);
}
using (Graphics g = Graphics.FromImage(bmp))
{
int fontSize = GetMaximumFontSizeFitInRectangle(text, font, rectanglef, true);
SetGraphicsHighQualityForTextRendering(g);
g.DrawString(text, new Font(font.FontFamily, fontSize, font.Style, GraphicsUnit.Point), new SolidBrush(color), rectanglef, drawFormat);
return bmp;
}
}
}
return null;
}
}
}
Usage Example:
Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
// add WindowsBase and PresentationCore assemblies
namespace ReportPrepare
{
public partial class Form1 : Form
{
PictureBox picbox = new PictureBox();
int i = 0;
Timer t = new Timer();
public Form1()
{
InitializeComponent();
this.Controls.Add(picbox);
picbox.Dock = DockStyle.Fill;
t.Interval = 5000;
t.Tick += t_Tick;
t.Enabled = true;
this.Shown += Form1_Shown;
this.SizeChanged += Form1_SizeChanged;
this.Size = new Size(812, 400);
this.StartPosition = FormStartPosition.CenterScreen;
}
void Form1_Shown(object sender, EventArgs e)
{
DrawText();
}
void t_Tick(object sender, EventArgs e)
{
i++;
if (i > 3)
{
i = 0;
}
DrawText();
}
private void DrawText()
{
// text and font
string text = "one two three four five six seven eight nine ten eleven twelve";
Font font = new System.Drawing.Font("Arial", 30, FontStyle.Regular, GraphicsUnit.Point);
switch (i)
{
case 0:
picbox.Image = TextDrawing.DrawTextToBitmap(text, font, Color.Red, TextDrawing.DrawMethod.AutosizeAccordingToText, new RectangleF(0, 0, picbox.Width, picbox.Height));
break;
case 1:
picbox.Image = TextDrawing.DrawTextToBitmap(text, font, Color.Red, TextDrawing.DrawMethod.AutoFitInConstantRectangleWithoutWarp, new RectangleF(0, 0, picbox.Width, picbox.Height));
break;
case 2:
picbox.Image = TextDrawing.DrawTextToBitmap(text, font, Color.Red, TextDrawing.DrawMethod.AutoWarpInConstantRectangle, new RectangleF(0, 0, picbox.Width, picbox.Height));
break;
case 3:
picbox.Image = TextDrawing.DrawTextToBitmap(text, font, Color.Red, TextDrawing.DrawMethod.AutoFitInConstantRectangleWithWarp, new RectangleF(0, 0, picbox.Width, picbox.Height));
break;
}
this.Text = ((TextDrawing.DrawMethod)(i)).ToString() + " Please resize window size by mouse to see drawing methods differences";
}
private void Form1_SizeChanged(object sender, EventArgs e)
{
t.Enabled = false;
t.Enabled = true;
DrawText();
}
}
}
The example toggles between drawing modes automatically once every 5 seconds. The picturebox is docked inside main form. Resizing of the form shows the user the difference between the drawing modes.

Extracting an Event (tabControl_DrawItem) to Class Library

I have the following code under a TabConttrols DrawItem event that I am trying to extract into a class file. I am having trouble since it is tied to an event. Any hints or pointers would be greatly appreciated.
private void tabCaseNotes_DrawItem(object sender, DrawItemEventArgs e)
{
TabPage currentTab = tabCaseNotes.TabPages[e.Index];
Rectangle itemRect = tabCaseNotes.GetTabRect(e.Index);
SolidBrush fillBrush = new SolidBrush(Color.Linen);
SolidBrush textBrush = new SolidBrush(Color.Black);
StringFormat sf = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
//If we are currently painting the Selected TabItem we'll
//change the brush colors and inflate the rectangle.
if (System.Convert.ToBoolean(e.State & DrawItemState.Selected))
{
fillBrush.Color = Color.LightSteelBlue;
textBrush.Color = Color.Black;
itemRect.Inflate(2, 2);
}
//Set up rotation for left and right aligned tabs
if (tabCaseNotes.Alignment == TabAlignment.Left || tabCaseNotes.Alignment == TabAlignment.Right)
{
float rotateAngle = 90;
if (tabCaseNotes.Alignment == TabAlignment.Left)
rotateAngle = 270;
PointF cp = new PointF(itemRect.Left + (itemRect.Width / 2), itemRect.Top + (itemRect.Height / 2));
e.Graphics.TranslateTransform(cp.X, cp.Y);
e.Graphics.RotateTransform(rotateAngle);
itemRect = new Rectangle(-(itemRect.Height / 2), -(itemRect.Width / 2), itemRect.Height, itemRect.Width);
}
//Next we'll paint the TabItem with our Fill Brush
e.Graphics.FillRectangle(fillBrush, itemRect);
//Now draw the text.
e.Graphics.DrawString(currentTab.Text, e.Font, textBrush, (RectangleF)itemRect, sf);
//Reset any Graphics rotation
e.Graphics.ResetTransform();
//Finally, we should Dispose of our brushes.
fillBrush.Dispose();
textBrush.Dispose();
}
Depends on what you're trying to achieve. You could always sub class TabControl or you could encapsulate the drawing code in a class that you pass a TabControl to.
public class TabRenderer
{
private TabControl _tabControl;
public TabRenderer(TabControl tabControl)
{
_tabControl = tabControl;
_tabControl.DrawMode = TabDrawMode.OwnerDrawFixed;
_tabControl.DrawItem += TabControlDrawItem;
}
private void TabControlDrawItem(object sender, DrawItemEventArgs e)
{
// Your drawing code...
}
}

Categories

Resources