Creating ImageList object in a loop - c#

I have very simple logic for list view items creating. I'm storing all the col headers name from dataGridView into ColNamesForm1 array and then only comparing if last 5 characters match with (num) or (cat) string. For these two options I'm appending different picture into listview lvMoveFrom using of Populate function stored in static classs OtherFunctions.
But something is wrong in my code because after last iteration it is appending images from the last column - if the first column is (num) and the last column is (cat) the images in listview are all the same - from cat image.
How can I fix this issue? I was wondering about creating new ImageList object for every columns but I have no idea, how can I do that dynamically, e.g. using of loop index i.
Please, can you help to solve my problem.
private void Form1_Load(object sender, EventArgs e)
{
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() != "COL_" && Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$") == false)
{
if (ColNamesForm1[i].ToString().Substring(ColNamesForm1[i].ToString().Length - 5) == "(num)")
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\num.png");
}
else
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\cat.png");
}
}
}
}
public static void Populate(ListView lv, string itemName, string pathToPicture)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
img.Images.Add(Image.FromFile(pathToPicture));
lv.SmallImageList = img;
lv.Items.Add(itemName, 0);
}

The Problem
So basically this:
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
img.Images.Add(Image.FromFile(pathToPicture));
lv.SmallImageList = img;
lv.Items.Add(itemName, 0);
Was adding a new image list to the ListView. The same ListView you passed it everytime (so you were effectively overwriting it). Secondly, the line:
lv.Items.Add(itemName, 0);
The second argument is the index in the image list (that you assigned to the ListView). So giving it 0 will ask the ListView to pick the image from lv.SmallImageList[0] (psuedo code) basically.
The Solution
To remove the overwriting I pulled the image setup logic out of the Populate and put it back in the main method. I'll break down just the setup logic:
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(MediaTypeNames.Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
I put all the image paths into a List<string> and just used the LINQ ForEach to iterate over each one adding it to the ImageList img. There is no difference from your original code, apart from I add all the images to the ListView and I only do it once.
So then to make your code easier to understand I did some simple refactorings.
First was invert if statement:
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() != "COL_" && Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$") == false)
To:
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
This is almost like a guard clause, it says if we don't meet these minimum conditions then move to the next item.
Then I simplified your method execution by reducing duplication:
if (ColNamesForm1[i].ToString().Substring(ColNamesForm1[i].ToString().Length - 5) == "(num)")
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\num.png");
}
else
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\cat.png");
}
To:
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), image);
Finally you will see that I change your Populate method. Firstly we are prepopulating your ListView with the images, then using that ternary operator to choose which index of the image to show.
The code all together is:
private void Form1_Load(object sender, EventArgs e)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), image);
}
}
public static void Populate(ListView lv, string itemName, int imageIndex)
{
lv.Items.Add(itemName, imageIndex);
}
Now you can simplify further:
private void Form1_Load(object sender, EventArgs e)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
lvMoveFrom.Items.Add(ColNamesForm1[i].ToString(), image);
}
}

Related

Cell selection hides behind image DataGridViewImageCell

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:
When an cell is selected, I expect make cell whole transparent blue, not just sides or sides which isn't occupied by image. like:
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.
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;
}
}

Virtual Listview with pictures : How to fill all the column space with the picture

I'm trying to setup a listview with pictures.
As i have 3000+ items in that list i'm using the VirtualMode.
Here is my code :
ImageList il = new ImageList();
private void artistList_Load(object sender, EventArgs e)
{
var art_list = from res in Globals.ds.Tables[0].AsEnumerable()
.GroupBy(x => x.Field<int>("Artist_ID"))
.Select(x => new { artiste = x.Select(p => p.Field<string>("artiste")).First(), fp = x.Select(p => p.Field<string>("file_path")).First() })
.OrderBy(x=>x.artiste)
select res.fp;
fp = art_list.ToList();
int buffer = 20;
il.ImageSize = new Size(90, 90);
listView1.VirtualMode = true;
for (int i = 0; i< buffer; i++)
{
il.Images.Add(i.ToString(), Globals.getMp3Image(fp[i]));
}
listView1.LargeImageList = il;
listView1.VirtualListSize = fp.Count();
}
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (e.Item == null) e.Item = new ListViewItem(e.ItemIndex.ToString());
e.Item.ImageIndex = il.Images.IndexOfKey(e.ItemIndex.ToString());
}
I'm using a buffer of 20 and i'll try to implement a cache mechanism later.
My issue is the visual result i'm getting, here is a picture :
I have several issues here :
1/ the number below the picture should be the index, but why is it displayed ?
2/ i'd like to expand the picture to the to avoid the white spaces in the 4 directions (there is some white spaces in the left)
3/ i'd like to avoid the horizontal scrollbar (but i think this issue is linked to #2)
Here is the Designer info : (the view is set to LargeIcon)
this.listView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.listView1.HideSelection = false;
this.listView1.Location = new System.Drawing.Point(0, 0);
this.listView1.Margin = new System.Windows.Forms.Padding(0);
this.listView1.Name = "listView1";
this.listView1.ShowGroups = false;
this.listView1.Size = new System.Drawing.Size(137, 657);
this.listView1.TabIndex = 0;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.listView1_RetrieveVirtualItem);
Of course i've tried to set the Size of the listview at 90 but it's worst.

