Cell selection hides behind image DataGridViewImageCell

134 views Asked by At

I'm using Winform DataGridView to display images. But when image fills cell, I don't see blue selection or in very low quantity. Please see: actual behaviour

When an cell is selected, I expect make cell whole transparent blue, not just sides or sides which isn't occupied by image. like:

expected behaviour

Currently I tried coloring blue myself in paint event but it updates too frequently which hangs software.

I also modify image to look bluish in selection changed event, but again it slows down software.

Is there fix to this ? any workaround or something ? without compromising performance ?

EDIT: This is source code on how I display images on datagridview:

int colms = 4; // total no. of columns in our datagridview

//this create 4 image columns in datagridview
for (int c = 0; c < colms; c++)
{
    var imgColm = new DataGridViewImageColumn();
    imgColm.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
    imgColm.ImageLayout = DataGridViewImageCellLayout.Zoom;
    grid.Columns.Add(imgColm);
}

int colm = 0;
int row = 0;

//this get all images and display on datagridview 
foreach (var img in Directory.GetFiles(@"C:\Users\Administrator\Desktop\images"))
{
    if (colm >= colms)
    {
        row++;
        colm = 0;
        grid.Rows.Add();
    }
    ((DataGridViewImageCell)grid.Rows[row].Cells[colm]).Value = Thumb.GetThumbnail(img, ThumbSize.LowRes);
    colm++;
}

Currently cell painting I use just a workaround, that draws border on selected cell. But its slow when data is large and secondly draws on unselected cell as well.

enter image description here

1

There are 1 answers

2
dr.null On BEST ANSWER

Here's two examples to test yourself regarding the performance and the fill style of the selected cells. Since your code snippet does not show in what context the code is called, especially creating the image columns part, and to avoid repeating unnecessary routines, use the grid designer to add 4 columns of type DataGridViewImageColumn and set the auto size and layout properties from there.

Normal Mode
In the Form's ctor, use Reflection to enable the grid's DoubleBuffered property to reduce the flicker. The emptyImage bitmap is the null value of the empty cells.

public partial class SomeForm : Form
{
    private Bitmap emptyImage;

    public SomeForm()
    {
        dgv.GetType()
            .GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
            .SetValue(dgv, true);

        emptyImage = new Bitmap(1, 1);

        foreach (var col in dgv.Columns.OfType<DataGridViewImageColumn>())
            col.DefaultCellStyle.NullValue = emptyImage;
    }

Override the OnLoad method to populate the grid or to call a method for that.

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        var imgFolder = @"Your-Image-Folder-Path";
        LoadImages(imgFolder);
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        emptyImage.Dispose();
    }

    private void LoadImages(string path)
    {            
        string[] allFiles = Directory.GetFiles(path);

        for (int i = 0; i < allFiles.Length; i += 4)
        {
            var files = allFiles.Skip(i).Take(4);

            dgv.Rows.Add(
                files.Select(f =>
                {
                    using (var img = Image.FromFile(f, true))
                        return Thumb.GetThumbnail(img, ThumbSize.LowRes);
                }).ToArray());
        }
    }

Note, your Thumb.GetThumbnail method returns a new image so you need to dispose of the original image.

Implement the CellPainting event to draw everything except the DataGridViewPaintParts.SelectionBackground and fill the selected cells with a semi-transparent color.

    private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
    {    
        e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);
    
        if (e.RowIndex >= 0 &&
            e.Value != null && e.Value != emptyImage &&
            (e.State & DataGridViewElementStates.Selected) > 0)
        {            
            using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
                e.Graphics.FillRectangle(br, e.CellBounds);
        }

        e.Handled = true;
    }
}

Virtual Mode
You need here to have data store to cache only the images you need to display. The images of each cell of the visible rows. For that, The Cache class is created to manage the relevant functionalities including:

  • Calculating the total number of rows required to display 4 images per visible row. So the SetMaxRows method should be called when the grid is first created and resized to recalculate the visible rows.
  • Loading, creating, and caching the current visible rows images in a Dictionary<int, Image> where the keys are the cell numbers.
  • Passing the requested images when the CellValueNeeded event is raised.
public partial class SomeForm : Form
{
    private readonly Cache cache;

    public SomeForm()
    {
        dgv.GetType()
            .GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
            .SetValue(dgv, true);
        dgv.VirtualMode = true;

        var imgFolder = @"Your-Image-Folder-Path";
        cache = new Cache(imgFolder, dgv.ColumnCount);
        dgv.RowCount = cache.GetRowCount();
        SetMaxRows();
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        cache.Dispose();
    }

    private void dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) =>
        e.Value = cache.GetImage(e.RowIndex, e.ColumnIndex);

    private void dgv_Resize(object sender, EventArgs e) => SetMaxRows();

    // Change dgv.RowTemplate.Height as needed...
    private void SetMaxRows() =>
        cache.MaxRows = (int)Math
        .Ceiling((double)dgv.ClientRectangle.Height / dgv.RowTemplate.Height);

    private class Cache : IDisposable
    {
        private readonly Dictionary<int, Image> dict;
        private readonly Bitmap nullImage;
        private int currentRowIndex = -1;

        private Cache()
        {
            dict = new Dictionary<int, Image>();
            nullImage = new Bitmap(1, 1);
        }

        public Cache(string path, int columnCount) : this()
        {
            ImageFolder = path;                
            ColumnCount = columnCount;
        }

        public string ImageFolder { get; set; }
        public int ColumnCount { get; set; }
        public int MaxRows { get; set; }
        public Bitmap NullImage => nullImage;

        public Image GetImage(int rowIndex, int columnIndex)
        {
            var ri = rowIndex - (rowIndex % MaxRows);

            if (ri != currentRowIndex)
            {
                foreach (var img in dict.Values) img?.Dispose();
                currentRowIndex = ri;
                dict.Clear();
            }

            var i = (rowIndex * ColumnCount) + columnIndex;
            Image res = nullImage;

            if (!dict.ContainsKey(i))
            {
                var file = Directory.EnumerateFiles(ImageFolder)
                    .Skip(i).FirstOrDefault();

                if (file != null)
                {
                    using (var img = Image.FromFile(file, true))
                        dict[i] = res = Thumb.GetThumbnail(img, ThumbSize.LowRes);
                }
            }
            else
            {
                res = dict[i];
            }

            return res;
        }

        public int GetRowCount()
        {
            var count = Directory.EnumerateFiles(ImageFolder).Count();
            return (int)Math.Ceiling((double)count / ColumnCount);
        }

        public void Dispose()
        {
            foreach (var img in dict.Values) img?.Dispose();
            nullImage.Dispose();
        }
    }

Finally, the CellPainting event remains almost the same except that you get the null image from the cache instance.

    private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
    {
        e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);

        if (e.RowIndex >= 0 &&
            e.Value != null && e.Value != cache.NullImage &&
            (e.State & DataGridViewElementStates.Selected) > 0)
        {                
            using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
                e.Graphics.FillRectangle(br, e.CellBounds);
        }

        e.Handled = true;
    }
}