-3

I was searching for around 4 months in order to solve image-flicker problem for my 180 controls, (similar to image-boxes) which are inserted into a flow-layout-panel. I nearly tried everything (Enabling double flicker and so on...) and nothing worked for me. But, today I figured a solution and that's by setting image size-mode to normal instead of zoom or to resize my images to fit exactly into those controls similar to picture-boxes. So I chose the second option and here is my code:

public MovieControl(Movies M1, bool show, MainForm current)
{
    InitializeComponent();
    Current = current;
    if (M1.movie_Banner == "") bunifuImageButton1.Image = Properties.Resources.no_image_available;
    else
    {
        WebClient client = new WebClient();
        Stream stream = client.OpenRead(M1.movie_Banner);
        Bitmap bitmap; bitmap = new Bitmap(stream);
        if (bitmap != null)
            bunifuImageButton1.Image = new Bitmap((Image)bitmap, new Size(139, 208));
        stream.Flush();
        stream.Close();
        client.Dispose();
    }
    bunifuImageButton1.Tag = M1;
    M1 = null;
}

Also, I want to mention that this code is being called by a thread using the following lines:

private void RunMoviesThread()
{
    ClearMovies();
    LoadMoviesThread = new Thread(() => LoadMoviesControls());
    LoadMoviesThread.Start();
}

private void LoadMoviesControls()
{
    int x = 1;
    if (MPageDropDown.InvokeRequired)
    this.Invoke((MethodInvoker)delegate { if (MPageDropDown.SelectedItem != null)  x = int.Parse(MPageDropDown.SelectedItem.ToString()); });
    else if (MPageDropDown.SelectedItem != null) x = int.Parse(MPageDropDown.SelectedItem.ToString());
    for (int i = (x - 1) * Max; i < MoviesList.Count() && i < x * Max; i++)
    {
        MovieControl tmp = new MovieControl(MoviesList[i], ShowMError, this);
        if (tmp.InvokeRequired || MoviesFlowPanel.InvokeRequired)
        {
            MovieControl m1 = null;
            try
            {
                m1= new MovieControl(MoviesList[i], ShowMError, this);
            }
            catch { }
            this.Invoke((MethodInvoker)delegate { MoviesFlowPanel.Controls.Add(m1); });
        }
        else
            MoviesFlowPanel.Controls.Add(tmp);
            tmp = null;
    }
}

It worked very well, instead of the fact that it took a very long time to process. for example; and image could take about half a second and up to 20 seconds in some rare cases! could you help me figure a more efficient way... Please Note: my images are previously uploaded on the internet and this code is written in C#. Also, without the resizing process it took only few seconds to load. **Max value is set to 180 Thanks in advance.

