-4

I need your help in the following situation. I know it's been discussed many many times, the way one should work with threads, using Synchronize / Critical Sections and so on. So do not blame me for asking this question again, because in my situation neither synchronize, nor Critical Sections help to deal with TBitmap in a TThread.

  1. What I use:

I'm using Delphi XE, Firemonkey Application with GlobalUseDirect2D:= True;

I NEED to use GlobalUseDirect2D because I draw a lot and I need fast drawing. Still Disabling GlobalUseDirect2D or using GlobalUseGPUCanvas:= True, my problem disappears, but that is not an option!

  1. What I Do:

Ok. So this is a simple implementation of some other project, but the idea is to display image thumbnails. First I build a list of items (TImageData) and then I start a Thread to load image thumbnails. When scrolling (using TScrollBar) I call Arrange method to arrange items on a form and than call Invalidate to repaint the displaying area;

  1. So what is the problem?

The problem is that some thumbnails are either blank or not fully loaded (corrupted).

  1. When the problem occures?

After many experiments I found out when the images become corrupted;

So. If I build a list of items, then start the thumbnails thread and do nothing with the form while the thread is running (do not change scrollbar position / do not resize the form / do not move cursor) then EVERYTHING IS FINE. All is loaded well;

In case I build a list of items, then start the thumbnails thread and start scrolling while the thread is running(changing scrollbar position - it calls Arrange + Invalidate methods), My thumbnails (not all) become corrupted.

  1. What I tried.

Since I thought it might be because my Thumbnail Thread gets access to Items and at the same time when I call Arrange, main thread also accesses these items, it makes some interference. So I tried using Synchronize and Critical sections, but it did not help. I won't show how and where exactly I used them, because there is no need in it. Why? I found out when this corruption occures. See number 6;

  1. The exact problem.

After many experiments (once again) it turns out that it is weird:

  1. I Build a list of Items;
  2. Start a Thumbnails Thread;
  3. Start changing ScrollBar's position while the thread is running 3.1 ScrollBar calls Arrange; 3.2 ScrollBar calls Invalidate;

  4. Result:= BAD THUMBNAILS;

    Why I said it was "weird"? I added another scrollbar to the form. Now I have 2 scrollbars. one to the right is the scrollbar that calls Arrange + Invalidate; The second ScrollBar simply does NOTHING;

    So when I do:

    1. I Build a list of Items;
    2. Start a Thumbnails Thread; !!! 3. Start changing position of THE NEW SCROLLBAR while the thread is running (the second one), which performs nothing!!!

4.Result:= THE SAME. That is, I still get corrupted thumbnails.

IT's weird, is not it???? Atleast I do not understand why this happens. So please tell me how to fix it?

  1. And here is a link to download this sample application, just change the path to where you have many .jpeg images and try it yourself. https://www.dropbox.com/s/spc8k4d4qry4979/WeirdApp.rar?dl=0

and the Video where I show what I mean : https://youtu.be/dfe111odrUM

type
TImageData = class (TObject)
public
idPath:String;
idImage:TBitmap;
idloaded:Boolean;
x, y:Single;
w, h:Integer;
iCriticalSection:TRTLCriticalSection;
constructor Create;
destructor destroy; override;
end;



 TImageThread = class(TThread)
  private
    tfileslist:TObjectList;
    ttChangeHandle: THandle;
    ttShutdownHandle: THandle;
    ttPaused:Boolean;
    ttCriticalSection:TCriticalSection;
    procedure DoFolderItemChange;
  protected
    procedure Execute; override;
  public
    constructor Create(fileslist:TObjectList); reintroduce;
    destructor  Destroy; override;
    procedure  Shutdown;
    procedure  Reset;
  end;



procedure TForm1.Button1Click(Sender: TObject);
var
  SR: TSearchRec;
  ImageData:TImageData;
  path:String;
