Hallo zusammen,
ich habe mir mal den
BackgroundWorker von Sir Rufo angesehen, und versuche den mit anonymen Prozeduren zusammenzubringen um evtl. das Handling z.B. mit TListView zu vereinfachen.
Erstmal dankesehr für diese
Unit, das ist wirklich schöner Threading-Code.
Weil anscheinen LiveBinding nicht verlässlich funktioniert und ich auch die ListViews nicht komplett von Hand setzen möchte suche ich nach einfachen Wegen wie man das machen kann.
Insbesondere möchte ich auch die Bitmaps in der ListView einfach benutzen, und zwar so das es auf allen Platformen läuft.
Das Problem ist das Laden von Daten in die ListView kann die App ziemlich lange einfrieren, das möchte ich möglichst optimieren und mit BackgroundWorker-Threads verstecken was eben geht.
Mein erster Versuch sieht so aus:
Ich habe eine Klasse vom BackgroundWorker abgeleitet, die dann im Hintergrund Daten verarbeiten soll um diese der ListView dann zuzuordnen.
Dabei soll das Vorbereiten und Holen der Daten im Hintergrund ablaufen, Das Übertragen der Daten wird dann wieder im UI-Thread gemacht.
Damit alles schön übersichtlich bleibt möchte ich anonyme Prozeduren benutzen (sorry das Demo ist schon etwas unübersichtlich, aber das kommt durch die verschiedenen Versuche, wir sich hoffentlich bald lichten).
Im Aufruf benutze ich class procedures um einfach und direkt Daten einzupflegen.
Diese benutzt drei anonyme Prozeduren für
- Prepare (UI-Thread stuff)
- Fetch (Abfrage von Daten durch Thread, sollte threadsafe sein)
- Complete (UI-Thread stuff)
und übergibt weitere Parameter
- für die ListView InsertPosition
- und Von/Bis Parameter für die Fetch-Bedingung
Das sollte nur ein grundsätzliches Beispiel sein, ob es so mit BackgroundWorker funktioniert und Sinn macht,
das Fetch könnte dann auch von einem DataSet kommen, oder Images von einem Fiolder im Hintergrund lesen, etc.
Der Gedanke ist jedenfalls:
- ich rufe TS4ListView_Worker.Items_Add( auf um mein ListView zu füllen oder zu aktualisieren
- Das Items_Add kann je nach Parameter per Append oder Insert neue Daten an bestimmte Positionen BULK-laden
- ListView wird im Hintergrund bearbeitet, die Daten werden im DoAdd bereitgestellt.
- danach wird die ListView wieder zum Bearbeiten freigegeben, der TS4ListView_Worker gibt sich selbst frei
- das Ganze ListView Handling ist bestmöglich in der Klasse gekapselt, so das man nur dafür sorgen muss das
die Daten für jeden Record beschafft werden
Delphi-Quellcode:
//
... vereinfachter Code
//
procedure TForm1.DoAdd(iPosition, iCount : Integer);
var
lvw : TS4ListView_Worker;
begin
lvw := TS4ListView_Worker.Items_Add(ListView1,
//
// PrepareProc: Begin process, Prepare UI
//
procedure
begin
AniIndicator1.Enabled := True;
AniIndicator1.Visible := True;
AniIndicator1.Repaint;
ListView1.Enabled := False;
ListView1.BeginUpdate;
end,
//
// WorkProc: Fetch the single items syncd from background
//
procedure (const ALvi : TListViewItem;
AIdNew : Integer;
var AColumnData : TS4ListView_Worker.TColumnData
)
begin
//
//
... vereinfachter Code
//
//
//
// Setup the ColumnsData record here
//
AColumnData.AId := AIdNew;
AColumnData.AText := 'New ' + AIdNew.ToString;
AColumnData.ADetail := 'More Details on New ' + AIdNew.ToString +
' Img. ' + iImg.ToString;
AColumnData.ABmp := bmp;
end,
//
// CompleteProc: End the process, release UI change
//
procedure (e : TS4ListView_Worker.TCompletedEventArgs)
begin
AniIndicator1.Enabled := False;
AniIndicator1.Visible := False;
ListView1.EndUpdate;
ListView1.Enabled := True;
ListView1.Repaint;
Label1.Text := ListView1.Items.Count.ToString + ' items';
lvw.Free; // Is this really working, to avoid memory leaks ???
end,
//
// Pre-Settings for the Fetch process
//
iPosition, // Insert Position in Items
iFetchFrom, // Fetch conditions, used in WorkProc to determin from/to
iFetchTo
);
In dem ListView_Worker bearbeite ich die Prozeduren in verschiedenen Steps, und synchronisiere die ListView relevanten Zugriffe, wenn nötig.
Delphi-Quellcode:
procedure TS4ListView_Worker.NotifyDoWork(e: TDoWorkEventArgs);
var
I: Integer;
begin
//
// First Prepare possible States in the UI-Thread, if needed
//
if Assigned(FCd_Prepare_Proc) then
begin
TThread.Synchronize(nil,
procedure
begin
FCd_Prepare_Proc;
end
);
end;
//
// 1.Part: Prepare new List Items safely in the UI-Thread
//
// !! Firstly only the visible items, to accelerate the UX
//
TThread.Synchronize(nil,
procedure
var
I, iCount : Integer;
begin
iCount := FIdNewTo - FIdNewFrom;
if FInsertPos < 0 then
begin
for I := 0 to iCount-1 do
begin
if I = 20 then
Sleep(20); // TEST: Sleep a little, to allow fastest UI repaint
FListViewItems[I] := FListView.Items.Add as TListViewItem;
end;
end
else
begin
for I := 0 to iCount-1 do
begin
if I = 20 then
Sleep(20); // TEST: Sleep a little, to allow fastest UI repaint
FListViewItems[I] := FListView.Items.Insert(FInsertPos) as TListViewItem;
end;
end;
end
);
//
// Finally Start to fetch the new data WITHIN the BackgroundThread
//
if Assigned(FCd_Work_Proc) then
begin
if (FIdNewTo - FIdNewFrom) > 0 then
begin
for I := 0 to (FIdNewTo - FIdNewFrom)-1 do
begin
if Assigned(FListViewItems[I]) then
begin
FColumnDatas[I].AId := FIdNewFrom + I;
FColumnDatas[I].ABmp := nil;
FColumnDatas[I].ABmpRef := nil;
FCd_Work_Proc(FListViewItems[I],
FIdNewFrom + I,
FColumnDatas[I]); // Fetch the data here
end;
end
end;
end;
inherited;
end;
Als Beispiel lade ich Texte und Bilder, und zeige bei der Verarbeitung einen Animator an.
- Texte funktionieren ja ganz gut (CheckBox Bitmap abgewählt), aber ich bin nicht ganz sicher ob die
Ausführung so Speicherlecks erzeugen wird (zumindest gibt es Unterschiede unter diversen Plattformen).
- Bei Bitmaps fängt das Problem schon an, die hole ich mir der Einfachheit halber aus einer ImageList,
idealerweise sollten diese als komprimierte PNG Bilder gespeichert sein um wenig Platz und unter
allen Platformen zu Laufen.
Es können aber sporadisch Bitmaps in der Anzeige fehlen, manchmal geht es aber auch,
ich vermute mal das Bitmaps der ImageList auch im UI-Thread bearbeitet werden möchten.
Je nachdem welche Methode man benutzt funktioniert es besser oder schlechter, aner ich habe da auch das
Abfragen und Generieren der Bitmaps via ImageList im Verdacht.
Jedenfalls habe ich andere Methoden (Lesen aus Folder) noch nicht ausprobiert, es geht im Wesentlichen
erstmal um das generelle Konzept einer solchen Klasse.
Ich habe probiert:
- Speichern via Stream als PNG
- direktes Assign
- über BitmapRef nur die Referenz zu übergeben
- per z.B. TGlyph die Images über eine Komponente zu holen (was aber im Thread ein Problem ist)
Ich vermute mal: Bitmaps im Thread sind ein No-Go. Oder gibt es noch einen Trick mit Bitmaps ?
- Wenn man mal größere Mengen einträgt wird das Ganze schnell träger und kann Probleme machen, das
passiert bei mir schon ab 5000 - 20000 Einträgen.
Da würde ich eingentlich kein Speicherproblem erwarten, im TaskManager zeigt das nur 170MB belegt werden.
Aber ein Click zum Eintragen kann schon recht zäh verzögert werden, die interne Verarbeitung der ListView
scheint schon bei 20000 Textrecords ziemlich verlangsamt zu werden.
Ich sehe aber nicht das es MemoryLeaks wären, kann mich aber auch irren.
- Eine Frage wäre innerhalb der class procedure, ob sich nach CompleteProc die ganze Klasse selbst freigeben kann ?
Delphi-Quellcode:
Result := TS4ListView_Worker.Create(AListView, // The target tLv to Add an item
AProcPrepare,
AProcWork, // the ColumnData Getter Proc
AProcComplete,
iInsertPos,
iNewIdFrom,
iNewIdTo
);
if Assigned(Result) then
begin
Result.RunWorkerAsync;
end;
Oder ob man die Variable mitführen und von Hand freigeben muss ?
Es scheint eigentlich zu funktionieren, aber bin mir noch nicht 100% sicher ob es auch korrekt ist.
Bitte schaut euch mal das Demo und den Versuch an, es wäre schön ein paar hilfreiche Kommentare zu bekommen
ob dies ein sinnvoller Weg ist, oder ob man besser davon die Finger lassen sollte.
Unter
VCL benutze ich DataSets, aber unter FMX mit BindSourceDB ist irgendwie eine Sackgasse, und ich musste
bisher meine Datenschnittstelle von Hand bauen.
Ich möchte einfach einen Prototyp als Ersatz für LiveBindings hinbekommen, mit dem man solche Settings vereinfachen kann.
Der nächste Schritte wäre darüber eine rudimentäre Anbindung an DataSets zu bekommen.
Liebend gerne würde ich auch zu LiveBindings gehen, aber ich muss dann verlässliche Ergebnisse bekommen und
das UI muss sauber funktionieren.
Vielleicht gibt es in die Richtung ja auch Hinweise.
Das Ganze ist nur ein Test und Ergebnis einiger Versuche, also bitte erschlagt mich nicht gleich wenn
hier und da etwas nicht ganz Korrekt ist.
Rollo