Dynamically display an array of PictureBoxes - Performance issue - c#

I'd like to display coverarts for each album of an MP3 library, a bit like Itunes does (at a later stage, i'd like to click one any of these coverarts to display the list of songs).
I have a form with a panel panel1 and here is the loop i'm using :
int i = 0;
int perCol = 4;
int disBetWeen = 15;
int width = 250;
int height = 250;
foreach(var alb in mp2)
{
myPicBox.Add(new PictureBox());
myPicBox[i].SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
myPicBox[i].Location = new System.Drawing.Point(disBetWeen + (disBetWeen * (i % perCol) +(width * (i % perCol))),
disBetWeen + (disBetWeen * (i / perCol))+ (height * (i / perCol)));
myPicBox[i].Name = "pictureBox" + i;
myPicBox[i].Size = new System.Drawing.Size(width, height);
myPicBox[i].ImageLocation = #"C:/Users/Utilisateur/Music/label.jpg";
panel1.Controls.Add(myPicBox[i]);
i++;
}
I'm using the same picture per picturebox for convenience, but i'll use the coverart embedded in each mp3 file eventually.
It's working fine with an abstract of the library (around 50), but i have several thousands of albums. I tried and as expected, it takes a long time to load and i cannot really scroll afterward.
Is there any way to load only what's displayed ? and then how to assess what is displayed with the scrollbars.
Thanks

Winforms really isn't suited to this sort of thing... Using standard controls, you'd probably need to either provision all the image boxes up front and load images in as they become visible, or manage some overflow placeholder for the appropriate length so the scrollbars work.
Assuming Winforms is your only option, I'd suggest you look into creating a custom control with a scroll bar and manually driving the OnPaint event.
That would allow you to keep a cache of images in memory to draw the current view [and a few either side], while giving you total control over when they're loaded/unloaded [well, as "total" as you can get in a managed language - you may still need tune garbage collection]
To get into some details....
Create a new control
namespace SO61574511 {
// Let's inherit from Panel so we can take advantage of scrolling for free
public class ImageScroller : Panel {
// Some numbers to allow us to calculate layout
private const int BitmapWidth = 100;
private const int BitmapSpacing = 10;
// imageCache will keep the images in memory. Ideally we should unload images we're not using, but that's a problem for the reader
private Bitmap[] imageCache;
public ImageScroller() {
//How many images to put in the cache? If you don't know up-front, use a list instead of an array
imageCache = new Bitmap[100];
//Take advantage of Winforms scrolling
this.AutoScroll = true;
this.AutoScrollMinSize = new Size((BitmapWidth + BitmapSpacing) * imageCache.Length, this.Height);
}
protected override void OnPaint(PaintEventArgs e) {
// Let Winforms paint its bits (like the scroll bar)
base.OnPaint(e);
// Translate whatever _we_ paint by the position of the scrollbar
e.Graphics.TranslateTransform(this.AutoScrollPosition.X,
this.AutoScrollPosition.Y);
// Use this to decide which images are out of sight and can be unloaded
var current_scroll_position = this.HorizontalScroll.Value;
// Loop through the images you want to show (probably not all of them, just those close to the view area)
for (int i = 0; i < imageCache.Length; i++) {
e.Graphics.DrawImage(GetImage(i), new PointF(i * (BitmapSpacing + BitmapWidth), 0));
}
}
//You won't need a random, just for my demo colours below
private Random rnd = new Random();
private Bitmap GetImage(int id) {
// This method is responsible for getting an image.
// If it's already in the cache, use it, otherwise load it
if (imageCache[id] == null) {
//Do something here to load an image into the cache
imageCache[id] = new Bitmap(100, 100);
// For demo purposes, I'll flood fill a random colour
using (var gfx = Graphics.FromImage(imageCache[id])) {
gfx.Clear(Color.FromArgb(255, rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255)));
}
}
return imageCache[id];
}
}
}
And Load it into your form, docking to fill the screen....
public Form1() {
InitializeComponent();
this.Controls.Add(new ImageScroller {
Dock = DockStyle.Fill
});
}
You can see it in action here: https://www.youtube.com/watch?v=ftr3v6pLnqA (excuse the mouse trails, I captured area outside the window)