begin
  Path:= 'D:\Images\';
  if FindFirst(Path + '*.*', faAnyFile, SR) = 0 then
  begin
    repeat
      if (SR.Attr <> faDirectory) and (Pos ('.jpg', SR.Name) > 0) then
      begin
        ImageData:= TImageData.Create;
        ImageData.idPath:=  Path + SR.Name;
        datalist.Add(ImageData);

      end;
    until FindNext(SR) <> 0;
   FindClose(SR);
  end;

  arrange;
  ImageThread.Reset;


end;




procedure TImageThread.Execute;
var
  Events: array[0..1] of THandle;
  WaitResult: DWORD;

 ImageData:TImageData;
 I:Integer;
begin
    Events[0] := ttChangeHandle;
    Events[1] := ttShutdownHandle;
    while not Terminated do begin
        WaitResult := WaitForMultipleObjects(2, @Events[0], FALSE, INFINITE);
        if WaitResult = WAIT_OBJECT_0 then begin

          if Assigned(tfileslist) then begin

           for I:= 0 to tfileslist.Count - 1 do begin
            ImageData:= TImageData(tfileslist.Items[I]);

           try
            ImageData.idImage.LoadThumbnailFromFile(ImageData.idPath, 128, 128);

           except
             on E : Exception do
             begin
               //ShowMessage('Exception class name = '+E.ClassName);
               ShowMessage(ImageData.idPath +  ' ----- Exception message = '+E.Message);
             end;
           end;

            ImageData.idloaded:= True;
           end;

          end;
        end;


  self.Synchronize(nil, procedure ()
   begin
     Form1.Button1.Text:= 'DONE';
     beep;

   end);


      end;
end;


procedure TForm1.ScrollBar1Change(Sender: TObject);
begin

arrange;
Invalidate;

end; 


procedure TForm1.arrange;
var
  I:Integer;
  ImageData, ImageDataP:TImageData;
begin

  for I:= 0 to datalist.Count - 1 do begin

   ImageData:= TImageData(datalist.Items[I]);

   if I = 0 then begin
     ImageData.x:= 50;
     ImageData.y:= 50 - ScrollBar1.Value;
   end else begin
     ImageDataP:= TImageData(datalist.Items[I - 1]);
     ImageData.x:= ImageDataP.x + 128;
     ImageData.y:= ImageDataP.y;

     if ImageData.x + 128 > Width then begin
      ImageData.x:= 50;
      ImageData.y:= ImageDataP.y + 128 + 10;
     end;
   end;

   end;



end; 



procedure TForm1.FormPaint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  I:Integer;
  ImageData:TImageData;
begin
  Canvas.BeginScene();

  try
   for I:= 0 to datalist.Count - 1 do begin

            ImageData:= TImageData(datalist.Items[I]);

            if  Assigned(ImageData.idImage) and ImageData.idloaded then begin

              Canvas.DrawBitmap(ImageData.idImage, RectF(0, 0, ImageData.idImage.Width, ImageData.idImage.Height),
              RectF(ImageData.x, ImageData.y, ImageData.x + 128, ImageData.y + 128), 1, True );

            end;

   end;


  finally
  Canvas.EndScene;

  end;

end;
Arioch 'The
  • 15,799
  • 35
  • 62
