2

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();
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
Gokhan
  • 453
  • 4
  • 10
  • 1
    https://stackoverflow.com/questions/25261036/how-to-get-the-left-location-of-a-cell-in-a-tablelayoutpanel-in-pixels-on-form – Hans Passant Jul 03 '21 at 12:36

1 Answers1

2

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

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • First of all thank you so much for your effort and time. The information you gave is very important to me like a lesson. In addition it made me understand the process as well. I tried your codes but some times it gives error "out of memory exception".Please see link. [link] (https://ibb.co/gwf6DW8). – Gokhan Jul 03 '21 at 19:43
  • It is my mistake. I found the reason of error. I corrected. Your codes work perfectly. I spent two days to find solution. Thank you so much again. You are life saver:) – Gokhan Jul 03 '21 at 19:58
  • Well, I', glad it was helpful :) The `out of memory` exception is generated if you try to draw to the Bitmap outside its bounds. That's why there's `[...] new Bitmap(tlp.Width + 1, tlp.Height + 1)`: when the external borders are included, the normal rectangular area is 1 pixel wider, so the `Clone()` method may try to create a selection that goes outside the Bitmap bounds when the selection overlaps the TableLayoutPanel's bounds in one or more sides. – Jimi Jul 03 '21 at 23:36
  • It helped me a lot and I understood the logic of the subject. Many thanx again. I was getting this error when i selected the start cell which column's index is 0. I added condition for this part '= new Point(cellStart.X - 1, cellStart.Y - 1);'. I guess If cell column index 0, cellStart.X will be negative(-1), so this was the reason of error. – Gokhan Jul 04 '21 at 06:06