I want to add Buttons to a FlowLayoutPanel. The Buttons might contain longer texts with spaces between the words. The Buttons are Autosize=true and AutoSizeMode = AutoSizeMode.GrowAndShrink. Further more I set the MaximumSize property to (maxwidth,0). maxwidth is the width of the panel. So the button does not grow too wide.
What I see is, that the widht of the Button is limited by the MaximumSize property, but when text wrapping occurs, the Button's height doesn't autosize to the height of the wrapped text. Is there a solution to that problem?
I also tried this manually sizing the button like this:
using (Graphics cg = this.CreateGraphics()) {
SizeF size = cg.MeasureString(button.Text, button.Font, 200);
button.Width = (int)size.Width+20;
button.Height = (int)size.Height+20;
button.Text = someLongTextWithSpaces;
}
But please note that I added 20 to the calculated size. It's working, but is there a proper way to determin this additional size? Maybe 2x Padding + ?????
A few hours later...
I came to this version which seems to work quite fine.
using (Graphics cg = this.CreateGraphics()) {
var fmt = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak;
var prop = new Size(tableLayoutPanel1.Width - 20, 0);
var size = TextRenderer.MeasureText(button.Text, button.Font, prop, fmt);
int border = button.Height - button.Font.Height;
button.Width = (int)size.Width + border;
button.Height = (int)size.Height + border;
button.Text = someLongTextWithSpaces;
}
It seems that the initial button height is borders + the height the font. So I calculated the border subtracting button.Height-button.font.Height.
According to Hans, I now use the TextRenderer.MeasureText. I tested it without enabling VisualStyles and it worked fine. Any comments on that?
There is a proper way, but it isn't exactly very subtle. Reverse-engineering it from the ButtonRenderer class source code, the Winforms class that draws the button text, you must use the TextRenderer class to measure the text. And you must use the VisualStyleRenderer.GetBackgroundContentRectangle() method to obtain the effective drawing bounds. Note that it is smaller than the button's Size because of the border and a margin that depends on the selected visual style.
Non-trivial problems are mapping a calculated content rectangle back to the outer button size and dealing with old machines that don't have visual styles enabled. Sample code that appeared to arrive at the correct size:
private static void SetButtonSize(Graphics gr, Button button) {
VisualStyleElement ButtonElement = VisualStyleElement.Button.PushButton.Normal;
var visualStyleRenderer = new VisualStyleRenderer(ButtonElement.ClassName, ButtonElement.Part, 0);
var bounds = visualStyleRenderer.GetBackgroundContentRectangle(gr, button.Bounds);
var margin = button.Height - bounds.Height;
var fmt = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak;
var prop = new Size(bounds.Width, 0);
var size = TextRenderer.MeasureText(button.Text, button.Font, prop, fmt);
button.ClientSize = new Size(button.ClientSize.Width, size.Height - margin);
}
protected override void OnLoad(EventArgs e) {
using (var gr = this.CreateGraphics()) {
SetButtonSize(gr, this.button1);
}
base.OnLoad(e);
}
Not extensively tested for corner cases, can't say I recommend this.
It seems that the initial button height is borders + the height the font. So I calculated the border subtracting button.Height-button.font.Height. (See the last block of my original post)
This also works with VisualStyles enabled/disabled.
You should control the line breaks by adding newline characters in the text. Automatic text wrapping won't work with spaces alone:
button1.Text = "123232131232\r\nfgfdgfdgdfgdfgdf\r\nASDSADSDASD";
Or :
button1.Text = "123232131232" + Environment.NewLine +
"fgfdgfdgdfgdfgdf" + Environment.NewLine + "ASDSADSDASD";
If you'd rather get the automatic wrapping you could try to use TextMeasure to determine the height needed for the text and then set the button's height accordingly but that may need some extra attention..
But I suggest to consider using Labels instead. For a Label the wrapping works out of the box.. Huge Buttons with varying sizes are non-standard UI elements.
Related
I have a dynamic window with labels on it. The window is a HUD and changes size depending on its parent window. However, one of the labels becomes distorted when resized.
The font of the labels are resized according to the screen size like so:
float fontSize = this.Width / 128 /getScalingFactor();
and the scaling factor is calculated as follows:
//Gets the scaling factor of the current dpi settings
protected float getScalingFactor()
{
Graphics g = Graphics.FromHwnd(IntPtr.Zero);
IntPtr desktop = g.GetHdc();
int LogicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.VERTRES);
int PhysicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.DESKTOPVERTRES);
int logpixelsy = GetDeviceCaps(desktop, (int)DeviceCap.LOGPIXELSY);
float screenScalingFactor = (float)PhysicalScreenHeight / (float)LogicalScreenHeight;
float dpiScalingFactor = (float)logpixelsy / (float)96;
return dpiScalingFactor; // 1.25 = 125%
//return screenScalingFactor;
}
And the designer code for the labels. There are labels that holds numbers as well and they are using identical settings. However, they don't get distorted but the username label does.
this.labelUsername.AutoSize = true;
this.tableLayoutPanel1.SetColumnSpan(this.labelUsername, 2);
this.labelUsername.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.labelUsername.ForeColor = System.Drawing.Color.White;
this.labelUsername.Location = new System.Drawing.Point(3, 0);
this.labelUsername.Name = "labelUsername";
this.labelUsername.Size = new System.Drawing.Size(56, 14);
this.labelUsername.TabIndex = 3;
this.labelUsername.Text = "Username";
this.labelUsername.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
I've tried changing some numbers of the font size and scaling factor. I've tried changing some of the settings in the designer code as well. Unfortunately no success so far.
Maybe someone recognize the issue and can point me to what causes this. I'm assuming there is some mis-match with the label and window DPI perhaps. But that doesn't explain why the numbers doesn't get the same problem.
First a clarification. I'm building on a open source project which I'm not 100% familiar with.
So after poking around some more I realized that there is another function which also controls the HUD colors. Unless I added a forecolor to my label there as well, I got that distorted looking text.
I'm assuming that this method is called everytime the window is resizing and when my label wasn't updated with a color there, it got a transparent color. However, because the forecolor had been set at startup it showed as a border around the transparent text because it wasn't resized.
I'm currently trying to create a visual component to have scrolling text (left to right and right to left) - pretty much an html marquee.
I have a grid divided in several columns & rows, and I want to place my component inside one of the grid slots.
The grid (named UIGrid) is generated like this :
for (int i = 0; i < xDivisions; i++)
{
ColumnDefinition newColumn = new ColumnDefinition();
UIGrid.ColumnDefinitions.Add(newColumn);
}
for (int i = 0; i < yDivisions; i++)
{
RowDefinition newRow = new RowDefinition();
UIGrid.RowDefinitions.Add(newRow);
}
The component I'm adding is just a border with a textblock as a child. I place the border inside the Grid like this :
border = new Border();
Grid.SetColumn(border, xPosition);
Grid.SetRow(border, yPosition);
textBlock = new TextBlock();
border.Child = textBlock;
textBlock.Text = "Scrolling text from left to right";
UIGrid.Children.Add(border);
I'm using a timer to increment the textblock margin, here's the timer callback simplified body :
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double textWidth = textBlock.DesiredSize.Width;
double visibleWidth = componentBase.ActualWidth;
double targetMargin = textWidth < visibleWidth ? visibleWidth : textWidth;
if (margin.Left == targetMargin)
{
margin.Left = -textWidth;
} else
{
margin.Left++;
}
When the text slides from left to right, it behaves nicely :
https://s10.postimg.org/p0nt7vl09/text_good.png
Text "leaving" the grid slot is hidden.
However, when I set the textblock's margin as negative so it may come back inside the viewable area from the left, the text is visible even though it's outside its allocated slot :
https://s10.postimg.org/pownqtjq1/text_bad.png
I've tried using padding instead, but I can't set a negative padding. I've tried a few other things, but I feel like I've encountered a roadblock.
What could I do to get a nicely scrolling text ?
If you want nicely scrolling text ListView might be a better option. It is dynamic and you can bind it to your object. It would take a lot of this guess work out.
Ed Plunkett led me in the right direction with the Clip property. The idea is to do this :
border.Clip = new RectangleGeometry
{
Rect = new Rect(0, 0, border.ActualWidth, border.ActualHeight)
};
Of course, that doesn't work if the border hasn't been rendered yet (and of course it isn't when my code is running). You can force the measurement to take place using 'Measure' as I did to measure the text length in pixels, but it behaved strangely on my border. I wouldn't get the correct size at all.
In the end, I simply subscribed to the border's SizeChanged event :
border.SizeChanged += OnSizeComputed;
When that event is fired, I create the RectangleGeometry using ActualWidth & ActualHeight.
I redraw the graphics of the Tab for the TabControl but I can't set the Width of it.
What I want is that the text of the Tab and the icon is contained in its size.
Now is something like this:
My Code
private void tabControlForm_DrawItem(object sender, DrawItemEventArgs e)
{
try
{
using (Brush br = new SolidBrush(TabColors[tabControlForm.TabPages[e.Index]]))
{
Rectangle rect = e.Bounds;
rect.Width += 10;
e.Graphics.FillRectangle(br, rect);
SizeF sz = e.Graphics.MeasureString(tabControlForm.TabPages[e.Index].Text, e.Font);
e.Graphics.DrawString(tabControlForm.TabPages[e.Index].Text, e.Font, Brushes.Black, rect.Left + (rect.Width - sz.Width) / 2, rect.Top + (rect.Height - sz.Height) / 2 + 1);
using (var src = new Bitmap(System.Reflection.Assembly.GetEntryAssembly().GetManifestResourceStream("Castor.Gestionale.images.close-button.png")))
{
e.Graphics.DrawImage(src, rect.Right - 18, rect.Top + 10);
}
e.Graphics.DrawRectangle(Pens.DarkGray, rect);
e.DrawFocusRectangle();
}
}
catch {}
}
Thanks
Actually you can set the size of the tabs, but not individually.
The combination of SizeMode = Fixed and some suitable value for the TabControl.Itemsize will create any size, but always the same..:
So for individually enlarging each tab to make the icon fit you indeed need to use Ian's 'spacey' method..
Unfortunately, there isn't built-in property to control the width of the TabPages' tab header of the TabControl individually (Edit: apparently, there is TabControl.ItemSize to control it collectively. See TaW's answer to fix the width of all tab pages under a tab control).
But a little trick you could do is to give additional spaces in the left or in the right of the TabPage.Text to give you enough space for your icon.
Without space:
With 7 spaces:
It should be enough to put your icon
Try increasing "myTabControl.Padding.X". It works for me!
Use it...
private void FrmSqlMain_Load(object sender, EventArgs e)
{
myTabControl.SizeMode = TabSizeMode.Normal;
myTabControl.DrawMode = TabDrawMode.OwnerDrawFixed;
}
Also i would like to add one more thing. At first, I tried adding spaces but it didn't seem like working for me. Later, I realized I had to add more spaces than usual to see the difference.
I have a label with text inside i can change the label size or the label font size each time and check many times but maybe there is a way to calculate it:
label18.Text = "מכם מזג האוויר איננו פעיל כרגע";
This is how i see the text now:
The text in red is in hebrew this is the text i want to change it's size and also to put it in the middle according to the picturebox1 top not on the left like it is now.
And i did a black circle just to show what i mean by " the distance from the top of pictureBox1 and almost the top of form1 ".
I mean this gray area from the above the pictureBox1 and the form1 white area on the top only this gray area i want to make the text in this height and in the middle.
How can i calculate this two values ?
I tried this but it's not in the exact middle:
SizeF size = label18.CreateGraphics().MeasureString(label18.Text, label18.Font);
label18.Left = (pictureBox1.Width / 2) - (((int)size.Width) / 2) + pictureBox1.Left;
label18.Top = pictureBox1.Top - 20;
You don't need graphics or to measure anything. Just set in designer text align = middlecenter and autosize = true
label18.Location = new Point(pictureBox1.Location.X + (pictureBox1.Width / 2 - label18.Width / 2,
pictureBox1.Location.Y - label18.Height);
To center a label you need it get it actual size, then to center it using another control use some simple math to get the coordinate for the control (see below Example 1). I don't know what control the grey bar is but you could center in that by using the size.Width property and doing the same type of calculation.
If you want to fill the grey bar I have added Example 2.
Example 1:
private void CenterLabel()
{
//get the size of the text (you could do this before hand if needed)
SizeF size = label18.CreateGraphics().MeasureString(label18.Text, label18.Font);
//center over picture box control and slightly above
label18.Left = (pictureBox1.Width / 2) - (((int)size.Width) / 2) + pictureBox1.Left;
label18.Top = pictureBox1.Top - 20;
}
Example 2
private void CenterLabel()
{
int fontHeightPixels = (int)(greyBar.Height * .85);
Font font = new System.Drawing.Font("Arial", fontHeightPixels, FontStyle.Regular, GraphicsUnit.Pixel);
string text = "I am centered";
//get the size of the text (you could do this before hand if needed)
SizeF size = label18.CreateGraphics().MeasureString(text, font);
label18.Font = font;
label18.Text = text;
//center over picture box control and slightly above
label18.Left = (pictureBox1.Width / 2) - (((int)size.Width) / 2) + pictureBox1.Left;
label18.Top = (greyBar.Height / 2) - (((int)size.Height) / 2) + greyBar.Top;
}
This is relatively simple with Windows forms:
Dock your label to the top of the form by setting the appropriate property in the Forms designer. The property you want to set is Dock and it should be set to Top.
Change the label's AutoSize property to false.
Change the label's height as desired.
Change the label's TextAlign property to MiddleCentre.
That should do it.
There's more then one way to achieve this goal.
I would suggest the following:
First calculate the width of the picturebox (picturebox.Width)
Find the coordinates on the form where the picturebox resides (picturebox.Location) property of the picturebox)
Then you change the location of your label control --> to Label.Location.X = (picturebox.Width /2) and Label.Location.Y = picturebox.Location.Y ==> now you have the label correctly placed .
Next Set the Height of the Label Control to the Top(distance between the edge of the form and picturebox) value of the Picturebox.
No visual studion from where i am typing so cannot do full code example.
You're done.
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);
}
};