Search code examples
c#winformsgraphicstablelayoutpanelimage-capture

Capture image of specific area of a TableLayoutPanel


I have a Project that uses a TableLayoutPanel control.
I want capture image of specific area of that TableLayoutPanel.
I have an image (which is captured) width and height info. I also have start-end cell indexes info, I succeeded to capture but the area that captured is wrong.

enter image description here

This is my code:

private void getImage()
{
    int totalWidth = 250 // image width size;
    int totalHeight = 200 // image height size;

    int LeftBottomCellColumnIndex = 3
    int LeftBottomCellRowIndex = 4

    int LeftTopCellColumnIndex = 3
    int LeftBottomCellRowIndex = 2 // I also have all cells index info

    Bitmap bm = new Bitmap(totalWidth, totalHeight);
    tableLayoutPanel2.DrawToBitmap(bm, new Rectangle(0, 0, totalWidth, totalHeight)); // 0,0 problem is here. I do not know what I have to put there

    string path = @"C:\Users\prdn5\TestDrawToBitmap.bmp";
    FileStream fs = new FileStream(path, FileMode.OpenOrCreate);
    bm.Save(fs, ImageFormat.Bmp);
    fs.Flush();
    fs.Close();
}

Solution

  • The TableLayoutPanel, for some reason, doesn't expose the Cells bounds directly, through a Property or Method. But of course it calculates these values for internal use, since it needs to paint the borders of its Cells.

    Its OnPaintBackground method is responsible for this.
    When the Control needs to repaint itself, the Cells bounds are recalculated and, for each Cell, the Control raises the CellPaint event. Its TableLayoutCellPaintEventArgs argument returns both the current Cell's bounds and Column/Row coordinates.
    Since this event is raised each time the TableLayoutPanel is modified/resized, we can use the CellPaint event handler to store these references.

    Here, I'm using a Dictionary<TableLayoutPanelCellPosition, Rectangle> to store the coordinates of each Cell and map it to its bounds.

    The Layout event handler sets a boolean Field that causes the Dictionary to be rebuild only when necessary (because the TableLayoutPanel's Layout has changed).

    ▶ The Dictionary Key is a TableLayoutPanelCellPosition because it allows a quick and simple equality comparisons. Of course you can use a different type of object, if needed.

    ▶ Assume the TableLayoutPanel is named tlp1.

    ▶ Adding and/or removing Columns/Rows at run-time is not handled. In case it's needed, re-define the Dictionary in the Layout event handler instead of the Form Constructor.

    public partial class SomeForm : Form
    {
        Dictionary<TableLayoutPanelCellPosition, Rectangle> tlpCells = null;
        bool calcCells = false;
    
        public SomeForm() {
            InitializeComponent();
    
            // Set the DoubleBuffered property via reflection (if needed)
            var flags = BindingFlags.Instance | BindingFlags.NonPublic;
            tlp1.GetType().GetProperty("DoubleBuffered", flags).SetValue(tlp1, true);
    
            tlpCells = new Dictionary<TableLayoutPanelCellPosition, Rectangle>();
            for (int x = 0; x < tlp1.ColumnCount; x++) {
                for (int y = 0; y < tlp1.RowCount; y++) {
                    tlpCells.Add(new TableLayoutPanelCellPosition(x, y), Rectangle.Empty);
                }
            }
        }
    
        private void tlp1_Layout(object sender, LayoutEventArgs e) => calcCells = true;
    
        private void tlp1_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
        {
            if (calcCells) {
                var cellPos = new TableLayoutPanelCellPosition(e.Column, e.Row);
                tlpCells[cellPos] = e.CellBounds;
    
                if (cellPos.Column == tlp1.ColumnCount - 1 && 
                    cellPos .Row == tlp1.RowCount - 1) calcCells = false;
            }
        }
    }
    

    To get the Cell from the current Mouse position, call GetSelectedCell(), passing the current Pointer location, relative to the TableLayoutPanel:

    var cell = GetSelectedCell(tlp1.PointToClient(MousePosition));
    // If called from the MouseMove, MouseDown etc handlers, use the MouseEventArgs instead
    var cell = GetSelectedCell(e.Location);
    
    // [...]
    
    private TableLayoutPanelCellPosition GetSelectedCell(Point position) 
        => tlpCells.FirstOrDefault(c => c.Value.Contains(position)).Key;
    

    Now, to generate a Bitmap from a range of Cells, you just need to specify the starting (Left, Top) and ending (Right, Bottom) Cells and pass these values references to the TlpCellRangeToBitmap() method (it could also be used as an extension method).
    If the includeBorders argument is set to true, it will include the external borders of the Cells (see the visual example). Add code that checks whether the TableLayoutPanel actually has borders (BorderStyle Property).
    The Rectangle that includes the range of Cells is returned by GetCellRangeBounds(): this method is independent and can be used for other purposes.

    For example, set the Image property of a PictureBox to the generated Bitmap:

    var cellStart = new TableLayoutPanelCellPosition(2, 2);
    var cellEnd = new TableLayoutPanelCellPosition(3, 5);
    
    somePictureBox.Image?.Dispose();
    somePictureBox.Image = TlpCellRangeToBitmap(tlp1, cellStart, cellEnd, false);
    
    // [...]
    
    private Bitmap TlpCellRangeToBitmap(TableLayoutPanel tlp, TableLayoutPanelCellPosition cellStart, TableLayoutPanelCellPosition cellEnd, bool includeBorders)
    {
        // The 3rd parameter includes or excludes the external borders
        var selRect = GetCellRangeBounds(cellStart, cellEnd, includeBorders);
        using (var image = new Bitmap(tlp.Width + 1, tlp.Height + 1)) {
            tlp.DrawToBitmap(image, new Rectangle(Point.Empty, tlp.Size));
            return image.Clone(selRect, PixelFormat.Format32bppArgb);
        }
    }
    
    private Rectangle GetCellRangeBounds(TableLayoutPanelCellPosition start, TableLayoutPanelCellPosition end, bool extBorders)
    {
        var cellStart = tlpCells[start];
        var cellEnd = tlpCells[end];
        if (extBorders) {
            cellStart.Location = new Point(cellStart.X - 1, cellStart.Y - 1);
            cellEnd.Size = new Size(cellEnd.Width + 2, cellEnd.Height + 2);
        }
        return new Rectangle(cellStart.Location, new Size(cellEnd.Right - cellStart.X, cellEnd.Bottom - cellStart.Y));
    }
    

    This is how it works:

    TableLayoutPanel Cell Ranges to Bitmap