|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Drawing;
using System.Windows.Forms.Layout;
namespace System.Windows.Forms;
[ProvideProperty("ColumnSpan", typeof(Control))]
[ProvideProperty("RowSpan", typeof(Control))]
[ProvideProperty("Row", typeof(Control))]
[ProvideProperty("Column", typeof(Control))]
[ProvideProperty("CellPosition", typeof(Control))]
[DefaultProperty(nameof(ColumnCount))]
[DesignerSerializer($"System.Windows.Forms.Design.TableLayoutPanelCodeDomSerializer, {AssemblyRef.SystemDesign}",
$"System.ComponentModel.Design.Serialization.CodeDomSerializer, {AssemblyRef.SystemDesign}")]
[Docking(DockingBehavior.Never)]
[Designer($"System.Windows.Forms.Design.TableLayoutPanelDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionTableLayoutPanel))]
public class TableLayoutPanel : Panel, IExtenderProvider
{
private readonly TableLayoutSettings _tableLayoutSettings;
private static readonly object s_eventCellPaint = new();
public TableLayoutPanel()
{
_tableLayoutSettings = TableLayout.CreateSettings(this);
}
public override LayoutEngine LayoutEngine => TableLayout.Instance;
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public TableLayoutSettings LayoutSettings
{
get => _tableLayoutSettings;
set
{
if (value is not null && value.IsStub)
{
// WINRES only scenario.
// we only support table layout settings that have been created from a type converter.
// this is here for localization (WinRes) support.
using (new LayoutTransaction(this, this, PropertyNames.LayoutSettings))
{
// apply RowStyles, ColumnStyles, Row & Column assignments.
_tableLayoutSettings.ApplySettings(value);
}
}
else
{
throw new NotSupportedException(SR.TableLayoutSettingSettingsIsNotSupported);
}
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Localizable(true)]
public new BorderStyle BorderStyle
{
get => base.BorderStyle;
set
{
base.BorderStyle = value;
Debug.Assert(BorderStyle == value, "BorderStyle should be the same as we set it");
}
}
[DefaultValue(TableLayoutPanelCellBorderStyle.None)]
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.TableLayoutPanelCellBorderStyleDescr))]
[Localizable(true)]
public TableLayoutPanelCellBorderStyle CellBorderStyle
{
get { return _tableLayoutSettings.CellBorderStyle; }
set
{
_tableLayoutSettings.CellBorderStyle = value;
// PERF: don't turn on ResizeRedraw unless we know we need it.
if (value != TableLayoutPanelCellBorderStyle.None)
{
SetStyle(ControlStyles.ResizeRedraw, true);
}
Invalidate();
Debug.Assert(CellBorderStyle == value, "CellBorderStyle should be the same as we set it");
}
}
private int CellBorderWidth => _tableLayoutSettings.CellBorderWidth;
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[SRDescription(nameof(SR.ControlControlsDescr))]
public new TableLayoutControlCollection Controls => (TableLayoutControlCollection)base.Controls;
/// <summary>
/// This sets the maximum number of columns allowed on this table instead of allocating
/// actual spaces for these columns. So it is OK to set ColumnCount to Int32.MaxValue without
/// causing out of memory exception
/// </summary>
[SRDescription(nameof(SR.GridPanelColumnsDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DefaultValue(0)]
[Localizable(true)]
public int ColumnCount
{
get => _tableLayoutSettings.ColumnCount;
set
{
_tableLayoutSettings.ColumnCount = value;
Debug.Assert(ColumnCount == value, "ColumnCount should be the same as we set it");
}
}
/// <summary>
/// Specifies if a TableLayoutPanel will gain additional rows or columns once its existing cells
/// become full. If the value is 'FixedSize' then the TableLayoutPanel will throw an exception
/// when the TableLayoutPanel is over-filled.
/// </summary>
[SRDescription(nameof(SR.TableLayoutPanelGrowStyleDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DefaultValue(TableLayoutPanelGrowStyle.AddRows)]
public TableLayoutPanelGrowStyle GrowStyle
{
get => _tableLayoutSettings.GrowStyle;
set => _tableLayoutSettings.GrowStyle = value;
}
/// <summary>
/// This sets the maximum number of rows allowed on this table instead of allocating
/// actual spaces for these rows. So it is OK to set RowCount to Int32.MaxValue without
/// causing out of memory exception
/// </summary>
[SRDescription(nameof(SR.GridPanelRowsDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DefaultValue(0)]
[Localizable(true)]
public int RowCount
{
get => _tableLayoutSettings.RowCount;
set => _tableLayoutSettings.RowCount = value;
}
[SRDescription(nameof(SR.GridPanelRowStylesDescr))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[SRCategory(nameof(SR.CatLayout))]
[DisplayName("Rows")]
[MergableProperty(false)]
[Browsable(false)]
public TableLayoutRowStyleCollection RowStyles => _tableLayoutSettings.RowStyles;
[SRDescription(nameof(SR.GridPanelColumnStylesDescr))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[SRCategory(nameof(SR.CatLayout))]
[DisplayName("Columns")]
[Browsable(false)]
[MergableProperty(false)]
public TableLayoutColumnStyleCollection ColumnStyles => _tableLayoutSettings.ColumnStyles;
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected override ControlCollection CreateControlsInstance() => new TableLayoutControlCollection(this);
private bool ShouldSerializeControls()
{
TableLayoutControlCollection collection = Controls;
return collection is not null && collection.Count > 0;
}
#region Extended Properties
bool IExtenderProvider.CanExtend(object obj) =>
obj is Control control && control.Parent == this;
[SRDescription(nameof(SR.GridPanelGetColumnSpanDescr))]
[DefaultValue(1)]
[SRCategory(nameof(SR.CatLayout))]
[DisplayName("ColumnSpan")]
public int GetColumnSpan(Control control) =>
_tableLayoutSettings.GetColumnSpan(control);
public void SetColumnSpan(Control control, int value)
{
// layout.SetColumnSpan() throws ArgumentException if out of range.
_tableLayoutSettings.SetColumnSpan(control, value);
Debug.Assert(GetColumnSpan(control) == value, "GetColumnSpan should be the same as we set it");
}
[SRDescription(nameof(SR.GridPanelGetRowSpanDescr))]
[DefaultValue(1)]
[SRCategory(nameof(SR.CatLayout))]
[DisplayName("RowSpan")]
public int GetRowSpan(Control control) => _tableLayoutSettings.GetRowSpan(control);
public void SetRowSpan(Control control, int value)
{
// layout.SetRowSpan() throws ArgumentException if out of range.
_tableLayoutSettings.SetRowSpan(control, value);
Debug.Assert(GetRowSpan(control) == value, "GetRowSpan should be the same as we set it");
}
// get the row position of the control
[DefaultValue(-1)] // if change this value, also change the SerializeViaAdd in TableLayoutControlCollectionCodeDomSerializer
[SRDescription(nameof(SR.GridPanelRowDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DisplayName("Row")]
public int GetRow(Control control) => _tableLayoutSettings.GetRow(control);
// set the row position of the control
public void SetRow(Control control, int row)
{
_tableLayoutSettings.SetRow(control, row);
Debug.Assert(GetRow(control) == row, "GetRow should be the same as we set it");
}
// get the row and column position of the control
[DefaultValue(typeof(TableLayoutPanelCellPosition), "-1,-1")] // if change this value, also change the SerializeViaAdd in TableLayoutControlCollectionCodeDomSerializer
[SRDescription(nameof(SR.GridPanelCellPositionDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DisplayName("Cell")]
public TableLayoutPanelCellPosition GetCellPosition(Control control) =>
_tableLayoutSettings.GetCellPosition(control);
// set the row and column of the control
public void SetCellPosition(Control control, TableLayoutPanelCellPosition position) =>
_tableLayoutSettings.SetCellPosition(control, position);
// get the column position of the control
[DefaultValue(-1)] // if change this value, also change the SerializeViaAdd in TableLayoutControlCollectionCodeDomSerializer
[SRDescription(nameof(SR.GridPanelColumnDescr))]
[SRCategory(nameof(SR.CatLayout))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DisplayName("Column")]
public int GetColumn(Control control) => _tableLayoutSettings.GetColumn(control);
// set the column position of the control
public void SetColumn(Control control, int column)
{
_tableLayoutSettings.SetColumn(control, column);
Debug.Assert(GetColumn(control) == column, "GetColumn should be the same as we set it");
}
/// <summary>
/// get the control which covers the specified row and column. return null if we can't find one
/// </summary>
public Control? GetControlFromPosition(int column, int row) =>
(Control?)_tableLayoutSettings.GetControlFromPosition(column, row);
public TableLayoutPanelCellPosition GetPositionFromControl(Control? control) =>
_tableLayoutSettings.GetPositionFromControl(control);
/// <summary>
/// This returns an array representing the widths (in pixels) of the columns in the TableLayoutPanel.
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public int[] GetColumnWidths()
{
TableLayout.ContainerInfo containerInfo = TableLayout.GetContainerInfo(this);
if (containerInfo.Columns is null)
{
return [];
}
int[] cw = new int[containerInfo.Columns.Length];
for (int i = 0; i < containerInfo.Columns.Length; i++)
{
cw[i] = containerInfo.Columns[i].MinSize;
}
return cw;
}
/// <summary>
/// This returns an array representing the heights (in pixels) of the rows in the TableLayoutPanel.
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public int[] GetRowHeights()
{
TableLayout.ContainerInfo containerInfo = TableLayout.GetContainerInfo(this);
if (containerInfo.Rows is null)
{
return [];
}
int[] rh = new int[containerInfo.Rows.Length];
for (int i = 0; i < containerInfo.Rows.Length; i++)
{
rh[i] = containerInfo.Rows[i].MinSize;
}
return rh;
}
#endregion
#region PaintCode
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.TableLayoutPanelOnPaintCellDescr))]
public event TableLayoutCellPaintEventHandler? CellPaint
{
add => Events.AddHandler(s_eventCellPaint, value);
remove => Events.RemoveHandler(s_eventCellPaint, value);
}
/// <summary>
/// When a layout fires, make sure we're painting all of our
/// cell borders.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected override void OnLayout(LayoutEventArgs levent)
{
base.OnLayout(levent);
Invalidate();
}
protected virtual void OnCellPaint(TableLayoutCellPaintEventArgs e)
{
((TableLayoutCellPaintEventHandler?)Events[s_eventCellPaint])?.Invoke(this, e);
}
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
// paint borderstyles on top of the background image in WM_ERASEBKGND
int cellBorderWidth = CellBorderWidth;
TableLayout.ContainerInfo containerInfo = TableLayout.GetContainerInfo(this);
TableLayout.Strip[] colStrips = containerInfo.Columns;
TableLayout.Strip[] rowStrips = containerInfo.Rows;
TableLayoutPanelCellBorderStyle cellBorderStyle = CellBorderStyle;
if (colStrips is null || rowStrips is null)
{
return;
}
int cols = colStrips.Length;
int rows = rowStrips.Length;
int totalColumnWidths = 0, totalColumnHeights = 0;
Rectangle displayRect = DisplayRectangle;
Rectangle clipRect = e.ClipRectangle;
Graphics g = e.GraphicsInternal;
// Leave the space for the border
int startx;
bool isRTL = RightToLeft == RightToLeft.Yes;
if (isRTL)
{
startx = displayRect.Right - (cellBorderWidth / 2);
}
else
{
startx = displayRect.X + (cellBorderWidth / 2);
}
for (int i = 0; i < cols; i++)
{
int starty = displayRect.Y + (cellBorderWidth / 2);
if (isRTL)
{
startx -= colStrips[i].MinSize;
}
for (int j = 0; j < rows; j++)
{
Rectangle outsideCellBounds = new(
startx,
starty,
colStrips[i].MinSize,
rowStrips[j].MinSize);
Rectangle insideCellBounds = new(
outsideCellBounds.X + (cellBorderWidth + 1) / 2,
outsideCellBounds.Y + (cellBorderWidth + 1) / 2,
outsideCellBounds.Width - (cellBorderWidth + 1) / 2,
outsideCellBounds.Height - (cellBorderWidth + 1) / 2);
if (clipRect.IntersectsWith(insideCellBounds))
{
// First, call user's painting code
using (TableLayoutCellPaintEventArgs pcea = new(e, clipRect, insideCellBounds, i, j))
{
OnCellPaint(pcea);
if (!((IGraphicsHdcProvider)pcea).IsGraphicsStateClean)
{
// The Graphics object got touched, hit the public property on our original args
// to mark it as dirty as well.
g = e.Graphics;
}
}
// Paint the table border on top.
ControlPaint.PaintTableCellBorder(cellBorderStyle, g, outsideCellBounds);
}
starty += rowStrips[j].MinSize;
// Only sum this up once...
if (i == 0)
{
totalColumnHeights += rowStrips[j].MinSize;
}
}
if (!isRTL)
{
startx += colStrips[i].MinSize;
}
totalColumnWidths += colStrips[i].MinSize;
}
if (!HScroll && !VScroll && cellBorderStyle != TableLayoutPanelCellBorderStyle.None)
{
// Paint the border of the table if we are not auto scrolling.
Rectangle tableBounds = new(
cellBorderWidth / 2 + displayRect.X,
cellBorderWidth / 2 + displayRect.Y,
displayRect.Width - cellBorderWidth,
displayRect.Height - cellBorderWidth);
// If the borderStyle is Inset or Outset, we can only paint the lower bottom half since otherwise we
// will have 1 pixel loss at the border.
if (cellBorderStyle == TableLayoutPanelCellBorderStyle.Inset)
{
g.DrawLine(
SystemPens.ControlDark,
tableBounds.Right,
tableBounds.Y,
tableBounds.Right,
tableBounds.Bottom);
g.DrawLine(
SystemPens.ControlDark,
tableBounds.X,
tableBounds.Y + tableBounds.Height - 1,
tableBounds.X + tableBounds.Width - 1,
tableBounds.Y + tableBounds.Height - 1);
}
else if (cellBorderStyle == TableLayoutPanelCellBorderStyle.Outset)
{
g.DrawLine(
SystemPens.Window,
tableBounds.X + tableBounds.Width - 1,
tableBounds.Y,
tableBounds.X + tableBounds.Width - 1,
tableBounds.Y + tableBounds.Height - 1);
g.DrawLine(
SystemPens.Window,
tableBounds.X,
tableBounds.Y + tableBounds.Height - 1,
tableBounds.X + tableBounds.Width - 1,
tableBounds.Y + tableBounds.Height - 1);
}
else
{
ControlPaint.PaintTableCellBorder(cellBorderStyle, g, tableBounds);
}
ControlPaint.PaintTableControlBorder(cellBorderStyle, g, displayRect);
}
else
{
ControlPaint.PaintTableControlBorder(cellBorderStyle, g, displayRect);
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
protected override void ScaleCore(float dx, float dy)
{
base.ScaleCore(dx, dy);
ScaleAbsoluteStyles(new SizeF(dx, dy));
}
/// <summary>
/// Scale this form. Form overrides this to enforce a maximum / minimum size.
/// </summary>
protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
{
base.ScaleControl(factor, specified);
ScaleAbsoluteStyles(factor);
}
private void ScaleAbsoluteStyles(SizeF factor)
{
TableLayout.ContainerInfo containerInfo = TableLayout.GetContainerInfo(this);
int i = 0;
// The last row/column can be larger than the
// absolutely styled column width.
int lastRowHeight = -1;
int lastRow = containerInfo.Rows.Length - 1;
if (containerInfo.Rows.Length > 0)
{
lastRowHeight = containerInfo.Rows[lastRow].MinSize;
}
int lastColumnHeight = -1;
int lastColumn = containerInfo.Columns.Length - 1;
if (containerInfo.Columns.Length > 0)
{
lastColumnHeight = containerInfo.Columns[^1].MinSize;
}
foreach (ColumnStyle cs in ColumnStyles)
{
if (cs.SizeType == SizeType.Absolute)
{
if (i == lastColumn && lastColumnHeight > 0)
{
// the last column is typically expanded to fill the table. use the actual
// width in this case.
cs.Width = (float)Math.Round(lastColumnHeight * factor.Width);
}
else
{
cs.Width = (float)Math.Round(cs.Width * factor.Width);
}
}
i++;
}
i = 0;
foreach (RowStyle rs in RowStyles)
{
if (rs.SizeType == SizeType.Absolute)
{
if (i == lastRow && lastRowHeight > 0)
{
// the last row is typically expanded to fill the table. use the actual
// width in this case.
rs.Height = (float)Math.Round(lastRowHeight * factor.Height);
}
else
{
rs.Height = (float)Math.Round(rs.Height * factor.Height);
}
}
}
}
#endregion
}
|