0

I'd like to display coverarts for each album of an MP3 library, a bit like Itunes does (at a later stage, i'd like to click one any of these coverarts to display the list of songs). I have a form with a panel panel1 and here is the loop i'm using :

 int i = 0;
        int perCol = 4;
        int disBetWeen = 15;
        int width = 250;
        int height = 250;
        foreach(var alb in mp2)
        {
                myPicBox.Add(new PictureBox());
                myPicBox[i].SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
                myPicBox[i].Location = new System.Drawing.Point(disBetWeen + (disBetWeen * (i % perCol) +(width * (i % perCol))), 
                    disBetWeen + (disBetWeen * (i / perCol))+ (height * (i / perCol))); 
                myPicBox[i].Name = "pictureBox" + i;
                myPicBox[i].Size = new System.Drawing.Size(width, height);
                myPicBox[i].ImageLocation = @"C:/Users/Utilisateur/Music/label.jpg";
                panel1.Controls.Add(myPicBox[i]);
                i++;
        }

I'm using the same picture per picturebox for convenience, but i'll use the coverart embedded in each mp3 file eventually.

It's working fine with an abstract of the library (around 50), but i have several thousands of albums. I tried and as expected, it takes a long time to load and i cannot really scroll afterward.

Is there any way to load only what's displayed ? and then how to assess what is displayed with the scrollbars.

Thanks

  • 2
    since all images are 250x250 you can load them into a ImageList and display them in a ListView. Much better than creating loads of controls.. – TaW May 03 '20 at 15:22
  • 1
    `var folders = getFolders(500); foreach (string fn in folders) { ilFolders.Images.Add(fn, Image.FromFile(fn)); } int i = 0; foreach (string k in ilFolders.Images.Keys) { var lvi = new ListViewItem(k, i); lv_folders.Items.Add(lvi); i++; }` - This loads e.g. 500 covers in 10 secods. Scrolling is without any issues. Loading a lot more images calls for some sort of caching/paging scheme and/ background loading taks. – TaW May 03 '20 at 15:50
  • 1
    For a pageing demo see [here](https://stackoverflow.com/questions/39808934/fake-scrolling-containers-with-very-many-controls/39810717#39810717) – TaW May 03 '20 at 17:28
  • Thanks TaW, i tried your solution and it worked but i thought the answer below was providing more flexibility in the organisation of the form. Thanks anyway ! – Sebastien Chemouny May 03 '20 at 22:07

1 Answers1

1

Winforms really isn't suited to this sort of thing... Using standard controls, you'd probably need to either provision all the image boxes up front and load images in as they become visible, or manage some overflow placeholder for the appropriate length so the scrollbars work.

Assuming Winforms is your only option, I'd suggest you look into creating a custom control with a scroll bar and manually driving the OnPaint event.

That would allow you to keep a cache of images in memory to draw the current view [and a few either side], while giving you total control over when they're loaded/unloaded [well, as "total" as you can get in a managed language - you may still need tune garbage collection]

To get into some details....

Create a new control

namespace SO61574511 {
    // Let's inherit from Panel so we can take advantage of scrolling for free
    public class ImageScroller : Panel {
        // Some numbers to allow us to calculate layout
        private const int BitmapWidth = 100;
        private const int BitmapSpacing = 10;

        // imageCache will keep the images in memory. Ideally we should unload images we're not using, but that's a problem for the reader
        private Bitmap[] imageCache;

        public ImageScroller() {
            //How many images to put in the cache? If you don't know up-front, use a list instead of an array
            imageCache = new Bitmap[100];
            //Take advantage of Winforms scrolling
            this.AutoScroll = true;
            this.AutoScrollMinSize = new Size((BitmapWidth + BitmapSpacing) * imageCache.Length, this.Height);

        }

        protected override void OnPaint(PaintEventArgs e) {
            // Let Winforms paint its bits (like the scroll bar)
            base.OnPaint(e);
            // Translate whatever _we_ paint by the position of the scrollbar
            e.Graphics.TranslateTransform(this.AutoScrollPosition.X,
                           this.AutoScrollPosition.Y);

            // Use this to decide which images are out of sight and can be unloaded
            var current_scroll_position = this.HorizontalScroll.Value;

            // Loop through the images you want to show (probably not all of them, just those close to the view area)
            for (int i = 0; i < imageCache.Length; i++) {
                e.Graphics.DrawImage(GetImage(i), new PointF(i * (BitmapSpacing + BitmapWidth), 0));
            }

        }

        //You won't need a random, just for my demo colours below
        private Random rnd = new Random();

        private Bitmap GetImage(int id) {
            // This method is responsible for getting an image.
            // If it's already in the cache, use it, otherwise load it
            if (imageCache[id] == null) {
                //Do something here to load an image into the cache
                imageCache[id] = new Bitmap(100, 100);

                // For demo purposes, I'll flood fill a random colour
                using (var gfx = Graphics.FromImage(imageCache[id])) {
                    gfx.Clear(Color.FromArgb(255, rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255)));
                }
            }
            return imageCache[id];
        }

    }
}

And Load it into your form, docking to fill the screen....

    public Form1() {
        InitializeComponent();
        this.Controls.Add(new ImageScroller {
            Dock = DockStyle.Fill
        });
    }

You can see it in action here: https://www.youtube.com/watch?v=ftr3v6pLnqA (excuse the mouse trails, I captured area outside the window)