pstrjds
  • 16,840
  • 6
  • 52
  • 61
  • 2
    Are you aware that the PictureBox has both an ImageLocation property and a SizeMode? – Ňɏssa Pøngjǣrdenlarp Sep 07 '18 at 23:13
  • 2
    If what you posted is your actual code that you are using for all 180 buttons, then there is no shock to me that this would take a long time. You are downloading 180 images from the internet, and if you do that every time you resize your windows, well, that is going to take a long time. – pstrjds Sep 07 '18 at 23:16
  • @Plutonix could you please be more clear, how is this related to my problem. and sure I know that – Ahmad Egbaria Sep 07 '18 at 23:16
  • @pstrjds do you suggest other options, since I didn't find an easy way to upload those resized images online using C# on websites just like Imgur. since I cannot save them directly to my database. Keep in mind that downloading those 180 images without resizing takes only about 5 seconds – Ahmad Egbaria Sep 07 '18 at 23:19
  • 1
    @AhmadEgbaria - I don't understand why you need to upload them at all. From the posted code you are downloading them. If you need to resize them multiple times or something like that, then download them once and cache them somewhere. Make a temp directory and store the files there, just be sure you clean up after yourself. Then you can at least resize them locally without the download. How where you setting the images before you changed it to this code? I am assuming you downloaded them in the previous case. – pstrjds Sep 07 '18 at 23:21
  • Can you clarify your question a little further? I understand what you are asking "I want to do X faster, how?", but as you have not really described what you are trying to do, nor any constraints. You have not mentioned anything about the quality of the resizing, if that is important or not, etc, nor how often you are resizing. I made an assumption (because of you mentioning flicker) that this had something to do with resizing your window, but that is just a guess on my part. Does [this](https://stackoverflow.com/q/1922040/416574) help? – pstrjds Sep 07 '18 at 23:33
  • Ran out of room in the last comment - How big are these images that you are downloading? Are they the same every application run? Are you using more than the 180 (as in, do they change while your application is running)? You said they all download in about 5 seconds so I am guessing they are small images. Is that true? Could you hold all 180 original sized images in memory in addition to the images in the buttons? – pstrjds Sep 07 '18 at 23:38
  • Could someone tell me what's wrong with this path to save my images: SPath "C:\\Users\\Project\\Desktop\\Movie_PlusV2.1.0Updated\\Movie_Plus\\bin\\Debug\\tt0011841.jpg" – Ahmad Egbaria Sep 07 '18 at 23:41
  • Unrelated but still important, are you making sure to dispose the image/stream after? – Parrish Husband Sep 08 '18 at 00:35
  • 1
    So are you saying it takes up to 20 seconds to resize an image? its the download that takes 20 seconds. – TheGeneral Sep 08 '18 at 03:40
  • Related/expanding on @ParrishHusband 's comment. `Image`, `MemoryStream` and `WebClient` are disposable, and just looking at this again I realized that you are "newing" up a new one for each button/imagebox. So you are creating 180 `Images`, `WebClients` and `MemoryStreams`, not disposing of any of them. Especially in the case of the `WebClient` you should be reusing it. With this code, you are creating unnecessary memory pressure as well as unnecessary pressure on the GC and Finalizer. – pstrjds Sep 08 '18 at 06:46
  • @TheGeneral Actually not, without the resizing process, downloading those 180 photos took only few second. – Ahmad Egbaria Sep 08 '18 at 12:57
  • @pstrjds could you submit an edit to my code please? Keep in mind that those images are located into a custom made user control. Also, I'm running this loop of loading 180 photos using a thread. (the process of showing images and resizing them is being handled inside of the constructor for my user control). – Ahmad Egbaria Sep 08 '18 at 12:58
  • 1
    @AhmadEgbaria - Can you show your actual code then, I mean more than that one line. You absolutely are not executing **that** line of code in a background thread. Does your user control have 180 image boxes or buttons or whatever you are using? Is this WPF, WinForms, etc. It is impossible to provide a potential answer as your question stands now. – pstrjds Sep 08 '18 at 13:15
  • @pstrjds I have submitted a new edit, Please read it again and thanks for suggestion. – Ahmad Egbaria Sep 08 '18 at 13:36
  • You do realize that you are basically serializing all of this code. As soon as you call `Invoke` it is now operating in your main UI thread and so instead of downloading the images in paralell, you are doing it one at a time. – pstrjds Sep 08 '18 at 13:39
  • @pstrjds So what edits should be done. According to what I have read and my tries, using Invoke is necessary to let it work. – Ahmad Egbaria Sep 08 '18 at 13:44
  • Yes - you have to invoke because you can't create a Control or touch anything in the UI from a background thread. So you need to do the work of getting the images and resizing it outside of the UI – pstrjds Sep 08 '18 at 13:46
  • Then I should make another thread right? But let's fix time issues for resizing those images. – Ahmad Egbaria Sep 08 '18 at 13:50
  • @pstrjds I tried to make a new thread and my problem got fixed so thanks, but I got an error saying: The remote server returned an error (500) - Internal server error. and it appeared next to this line: Stream stream = client.OpenRead(((Movies)bunifuImageButton1.Tag).movie_Banner); – Ahmad Egbaria Sep 08 '18 at 13:57
  • @AhmadEgbaria - I added an answer to what sort of is your original question. In regards to the server error, maybe they limit how much you can hit their API, or server is busy, or whatever. It is something you should think about handling in your code and maybe using your "no image available" image when this occurs. – pstrjds Sep 08 '18 at 14:43

1 Answers1

0

Well I don't know how much this will help you, but it's worth a shot. I am not entirely confident that WebClient can be parallelized like this, but a similar example appears on page 187 of Pro .NET Performance: Optimize Your C# Applications so I am going to take a leap and say that you can use WebClient from multiple threads. I have added a new method which you could just call from your UI, although it is making a check to be sure it doesn't need to be invoked. This method reads the page number and then launches the background process which will download and resize the images and then after it does all of that it will create the controls in the UI thread.

private void GetPageAndLoadControls()
{
    if (MPageDropDown.InvokeRequired)
    {
        MPageDropDown.Invoke((MethodInvoker)(() => GetPageAndLoadControls();));
        return;
    }

    var page = MPageDropDown.SelectedItem != null ?
        int.Parse(MPageDropDown.SelectedItem.ToString()) - 1 : 0;
    Task.Run(() => LoadMoviesControls(page));
}

private void LoadMoviesControls(int page)
{
    var dict = new ConcurrentDictionary<string, Bitmap>();
    var movies = MovieList.Skip(page * Max).Take(Max).ToList();
    using (var client = new WebClient())
    {
        Parallel.ForEach(movies, (m) =>
        {
            Stream stream = null;
            Bitmap bmp = null;
            try
            {
                if (!string.IsNullOrWhitespace(m.movie_Banner)
                {
                    stream = client.OpenRead(s);
                    bmp =  new Bitmap(stream);
                    // Note: I am guessing on a key here, that maybe there is a title
                    // use whatever key is going to be best for your class
                    dict.TryAdd(m.movie_Title, new Bitmap(bmp, 139, 208));
                }
                else dict.TryAdd(m.movie_Title, Properties.Resources.no_image_available);
            }
            finally
            {
                bmp?.Dispose();
                stream?.Dispose();
            }
        });
    }

    // Here we have to invoke because the controls have to be created on
    // the UI thread. All of the other work has already been done in background
    // threads using the thread pool.
    MoviesFlowPanel.Invoke((MethodInvoker)() =>
    {
        foreach(var movie in movies)
        {
            Bitmap image = null;
            dict.TryGetValue(movie.movie_Title, out image);
            MoviesFlowPanel.Controls.Add(
                new MovieControl(movie, image, ShowMError, this);
        }
    });
}

// Changed the constructor to now take an image as well, so you can pass in
// the already resized image
public MovieControl(Movies M1, Bitmap image, bool show, MainForm current)
{
    InitializeComponent();
    Current = current;
    bunifuImageButton1.Image = image ?? Properties.Resources.no_image_available; // Sanity check
    bunifuImageButton1.Tag = M1;
}

Edit
Something that occurred to me after writing this and thinking more about it, you did not post the code for ClearMovies(), but from the posted code I am assuming that you are disposing the previous 180 controls and creating 180 new ones. What would be better would be to use the same approach I have shown in the code above and then instead of "creating" new controls you just update the existing ones. You could add an update method to your user control that just changes the image and the Movie item that is being stored in the control. This would save the overhead of creating new controls each time which should improve the performance. You may need to call Invalidate at the end of the Update method. Not sure and since I can't really test this all out, just wanted to mention it. Also, instead of using the Tag property to hold the Movie object, I would just add a member variable to your UserControl. This would add type safety and make it clearer what is being held where.

public class MovieControl : Control
{
    public Movies Movie { get; protected set; }

    // Changed the constructor to now take an image as well, so you can
    // pass in the already resized image
    public MovieControl(Movies M1, Bitmap image, bool show, MainForm current)
    {
        InitializeComponent();
        Current = current;
        bunifuImageButton1.Image = image ?? Properties.Resources.no_image_available; // Sanity check
        Movie = M1;
    }

    public void UpdateMovieAndImage(Movies movie, Image image)
    {
        // Only dispose if it isn't null and isn't the "No Image" image
        if (bunifuImageButton1.Image != null
            && bunifuImageButton1.Image != Properties.Resources.no_image_available)
        {
            binifuImageButton1.Image.Dispose();
        }
        bunifuImageButton1.Image = image;
        Movie = movie;
    }
}
pstrjds
  • 16,840
  • 6
  • 52
  • 61