I am trying to scale all controls in my program (the ones in the tabcontrol too) when user resizes the window. I know how to resize the controls, but i wanted my program to have everything scaled, so the ui i easier to read
i am not a good explainer, so here's a reference picture:
I have seen a lot of discussions about resizing the controls, but they are not very helpful. can i achieve this effect in winforms?
CodePudding user response:
One way to scale winforms controls with window size is to use nested TableLayoutPanel
controls and set the rows and columns to use percent rather than absolute sizing.
Then, place your controls in the cells and anchor them on all four sides. For buttons that have a background image set to BackgroundImageLayout = Stretch
this is all you need to do. However, controls that use text may require a custom means to scale the font. For example, you're using ComboBox controls where size is a function of the font not the other way around. To compensate for this, these actual screenshots utilize an extension to do a binary search that changes the font size until a target control height is reached.
The basic idea is to respond to TableLayoutPanel
size changes by setting a watchdog timer, and when the timer expires iterate the control tree to apply the BinarySearchFontSize
extension. You may want to clone the code I used to test this answer and experiment for yourself to see how the pieces fit together.
Something like this would work but you'll probably want to do more testing than I did.
static class Extensions
{
public static float BinarySearchFontSize(this Control control, float containerHeight)
{
float
vertical = BinarySearchVerticalFontSize(control, containerHeight),
horizontal = BinarySearchHorizontalFontSize(control);
return Math.Min(vertical, horizontal);
}
/// <summary>
/// Get a font size that produces control size between 90-100% of available height.
/// </summary>
private static float BinarySearchVerticalFontSize(Control control, float containerHeight)
{
var name = control.Name;
switch (name)
{
case "comboBox1":
Debug.WriteLine($"{control.Name}: CONTAINER HEIGHT {containerHeight}");
break;
}
var proto = new TextBox
{
Text = "|", // Vertical height independent of text length.
Font = control.Font
};
using (var graphics = proto.CreateGraphics())
{
float
targetMin = 0.9F * containerHeight,
targetMax = containerHeight,
min, max;
min = 0; max = proto.Font.Size * 2;
for (int i = 0; i < 10; i )
{
if(proto.Height < targetMin)
{
// Needs to be bigger
min = proto.Font.Size;
}
else if (proto.Height > targetMax)
{
// Needs to be smaller
max = proto.Font.Size;
}
else
{
break;
}
var newSizeF = (min max) / 2;
proto.Font = new Font(control.Font.FontFamily, newSizeF);
proto.Invalidate();
}
return proto.Font.Size;
}
}
/// <summary>
/// Get a font size that fits text into available width.
/// </summary>
private static float BinarySearchHorizontalFontSize(Control control)
{
var name = control.Name;
// Fine-tuning
string text;
if (control is ButtonBase)
{
text = "SETTINGS"; // representative max staing
}
else
{
text = string.IsNullOrWhiteSpace(control.Text) ? "LOCAL FOLDERS" : control.Text;
}
var protoFont = control.Font;
using(var g = control.CreateGraphics())
{
using (var graphics = control.CreateGraphics())
{
int width =
(control is ComboBox) ?
control.Width - SystemInformation.VerticalScrollBarWidth :
control.Width;
float
targetMin = 0.9F * width,
targetMax = width,
min, max;
min = 0; max = protoFont.Size * 2;
for (int i = 0; i < 10; i )
{
var sizeF = g.MeasureString(text, protoFont);
if (sizeF.Width < targetMin)
{
// Needs to be bigger
min = protoFont.Size;
}
else if (sizeF.Width > targetMax)
{
// Needs to be smaller
max = protoFont.Size;
}
else
{
break;
}
var newSizeF = (min max) / 2;
protoFont = new Font(control.Font.FontFamily, newSizeF);
}
}
}
return protoFont.Size;
}
}
Edit
The essence of my answer is use nested TableLayoutPanel controls and I didn't want to take away from that so I had given a reference link to browse the full code. Since TableLayoutPanel
is not a comprehensive solution without being able to scale the height of ComboBox and other Fonts (which isn't all that trivial for the example shown in the original post) my answer also showed one way to achieve that. In response to the comment below (I do want to provide "enough" info!) here is an appendix showing the MainForm code that calls the extension.
Example:
In the method that loads the main form:
- Iterate the control tree to find all the TableLayoutPanels.
- Attach event handler for the
SizeChanged
event. - Handle the event by restarting a watchdog timer.
- When the timer expires, _iterate the control tree of each
TableLayoutPanel
to apply theonAnyCellPaint
method to obtain the cell metrics and call the extension.
By taking this approach, cells and controls can freely be added and/or removed without having to change the scaling engine.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void onl oad(EventArgs e)
{
base.OnLoad(e);
if(!DesignMode)
{
comboBox1.SelectedIndex = 0;
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
tableLayoutPanel.SizeChanged = (sender, e) => _wdtSizeChanged.StartOrRestart();
}
});
_wdtSizeChanged.PropertyChanged = (sender, e) =>
{
if (e.PropertyName!.Equals(nameof(WDT.Busy)) && !_wdtSizeChanged.Busy)
{
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
IterateControlTree(tableLayoutPanel, (child) => onAnyCellPaint(tableLayoutPanel, child));
}
});
}
};
}
// Induce a size change to initialize the font resizer.
BeginInvoke(()=> Size = new Size(Width 1, Height));
BeginInvoke(()=> Size = new Size(Width - 1, Height));
}
// Browse full code sample to see WDT class
WDT _wdtSizeChanged = new WDT { Interval = TimeSpan.FromMilliseconds(100) };
SemaphoreSlim _sslimResizing= new SemaphoreSlim(1);
private void onAnyCellPaint(TableLayoutPanel tableLayoutPanel, Control control)
{
if (!DesignMode)
{
if (_sslimResizing.Wait(0))
{
try
{
var totalVerticalSpace =
control.Margin.Top control.Margin.Bottom
// I'm surprised that the Margin property
// makes a difference here but it does!
tableLayoutPanel.Margin.Top tableLayoutPanel.Margin.Bottom
tableLayoutPanel.Padding.Top tableLayoutPanel.Padding.Bottom;
var pos = tableLayoutPanel.GetPositionFromControl(control);
int height;
float optimal;
if (control is ComboBox comboBox)
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
optimal = comboBox.BinarySearchFontSize(height);
comboBox.Font = new Font(comboBox.Font.FontFamily, optimal);
comboBox.ItemHeight = height;
}
else if((control is TextBoxBase) || (control is ButtonBase))
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
optimal = control.BinarySearchFontSize(height);
control.Font = new Font(control.Font.FontFamily, optimal);
}
}
finally
{
_sslimResizing.Release();
}
}
}
}
internal void IterateControlTree(Control control, Action<Control> fx)
{
if (control == null)
{
control = this;
}
fx(control);
foreach (Control child in control.Controls)
{
IterateControlTree(child, fx);
}
}
}