c# Listview with images column strange behaviour

i have a list view in which i load some images from a folder through imagelist control.
when I initialize the form containing the listview i call a method CaricaListBox()
private void CaricaListBox()
{
if (TxtSourceFolder.FullText != string.Empty)
{
LstFile.Items.Clear();
foreach (string file in Directory.EnumerateFiles(TxtSourceFolder.FullText, "*.*").Where(s => s.EndsWith(".png") || s.EndsWith(".jpg")))
{
Image ImgObj;
using (var bmpTemp = new Bitmap(file))
{
ImgObj = new Bitmap(bmpTemp);
}
if (ImgObj.Width > ImgObj.Height)
{
imageList.Images.Add(Image.FromFile(file));
var LstI = LstFile.Items.Add(" ", imageList.Images.Count - 1); //(Path.GetFileName(file), imageList.Images.Count - 1);
LstI.SubItems.Add(Path.GetFileName(file));
}
ImgObj.Dispose();
}
}
}
when the form is shown it is as follow
and this is what i want.
If press the change folder and i run again the same method to load listbox
i have follow result :
showing only 1 column.
if i enlarge the listbox view i see as follow :
so nothing changes
what am I doing wrong ?
any suggestion ?
Tks in advance
Fabrizio
Edited:
private void CaricaListBox()
{
if (TxtSourceFolder.FullText != string.Empty)
{
LstFile.Items.Clear();
foreach (string file in Directory.EnumerateFiles(TxtSourceFolder.FullText, "*.*").Where(s => s.EndsWith(".png") || s.EndsWith(".jpg")))
{
var bmpTemp = Image.FromFile(file);
if (bmpTemp.Width > bmpTemp.Height)
{
using (var tempImage = bmpTemp)
{
Bitmap bmp = new Bitmap(192, 76);
using (Graphics g = Graphics.FromImage(bmp))
{
g.DrawImage(tempImage, new Rectangle(0, 0, bmp.Width, bmp.Height));
}
imageList.Images.Add(bmp);
var LstI = LstFile.Items.Add(" ", imageList.Images.Count - 1);
LstI.SubItems.Add(Path.GetFileName(file));
}
}
else bmpTemp.Dispose();
}
}
Conta();
}
it saves memory .. but still have visualization problem when the method is called for the second time.
just noticed that if i call method while form is not yet drawn, it shows 2 columns, if i call method when the form is already drawn, i see 1 column
Fabrizio

Remove controls of flowlayoutpanel and recreating it in C#