Related

Drawing simultaneously on two controls in two windows without delay

In my application, there are 2 windows and both contain a PictureBox. The first (pb1) allows interaction and the image can be changed through click- and mouseMove-events. These events call pb1.Invalidate(); which works fine.
I want the second PictureBox (pb2) to redraw as well so I call pb2.Invalidate() from the paint-event of pb1. [Just for context, the second PictureBox shows nearly the same Image but on a bigger scale and some parts of the drawing will be left out in the future so I use the same Method in both paint events which decides what to draw and what not]
It works but it's "laggy" and I want it to be as smooth as the paint on the first PictureBox. I reduced the paint event just to a grid for test purposes.
Both windows are double buffered.
I tried replacing the picture boxes with SKGLControls from SkiaSharp (which should have better performance). The example code still uses the SkiaEvents so don't be confused if the problem occurs with both controls.
I tried to use .Update() or .Refresh() instead of .Invalidate() but i guess its to much to handle, the application just crashes..
Here is the method that is called by both OnPaint events
public void Update(SKPaintGLSurfaceEventArgs e, bool bigscreen)
{
SKCanvas canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Beige);
//Zoom to specified area
SKMatrix matrix = SKMatrix.Identity;
if (!bigscreen)
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(canvasSize / (float)zoomArea.Width, canvasSize / (float)zoomArea.Height));
}
else
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(bigCanvasSize / (float)zoomArea.Width, bigCanvasSize / (float)zoomArea.Height));
}
matrix = matrix.PreConcat(SKMatrix.CreateTranslation(-zoomArea.X, -zoomArea.Y));
canvas.SetMatrix(matrix);
DrawGrid(canvas);
}
and the grid-draw method
private void DrawGrid(SKCanvas canvas)
{
using (SKPaint paint = new SKPaint() { IsAntialias = true,Color=SKColors.LightGray,StrokeWidth = 1})
{
canvas.DrawLine(0, 0, 0, gridCanvas.Height, paint); //Size gridCanvas is always the same at the moment and defines the space where the grid is drawn
canvas.DrawLine(0, 0, gridCanvas.Width, 0, paint);
for (int i = 0; i <= (gridCanvas.Width - gridoffsetX) / pxPerSquare; i++)
{
canvas.DrawLine(i * pxPerSquare + gridoffsetX, 0, i * pxPerSquare + gridoffsetX, gridCanvas.Height, paint);
}
for (int i = 0; i <= (gridCanvas.Height - gridoffsetY) / pxPerSquare; i++)
{
canvas.DrawLine(0, i * pxPerSquare + gridoffsetY, gridCanvas.Width, i * pxPerSquare + gridoffsetY, paint);
}
}
}
and finally the original Paint Event
private void Pb1_PaintSurface(object sender, SKPaintGLSurfaceEventArgs e)
{
win2.UpdateDrawing(); //Just calls .Invalidate() on pb2
painter.Update(e, false);
}
examplePicture
So my question is: Is there a way to make both controls draw at nearly the same time without delay, although I don't understand why the first PictureBox draws in real time and the second doesn't...
Thanks!
after searching for day i found this page right after posting, which helped me:
Onpaint events (invalidated) changing execution order after a period normal operation (runtime)

Resizing drawlines on a paint event

