Auto resizing items/controls in winforms c# showing error - c#

I've been trying to make my windows form application auto resizing i.e. when the user resizes the application its controls also resize accordingly and after searching online I've come across the below code in the form of a .cs file :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
public class clsResize
{
List<System.Drawing.Rectangle> _arr_control_storage = new List<System.Drawing.Rectangle>();
private bool showRowHeader = false;
public clsResize(Form _form_)
{
form = _form_; //the calling form
_formSize = _form_.ClientSize; //Save initial form size
_fontsize = _form_.Font.Size; //Font size
}
private float _fontsize { get; set; }
private System.Drawing.SizeF _formSize {get;set; }
private Form form { get; set; }
public void _get_initial_size() //get initial size//
{
var _controls = _get_all_controls(form);//call the enumerator
foreach (Control control in _controls) //Loop through the controls
{
_arr_control_storage.Add(control.Bounds); //saves control bounds/dimension
//If you have datagridview
if (control.GetType() == typeof(DataGridView))
_dgv_Column_Adjust(((DataGridView)control), showRowHeader);
}
}
public void _resize() //Set the resize
{
double _form_ratio_width = (double)form.ClientSize.Width /(double)_formSize.Width; //ratio could be greater or less than 1
double _form_ratio_height = (double)form.ClientSize.Height / (double)_formSize.Height; // this one too
var _controls = _get_all_controls(form); //reenumerate the control collection
int _pos = -1;//do not change this value unless you know what you are doing
foreach (Control control in _controls)
{
// do some math calc
_pos += 1;//increment by 1;
System.Drawing.Size _controlSize = new System.Drawing.Size((int)(_arr_control_storage[_pos].Width * _form_ratio_width),
(int)(_arr_control_storage[_pos].Height * _form_ratio_height)); //use for sizing
System.Drawing.Point _controlposition = new System.Drawing.Point((int)
(_arr_control_storage[_pos].X * _form_ratio_width),(int) (_arr_control_storage[_pos].Y * _form_ratio_height));//use for location
//set bounds
control.Bounds = new System.Drawing.Rectangle(_controlposition, _controlSize); //Put together
//Assuming you have a datagridview inside a form()
//if you want to show the row header, replace the false statement of
//showRowHeader on top/public declaration to true;
if (control.GetType() == typeof(DataGridView))
_dgv_Column_Adjust(((DataGridView)control), showRowHeader);
//Font AutoSize
control.Font = new System.Drawing.Font(form.Font.FontFamily,
(float)(((Convert.ToDouble(_fontsize) * _form_ratio_width) / 2) +
((Convert.ToDouble(_fontsize) * _form_ratio_height) / 2)));
}
}
private void _dgv_Column_Adjust(DataGridView dgv, bool _showRowHeader) //if you have Datagridview
//and want to resize the column base on its dimension.
{
int intRowHeader = 0;
const int Hscrollbarwidth = 5;
if (_showRowHeader)
intRowHeader = dgv.RowHeadersWidth;
else
dgv.RowHeadersVisible = false;
for (int i = 0; i < dgv.ColumnCount; i++)
{
if (dgv.Dock == DockStyle.Fill) //in case the datagridview is docked
dgv.Columns[i].Width = ((dgv.Width - intRowHeader) / dgv.ColumnCount);
else
dgv.Columns[i].Width = ((dgv.Width - intRowHeader - Hscrollbarwidth) / dgv.ColumnCount);
}
}
private static IEnumerable<Control> _get_all_controls(Control c)
{
return c.Controls.Cast<Control>().SelectMany(item =>
_get_all_controls(item)).Concat(c.Controls.Cast<Control>()).Where(control =>
control.Name != string.Empty);
}
}
I added this to my project used in code as below :
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace KryptonTest
{
/// <summary>
/// Description of MainForm.
/// </summary>
public partial class MainForm : KryptonForm
{
clsResize _form_resize;
public MainForm()
{
//
// The InitializeComponent() call is required for Windows Forms designer support.
//
InitializeComponent();
_form_resize = new clsResize(this);
this.Load += MainFormLoad;
this.Resize += MainFormResize;
}
void MainFormLoad(object sender, EventArgs e)
{
_form_resize._get_initial_size();
}
void MainFormResize(object sender, EventArgs e)
{
_form_resize._resize();
}
}
}
But when I run it I get the below error twice
How do I fix this?