Basic
  • 26,321
  • 24
  • 115
  • 201
  • ok that sounds beyond my skill unfortunately, but thanks anyway :) – Sebastien Chemouny May 03 '20 at 13:14
  • I've added an example – Basic May 03 '20 at 13:45
  • wow, that's impressive, thanks !!! i'll check it out tonight, i need time to digest this :) thanks again – Sebastien Chemouny May 03 '20 at 16:43
  • Basic, again, thanks. I went through some troubles (of course). First of all, want to scroll Vertically so i reversed the "AutoScrollMinSize" with `this.Width, (BitmapHeight + BitmapSpacing) * (imageCache.Length/perCol)` i also reversed the `TranslateTransform` and i changed the new PointF of the drawimage to keep 4 square per row. When i load, it looks fine, i have the vertical scrollbar and the squares drawn. The issue is when i scroll down. Using the arrow, the squares are slowly going to the left, if i click below the arrow, the squares disappear ... any idea why ? almost there :) – Sebastien Chemouny May 03 '20 at 20:56
  • found it ! i didnt have to change the `TranslateTransform` ! Going back to what it was and it works nicely ! thanks :) – Sebastien Chemouny May 03 '20 at 20:58
  • ok now i need to identify the picture i'm clicking but if i dont manage it (created the event so far) i'll post a new question. – Sebastien Chemouny May 03 '20 at 22:08
  • Sorry, I've been AFK today, so am only just checking in. If you post a new question, link it and I'll take a look. – Basic May 03 '20 at 23:51
  • 1
    Oh and from your last comment... Using the scroll position and control dimensions, you should be able to work out the visible rectangle. From there, it's simple maths to work out which image is under the cursor [assuming bitmap is 100px with 10px spacing]... `Math.Floor(x/110) + Math.Floor(y/110) * numImagesPerRow`. (it's late and I'm short on sleep, so double-check that, but you get the idea... The first part will increment as the mouse moves left/right, the latter once per row. I've used `Floor` to explain what's happening, but you can just convert to int, it'll have the same effect here. – Basic May 03 '20 at 23:59
  • 1
    Also look at `this.AutoScrollPosition` - It gets the Relative X/Y position of the top left corner of the viewable area. That along with the control/mouse position should be what you need. One last suggestion... Where you actually draw the image (`e.Graphics.DrawImage(GetImage(i),.....`), you can also do other drawing tasks. Eg you can add borders (rectangles) or a drop shadow by drawing a dark rectangle offset before the image). Check out https://learn.microsoft.com/en-us/dotnet/api/system.drawing.graphics?view=dotnet-plat-ext-3.1 for more info – Basic May 04 '20 at 00:13
  • Thanks ! yes, i thought about that calculation, and i'll try to make them into the class so the results (row, col, picture_ID ...) would be available in the EventArgs of the form. and thanks for the tip on the Drawing tasks, i'll definitely check this out ! – Sebastien Chemouny May 04 '20 at 08:10
  • Basic, quick question, in the code you provided, if i'm reducing the number of images i want to show in the `imageCache.Length` let's say if i'm having `50` here, it will redraw from 0 to 50 all the time right ? So i'd need to bring some logic to make sure the `int i = 0` will start from where we are, and not from 0, right ? – Sebastien Chemouny May 04 '20 at 11:49
  • Ideally you only actually draw the images that are fully/partially visible. Everything else is wasted effort (but won't matter much with low numbers). So... You need a 2-step process... One step is responsible for fetching images (from the cache, loading them as needed). This should also (eventually) unload image that are unlikely to be used again (if any). Then in the OnPaint, you only actually want to paint those images fully/partially visible. Similar to the logic for clicking an item, you can calculate which are (partially) visible and only paint those – Basic May 04 '20 at 12:08
  • Of course, you don't need to use an array with numbered IDs for the image cache. If you have (say) a list of paths on the way in, make the cache a dictionary where the key is the path as a string, and the value is the Bitmap... So a `Dictionary`, then you no longer need to worry about matching IDs to files... It all depends on your use-case. (although that might make detecting what was selected a little harder). An array was the simplest construct I could use to demonstrate the process but anything will do if it suits your needs. – Basic May 04 '20 at 12:38
  • yes i can definitely use a Dictionary with path and bitmap (the bitmap is contained in the MP3) and populate it at loading. Question : How to know what needs to be drawn (what's fully and partially visible) ? do i have to use the VerticalScroll.Value (and the Height) ? and again, thanks for the help ! – Sebastien Chemouny May 04 '20 at 13:03
  • You can work out the visible rectangle from the scroll position+ control width/height. That gives you a "viewing window" over your huge list of images. The "dumbest" option would be to calculate the image at the top left's index, subtract a whole row's worth, then starting from that image, paint however many can fit on the screen + 2 more rows (one to offset the one you just stepped back, the other to do the row off the bottom). If you're doing a single column (so 1-item rows) then change "Row" to "1" above. – Basic May 04 '20 at 14:47
  • 1
    Basic, i made it work :) you can see the video here : [link](https://www.youtube.com/watch?v=tAe2a-szAPw&feature=youtu.be) . I investigated the disposal process because as you can see, it's not fully fluid and i'm wondering if it's memory issues. I posted a question there : [link](https://stackoverflow.com/questions/61602041/dispose-bitmap-in-an-array-using-linq) thanks again for all the help :) :) – Sebastien Chemouny May 04 '20 at 21:48