I had been experimenting on writing a code to generate images inside a FlowLayoutPanel.
This is what i had done so far, when i click on a button for the first time (by using a checkboxes - read in number of images to open), it will generate the images, when i click on the button on second try, it will not update the flowlayoutpanel.
Even though i tried to remove the controls inside the FlowLayoutPanel, it still doesn't show the second try of the images.
This is the code snippet of the method:
FlowLayoutPanel fwPanel = null;
private void btnOpenFile_Click(object sender, EventArgs e)
{
//if there is content inside the flowpanel, dump it
if (fwPanel != null)
{
listOfFile.Clear();
}
//create a new FLP
fwPanel = new FlowLayoutPanel();
int panelWidth = width * 4 + 50;
int panelHeight = height * 4 + 50;
fwPanel.Size = new Size(panelWidth, panelHeight);
fwPanel.Location = new Point(0, 0);
this.Controls.Add(fwPanel);
//each checked item would be stored into an arraylist
foreach(object itemChecked in clbFile.CheckedItems)
{
listOfFile.Add((clbFile.Items.IndexOf(itemChecked)+1).ToString());
}
int noOfCheckedFile = listOfFile.Count;
PictureBox[] listOfPicture = new PictureBox[noOfCheckedFile];
int positionX = 0, positionY = 0;
int maxPaddingX = (width * MATRIX_SIZE) - 1;
int maxPaddingY = (height * MATRIX_SIZE) - 1;
//dynamically create images.
for (int i = 0; i < noOfCheckedFile; i++)
{
listOfPicture[i] = new PictureBox();
listOfPicture[i].Image = resizeImage((Image)show_picture(Convert.ToInt32(listOfFile[i])), new Size(width, height));
listOfPicture[i].Size = new Size(width, height);
if (positionX > maxPaddingX)
{
positionX = 0;
positionY += height;
}
if (positionY > maxPaddingY)
{
positionY = 0;
}
listOfPicture[i].Location = new Point(positionX,positionY);
listOfPicture[i].Visible = true;
fwPanel.Controls.Add(listOfPicture[i]);
positionX += width;
}
}
show_picture is a method that takes in and integer and returns a bitmap image.
listOfFile is to trace which file to return.
listOfPicture is to store each images.
i tried replacing this lines
//if there is content inside the flowpanel, dump it
if (fwPanel != null)
{
listOfFile.Clear();
}
i have added this line into it, when i do a second click, everything just gone missing, but this is not what i want, because it does not re-populating the FlowLayoutPanel.
if (fwPanel != null)
{
fwPanel.SuspendLayout();
if (fwPanel.Controls.Count > 0)
{
for (int i = (fwPanel.Controls.Count - 1); i >= 0; i--)
{
Control c = fwPanel.Controls[i];
c.Dispose();
}
GC.Collect();
}
fwPanel.ResumeLayout();
listOfFile.Clear();
}
I also tried this, but on second click, nothing will happen.
if (fwPanel != null)
{
List<Control> listControls = fwPanel.Controls.Cast<Control>().ToList();
foreach (Control control in listControls)
{
fwPanel.Controls.Remove(control);
control.Dispose();
}
listOfFile.Clear();
}
I wonder if i miss out anything, can someone enlighten me on what did i miss out ? Or guide me for the best practice of doing this.
as Suggested, i shifted the creation outside (credit to Sinatr for spotting it)
FlowLayoutPanel fwPanel = new FlowLayoutPanel();
private void createFLP()
{
int panelWidth = width * 4 + 50;
int panelHeight = height * 4 + 50;
fwPanel.Size = new Size(panelWidth, panelHeight);
fwPanel.Location = new Point(0, 0);
this.Controls.Add(fwPanel);
}
that solves the nothing happen part. Followed by using this to remove controls
if (fwPanel != null)
{
List<Control> listControls = fwPanel.Controls.Cast<Control>().ToList();
foreach (Control control in listControls)
{
fwPanel.Controls.Remove(control);
control.Dispose();
}
listOfFile.Clear();
}
and everything works like a charm, hope that this answer will be able to help others who are facing the same problem too.

Interchange positions of two buttons

I want to replace button location (Interchange location )by black button when i click it and it is next to black button (b9=black button and lable1 is a temp for saving location).
I made this method :
void checkLocation()
{
if (ActiveControl.Location == new Point(5, 7))//-----------for button 1
{
if (b9.Location == new Point(83, 7) || b9.Location == new Point(5, 71))
{
label1.Location = ActiveControl.Location;
ActiveControl.Location = b9.Location;
b9.Location = label1.Location;
}
}// it continue for every button
and I write this code for every button_click
private void button1_Click(object sender, EventArgs e)
{
checkLocation();
}
now,some button don't work currently . what is wrong ?
by thanks from p.s.w.g
I think it is shorter and fit :
void swapLocation()
{
var tmp = ActiveControl.Location;
if((ActiveControl.Location.X==b9.Location.X)&&(Math.Abs(b9.Location.Y-ActiveControl.Location.Y)<=60))
{
ActiveControl.Location = b9.Location;
b9.Location = tmp;
}
if ((ActiveControl.Location.Y == b9.Location.Y) && (Math.Abs(b9.Location.X-ActiveControl.Location.X) <= 70))
{
ActiveControl.Location = b9.Location;
b9.Location = tmp;
}
}
Just do this to swap the locations of two controls:
void swapLocation()
{
var tmp = ActiveControl.Location;
ActiveControl.Location = b9.Location;
b9.Location = tmp;
}
Or more generally
void swapLocation(Control x, Control y)
{
var tmp = x.Location;
x.Location = y.Location;
y.Location = tmp;
}
...
swapLocation(ActiveControl, b9);
Update
It looks like you're trying to implement a version of the 15-puzzle. There are numerous ways to solve this, but to avoid a radical rewrite of your program I'd recommend this:
private int buttonWidth = 82;
private int buttonHeight = 82; // adjust these values as needed
private void button_Click(object sender, EventArgs e)
{
if ((Math.Abs(ActiveControl.Location.X - b9.Location.X) == 0 &&
Math.Abs(ActiveControl.Location.Y - b9.Location.Y) == buttonHeight) ||
(Math.Abs(ActiveControl.Location.X - b9.Location.X) == buttonWidth &&
Math.Abs(ActiveControl.Location.Y - b9.Location.Y) == 0))
{
swapLocation(ActiveControl, b9);
}
}
This basically checks to see if the ActiveControl is either directly above, below, to the left, or the right of b9, and if it is, swaps them. You can use this click handler for all buttons 1 through 8. Note this method only works with the buttons are a fixed width and height.

Categories