When we resize a form we normally pick some controls to get bigger and others not - it doesn't really make much sense to double the width and height of a name TextBox, or a Checkbox because you don't type/see significantly more text in them. Other items like a logging list box, email body designer TextBox etc, it makes sense to have those things resize.
Arranging a layout that resizes is quite simple: every control has an Anchor property and when the anchor for a particular side is set then the control will move or resize so there is the same distance between the control and the given edge of the container. Obviously then if a control is anchored on opposing sides, then it will stretch when the container it is in gets bigger
Some examples:
TextBox is 100 wide and 20 high, anchored on Top Left. The form is made 200 pixels wider and 200 higher. The TextBox appears not to move or grow; it remains the same distance from the top left corner
TextBox is 100 wide and 20 high, anchored on Top Right. The form is made 200 pixels wider and 200 higher. The TextBox moves 200 pixels to the Right because it is Right anchored and keeps the same distance between the form right edge. It does not grow wider; it remains the same distance from the top left corner. It does not grow taller because it is Top anchored but not Bottom anchored
TextBox is 100 wide and 20 high, anchored on Top Left Bottom Right. It is a multi line TextBox. The form is made 200 pixels wider and 200 higher. The TextBox grows 200 pixels wider and 200 pixels higher because the form has grown by 200x200 - the anchors on all sides means the textbox edges stay the same distance from the form edges
You want 3 textboxes in a row, the middle one should get wider when the form gets wider; the other two textboxes should not grow
You anchor the left TextBox on the Top Left
You anchor the middle TextBox on Top Left Right
You anchor the right TextBox on the Top Right
When the form gets wider the left TextBox stays put, the right TextBox moves to the right by as much as the form got wider, the middle TextBox expands sideways by as much as the form got wider
When neither opposing side is anchored the control moves by half the distance of the resize in that direction
Don't forget you can put panels in that are anchored one way and then controls inside them that are anchored another way. For other kinds of layouts you might need a table or flow layout panel but for most UIs the anchoring system works quite well