serhiyiv
  • 183
  • 1
  • 2
  • 10
  • 10
    This is the usual situation. *I know I'm not supposed to access the UI from threads, but my situation is different because I'm doing (code that is accessing the UI from threads) and I don't understand why it's not working.* It's the usual answer: You cannot access the UI from threads. All UI access has to occur in the *main thread* via Queue or Synchronize, which means your thread **cannot** draw thumbnails, update status bars, move scrollbars, or anything else. **Threads cannot access the UI.**. I"m not sure how repeating it for the millionth time can make it more clear. – Ken White Apr 07 '16 at 20:17
  • Ok. If you saw my video, I only change the left scrollbar while thumbnails are being loaded. That left scrollbar does nothing. I do not access anything from main thread at all!!! ONLY AFTER ALL IS FINISHED I do PAINT the thumbnails and arrange them. – serhiyiv Apr 07 '16 at 20:24
  • That's it. I do agree that when the thread is working and call Invalidate, then the main thread accesses the items and paints those thumbnails, but I only call Invalidate after all is loaded. Be sided as I showed in the video I only change Dummy scrollbar and get corrupted thumbnails!!! If I do nothing wit the form then everything is loaded correctly. And as I wrote above, it only happens when I have GlobalUseDirect2D ENABLED. – serhiyiv Apr 07 '16 at 20:24
  • Please download the application and try the steps I did. – serhiyiv Apr 07 '16 at 20:26
  • You don't understand the point. This problem has been addressed numerous times on this site with the same answer every time. @KenWhite is completely correct. – GrizzlyManBear Apr 07 '16 at 20:48
  • 5
    You're **displaying the thumbnails from the thread**, which is **accessing the UI from a thread**. For the million and first time, **you cannot access the UI from a thread**. The thread **cannot do things that change the UI**, which includes **displaying thumbnails**. You have to do that through the **main thread** by using `Queue` or `Synchronize`. I don't care what your video shows, or how firmly you protest - you **cannot access the UI from a thread other than the main thread**. – Ken White Apr 07 '16 at 21:00
  • Ok. The point is: When doing the same with VCL.Graphics.TBitmap, + TJpegimage I had no problems. Neither do I have problems when I Disable Direct2D !!!! If you had a look at my video / source code, you would see what I mean. – serhiyiv Apr 07 '16 at 21:03
  • Even if I do not have any painting of thumbnails but just load thumbnails into TObjectList, then still are corrupted. How do I know that? I simply load thumbnails in a thread and save into a folder and see corrupted images. And once again I repeat, this happens with Firemonkey!!! – serhiyiv Apr 07 '16 at 21:08
  • Please have a look at my video + src, I have no one to ask it's not easy to explain everything in written form – serhiyiv Apr 07 '16 at 21:11
  • 1
    It may appear to be working, but it's a fault waiting to happen. Just like breaking the law. You may rob a store one time and not get caught, then rob the store the next time and go to jail. Same concept here. You're just lucky that certain things are working **for right now**, but roll this out into production and face hell. It doesn't matter if it's VCL or FMX, it's the exact same story. – Jerry Dodge Apr 07 '16 at 21:16
  • If you got lucky when doing it with the VCL, you got lucky. It's wrong, it does not work properly, and it's been documented as not being proper. What more do you need? *I know this has been asked many times* should tell you that you should **listen to all those previous answers that say don't do this because it won't work**. – Ken White Apr 07 '16 at 22:05
  • From the video you are using Delphi 10 Seattle though and not XE? – Craig Apr 07 '16 at 22:07
  • As you seem to be using Seattle, see the FireFlow sample in C:\Users\Public\Documents\Embarcadero\Studio\17.0\Samples\Object Pascal\Multi-Device Samples\User Interface\FireFlow, particularly the `AddFolder` procedure, which creates a thread to load an image, and then see the `TImageThread.Execute` method, which shows **using Synchronize** properly. – Ken White Apr 07 '16 at 22:19
  • I've seen the FireFlow too. The only thing synchronized is the FImage.Bitmap.Assign(FTempBitmap); but LoadThumbnailFromFile is done in the Execute method. I've dried this before making the post!!! And I know about it, that's why I wrote that I did not post any Synchonize code because I've used it. When I Synchronize LoadThumbnailFromFile, then all is loaded well, but one drawback of this is that the application becomes unresponsible. Just like in the FireFlow ex. But they create NEW THREAD FOR EACH IMAGE. And I Use Only one thread. – serhiyiv Apr 08 '16 at 05:14
  • Funny... I recall Auslogics used this task to test contenders to their vacancies. But they demanded using bug-ridden Delphi 2009... – Arioch 'The Apr 08 '16 at 10:52
  • I played a bit with the FireFlow example. There is a condition that you can't scroll images until all are loaded ( if AniIndicator1.Visible.... Abort). If you chose a folder with at least 200+ images you'll notice that the FireFlow app is not responsible until every image is loaded. Now just remove the ABORT and compile the example. Now after you chose a folder with images you can scroll the images while they are being loaded. Try to scroll!!! And what will you see??? Yes, CORRUPTED IMAGES. – serhiyiv Apr 08 '16 at 17:04
  • Then I have a question to Ken White - yes I'm not an experienced programmer, it's my hobby, I agree. You did not look at my code example and just said I did wrong. But what about those guys from Embrcadero and their FireFlow example? Does their example differ much from mine?? NO. They also use LoadthumbnailFromFile in a thread and all they Synchronize is just the Assign method. – serhiyiv Apr 08 '16 at 17:04
  • But they are more clever - they Forbid users to do ANYTHING with the application until it's done loading and here you go - you have an application that is kinda loading images in a thread but it is useless because if you choose a folder with 10000+ images you'll have to wait for 10+ minutes until hey are loaded. I can do the same in the main thread + ProcessMessages(). And all I'm trying to say is that if I set GlobalUseDirect2D to FALSE, I do not have such a problem, but I need to have it enabled just because of drawing speed. – serhiyiv Apr 08 '16 at 17:04
  • BTW, this guy http://rmklever.com/?p=105 does not synchronize anything except of the Invalidate message, loads images in a thread but this is done with VCL. \ – serhiyiv Apr 08 '16 at 17:10