I've seen few questions about this problem, I tried every solution but none of them worked for my case.
My code is working; this image shows what happens when I click on Draw button.
I need to zoom on that drawing.Is it possible to code something like autocad feature "zoom/extent"?
Pen myPen = new Pen(Color.Black);
int centerpointx, centerpointy;
private void pictureBoxDraw_Paint(object sender, PaintEventArgs e)
{
centerpointx = pictureBoxDraw.Size.Width/2;
centerpointy = pictureBoxDraw.Size.Height/2;
myPen.Width = 2;
if (binary > 0)
{
var sizecrestgeo = 40;
var distancearraycrestgeo = new float[sizecrestgeo];
var elevationarraycrestgeo = new float[sizecrestgeo];
for (int i = 0; i < sizecrestgeo; i++)
{
distancearraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[0].Value.ToString());
elevationarraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[1].Value.ToString())*-1;
}
for (int i=0; i < sizecrestgeo-1; i++)
{
e.Graphics.DrawLine(myPen, distancearraycrestgeo[i]+centerpointx, elevationarraycrestgeo[i]+centerpointy, distancearraycrestgeo[i + 1]+centerpointx, elevationarraycrestgeo[i + 1]+centerpointy);
}
}
else
{
}
}
private void buttonDraw_Click_1(object sender, EventArgs e)
{
if (Hd > 0.0001)
{
binary = 1;
pictureBoxDraw.Invalidate();
}
else
{
MessageBox.Show("No data to draw, perform analysis first.");
}
}
private void buttoncleardraw_Click(object sender, EventArgs e)
{
binary = 0;
pictureBoxDraw.Invalidate();
}
}
This is not so hard, provided you know all the puzzle pieces.
Let's start with the obvious one:
You can scale the Graphics object to create zoomed graphics with ScaleTransform.
As I mentioned, this will include the widths of pens, font sizes and also any images you draw (though not the hatches of a HatchBrush).
You also asked about keeping the drawing 'centered'. This is a non-obvious concept: Just what is the center of your drawing surface??
When zooming (just like rotating) you always need to know the center point of the zoom (or the rotation.) By default this is the origin (0,0). I chose the center of the Panel. You may want to pick some other point..
Once you do you can move the origin of the graphics viewport to this point with TranslateTransform.
Once you have achieved all this you almost certainly will want to allow scrolling.
To do so you have two options:
You can keep AutoScroll = false and nest the canvas control inside another control, usually a Panel, which has AutoScroll = true; next make the canvas control big enough to always hold your drawing and you're done.
Or you can turn on AutoScroll for the canvas control and also set a large enough AutoScrollMinSize. If you then add the current scrolling position to the translation you are also done. Let's see this solution in action:
This is the code in the Paint event:
Size sz = panel3.ClientSize;
Point center = new Point(sz.Width / 2, sz.Height / 2);
Graphics g = e.Graphics;
// center point for testing only!
g.DrawEllipse(Pens.Orange, center.X - 3, center.Y - 3, 6, 6);
// you determine the value of the zooming!
float zoom = (trackBar1.Value+1) / 3f;
// move the scrolled center to the origon
g.TranslateTransform(center.X + panel3.AutoScrollPosition.X,
center.Y + panel3.AutoScrollPosition.Y);
// scale the graphics
g.ScaleTransform(zoom, zoom);
// draw some stuff..
using(Pen pen = new Pen(Color.Yellow, 0.1f))
for (int i = -100; i < 100; i+= 10)
g.DrawEllipse(Pens.Yellow, i-22,i-22,44,44);
A few notes:
I draw an orange circle in the center to show this point is invariant.
My coordinates go from the negative to the positive so you can see that this works nicely.
I draw with a tiny pen width; so the width of the drawing only changes once the resulting pen goes over 1 pixel. Anything draw will always be draw with 1 pxiel width, though.
I first translate and then scale so I don't have to calculate scaled poitions.
The only line in the TrackBar's Scroll event is to trigger the Paint event: panel3.Invalidate();
The only settings needed for the Panel are
panel3.AutoScroll = true;
panel3.AutoScrollMinSize = new Size(500, 500); // use the size you want to allow!
However to avoid flicker it is highly recommended to use a DoubleBuffered control, maybe a Panel subclass like this:
class DrawPanel : Panel
{
public DrawPanel() { DoubleBuffered = true; }
}
Update: Instead of a Panel, which is a Container control and not really meant to draw onto you can use a Picturebox or a Label (with Autosize=false); both have the DoubleBuffered property turned on out of the box and support drawing better than Panels do.
Graphics.ScaleTransform() is how you can zoom. Try using something like this inside your paint event handler:
e.Graphics.ScaleTransform(2.0F, 2.0F);

How to stop Paint events from stacking up