Are you sure the enum is working properly? The error message is telling you the list has less items than you're trying to call.
Index was out of range. Must be non-negative and less than the size
of the collection. Parameter name: index
Edit:
I just copied and pasted your code into a new WinForms project in VS 2022 and it worked with no issues though I did have to drop the inheritance reference to KryptonForm since I didn't have it. If it helps, I ran it using .Net 6.0 (LTS)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WinFormsApp1
{
/// <summary>
/// Description of MainForm.
/// </summary>
public partial class MainForm : Form
{
clsResize _form_resize;
public MainForm()
{
InitializeComponent();
_form_resize = new clsResize(this);
this.Load += MainFormLoad;
this.Resize += MainFormResize;
}
void MainFormLoad(object sender, EventArgs e)
{
_form_resize._get_initial_size();
}
void MainFormResize(object sender, EventArgs e)
{
_form_resize._resize();
}
}
public class clsResize
{
List<System.Drawing.Rectangle> _arr_control_storage = new List<System.Drawing.Rectangle>();
private bool showRowHeader = false;
public clsResize(Form _form_)
{
form = _form_; //the calling form
_formSize = _form_.ClientSize; //Save initial form size
_fontsize = _form_.Font.Size; //Font size
}
private float _fontsize { get; set; }
private System.Drawing.SizeF _formSize { get; set; }
private Form form { get; set; }
public void _get_initial_size() //get initial size//
{
var _controls = _get_all_controls(form);//call the enumerator
foreach (Control control in _controls) //Loop through the controls
{
_arr_control_storage.Add(control.Bounds); //saves control bounds/dimension
//If you have datagridview
if (control.GetType() == typeof(DataGridView))
_dgv_Column_Adjust(((DataGridView)control), showRowHeader);
}
}
public void _resize() //Set the resize
{
double _form_ratio_width = (double)form.ClientSize.Width / (double)_formSize.Width; //ratio could be greater or less than 1
double _form_ratio_height = (double)form.ClientSize.Height / (double)_formSize.Height; // this one too
var _controls = _get_all_controls(form); //reenumerate the control collection
int _pos = -1;//do not change this value unless you know what you are doing
foreach (Control control in _controls)
{
// do some math calc
_pos += 1;//increment by 1;
System.Drawing.Size _controlSize = new System.Drawing.Size((int)(_arr_control_storage[_pos].Width * _form_ratio_width),
(int)(_arr_control_storage[_pos].Height * _form_ratio_height)); //use for sizing
System.Drawing.Point _controlposition = new System.Drawing.Point((int)
(_arr_control_storage[_pos].X * _form_ratio_width), (int)(_arr_control_storage[_pos].Y * _form_ratio_height));//use for location
//set bounds
control.Bounds = new System.Drawing.Rectangle(_controlposition, _controlSize); //Put together
//Assuming you have a datagridview inside a form()
//if you want to show the row header, replace the false statement of
//showRowHeader on top/public declaration to true;
if (control.GetType() == typeof(DataGridView))
_dgv_Column_Adjust(((DataGridView)control), showRowHeader);
//Font AutoSize
control.Font = new System.Drawing.Font(form.Font.FontFamily,
(float)(((Convert.ToDouble(_fontsize) * _form_ratio_width) / 2) +
((Convert.ToDouble(_fontsize) * _form_ratio_height) / 2)));
}
}
private void _dgv_Column_Adjust(DataGridView dgv, bool _showRowHeader) //if you have Datagridview
//and want to resize the column base on its dimension.
{
int intRowHeader = 0;
const int Hscrollbarwidth = 5;
if (_showRowHeader)
intRowHeader = dgv.RowHeadersWidth;
else
dgv.RowHeadersVisible = false;
for (int i = 0; i < dgv.ColumnCount; i++)
{
if (dgv.Dock == DockStyle.Fill) //in case the datagridview is docked
dgv.Columns[i].Width = ((dgv.Width - intRowHeader) / dgv.ColumnCount);
else
dgv.Columns[i].Width = ((dgv.Width - intRowHeader - Hscrollbarwidth) / dgv.ColumnCount);
}
}
private static IEnumerable<Control> _get_all_controls(Control c)
{
return c.Controls.Cast<Control>().SelectMany(item =>
_get_all_controls(item)).Concat(c.Controls.Cast<Control>()).Where(control =>
control.Name != string.Empty);
}
}
}

Related

Loading multiple Groupboxes in the form using the button_click event

I wish to load multiple groupboxes in the windows form application using a button_click event.
A groupbox should appear in the form each time the button is clicked.
Expected output.
I am having trouble making the location of the groupbox dynamic, as the second groupbox should be some distance away from the first groupbox. I thought of manually calculating the coordinates and using an array of points for the location, but I feel that there should be a better a way to go about it.
I have defined 'int count=0' variable to count the number of times the button is clicked. Based on that I am naming the new groupbox. But I think there is some problem in the logic used in the count++ line. It is not going after 1. Therefore I am only getting one groupbox "groupBox1". Nothing happens when I click the button again.
I appreciate your help.
Thank you
int count=0;
private GroupBox GetGroupBox(int a)
{
GroupBox groupBox = new GroupBox();
groupBox.Text = "groupBox"+(a.ToString());
groupBox.Width= 200;
groupBox.Height= 200;
groupBox.Location = new Point(50,400);
return groupBox;
}
private void button1_Click(object sender, EventArgs e)
{
count++;
this.Controls.Add(GetGroupBox(count));
}
Your question states these objectives:
Dynamically add a GroupBox based on an event (like button click).
Assign the new GroupBox location.
Pad the location with "some distance away".
You say you "feel that there should be a better a way to go about it" and there is!
Try experimenting with a FlowLayoutPanel which handles all three of these by its nature.
Here's the code I used to add and remove instances of CustomGroupBox. This is a UserControl that I added to my project, but this will work with any type of control.)
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
numericUpDownGroupboxes.ValueChanged += onGroupBoxCountChanged;
foreach (var radio in Controls.OfType<RadioButton>())
{
radio.CheckedChanged += onFlowLayoutDirectionChanged;
}
}
When the numeric up-down changes, compare the expected number of groupboxes to the current count. Alternatively, you can continue to use a button click and go straight to flowLayoutPanel.Controls.Add(...).
private void onGroupBoxCountChanged(object sender, EventArgs e)
{
// Need an int for comparison.
int
countIs = flowLayoutPanel.Controls.OfType<CustomGroupBox>().Count(),
countShouldBe = (int)numericUpDownGroupboxes.Value;
switch(countIs.CompareTo(countShouldBe))
{
case -1:
flowLayoutPanel.Controls.Add(
new CustomGroupBox
{
Name = $"groupBox{countShouldBe}",
Text = $"GroupBox {countShouldBe}",
Size = new Size(300, 150),
Margin = new Padding(10),
BackColor = Color.White,
});
break;
case 1:
Control last = flowLayoutPanel.Controls.OfType<CustomGroupBox>().Last();
flowLayoutPanel.Controls.Remove(last);
break;
}
}
The direction of the flow can also be specified.
private void onFlowLayoutDirectionChanged(object sender, EventArgs e)
{
if(radioButtonHorizontal.Checked)
{
flowLayoutPanel.FlowDirection = FlowDirection.LeftToRight;
}
else
{
flowLayoutPanel.FlowDirection = FlowDirection.TopDown;
}
}
}
Since you want to create boxes from left to right you should adjust Left: say, 1st box should have Left = 50, 2nd Left = 270, 3d Left = 490 etc.
Code:
const int deltaX = 20;
...
//TODO: check do you really want Top = 400, not, say, 20?
groupBox.Location = new Point(50 + (a - 1) * (groupBox.Width + deltaX), 400);
...
Simplified implementation can be
int count = 0;
// Let's rename the method: we actually create GroupBox, not get existing
private GroupBox CreateGroupBox(int index) => new GroupBox() {
Text = $"groupBox{index}",
Size = new Size(200, 200),
Location = new Point(50 + (index - 1) * (20 + 200), 400),
Parent = this, // Instead of Controls.Add()
};
private void button1_Click(object sender, EventArgs e) {
CreateGroupBox(++count);
}

