I was testing a DX scrolling banner that I've recently completed. The problem is that it's consuming very large amounts of memory.
The string to be scrolled is created using an array of characters and their sizes (using DX measure string) created from the source string (rss feed) and stored in an array. The rss feed, in this case, was about 500 characters long.
To create the scrolled string, I simply add/remove characters as the come into/outof view (using panel width & character sizes to determine the time to add/remove characters from the string) on the display panel. It works very well but is using 200M of memory. The string is never more that about 80 characters long (I've made sure of this by using an alert if it exceeds 80 chars). The amount of characters in the string is dependant, of course, on the size of the font.
If I comment out the DrawText command, the app uses little memory (proving that it's not some other part of the code). If I just scroll the entire string (500 chars) the memory used is only 32M. However, I'm now using lots of Proc scrolling such a large string.
I've noticed that when you draw a static (not scrolling) large string and then follow it with, say, a single character string, DrawText does not free the memory used to draw the large string. I'm using theString.Remove & theString.Insert to create the string to be scrolled. The memory seems to crawls up as each character is Added/Subtracted and once the entire RSS feed string has been scrolled, the memory usage stays static - at 200M - from then on in.
What's going on? Any help very much appreciated... it's driving me nuts! I could just split the feed into strings but it makes more sense to do it character by character.
private void sync()
{
if (device.RasterStatus.ScanLine)
{
UpdateDisp();
if (ArStringSegments.Count != 0)
{
for (int i = 0; i != Speed; i++)
{
# region Add a character to the displayed string
if (FirstCharLength == 0 && AddChrIndex != ArStringSegments.Count)
{
StringProps StringProps = (StringProps)ArStringSegments[AddChrIndex];
if (ScrollDirection == Direction.ToLeft)
{
theString = theString.Insert(theString.Length, StringProps.String);
}
AddChrIndex++;
}
# endregion Add a character to the string
# region remove a character from the string as it goes beyond the veiwable area
if (RemoveChrIndex != ArStringSegments.Count)
{
if (ScrollDirection == Direction.ToLeft)
{
if(ScrollInc == 0 - LargestChar)
{
StringProps RemoveString = (StringProps)ArStringSegments[RemoveChrIndex];
theString = theString.Remove(0, 1);
ScrollInc += RemoveString.Size1;
RemoveChrIndex++;
}
}
}
# endregion remove a character from the string as it goes beyond the veiwable area
# region Increment/decrement position
if (ScrollDirection == Direction.ToLeft)
{
ScrollInc--;
FirstCharLength--;
}
# endregion Increment/decrement position
# region Entire string has gone out of viewable area, scroll out by an amount and then start again.
if ((ScrollInc == 0 - (ScrollOutLength + LargestChar ) && ScrollDirection == Direction.ToLeft) ||
(ScrollInc == PanWidth + (ScrollOutLength + LargestChar) && ScrollDirection == Direction.ToRight))
{
theString = "";
AddChrIndex = 0;
RemoveChrIndex = 0;
FirstCharLength = 0;
if (ScrollDirection == Direction.ToLeft)
{
ScrollInc = this.PanWidth;
}
else
{
ScrollInc = 0;
RightBoundary = 0;
}
}
# endregion entire string has gone out of viewable area, scroll out by an amount and then start again.
}
}
}
ScanCount = device.RasterStatus.ScanLine;
}
private void UpdateDisp()
{
try
{
if (device.CheckCooperativeLevel(out DeviceStatus))
{
if (UpdateDisplayEnabled == true)
{
device.Clear(ClearFlags.Target, Color.Black, 1.0f, 0);
device.BeginScene();
// background image Texture
BackSprite = new Sprite(device);
BackSprite.Begin(SpriteFlags.AlphaBlend);
BackSprite.Draw2D(PanelTexture, new Point(0, 0), 0.0F, new Point(0, 0), Color.White);
BackSprite.End();
BackSprite.Dispose();
// draw the text
Draw2DText(FeedText[i], ScrollInc, 0, FontColor);
font.DrawText(null, theString, new Rectangle(ScrollInc, 0,
device.EndScene();
if (device.CheckCooperativeLevel(out DeviceStatus)) device.Present((IntPtr)DxRenderPanel);
}
}
}
We can't see your code, and so all I can give you is conjecture. With that in mind:
.Net uses garbage collection to manage memory. To make things more efficient, the garbage collector might decide it doesn't need to free memory until there's pressure from somewhere to do so. If you have a machine with several gigabytes of free ram, than there's no pressure to release it and this might be just fine.
That said, there are some things you could be doing wrong to prevent the garbage collector from release ram that it might otherwise be perfectly happy to return to your system.
Are you holding on to reference to your string images somewhere long after you need to?
Are you creating the images much faster than is needed?
If the scrolling string is marquee that will cycle, could you instead cache each image in the cycle rather than draw a new one?
Could you build one image (or two for the wrap-around) and only show the relevant portion by moving your control and hiding the edges behind something?
Are you creating a new string object for every DrawText() call, rather than re-using them?
Are you creating a lot of new font objects (maybe one per character) that you don't really need?
Most of all, are you drawing into a brand new bitmap every time, or could you clear an existing bitmap and draw the new string over that?
Related
I know that there are lot of different threads about horizontal text animation/text scrolling, but unfortunately none of them give smooth scrolling with repeatable text. I have tried double/thickness animation using various WPF controls containing text. Also tried animating with visual brush which gives me by far the most elegant scrolling compared to other approaches (for e.g. playing with Canvas.Left property etc.) but that too goes blur the text, if the text length or the animation speed is too high.
I'm over to a pure DirectX C# implementation using SharpDX library. Should also mention that I'm a beginner with DirectX programming. Here is the code:
public void RunMethod()
{
// Make window active and hide mouse cursor.
window.PointerCursor = null;
window.Activate();
var str = "This is an example of a moving TextLayout object with no snapped pixel boundaries.";
// Infinite loop to prevent the application from exiting.
while (true)
{
// Dispatch all pending events in the queue.
window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessAllIfPresent);
// Quit if the users presses Escape key.
if (window.GetAsyncKeyState(VirtualKey.Escape) == CoreVirtualKeyStates.Down)
{
return;
}
// Set the Direct2D drawing target.
d2dContext.Target = d2dTarget;
// Clear the target.
d2dContext.BeginDraw();
d2dContext.Clear(Color.CornflowerBlue);
//float layoutXOffset = 0;
float layoutXOffset = layoutX;
// Create the DirectWrite factory objet.
SharpDX.DirectWrite.Factory fontFactory = new SharpDX.DirectWrite.Factory();
// Create a TextFormat object that will use the Segoe UI font with a size of 24 DIPs.
textFormat = new TextFormat(fontFactory, "Verdana", 100.0f);
textLayout2 = new TextLayout(fontFactory, str, textFormat, 2000.0f, 100.0f);
// Draw moving text without pixel snapping, thus giving a smoother movement.
// d2dContext.FillRectangle(new RectangleF(layoutXOffset, 1000, 1000, 100), backgroundBrush);
d2dContext.DrawTextLayout(new Vector2(layoutXOffset, 0), textLayout2, textBrush, DrawTextOptions.NoSnap);
d2dContext.EndDraw();
//var character = str.Substring(0, 1);
//str = str.Remove(0, 1);
//str += character;
layoutX -= 3.0f;
if (layoutX <= -1000)
{
layoutX = 0;
}
// Present the current buffer to the screen.
swapChain.Present(1, PresentFlags.None);
}
}
Basically it creates an endless loop and subtracts the horizontal offset. Here are the challenges: I need repeatable text similar to HTML marquee without any gaps, Would probably need to extend it to multiple monitors.
Please suggest.
I don't know neither how to use DirectX nor sharpdx, but if you want you can consider this solution
I had a similar problem a while ago, but with the text inside a combobox. After a bounty i got what i was looking for. I'm posting the relevant piece of code as an example, but you can check the complete answer here
Basically, whenever you have a textblock/textbox that contain a string that cannot be displayed completely, cause the length exceed the textblock/box lenght you can use this kind of approach. You can define a custom usercontrol derived from the base you need (e.g. SlidingComboBox : Combobox) and define an animation for you storyboard like the following
_animation = new DoubleAnimation()
{
From = 0,
RepeatBehavior = SlideForever ? RepeatBehavior.Forever : new RepeatBehavior(1), //repeat only if slide-forever is true
AutoReverse = SlideForever
};
In my example i wanted this behaviour to be active only when the mouse was on the combobox, so in my custom OnMouse enter i had this piece of code
if (_parent.ActualWidth < textBlock.ActualWidth)
{
_animation.Duration = TimeSpan.FromMilliseconds(((int)textBlock.Text?.Length * 100));
_animation.To = _parent.ActualWidth - textBlock.ActualWidth;
_storyBoard.Begin(textBlock);
}
Where _parent represent the container of the selected item. After a check on the text lenght vs combobox lenght i start the animation and end it at the end of the text to be displayed
Note that in the question i mentioned there are also other soltions. I'm posting the one that worked for me
Situation:
I have one Chart and three ChartArea that are aligned in view, zoom, cursor through the AxisViewChanged method that act in this way:
if (e.Axis == ax1)
{
ax2.ScaleView.Size = ax1.ScaleView.Size;
ax2.ScaleView.Position = ax1.ScaleView.Position;
ax3.ScaleView.Size = ax1.ScaleView.Size;
ax3.ScaleView.Position = ax1.ScaleView.Position;
min = (int)ax1.ScaleView.ViewMinimum;
max = (int)ax1.ScaleView.ViewMaximum;
}
if (e.Axis == ax2)
{
....
And it works very well in both cases: when I zoom in/out or scroll.
Problem:
The problem is that my graph source is made by a lot of points, in the worst case we talk about 3'600'000 samples. With this amount of samples, when I move around points with cursor and try to show a tooltip with the values, the interaction quality collapse and becomes unusables (even having set Fast Line).
So I tried to implement a simple decimation algorithm to reducethe number of showed points:
void draw_graph(int start, int end)
{
double fract_value = 0;
int int_value = 0;
int num_saples_selected = end - start;
if(num_saples_selected <= MAX_GRAPH_NUM_SAMPLES)
fill_graphs_point(0, end, 1);
else
{
fract_value = ((double)num_saples_selected) / ((double)MAX_GRAPH_NUM_SAMPLES);
int_value = (int)fract_value;
if (fract_value > int_value)
int_value++;
fill_graphs_point(0, end, int_value);
}
}
void fill_graphs_point(int start, int end, int step)
{
int i = 0;
for (i = start; i < end; i=i+step)
{
dlChart.Series[SERIES_VOLTAGE].Points.AddXY(timestamps_array[i], voltage_array[i]);
dlChart.Series[SERIES_CURRENT].Points.AddXY(timestamps_array[i], current_array[i]);
dlChart.Series[SERIES_ENERGY].Points.AddXY(timestamps_array[i], energy_array[i]);
// I will use this to came back to real position of the initial array
decimation_positions.Add(i);
}
}
Assuminig I had a good idea with this method to reduce the points number, I do not know where to put the call to the function "draw_graph". If I put it in the AxisViewChanged method it will call my method also when I scroll (horizontally) my graph and this is not what I want. I want to call my method only on zoom and unzoom event.
Expected behavior: in the first view (without any zoom) the graph has to show an "idea" of the trend of the graph. Then for every selection/(un)zoom I want to call my function to check if the points number of selected portion will fit in my window size that is MAX_GRAPH_NUM_SAMPLES(=10000).
Hope someone can help me. Whatever kind of suggestion will be appreciated. Thanks in advance.
I forgot to say that strangely the problem appeared when I zoomed in more than one time. At some point also the PC fan started. I resolved disabling the library zoom and implement my self a sort of zoom (a little bit more simple). The solution is in this method and the method I write in the question:
private void dlChart_SelectionRangeChange(object sender, CursorEventArgs e)
{
double startSelection = e.NewSelectionStart;
double endSelection = e.NewSelectionEnd;
// Allow the selection to disappear
reset_selection();
try
{
// before convert point from "showed point" to "real point" I check the limits
if (
(startSelection >= 0) && (startSelection <= decimation_positions.Count()) &&
(endSelection >= 0) && (endSelection <= decimation_positions.Count()) &&
(endSelection != startSelection)
)
{
// convert
startSelection = decimation_positions[(int)startSelection];
endSelection = decimation_positions[(int)endSelection];
// check for reverse zoom (from right to left)
if (startSelection > endSelection)
{
double tmp = startSelection;
startSelection = endSelection;
endSelection = tmp;
}
// clean the series
this.reset_series();
// draw the selected range
draw_graph((int)startSelection, (int)endSelection);
}
}catch(ArgumentOutOfRangeException ex)
{
// todo
}
//System.Console.WriteLine("SelSTART = "+e.NewSelectionStart+" SelEND = "+e.NewSelectionEnd);
}
I plot animated graph with Dynamic Data Display - graph that has source with constantly adding new points (about 10 times per second and 10 points per time). The problem is the performance degrade if these points form line segments which are big in compare to plotter size. In other words I can add thousands of points but if they forms comparably small line segments the performance will be good. But if I add only few hundreds points which form big line segments (they occupy all the plotter's area) the performance will be very poor.
The following images illustrate the problem. The first image is just hundreds line segments and performance is very poor on my machine. In the second image I just add two points to make the rest points took small place on the plotter due to plotter's scaling.
It doesn't matter how fast your machine is (mine is Pentium G3220 with integrated GPU, 16 GB RAM, and another one is relatively same CPU with discrete GeForce 640 GT) - I can add much more points and will fall your machine down. The behavior will be the same - if I add two "defend scaling" points and make the line segments relatively small on the plotter the performance will be much better.
The following code - how I get such pictures. Leave the adding of two points in MainWindow() commented to obtain the first picture, and uncomment it to obtain the second one. This is just a plane WPF application with all code in one file - in MainWindow's "code behind":
public partial class MainWindow : Window
{
readonly DispatcherTimer _timer = new DispatcherTimer();
int _counter = 0;
int _batchSize= 10;
RingArray<Point> _data = new RingArray<Point>(2000);
EnumerableDataSource<Point> _ds;
public MainWindow()
{
InitializeComponent();
_ds = new EnumerableDataSource<Point>(_data);
_ds.SetXMapping(d => d.X);
_ds.SetYMapping(d => d.Y);
LineGraph chart = plotter.AddLineGraph(_ds, Colors.Blue, 2.0);
//_data.Add(
// new Point
// {
// X = -1,
// Y = 200
// });
//_data.Add(
// new Point
// {
// X = -0.5,
// Y = -200
// });
_timer.Tick += OnTimerTick;
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Start();
}
void OnTimerTick(object sender, EventArgs e)
{
_data.AddMany(
Enumerable.Range(0, _batchSize)
.Select(v =>
{
// I don't use the "v" here - the "_counter" is enough.
var p = new Point
{
X = (double)_counter,
Y = (double)(_counter % 2 == 0 ? 0 : 1)
};
_counter++;
return p;
}));
}
}
public class Point
{
public double X { get; set; }
public double Y { get; set; }
}
It doesn't matter whether I use EnumerableDataSource or ObservableDataSource with AppendAsync, Add or AddMany - it is all the same behavior: the performance depends only on the size of line segments relatively to screen size (or plotter size). The problem lays definitely in the field of 1) WPF's rendering system, and 2) rasterization.
1) bad WPF's rendering system https://jeremiahmorrill.wordpress.com/2011/02/14/a-critical-deep-dive-into-the-wpf-rendering-system/
2) rasterization - the more pixels the line segments occupy on the screen the less performance will be... but dude, several hundreds lines fall down my machine but Crysis on max settings not! Ridiculous!
I know the following approaches to cope with this problem:
reduce the size of the plotter (but in my case it have to be as large as possible because it is the main part of the user interface and it is dedicated for monitoring purposes);
reduce the size of line segments (not in my case because the data must be displayed as they are);
apply a filter to the data like plotting only each tenth point instead of every first one - already done but it has limited application (my graph looks too jugged);
What else would you offer?
1) I have a list of line segments (defined by their two endpoints and a width.)
2) These line segments are drawn in a panel.
3) When my mouse moves (Panel.MouseMove event), I loop over the list of line segments.
4) Foreach:
gPath.Reset();
Pen Pen = new Pen(Color.White, 20);
gPath.AddLine(P1, P2);
return gPath.IsOutlineVisible(cursorPos, Pen);
5) If I get true, then I know my cursor is hovering over the current line segment.
This works fine for about... 300 lines or so. When I reach 1,000 my program slows to a halt (profiling shows that it's caused by IsOutlineVisible). So, is there any way I could increase performance of my hit-testing algorithm? I don't know how efficient IsOutlineVisible is, so I don't want to implement any optimizations that the method already uses. Any ideas?
EDIT:
After digging into my data I noticed some of the lines were extremely large. For example:
endpoint1 = (16000, -16000)
endpoint2 = (5041448, -32868734)
(yes, one of the coordinates is in the negative tens of millions...)
I verified that hit-testing against just one such line segment is enough to bring my program to a halt, (IsOutlineVisible takes 2-3 seconds to do the test, and the test is run whenever the cursor moves...).
I should have tested this more thoroughly before posting. Thanks to all that responded (a 2d spatial index is a great suggestion if I end-up handling thousands of lines).
p.s. If anyone knows why a large line-segment is that big a problem for IsOutlineVisible, that would be great.
Try this:
public Line GetHotSpottedLine(Point mousePos){
var line = lines.Where(line =>
{
Point p1 = new Point(Math.Min(line.P1.X, line.P2.X), Math.Min(line.P1.Y, line.P2.Y));
Point p2 = new Point(Math.Max(line.P1.X, line.P2.X), Math.Max(line.P1.Y, line.P2.Y));
return mousePos.X >= p1.X && mousePos.X <= p2.X && mousePos.Y >= p1.Y && mousePos.Y <= p2.Y;
}).FirstOrDefault(line => {
using (GraphicsPath gp = new GraphicsPath())
{
gp.AddLine(line.P1, line.P2);
//You can declare your pen outside and this pen should be used to draw your lines.
using (Pen p = new Pen(Color.Red, 20))
{
return gp.IsOutlineVisible(mousePos, p);
}
}
});
return line;
}
public class Line{
public Point P1 { get; set; }
public Point P2 { get; set; }
}
List<Line> lines = new List<Line>();
It depends on how you want to use your lines, if you want to draw them, we have to notice the performance of drawing not detecting the hovered line, yes in that case, drawing is your problem. I think we can use some Thread here. Anyway, I tested with 1000 lines and it works OK (with drawing all the lines in Form Paint) without using any thread.
IsOutlineVisible calls the Gdi+, maybe this slows it down a bit.
public bool GraphicsPath.IsOutlineVisible(PointF pt, Pen pen, Graphics graphics)
{
int num;
if (pen == null)
{
throw new ArgumentNullException("pen");
}
int status = SafeNativeMethods.Gdip.GdipIsOutlineVisiblePathPoint(new HandleRef(this, this.nativePath), pt.X, pt.Y, new HandleRef(pen, pen.NativePen), new HandleRef(graphics, (graphics != null) ? graphics.NativeGraphics : IntPtr.Zero), out num);
if (status != 0)
{
throw SafeNativeMethods.Gdip.StatusException(status);
}
return (num != 0);
}
Beside that, these hit testing do not use optimizations like building an 2d index of all graphical elements used. To improve the performance I would try to
implement hit testing yourself and iterating over all elements
maybe use a 2D index, although for < 1000 elements I hardly believe this is necessary.
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);
}
};