I'm creating an application to schedule different tasks. These are displayed by drawing rectangles on a panel. This has to be responsive. So I need to draw and invalidate on every size change. When I reach a maximum height of my planning panel it autoscrolls.
The problem is when I grab the scrollbar and start scrolling away for a bit, when I release the scrollbar my whole application and computer freezes.
Most likely this is due to the onpaint event being called on every little scroll and stacking up, leaving the application to hang until they are all done.
Now my question is: How would I be able to fix this? Possibly by keeping the paint event from being called multiple times, but how?
The method called by the paint event:
private void panelPlanning_Paint(object sender, PaintEventArgs e)
{
for (int i = 0; i < userList.Count; i++)
{
Label temp = new Label();
temp.Text = userList[i].Text;
temp.Width = panelUsers.Width;
temp.Height = 50;
temp.BorderStyle = BorderStyle.FixedSingle;
temp.Location = new Point(0, i * 50);
temp.TextAlign = ContentAlignment.MiddleCenter;
panelUsers.Controls.Add(temp);
foreach (FullTask task in taskList)
{
if (task.AssignedTo == userList[i].Text && task.StartDate != "" && task.DueDate != "")
{
DateTime start = DateTime.ParseExact(task.StartDate, "dd/MM/yyyy", CultureInfo.InvariantCulture);
DateTime end = DateTime.ParseExact(task.DueDate, "dd/MM/yyyy", CultureInfo.InvariantCulture);
Brush brush;
if (task.Priority == Enums.priorities[2])
{
brush = Brushes.Yellow;
}
else if (task.Priority == Enums.priorities[1])
{
brush = new SolidBrush(Color.FromArgb(255, 0, 80, 123));
}
else
{
brush = Brushes.Red;
}
panelPlanning.CreateGraphics().FillRectangle(brush, new Rectangle((start.Subtract(dtPickerStart.Value).Days + 1) * labelDaysWidth, i * 50, (end.Subtract(start).Days + 1) * labelDaysWidth, 25));
}
}
}
}
You are adding new Label controls on every single paint event. Don't do this:
// Label temp = new Label();
// temp.Text = userList[i].Text;
// temp.Width = panelUsers.Width;
// temp.Height = 50;
// temp.BorderStyle = BorderStyle.FixedSingle;
// temp.Location = new Point(0, i * 50);
// temp.TextAlign = ContentAlignment.MiddleCenter;
// panelUsers.Controls.Add(temp);
Also, use the e.Graphics object supplied by the argument, not CreateGraphics,
The following line of code should only be executed once:
panelUsers.Controls.Add(temp);
You are constantly adding a new instance of temp to the panelUsers control.
You have to trace which FullTask actually has to be displayed on the screen. This can be achieved by looking on Control.ClientRectangle and keeping in mind current scroll position.
In this way from the all set of the FullTasks to draw you will get only that subset of tasks that actually visible on the screen, and draw only those ones.
This is a correct way of dealing with drawable artifacts which is implemented in more or less all drawing frameworks 2D or even 3D.
There is also concept of tracing if some element is covered (or part of it is covered) by another element, so it will be "culed", but in your case this doesn't seem to matter.
Pay attention also on fact that you adding controls. Do not do that. If the amount of FullTasks is big, just draw them with Graphics object.
as already Described by #LarsTech, don't add controls in the paint event, and use the supplied Graphics object instead of your own ...
If you still face performance problems, consider painting on a bitmap whenever you change something, and only draw that bitmap onto the screen in onPaint

C# Drawing - best solution