Scale winforms controls with window size

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?
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 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 OnLoad(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);
}
}
}

Dynamically resize TabControl and Form width to the number of TabPages

I have a windows form with a TabControl and a ListView.
When I run the application, I want the Width of the TabControl to increase/decrease to show all the TabPages without horizontal scrollbar and have the Form resize it's Width accordingly, to insure that the TabControl and ListView are visible.
A screenshot is below.
To auto-size a TabControl to the size of its Headers, you need to calculate the width of the text of each Header. It's simpler in case the TabControl.SizeMode is set to Fixed, since you can set the ItemSize.Width and all Headers will have the same width.
If the TabControl.SizeMode is set to the default Normal, you have to measure the Text of each Header, adding 1px for the Border (2px if it's the second TabPage - small bug in the base Control).
In the first case, the size of the TabControl is:
tabControl1.Width = tabControl1.TabPages.Count * (tabControl1.ItemSize.Width + 1);
in the second case, measure the text of each Header using TextRendrer.MeasureText:
private int MeasureTabPagesWidth(TabControl tc)
{
if (tc.TabPages.Count == 0) return tc.Width;
int newWidth = 0;
int border = tc.TabPages.Count == 2 ? 2 : 1;
var flags = TextFormatFlags.LeftAndRightPadding;
using (var g = tc.CreateGraphics()) {
foreach (TabPage tab in tc.TabPages) {
newWidth += TextRenderer.MeasureText(g, tab.Text, tc.Font,
new Size(int.MaxValue, tc.Font.Height + 4), flags).Width + border;
}
}
return newWidth;
}
Setup the Layout:
Add a TableLayoutPanel to your Form, with one Row and two Columns (i.e., remove one Row)
Add the TabControl to the Cell on the left and the ListBox to the other Cell.
Set both Cells's style to AutoSize (after you have added your Controls).
Set the TableLayoutPanel to: AutoSize = true, AutoSizeMode = GrowAndShrink
Set the Form to auto-size in the same way
Set the Form's MinimumSize and MaximumSize. The former is usually set to the design size, the latter is up to you; you could use the current Screen WorkingArea as reference.
Calculate the new Width of the TabControl when the Form is created or loaded (i.e., in its Constructor or OnLoad() or Form.Load), so the Form will auto-size to the size of the TableLayoutPanel, whici in turn auto-sizes to the size of its child Controls.
Now you can add or remove TabPages at run-time and the Form will auto-size to the width you calculate in the TabControl.ControlAdded and TabControl.ControlRemoved event handlers (also checking whether the Control added is of Type TabPage).
Example:
The MeasureTabPagesWidth() method is the one shown above.
The TableLayoutPanel is named tlp1
The TabControl is named tabControl1
The Buttons used in the visual example have names that define their role.
public partial class AutoSizeForm : Form
{
public AutoSizeForm()
{
InitializeComponent();
tabControl1.Width = MeasureTabPagesWidth(tabControl1);
}
private void tabControl1_ControlAdded(object sender, ControlEventArgs e)
{
// Event notified after the TabPage has been added
if (e.Control is TabPage) {
tabControl1.Width = MeasureTabPagesWidth(tabControl1);
}
}
private void tabControl1_ControlRemoved(object sender, ControlEventArgs e)
{
if (e.Control is TabPage) {
// Use deferred execution, since the TabPage is removed after
// the event handler method completes.
BeginInvoke(new Action(()=> tabControl1.Width = MeasureTabPagesWidth(tabControl1)));
}
}
private void btnAddPage_Click(object sender, EventArgs e)
{
tabControl1.TabPages.Add(new TabPage("New TabpPage Text"));
}
private void btnRemovePage_Click(object sender, EventArgs e)
{
if (tabControl1.TabPages.Count > 0) {
tabControl1.TabPages.RemoveAt(tabControl1.TabPages.Count - 1);
}
}
private void btnAddCtlToTLP_Click(object sender, EventArgs e)
{
tlp1.ColumnCount += 1;
tlp1.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var mc = new MonthCalendar();
tlp1.SetColumn(mc, tlp1.ColumnCount - 1);
tlp1.Controls.Add(mc);
}
}
This is how it works:
Tested in Windows 7, since this appears to be the System in use
Sample Project:
Sample Project on Google Drive (.Net Framework 4.8 - C# 7.3)
Rebuild the Solution before running
Starting out with that form, I'm going to add 8 tabs at run-time, calculate width of the text in the tabs + padding size x2 (both sides of the tabs) and then resize controls as needed.
public Form1()
{
InitializeComponent();
//Clear our default tabs.
tabControl1.TabPages.Clear();
//Add more tabs than would be visible by default
for (int i=1;i<=8;i++)
{
tabControl1.TabPages.Add("Tab " + i.ToString());
}
ResizeTabControl();
ResizeListViewControl();
ResizeForm();
}
void ResizeTabControl()
{
int tabCount = tabControl1.TabCount;
float length = 0;
using (Graphics g = CreateGraphics())
{
//Iterate through the tabs and get the length of the text.
for (int i = 0; i <= tabCount - 1; i++)
length += g.MeasureString(tabControl1.TabPages[i].Text, tabControl1.Font).Width;
}
//Resize the tab control where X is the length of all text in the tabs plus padding x 2 x total tabs.
tabControl1.Size = new Size(Convert.ToInt32(length) + (tabCount * 2 * tabControl1.Padding.X), tabControl1.Width);
}
void ResizeListViewControl()
{
//Move listview 10 pixels away from tabcontrol's edge
listView1.Location = new Point(tabControl1.Location.X + tabControl1.Width + 10, listView1.Location.Y);
}
void ResizeForm()
{
//Resize form to accomodate changes.
this.Width = listView1.Location.X + listView1.Width + 20;
}
After it's all said and done, this is what it looks like:
And with 20 tabs, because why not.

How can a scrollable area retrieve the size of what is being displayed?

In my application I've got the following situation:
I've got a Windows Form with a Tab Control with several tabs. Each tab contains arbitrary content which is added by other classes upon startup or during runtime.
I want to set up the tabs in a way that scrollbars appear automatically as soon as the Form is too small for the tab's panel to display everything.
What I've tried so far is setting the tab page's AutoScroll = true and setting the AutoScrollMinSize property to the size of the panel.
This did not work as expected as the panel's Size always seems to be (200, 100) independent of its contents.
I've created a small example application (code below) which demonstrates the issue. If you resize the form, you'll see that scroll bars only appear if the Form gets smaller than the panel (default size of (200, 100)) rather than the text box in the panel (size of 300, 150). If you set AutoScrollMinSize manually (uncomment line 34), it behaves as expected.
The question is: How can the tab page retrieve the actual size of what is displayed in it?
I could probably recurse through all controls and try calculating the size myself - but this feels really bad.
PS: Please do not suggest setting the size of the panel to the size of the label, as the actual panels are much more complex than that. ;-)
Code:
Simply create an Application in Visual Studio and override Program.cs with the following code:
using System;
using System.Windows.Forms;
namespace ScrollbarTest
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var sampleForm = CreateSampleForm();
Application.Run(sampleForm);
}
private static Form CreateSampleForm()
{
var sampleForm = new Form() { };
var tabControl = new TabControl() { Dock = DockStyle.Fill };
var tabPage = new TabPage("Test") { AutoScroll = true };
sampleForm.Controls.Add(tabControl);
tabControl.TabPages.Add(tabPage);
var samplePanel = CreateSamplePanel();
tabPage.Controls.Add(samplePanel);
// this does not provide the right size
tabPage.AutoScrollMinSize = samplePanel.Size;
// uncomment this to make it work
//tabPage.AutoScrollMinSize = new System.Drawing.Size(300, 150);
return sampleForm;
}
private static Control CreateSamplePanel()
{
// As an example, create a panel with a text box with a fixed size.
var samplePanel = new Panel() { Dock = DockStyle.Fill };
var sampleSize = new System.Drawing.Size(300, 150);
var textBox = new TextBox()
{
Dock = DockStyle.Fill,
MinimumSize = sampleSize,
MaximumSize = sampleSize,
Size = sampleSize
};
samplePanel.Controls.Add(textBox);
return samplePanel;
}
}
}
The samplePanel.Size returns (200,100). In your CreateSamplePanel method, if you set samplePanel.MinimumSize = sampleSize; then your code will work.
Panels don't calculate their size properties (e.g. Size, MinimumSize, PreferredSize) based on their child controls. You will have to subclass Panel and provide that behavior. Even TableLayoutPanel and FlowLayoutPanel don't correctly calculate the PreferredSize property, which is surprising. At the very least, normally you override the GetPreferredSize(Size proposedSize) method, and optionally have the MinimumSize property return the PreferredSize property.
It's worth noting that DockStyle.Fill and MinimumSize are at odds with each other. TabPage controls are inherently DockStyle.Fill mode, which is why you have to set the AutoScrollMinSize property.
Edit: Isn't there any existing function which retrieves the total required size of a list of controls (recursively), e.g. through their X/Y and Size?
It's up to the host container itself (e.g. TableLayoutPanel) to calculate its PreferredSize correctly because only it knows the exact details of how its layout is performed.
You can set the AutoSize property to true and then hope that GetPreferredSize(...)/PreferredSize calculates the right size. For TableLayoutPanel, I recall there was a case where it wasn't calculating correctly and I had to subclass it and override the GetPreferredSize(...) method. GetPreferredSize(...) won't be called unless AutoSize is true.
If you're talking about a plain Panel or UserControl, by default these use the WYSIWYG LayoutEngine, and do not calculate the PreferredSize. You could subclass and then calculate maximum control.X + control.Width and same thing for height, and use that as the preferred size.
First try setting AutoSize to true and see if that works for you. If not, you might have to override the GetPreferredSize(...) method. Here is a crude example:
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var sampleForm = new Form() { AutoScroll = true };
var panel = new MyPanel() { AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, BackColor = Color.LightYellow };
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 3; j++) {
Button b = new Button { Text = "Button" + panel.Controls.Count, AutoSize = true };
b.Click += delegate {
MessageBox.Show("Preferred Size: " + panel.PreferredSize);
};
panel.Controls.Add(b, j, i);
}
}
sampleForm.Controls.Add(panel);
Application.Run(sampleForm);
}
private class MyPanel : TableLayoutPanel {
public override Size MinimumSize {
get {
return PreferredSize;
}
set {
}
}
public override Size GetPreferredSize(Size proposedSize) {
Size s = new Size();
int[] harr = new int[100];//this.RowCount];
int[] warr = new int[100];//this.ColumnCount];
foreach (Control c in this.Controls) {
var cell = this.GetPositionFromControl(c);
var ps = c.PreferredSize;
Padding m = c.Margin;
int w = ps.Width + m.Horizontal;
int h = ps.Height + m.Vertical;
if (w > warr[cell.Column])
warr[cell.Column] = w;
if (h > harr[cell.Row])
harr[cell.Row] = h;
}
foreach (int w in warr)
s.Width += w;
foreach (int h in harr)
s.Height += h;
return s;
}
}