2 Answers2

4

I believe your problem is that you do not realize that TBitmap is not thread-safe. Everything else looks good to me. To remedy this, change the following line of code in your project

    ImageData.idImage.LoadThumbnailFromFile(ImageData.idPath, 256, 256);

so that it is within a Synchronize block.

 Synchronize(nil, procedure ()
 begin
    ImageData.idImage.LoadThumbnailFromFile(ImageData.idPath, 256, 256);
 end);

I tried this change in your project and did not notice any bitmaps that fail to load.

Roy Woll
  • 312
  • 1
  • 4
  • Or, in other words: *Don't access UI elements from a thread; use Synchronize so that the access is done by the main thread instead*. – Ken White Apr 08 '16 at 02:24
  • TBitmap would not normally be considered UI as it seems just a data structure, so I understand why this mistake would be made. I wish TBitmap was thread-safe as it would make life much easier. – Roy Woll Apr 08 '16 at 03:01
  • I might understand it too, if the poster hadn't (often) expressed their knowledge of using Synchronize and claims to have tried it, but using it clears up the issue immediately and simply. (And of course, didn't include any code attempting to use it at all.) I wasn't commenting on your answer, BTW - I upvoted it. I was leaving the comment for the OP's benefit when they saw your answer. – Ken White Apr 08 '16 at 03:04
  • Roy Woll, I've tried it before and I know it helps, but using Synchronize way application freezes. So what's the use of another thread? Just start the thread and try scrolling at the same time. Then how would I use synchronize so that application remains responsible? – serhiyiv Apr 08 '16 at 05:00
  • Loading data from files are expensive in term of performance. For smooth scrolling, you need to load thumbnail from file on initialization or when content of TFileList changed. You do not need to load it every time. Create thumbnail version of image then cache it somewhere in memory. – Zamrony P. Juhara Apr 08 '16 at 09:51
  • 2
    @serhiyiv then do not use TBitmap, use other non-UI classes. Maybe TBitmap32 of Graphics32 or maybe a class from Vampyre Imaging. Another lazy way would be using a `TMemoryStream` objects - load the file into it within auxiliary thread, then pass it into a threadsafe queue, either a queue class like one of OmniThreading libs, or just via anonymouse procedures and http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Classes.TThread.Queue - then your VCL thread would get the already read data and load bitmaps from the memory streams and then free the passed `TMemoryStream` objects. Voila. – Arioch 'The Apr 08 '16 at 10:49
  • @serhiyiv The application shouldn't freeze unless either you have created some blocking condition where your main thread waits for it to complete or it is hogging all the cpu so your app becomes unresponsive. If the latter, you can simply put a sleep(10) in the middle of your loop in the thread so that the system yields more often. – Roy Woll Apr 08 '16 at 16:51
  • Initially I used Graphics32 and everything was fine. Then I decided to use Firemonkey application because I draw a lot and TBitmap32 is not fast enough, though as I can see now with better drawing speed I got more problems – serhiyiv Apr 08 '16 at 17:08