Today I am trying to solve problem with a blinking panel, when I draw onto it.
Lots of threads I read, like these:
how to stop flickering C# winforms,
Double buffering with Panel,
How can I draw on Panel so it does not blink?
So I tried to draw onto PictureBox, MyPanel with doubleBuffered, but the best solution I found, when I read, that I can't use g.Clear() every time, after that, even on non-doubleBuffered panel, blinking disappeared.
I even read, that I should free Graphics after draw is done. So I use everywhere using(Graphics g = panel.CreateGraphics()).
So my question, is it a great idea to create graphics for bitmap only when I draw something to it? Because before I created Bitmap, and Graphics (only for this bitmap, not for all components), so I had Graphics available for this bitmap every time
Here is my code:
public void newSizeDrawing()
{
Size size = collector.getLetterSize(selectedName);
Size drawingSize = new Size(size.Width * (pixelSizeArray[pixelSize] + 1),size.Height * (pixelSizeArray[pixelSize] + 1));
bitmapDraw = new Bitmap(drawingSize.Width, drawingSize.Height);
int width = (this.MinimumSize.Width - panelDraw.MinimumSize.Width) + drawingSize.Width + 10;
int height = (this.MinimumSize.Height - panelDraw.MinimumSize.Height) + drawingSize.Height + 10;
this.Size = new Size(
(width > this.MinimumSize.Width) ? width : this.MinimumSize.Width,
(height > this.MinimumSize.Height) ? height : this.MinimumSize.Height);
zeroDrawPosition = new Point((panelDraw.Size.Width - bitmapDraw.Width) / 2 - 1, (panelDraw.Size.Height - bitmapDraw.Height) / 2 - 1);
using (Graphics g = panelDraw.CreateGraphics())
{
g.Clear(panelDraw.BackColor);
}
redrawDrawingLetter();
}
public void redrawDrawingLetter()
{
bool[][] grid = collector.getArray(selectedName);
using (Graphics graphicDraw = Graphics.FromImage(bitmapDraw))
{
graphicDraw.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
graphicDraw.Clear(panelDraw.BackColor);
int pxSize = pixelSizeArray[pixelSize];
for (int y = 0; y < grid.Length; y++)
{
for (int x = 0; x < grid[y].Length; x++)
{
graphicDraw.FillRectangle((grid[y][x] ? Brushes.Black : Brushes.White), x * (pxSize + 1), y * (pxSize + 1), pxSize, pxSize);
}
}
}
redrawDrawingPanel();
}
private void redrawDrawingPanel()
{
using (Graphics g = panelDraw.CreateGraphics())
{
if (bitmapDraw != null)
g.DrawImage(bitmapDraw, zeroDrawPosition);
}
}
private void panelDraw_Paint(object sender, PaintEventArgs e)
{
redrawDrawingPanel();
}
Nobody can explain to me how to draw in C# the best way. So maybe my code isn't good, but that is reason why I asking how to do it correctly.
newSizeDrawing is called by myself only, when user click on + or - button. I have bool double-dimension array if pixel is on or off. This is program for drawing letters for microchips and LED display (often 8px height of letter).
I wrote a method that checks if the mouse moved from one "pixel" to another, so I don't redraw it after every call mouseMove event, because "pixel" can be from 10x10 px to 30x30 px.
private void panelDraw_Paint(object sender, PaintEventArgs e)
{
redrawDrawingPanel();
}
This is fundamentally wrong. The Paint event passes e.Graphics to let you draw whatever you want to paint. When you turn on double-buffering, e.Graphics refers to a bitmap, it is initialized with the BackColor. You then proceed to drawing using another Graphics object you got from CreateGraphics(). That one draws directly to the screen.
The flicker effect you see if very pronounced. For a split second you see what the other Graphics context draws. Then your panelDraw_Paint() method returns and Winforms draws the double-buffered bitmap. There's nothing on it so it immediately erases what you drew.
Modify the redrawDrawingPanel() method and give it an argument of type Graphics. Pass e.Graphics in the call. And only use that Graphics object, remove all calls to CreateGraphics().

Autoscale Font in a TextBox Control so that its as big as possible and still fits in text area bounds