Keep Buttons location with proportion to Resizing Image

I am trying to create a Winforms App and in one of screens, I have an image and some buttons generated at runtime, Now What i want is these buttons should always stay in proportion to the Image
for example Hand1 Button should always remain where Image's Hand1 is
This is my initial Image
but while resizing the form, it becomes like this
What I want is these buttons should not change their size and always stay in proportion to the Image.
The ImageSizeMode of the Image is StretchImage and buttons are Not Anchored (otherwise they don't move or start stretch/shrink.
How can i achieve this behavior. ?
Here is a simple, complete solution for a single button:
Note: The scale is calculated relative to the upper, left corner of the control (button). If the scaling needs to be modified such as to the middle or lower/right, then the scaling calculation has to be adjusted. Otherwise the location will not look quite precise and the button will "wander" as the image is resized.
using System;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
// X, Y scaling variables for btn1
private float _btn1xScale;
private float _btn1yScale;
public Form1()
{
InitializeComponent();
// The scale is really the % of btn X & Y along image width and height:
// Calculate X and Y scale from initial location and position in image
// Has to happen AFTER InitializeComponent is called!
_btn1xScale = btn1.Location.X / (float)pictureBox1.Width;
_btn1yScale = btn1.Location.Y / (float)pictureBox1.Height;
}
private void pictureBoxResize(object sender, EventArgs e)
{
// adjust position based on
btn1.Location = new Point(
(int)(pictureBox1.Width * _btn1xScale),
(int)(pictureBox1.Height * _btn1yScale));
}
}
Fancier version:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
{
private List<ControlScaler> _buttonsToScale = new List<ControlScaler>();
public Form1()
{
InitializeComponent();
// Has to happen AFTER InitializeComponent is called!
_buttonsToScale.Add(new ControlScaler(btn1, pictureBox1));
_buttonsToScale.Add(new ControlScaler(btn2, pictureBox1));
}
private void pictureBoxResize(object sender, EventArgs e)
{
foreach (var control in _buttonsToScale)
control.AdjustPositionToScale();
}
}
public class ControlScaler
{
// X, Y scaling variables
private float _btn1xScale;
private float _btn1yScale;
private Control _scaledControl;
private readonly Control _scaleTo;
public ControlScaler(Control scaledControl, Control scaleTo)
{
_scaledControl = scaledControl;
_scaleTo = scaleTo;
_btn1xScale = scaledControl.Location.X / (float)scaleTo.Width;
_btn1yScale = scaledControl.Location.Y / (float)scaleTo.Height;
}
public void AdjustPositionToScale()
{
var newLocation = new Point(
(int)(_scaleTo.Width * _btn1xScale),
(int)(_scaleTo.Height * _btn1yScale));
_scaledControl.Location = newLocation;
}
}
This solution is for hand1 button. The same logic applies to the other buttons too.
You want to keep distances c and d constant. First measure a, b, c, d and:
double dblHand1X, dblHand1Y;
private void Form1_Load(object sender, EventArgs e)
{
dblHand1X= (double)b / (double)pictureBox1.Width;
dblHand1Y= (double)a / (double)pictureBox1.Height;
}
private void pictureBox1_SizeChanged(object sender, EventArgs e)
{
int x, y;
x = (int)(dblHand1X* (double)pictureBox1.Width) + pictureBox1.Location.X;
y = (int)(dblHand1Y* (double)pictureBox1.Height) + pictureBox1.Location.Y;
x -= d;
y -= c;
Hand1.Location = new Point(x, y);
}
valter
You'll have to move buttons manually.
Choose origins for all buttons. For example, origin the "Hand 1" button is probably (button.Width / 2; button.Height) and origin of the "Leg 2" button is (0; button.Height).
Calculate and save positions of button origins: (OriginalX; OriginalY).
When the form is resized, multiply original positions by scale: (OriginalX * ScaleX; OriginalY * ScaleY). Then set button's position based on new positions of origins: (OriginalX * ScaleX - OriginX; OriginalY * ScaleY - OriginY).
Buttons should be anchored to top left.

Categories