Home > Back-end >  Scale winforms controls with window size
Scale winforms controls with window size

Time:01-18

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: scaled controls (not resized)

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.

nested table layout panels


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.

resizing performance


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 the onAnyCellPaint 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);
        }
    }
}
  • Related