I need a TextBox or some type of Multi-Line Label control which will automatically adjust the font-size to make it as large as possible and yet have the entire message fit inside the bounds of the text area.
I wanted to see if anyone had implemented a user control like this before developing my own.
Example application: have a TextBox which will be half of the area on a windows form. When a message comes in which is will be approximately 100-500 characters it will put all the text in the control and set the font as large as possible. An implementation which uses Mono Supported .NET libraries would be a plus.
If know one has implemented a control already... If someone knows how to test if a given text completely fits inside the text area that would be useful for if I roll my own control.
Edit: I ended up writing an extension to RichTextBox. I will post my code shortly once i've verified that all the kinks are worked out.
I had to solve the same basic problem. The iterative solutions above were very slow. So, I modified it with the following. Same idea. Just uses calculated ratios instead of iterative. Probably, not quite as precise. But, much faster.
For my one-off need, I just threw an event handler on the label holding my text.
private void PromptLabel_TextChanged(object sender, System.EventArgs e)
{
if (PromptLabel.Text.Length == 0)
{
return;
}
float height = PromptLabel.Height * 0.99f;
float width = PromptLabel.Width * 0.99f;
PromptLabel.SuspendLayout();
Font tryFont = PromptLabel.Font;
Size tempSize = TextRenderer.MeasureText(PromptLabel.Text, tryFont);
float heightRatio = height / tempSize.Height;
float widthRatio = width / tempSize.Width;
tryFont = new Font(tryFont.FontFamily, tryFont.Size * Math.Min(widthRatio, heightRatio), tryFont.Style);
PromptLabel.Font = tryFont;
PromptLabel.ResumeLayout();
}
I haven't seen an existing control to do this, but you can do it the hard way by using a RichTextBox and the TextRenderer's MeasureText method and repeatedly resizing the font. It's inefficient, but it works.
This function is an event handler for the 'TextChanged' event on a RichTextBox.
An issue I've noticed:
When typing, the text box will scroll to the current caret even if scrollbars are disabled. This can result in the top line or left side getting chopped off until you move back up or left with the arrow keys. The size calculation is correct assuming you can get the top line to display at the top of the text box. I included some scrolling code that helps sometimes (but not always).
This code assumes word wrap is disabled. It may need modification if word wrap is enabled.
The code:
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, uint wMsg, int wParam, uint lParam);
private static uint EM_LINEINDEX = 0xbb;
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
// If there's no text, return
if (richTextBox1.TextLength == 0) return;
// Get height and width, we'll be using these repeatedly
int height = richTextBox1.Height;
int width = richTextBox1.Width;
// Suspend layout while we mess with stuff
richTextBox1.SuspendLayout();
Font tryFont = richTextBox1.Font;
Size tempSize = TextRenderer.MeasureText( richTextBox1.Text, richTextBox1.Font);
// Make sure it isn't too small first
while (tempSize.Height < height || tempSize.Width < width)
{
tryFont = new Font(tryFont.FontFamily, tryFont.Size + 0.1f, tryFont.Style);
tempSize = TextRenderer.MeasureText(richTextBox1.Text, tryFont);
}
// Now make sure it isn't too big
while (tempSize.Height > height || tempSize.Width > width)
{
tryFont = new Font(tryFont.FontFamily, tryFont.Size - 0.1f, tryFont.Style);
tempSize = TextRenderer.MeasureText(richTextBox1.Text, tryFont);
}
// Swap the font
richTextBox1.Font = tryFont;
// Resume layout
richTextBox1.ResumeLayout();
// Scroll to top (hopefully)
richTextBox1.ScrollToCaret();
SendMessage(richTextBox1.Handle, EM_LINEINDEX, -1, 0);
}
The solution i came up with was to write a control which extends the standard RichTextBox control.
Use the extended control in the same way you would a regular RichTextBox control with the following enhancements:
Call the ScaleFontToFit() method after resizing or text changes.
The Horizontal Alignment field can be used to center align the text.
The Font attributes set in the designer will be used for the entire region. It is not possible to mix fonts as they will changed once the ScaleFontToFit method is called.
This control combines several techniques to determine if the text still fits within it's bounds. If the text area is multiline, it detects if scrollbars are visible. I found a clever way to detect whether or not the scrollbars are visible without requiring any winapi calls using a clever technique I found on one of Patrick Smacchia's posts.. When multiline isn't true, vertical scrollbars never appear so you need to use a different technique which relies on rendering the text using a the Graphics object. The Graphic rendering technique isn't suitable for Multiline boxes because you would have to account for word wrapping.
Here are a few snippets which shows how it works (link to source code is provided below). This code could easily be used to extend other controls.
/// <summary>
/// Sets the font size so the text is as large as possible while still fitting in the text
/// area with out any scrollbars.
/// </summary>
public void ScaleFontToFit()
{
int fontSize = 10;
const int incrementDelta = 5; // amount to increase font by each loop iter.
const int decrementDelta = 1; // amount to decrease to fine tune.
this.SuspendLayout();
// First we set the font size to the minimum. We assume at the minimum size no scrollbars will be visible.
SetFontSize(MinimumFontSize);
// Next, we increment font size until it doesn't fit (or max font size is reached).
for (fontSize = MinFontSize; fontSize < MaxFontSize; fontSize += incrementDelta)
{
SetFontSize(fontSize);
if (!DoesTextFit())
{
//Console.WriteLine("Text Doesn't fit at fontsize = " + fontSize);
break;
}
}
// Finally, we keep decreasing the font size until it fits again.
for (; fontSize > MinFontSize && !DoesTextFit(); fontSize -= decrementDelta)
{
SetFontSize(fontSize);
}
this.ResumeLayout();
}
#region Private Methods
private bool VScrollVisible
{
get
{
Rectangle clientRectangle = this.ClientRectangle;
Size size = this.Size;
return (size.Width - clientRectangle.Width) >= SystemInformation.VerticalScrollBarWidth;
}
}
/**
* returns true when the Text no longer fits in the bounds of this control without scrollbars.
*/
private bool DoesTextFit()
{
if (VScrollVisible)
{
//Console.WriteLine("#1 Vscroll is visible");
return false;
}
// Special logic to handle the single line case... When multiline is false, we cannot rely on scrollbars so alternate methods.
if (this.Multiline == false)
{
Graphics graphics = this.CreateGraphics();
Size stringSize = graphics.MeasureString(this.Text, this.SelectionFont).ToSize();
//Console.WriteLine("String Width/Height: " + stringSize.Width + " " + stringSize.Height + "form... " + this.Width + " " + this.Height);
if (stringSize.Width > this.Width)
{
//Console.WriteLine("#2 Text Width is too big");
return false;
}
if (stringSize.Height > this.Height)
{
//Console.WriteLine("#3 Text Height is too big");
return false;
}
if (this.Lines.Length > 1)
{
//Console.WriteLine("#4 " + this.Lines[0] + " (2): " + this.Lines[1]); // I believe this condition could be removed.
return false;
}
}
return true;
}
private void SetFontSize(int pFontSize)
{
SetFontSize((float)pFontSize);
}
private void SetFontSize(float pFontSize)
{
this.SelectAll();
this.SelectionFont = new Font(this.SelectionFont.FontFamily, pFontSize, this.SelectionFont.Style);
this.SelectionAlignment = HorizontalAlignment;
this.Select(0, 0);
}
#endregion
ScaleFontToFit could be optimized to improve performance but I kept it simple so it'd be easy to understand.
Download the latest source code here. I am still actively working on the project which I developed this control for so it's likely i'll be adding a few other features and enhancements in the near future. So, check the site for the latest code.
My goal is to make this control work on Mac using the Mono framework.
I had a similar requirement for a text box in a panel on a windows form hosted window. (I injected the panel onto the existing form). When the size of the panel changes (in my case) the text would resize to fit the box. Code
parentObject.SizeChanged += (sender, args) =>
{
if (textBox1.Text.Length > 0)
{
int maxSize = 100;
// Make a Graphics object to measure the text.
using (Graphics gr = textBox1.CreateGraphics())
{
for (int i = 1; i <= maxSize; i++)
{
using (var test_font = new Font(textBox1.Font.FontFamily, i))
{
// See how much space the text would
// need, specifying a maximum width.
SizeF text_size =
TextRenderer.MeasureText(
textBox1.Text,
test_font,
new Size(textBox1.Width, int.MaxValue),
TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl);
try
{
if (text_size.Height > textBox1.Height)
{
maxSize = i - 1;
break;
}
}
catch (System.ComponentModel.Win32Exception)
{
// this sometimes throws a "failure to create window handle" error.
// This might happen if the TextBox is invisible and/or
// too small to display a toolbar.
// do whatever here, add/delete, whatever, maybe set to default font size?
maxSize = (int) textBox1.Font.Size;
}
}
}
}
// Use that font size.
textBox1.Font = new Font(textBox1.Font.FontFamily, maxSize);
}
};

Categories