I want to change the Item's/Row's height in listview.
I searched every where and I figured that in order to change the height I need to use LBS_OWNERDRAWFIXED or MeasureItem or something like that.
The problem is that I dont know exactly what to do and how to use it..
Can anyone help me with it?
Edit:
I cant use the ImageList hack because I am using the SmallImageList for real and I need different line height from the ImageList images size.
Thanks!
For the people that are still struggling with this, here is the code I use:
private void SetHeight(ListView listView, int height)
{
ImageList imgList = new ImageList();
imgList.ImageSize = new Size(1, height);
listView.SmallImageList = imgList;
}
To use this, just do:
SetHeight(lvConnections, 25);
You have to use a bit of a hack. The trick is to use an image list in the StateImageList property. The ListView will adjust its item height, based on the height of the ImageList's ImageSize property. You do not have to specify an image for your items, but just using the StateImageList will force the ListView to adjust. In the example below, I had set the image list size to 32x32, thus resulting in a 32px height ListViewItem(s).
It can be done using the SmallImageList trick -- you just have to be careful. ObjectListView -- an open source wrapper around a standard .NET ListView -- uses that trick to successfully implement a RowHeight property.
If you want 32 pixels for each row, allocate an ImageList that is 16x32 (width x height), and then position each of your images in the vertical middle of the 32-pixel height.
This screen shot shows 32-pixel rows and the word wrapping that is possible because of the extra space:
ObjectListView does all this work for you. In fact, if you are trying to do anything with a ListView, you should seriously looked at using an ObjectListView instead. It makes many difficult things (e.g. sorting by column type, custom tooltips) trivial, and several impossible things (e.g. overlays, groups on virtual lists) possible.
Sadly nobody answered your original question how to use LBS_OWNERDRAWFIXED in all these years.
The answer that you have accepted is integrating a huge project (with demos and documentation 3,3MB). But just for setting the line height of a ListView this is overbloated.
The other workaround suggested here (adding an ImageList) works only to increase the row height. But it does not allow to really set the RowHeight independent of the image height. Additionally the default row height depends on the operating system. For example on Windows 7 the rows are much higher than on XP. You cannot chose to make them tighter, only higher.
But with very few lines you can do what you want.
Just copy and paste the following class:
using System;
using System.Drawing;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace ExtendedControls
{
public class ListViewEx : ListView
{
#region Windows API
/*
struct MEASUREITEMSTRUCT
{
public int CtlType; // Offset = 0
public int CtlID; // Offset = 1
public int itemID; // Offset = 2
public int itemWidth; // Offset = 3
public int itemHeight; // Offset = 4
public IntPtr itemData;
}
*/
[StructLayout(LayoutKind.Sequential)]
struct DRAWITEMSTRUCT
{
public int ctlType;
public int ctlID;
public int itemID;
public int itemAction;
public int itemState;
public IntPtr hWndItem;
public IntPtr hDC;
public int rcLeft;
public int rcTop;
public int rcRight;
public int rcBottom;
public IntPtr itemData;
}
// LVS_OWNERDRAWFIXED: The owner window can paint ListView items in report view.
// The ListView control sends a WM_DRAWITEM message to paint each item. It does not send separate messages for each subitem.
const int LVS_OWNERDRAWFIXED = 0x0400;
const int WM_SHOWWINDOW = 0x0018;
const int WM_DRAWITEM = 0x002B;
const int WM_MEASUREITEM = 0x002C;
const int WM_REFLECT = 0x2000;
#endregion
bool mb_Measured = false;
int ms32_RowHeight = 14;
/// <summary>
/// Constructor
/// </summary>
public ListViewEx()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}
/// <summary>
/// Sets the row height in Details view
/// This property appears in the Visual Studio Form Designer
/// </summary>
[Category("Appearance")]
[Description("Sets the height of the ListView rows in Details view in pixels.")]
public int RowHeight
{
get { return ms32_RowHeight; }
set
{
if (!DesignMode) Debug.Assert(mb_Measured == false, "RowHeight must be set before ListViewEx is created.");
ms32_RowHeight = value;
}
}
protected override CreateParams CreateParams
{
get
{
CreateParams k_Params = base.CreateParams;
k_Params.Style |= LVS_OWNERDRAWFIXED;
return k_Params;
}
}
/// <summary>
/// The messages WM_MEASUREITEM and WM_DRAWITEM are sent to the parent control rather than to the ListView itself.
/// They come here as WM_REFLECT + WM_MEASUREITEM and WM_REFLECT + WM_DRAWITEM
/// They are sent from Control.WmOwnerDraw() --> Control.ReflectMessageInternal()
/// </summary>
protected override void WndProc(ref Message k_Msg)
{
base.WndProc(ref k_Msg); // FIRST
switch (k_Msg.Msg)
{
case WM_SHOWWINDOW: // called when the ListView becomes visible
{
Debug.Assert(View == View.Details, "ListViewEx supports only Details view");
Debug.Assert(OwnerDraw == false, "In ListViewEx do not set OwnerDraw = true");
break;
}
case WM_REFLECT + WM_MEASUREITEM: // called once when the ListView is created, but only in Details view
{
mb_Measured = true;
// Overwrite itemHeight, which is the fifth integer in MEASUREITEMSTRUCT
Marshal.WriteInt32(k_Msg.LParam + 4 * sizeof(int), ms32_RowHeight);
k_Msg.Result = (IntPtr)1;
break;
}
case WM_REFLECT + WM_DRAWITEM: // called for each ListViewItem to be drawn
{
DRAWITEMSTRUCT k_Draw = (DRAWITEMSTRUCT) k_Msg.GetLParam(typeof(DRAWITEMSTRUCT));
using (Graphics i_Graph = Graphics.FromHdc(k_Draw.hDC))
{
ListViewItem i_Item = Items[k_Draw.itemID];
Color c_BackColor = i_Item.BackColor;
if (i_Item.Selected) c_BackColor = SystemColors.Highlight;
if (!Enabled) c_BackColor = SystemColors.Control;
using (SolidBrush i_BackBrush = new SolidBrush(c_BackColor))
{
// Erase the background of the entire row
i_Graph.FillRectangle(i_BackBrush, i_Item.Bounds);
}
for (int S=0; S<i_Item.SubItems.Count; S++)
{
ListViewItem.ListViewSubItem i_SubItem = i_Item.SubItems[S];
// i_Item.SubItems[0].Bounds contains the entire row, rather than the first column only.
Rectangle k_Bounds = (S>0) ? i_SubItem.Bounds : i_Item.GetBounds(ItemBoundsPortion.Label);
// You can use i_Item.ForeColor instead of i_SubItem.ForeColor to get the same behaviour as without OwnerDraw
Color c_ForeColor = i_SubItem.ForeColor;
if (i_Item.Selected) c_ForeColor = SystemColors.HighlightText;
if (!Enabled) c_ForeColor = SystemColors.ControlText;
TextFormatFlags e_Flags = TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine;
switch (Columns[S].TextAlign)
{
case HorizontalAlignment.Center: e_Flags |= TextFormatFlags.HorizontalCenter; break;
case HorizontalAlignment.Right: e_Flags |= TextFormatFlags.Right; break;
}
TextRenderer.DrawText(i_Graph, i_SubItem.Text, i_SubItem.Font, k_Bounds, c_ForeColor, e_Flags);
}
}
break;
}
}
}
} // class
} // namespace
After adding a ListViewEx to your Form you will see a new property in the Visual Studio Forms Designer which allows to set the row height in pixels:
The value you enter there will be the row height in pixels and it will be respected exatctly on all operating systems. I tested it on Windows XP, 7 and 10:
Additionally my class has two more advantages over the original ListView: It draws flicker-free and it respects the ForeColor and Font set in ListViewSubItem which is ignored by the original Microsoft ListView. So you can draw each cell with a different color and font.
IMPORTANT: As the MSDN says LBS_OWNERDRAWFIXED has been designed only for Details view (Report view). My code works only for this mode and this is because Microsoft has designed it like that.
Additionally please note that setting ListView.OwnerDraw = true is a completely different thing than using LVS_OWNERDRAWFIXED.
I did not implement drawing icons, because I don't need that. But you can easily add this.
The default line height of a ListView (in report view mode) is computed based on the control's font size.
So to select the line height, choose a font with the right height in the ListView properties.
For example, select MS Sans Serif 18.
Then you can change the font used by all items:
when you insert a new item, set its font property.
To optimize font assignment you should declare the item font as a private member of the form:
Private Font stdfont = new Font( "Consolas", 9.0f, FontStyle.Regular );
Then when adding items :
ListViewItem i = new ListViewItem( "some text" );
i.Font = stdfont;
MyListView.Items.Add( i );
This trick is the only easy one allowing to have SMALLER line height ;)
i.E. set control's font size to 7 and set items' font size to 10.
(Tested with VS 2008 )
Plasmabubble has the right idea. This expands on that and is what I use to use a narrow line-width for the items.
The linespacing in a ListView is dependent on the ListView's font and can't be changed. However, you can set the font for the items in the ListView to something larger than the ListView's font.
If you want it to be proportional, create a font based on the item's font.
I want the item height to be 90% of normal, whatever the font chosen.
When I populate the list I used a font stored in settings but you could also use a literal font like "Consolas".
lvResults.Font =
new Font(Properties.Settings.Default.usrHookFont.FontFamily,
(float)(Properties.Settings.Default.usrHookFont.Size * .9));
foreach (HookSet item in resultSet)
{
ListViewItem lvi = new ListViewItem();
lvi.Font = Properties.Settings.Default.usrHookFont;
<dot><dot><dot>
}
After reading the answers for so many years, one is to use ListView based extension controls and the other is to use fonts or icons for extensions.
If your project already uses a ListView -- as in my case, where you need to extend the column height while keeping the original icon and font size -- I suggest
You can roughly calculate the column height you need, and the ratio of the normal icon size, and thus use the transparent border to extend the icon size, e.g. if the 24x24 icon is actually 35 in height, you can use windows paint 3D to extend the icon to 35x35 using the canvas to keep the original ratio of the icon, I think this is probably the most time and cost effective way.
Related
I want to display a picture in another scaling mode in an UltraGridCell. The mode should scale the image to the cell height and clip the rest of the image from the right to fit into the cell. This is easy if I could just draw it myself as the EmbeddableImageRenderer does not allow me to set its scaling behaviour (I am not talking about MaintainAspectRatio as I still want to maintain the aspect ratio).
I tried it with the tutorial to embed any control in a cell. And it is working fine with the given example of a TrackBar (and in my tiny testing project also with a ProgressBar as RendererControl). But it appears not to work with columns that are displaying images.
As a DataSource I have a list of my own class with an Image property which is displayed in the grid. As Editor-/RendererControl I set two regular PictureBoxes.
Any suggestions to solve the main problem of scaling the image or to set any control to the picture column (that would then deal with the scaling)?
I can't see any reason why the UltraControlContainerEditor shouldn't work with an image column, provided that the control you are using has a property that takes an Image and that you specify the correct PropertyName on the editor. But that's probably not the most efficient approach, anyway.
A better approach would be to use a DrawFilter to draw the image yourself.
public class ImageScalingDrawFilter : IUIElementDrawFilter
{
bool IUIElementDrawFilter.DrawElement(DrawPhase drawPhase, ref UIElementDrawParams drawParams)
{
switch (drawPhase)
{
case DrawPhase.BeforeDrawImage:
ImageUIElement imageElement = (ImageUIElement)drawParams.Element;
Image image = imageElement.Image;
int availableHeight = drawParams.Element.RectInsideBorders.Height;
float ratio = (float)availableHeight / (float)image.Height;
float newHeight = image.Height * ratio;
float newWidth = image.Width * ratio;
Rectangle rect = new Rectangle(
imageElement.Rect.X,
imageElement.Rect.Y,
(int)(newWidth),
(int)(newHeight)
);
// Draw the scaled image.
drawParams.Graphics.DrawImage(image, rect);
// This tells the grid not to draw the image (since we've already drawn it).
return true;
}
return false;
}
DrawPhase IUIElementDrawFilter.GetPhasesToFilter(ref UIElementDrawParams drawParams)
{
UIElement element = drawParams.Element;
// Look for an ImageUIElement
if (element is ImageUIElement)
{
// Presumably, we only want to this images in cells
// and not every image in the entire grid, so make sure it's in a cell.
CellUIElement cellElement = element.GetAncestor(typeof(CellUIElement)) as CellUIElement;
if (null != cellElement)
{
// We could also limit this to a particular column or columns.
switch (cellElement.Cell.Column.Key)
{
case "Image":
return DrawPhase.BeforeDrawImage;
}
}
}
return DrawPhase.None;
}
}
You assign the DrawFilter to the grid like so:
private void Form1_Load(object sender, EventArgs e)
{
this.ultraGrid1.DrawFilter = new ImageScalingDrawFilter();
}
I want to create a listView that shows users nicknames for a chat program. For that I created a new class that inherits from listViewItem.
What i want to do is, depending on the length of the nickname scale my font size.
I have read lots of articles about scaling but ALL of them depend on a graphics object and i have no clue how i get one of those ??? i tried it with a label and there it would be from the paint event but listView doesnt have such an event? so how do i scale this font ?
Q:
How do I get the right fontsize that the Nickname will fit into a specified rectangle ?
EDIT: Forgot to say I'm completly new to anything with grafic stuff i only used the Designer and set some properties.
You should set OwnerDraw property of the ListView to true, add draw item event handler like this:
listView1.DrawItem += listView1_DrawItem;
And here is a simple implementation of what you want so you can play with and tune it up:
void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
{
float emSize = e.Item.Font.Size;
Font font = new Font(e.Item.Font.FontFamily, emSize);
while(e.Graphics.MeasureString(e.Item.Text, e.Item.Font).Width>e.Item.Bounds.Width)
{
emSize--;
font = new Font(e.Item.Font.FontFamily, emSize);
e.Item.Font = font;
}
e.DrawText();
}
You see that you need to change the font size and measure the string you want to display so it fits in the cell completely. Presuming that if your current font size doesn't fit, you want to make it smaller.
I marked #Nikola answer right because it explained a lot but in my case i needed something way simpler and thanks to #TaW i also got the problem with the Graphics solved here my code snippet
public static Font getNewFont(Font origFont, string text, float maxWidth, Graphics g)
{
float emSize = origFont.Size;
Font font = origFont;
while (g.MeasureString(text, font).Width > maxWidth)
{
emSize--;
font = new Font(origFont.FontFamily, emSize);
}
return font;
}
This highlights the entire width of each line by painting a transparent color as the backcolor on the current line. When the line switches, the original background color is restored.
So, what we want to do is:
Verify the previous and current rectangles don't match, so not to paint the same area twice
Replace the last line's highlight using the controls backcolor
Highlight the current line using a transparent color
Set mLastHighlight with the index and rectangle for each applied line
However, when removing the highlight, the text is painted over. This does not occur when applying the highlight.
One solution would be to repaint the text back on the control after resetting the back color. though the text formatting, selection colors, font styles, hyperlinks, etc would be tedious to filter. Not very elegant.
This leads to a simpler solution, refreshing the control. Though that would cause massive flickering. Not acceptable either.
Is there an elegant solution? I'm completely baffled why this occurs.
EDIT: Edited to reflect Code Gray's response.
using System;
public class RTBHL : RichTextBox
{
private LastHighlight mLastHighlight = new LastHighlight(0, Rectangle.Empty);
private class LastHighlight
{
public int mCharIndex;
public Rectangle mRectangle;
public LastHighlight(int index, Rectangle r)
{
mCharIndex = index;
mRectangle = r;
}
}
public void PaintLineHighlight()
{
using (Graphics g = this.CreateGraphics)
{
// highlight color
Color c = Color.Beige;
// current pen color
Pen cp = new Pen(Color.Beige);
// color for removing highlight
Pen lp = new Pen(this.BackColor);
// brush for removing highlight
SolidBrush lb = new SolidBrush(this.BackColor);
// brush for applying highlight
SolidBrush cb = new SolidBrush(Color.FromArgb(64, c.R, c.G, c.B));
// index of the current line
int index = this.GetFirstCharIndexOfCurrentLine;
// rectangle to specify which region to paint too
Rectangle r = new Rectangle();
// specify dimensions
r.X = 0;
r.Y = this.GetPositionFromCharIndex(index).Y;
r.Width = this.HorizontalScrollBarWidth;
r.Height = Convert.ToInt32(this.Font.Height * this.ZoomFactor);
// this will always be true unless the current line remains the same
if (!(mLastHighlight.mCharIndex == index) && !(mLastHighlight.mRectangle == r))
{
// remove the last highlight. regardless of the brush specified, white is always applied, and the text is painted over
g.DrawRectangle(lp, mLastHighlight.mRectangle);
g.FillRectangle(lb, mLastHighlight.mRectangle);
// apply highlight to the current line
g.DrawRectangle(cp, r);
g.FillRectangle(cb, r);
}
mLastHighlight = new LastHighlight(index, r);
}
}
#region RichScrollBars
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetScrollInfo(IntPtr hWnd, int fnBar, ref SCROLLINFO si);
[StructLayout(LayoutKind.Sequential)]
public class SCROLLINFO
{
public int cbSize;
public int fMask;
public int nMin;
public int nMax;
public int nPage;
public int nPos;
public int nTrackPos;
public SCROLLINFO()
{
this.cbSize = Marshal.SizeOf(typeof(SCROLLINFO));
}
public SCROLLINFO(int mask, int min, int max, int page, int pos)
{
this.cbSize = Marshal.SizeOf(typeof(SCROLLINFO));
this.fMask = mask;
this.nMin = min;
this.nMax = max;
this.nPage = page;
this.nPos = pos;
}
}
private const int SIF_ALL = 0X17;
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
public int HorizontalScrollBarWidth()
{
SCROLLINFO si = new SCROLLINFO() {fMask = SIF_ALL};
GetScrollInfo(this.Handle, SB_HORZ, si);
return Math.Max(si.nMax, this.Width);
}
public int VerticalScrollBarHeight()
{
SCROLLINFO si = new SCROLLINFO() {fMask = SIF_ALL};
GetScrollInfo(this.Handle, SB_VERT, si);
return Math.Max(si.nMax, this.Height);
}
#endregion
}
The problem here is that the code you're copying is designed for Scintilla. The SCI_* constants are defined internally by the Scintilla headers, and the messages they refer to only have meaning to the Scintilla controls.
Sending those messages to the native Win32 rich edit control isn't going to do anything because it wasn't designed to process those messages. (Or worse, one or more of the SCI_* constants happen to clash with one or more of the message identifiers that the rich edit control does recognize, producing some potentially interesting behavior.)
Unless you're actually using a Scintilla edit control in your project (which you said you don't want to do), that code isn't going to do anything interesting. It isn't written for the Win32 rich edit control, it's written to interface with the Scintilla control.
The Scintilla control is far more than just a wrapper around the Win32 rich edit control. It has to do a lot of custom drawing to make its magic, and all of that code is hard to get right on your own. That's why so many people use Scintilla in the first place. If you need its feature set, I highly recommend that you follow suit.
Anyway, I don't actually know if this is possible with the Win32 rich edit control. I don't think it is, but I couldn't swear to that fact. I guess you could hack it by setting the selection color, but that doesn't seem like a very good solution. Something like Daniel suggests here. I'm not a Scintilla expert, but to my untrained eyes, this looks kind of like the moral equivalent of your Scintilla-based code, but written for the Win32 rich edit control (through the .NET WinForms wrapper thereof).
I am trying to evenly space dynamically created controls by giving them a width and height that is a fraction of the width and height of the container control.
However, although my functions calculating the dimensions are correct, for some reason the assignment to the Width property is being stubbornly refused. The WidthToUse() function is returning 19, yet when an attempt is made to assign that vaue to the dynamically created TextBox, it remains at...630! Why 630 to begin with, and why does it refuse to be assigned to?
Even the height, although "working" doesn't work quite right - there are too many controls or not enough space on the panel to accommodate all of the controls using this math.
Here's my code:
int WidthToUse = getTextBoxWidthToUse(tableLayoutPanelGreatGooglyMoogly.Width);
int HeightToUse = getControlHeightToUse(tableLayoutPanelGreatGooglyMoogly.Height);
TextBox txtbx = new TextBox();
txtbx.Parent = tableLayoutPanelGreatGooglyMoogly;
txtbx.Margin = new Padding();
txtbx.Dock = DockStyle.Fill;
txtbx.AutoSize = false;
txtbx.Width = WidthToUse; // WidthToUse is 19, but txtbx.Width is 630 both before AND after this assignment!
txtbx.Height = HeightToUse; // HeightToUse is 27
private static int getControlHeightToUse(int theDynPanelHeight) {
return (theDynPanelHeight / NUMBER_OF_ROWS);
}
private static int getTextBoxWidthToUse(int theDynPanelWidth) {
return (theDynPanelWidth / 32);
}
private static int getLabelWidthToUse(int theDynPanelWidth) {
return ((theDynPanelWidth / 64) * 3);
}
DockStyle.Fill is a property describing the size of a control relative to the size of it's container. A control will resize to fit all of the empty space in it's parent container with this property set.
Because you have the Dock property set to Fill would be my guess.
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);
}
};