-2

I tried to say that problem was with FIREMONKEY Bitmap, and no one listened to me. Still I found the solution and I was RIGHT :)

So, as I said before, when I used VCL.Graphics.TBitmap I had no problems loading image thumbnails and displaying them the way I did in this example. I used TBitmap.Canvas.Lock , I used Synchronize.
With Firemonkey this way it did not work, and the problem hided in the TBitmap.LoadThumbnailFromFile method.

when I tried

Synchronize(nil, procedure ()
 begin
    ImageData.idImage.LoadThumbnailFromFile(ImageData.idPath, 128, 128);
 end);

Then the thumbnail was loaded in the main thread and my application froze until all the thumbnails were loaded, but thumbnails were loaded correctly;

If you have a look at the LoadThumbnailFromFile method:

procedure TBitmap.LoadThumbnailFromFile(const AFileName: string; const AFitWidth, AFitHeight: Single;
  const UseEmbedded: Boolean = True);
var
  Surf: TBitmapSurface;
begin
  Surf := TBitmapSurface.Create;
  try
    if TBitmapCodecManager.LoadThumbnailFromFile(AFileName, AFitWidth, AFitHeight, UseEmbedded, Surf) then
      Assign(Surf)
    else
      raise EThumbnailLoadingFailed.CreateFMT(SThumbnailLoadingFailedNamed, [AFileName]);
  finally
    Surf.Free;
  end;
end;

it turns out that Assign(Surf) caused the problem!!!

All you have to do is just Synchronize it, and only it, BUT NOT the whole LoadThumbnailFromFile method;

Like This:

procedure GetThumbnail(DestBMP:TBitmap; W, H:Integer; Path:String; Thread:TThread);
var
  Surf: TBitmapSurface;
begin
  Surf := TBitmapSurface.Create;
  try
    if TBitmapCodecManager.LoadThumbnailFromFile(Path, W, H, False, Surf) then
    begin
    Thread.Synchronize(nil, procedure ()
      begin
       DestBMP.Assign(Surf) ;
       end);
    end;
  finally
    Surf.Free;
  end;
end;

Just change TImageThread.Execute in my example and try it yourself; This way application loads thumbnails REALLY in a background thread, still all images are loaded correctly and you can scroll/resize the application while thumbnails are loading.

procedure TImageThread.Execute;
var
  Events: array[0..1] of THandle;
  WaitResult: DWORD;

 ImageData:TImageData;
 I:Integer;
   Surf: TBitmapSurface;
begin
    Events[0] := ttChangeHandle;
    Events[1] := ttShutdownHandle;
    while not Terminated do begin
        WaitResult := WaitForMultipleObjects(2, @Events[0], FALSE, INFINITE);
        if WaitResult = WAIT_OBJECT_0 then begin

          if Assigned(tfileslist) then begin

           for I:= 0 to tfileslist.Count - 1 do begin
            ImageData:= TImageData(tfileslist.Items[I]);
           try

          GetThumbnail(ImageData.idImage, 128, 128, ImageData.idPath,Self);
          ImageData.idloaded:= True;

           except
             on E : Exception do
             begin
               //ShowMessage('Exception class name = '+E.ClassName);
               ShowMessage(ImageData.idPath +  ' ----- Exception message = '+E.Message);
             end;
           end;

           end;

          end;
        end;


  self.Synchronize(nil, procedure ()
   begin
     Form1.Button1.Text:= 'DONE';
     beep;

   end);

      end;
end;
serhiyiv
  • 183
  • 1
  • 2
  • 10
  • Your caps-lock seems to be getting stuck randomly. May benefit from a new keyboard. – Sertac Akyuz Apr 08 '16 at 21:45
  • 1
    You seem to ignore my last comment. The code I provided does NOT halt the main thread. I have used this technique myself in FireMonkey. I also did the change to your referenced project and had no trouble scrolling the main window while the background thread was running and loading bitmaps. However since you never invalidate the form in the loop of your thread, it won't repaint while it is loading. – Roy Woll Apr 09 '16 at 00:40