Entwickler-Ecke

Grafische Benutzeroberflächen (VCL & FireMonkey) - OnDrawItem in einer Unit für TListBox einer anderen Unit


galagher - So 22.07.18 20:42
Titel: OnDrawItem in einer Unit für TListBox einer anderen Unit
Hallo!

Ich habe eine Unit (keine Komponente), die ein OnDrawItem für eine TListBox bereitstellt. Ich möchte nun auf ein TForm eine ganz normale TListBox setzen, welche statt ihres eigenen OnDrawItem's jenes der Unit benutzt.

Geht das überhaupt, und wenn ja, wie?

//Edit:
Um das zu präzisieren: Ich möchte in der Unit angeben, dass eine TListBox, die ich als "var" an die Unit übergebe, das OnDrawItem der Unit benutzt.
Pseudocode:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
Unit MyUnit;

procedure MyOnDrawItem //Für eine beliebige TListBox einer anderen Unit
procedure MachWas(var aListBox: TListBox);
//...
procedure MachWas(var aListBox: TListBox);
begin
  aListBox.OnDrawItem := MyOnDrawItem;
end;


Wenn ich nun in Unit1 in der uses-Klausel MyUnit hinzufüge und auf TForm1 eine ListBox draufpacke, soll diese ListBox das OnDrawItem von MyUnit nutzen!


Moderiert von user profile iconNarses: Topic aus Delphi Language (Object-Pascal) / CLX verschoben am So 22.07.2018 um 22:04


jaenicke - So 22.07.18 22:49

Ich sehe das Problem nicht. Klappt das nicht? Meinst du den Unterschied zwischen Prozedur und Methode?

Hier mal ein vollständiges Beispiel:

ListBoxDrawer.pas
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
unit ListBoxDrawer;

interface

uses
  Vcl.StdCtrls, Vcl.Controls, System.Types, Vcl.Graphics, System.SysUtils;

type
  TListBoxDrawer = class
  private
    class var FInstance: TListBoxDrawer;
    procedure DoOnDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
  public
    class constructor Create;
    class destructor Destroy;
    class procedure RegisterListBox(const ATarget: TListBox);
    class procedure UnregisterListBox(const ATarget: TListBox);
  end;


implementation

{ TListBoxDrawer }

class constructor TListBoxDrawer.Create;
begin
  FInstance := TListBoxDrawer.Create;
end;

class destructor TListBoxDrawer.Destroy;
begin
  FInstance.Free;
end;

procedure TListBoxDrawer.DoOnDrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  ListBoxCanvas: TCanvas;
begin
  ListBoxCanvas := (Control as TListBox).Canvas;
  ListBoxCanvas.FillRect(Rect);
  ListBoxCanvas.TextOut(Rect.Left + 2, Rect.Top, 'Test ' + IntToStr(Index));
end;

class procedure TListBoxDrawer.RegisterListBox(const ATarget: TListBox);
begin
  ATarget.OnDrawItem := FInstance.DoOnDrawItem;
end;

class procedure TListBoxDrawer.UnregisterListBox(const ATarget: TListBox);
begin
  ATarget.OnDrawItem := nil;
end;

end.

Und im OnCreate des Formulars z.B.:

Delphi-Quelltext
1:
2:
3:
4:
procedure TFormTest.FormCreate(Sender: TObject);
begin
  TListBoxDrawer.RegisterListBox(ListBox1);
end;


Nebenbei:
Im Profil steht bei dir die 10.1 Starter Edition. Falls das noch aktuell ist, würde ich dir wärmstens die aktuelle 10.2 Community Edition empfehlen, die nicht so eingeschränkt wie die Starter ist. ;-)


galagher - Mo 23.07.18 10:57

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Ich sehe das Problem nicht. Klappt das nicht?
Nein, das ist ja nur Pseudocode! War nur als Beispiel gedacht!

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Hier mal ein vollständiges Beispiel:
Und im OnCreate des Formulars z.B.:

Delphi-Quelltext
1:
2:
3:
4:
procedure TFormTest.FormCreate(Sender: TObject);
begin
  TListBoxDrawer.RegisterListBox(ListBox1);
end;

Da kann ich ja genau so gut ins OnDrawItem der TListBox den Aufruf einer OnDrawItem-Routine der anderen Unit schreiben!

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Nebenbei:
Im Profil steht bei dir die 10.1 Starter Edition. Falls das noch aktuell ist, würde ich dir wärmstens die aktuelle 10.2 Community Edition empfehlen, die nicht so eingeschränkt wie die Starter ist. ;-)
Darauf komme ich in einem eigenen Thread zurück!


jaenicke - Mo 23.07.18 12:40

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
Da kann ich ja genau so gut ins OnDrawItem der TListBox den Aufruf einer OnDrawItem-Routine der anderen Unit schreiben!
Dann müsstest du aber die Instanz aus der anderen Unit veröffentlichen. Globale Variablen wiederum sind aber keine gute Idee, so dass du dann am sinnvollsten die Referenz in der aufrufenden Klasse erzeugst. Dadurch hast du dort aber mehr Overhead.

Gehen tut es aber natürlich.


galagher - Di 24.07.18 21:31

Ich möchte den Aufruf TListBoxDrawer.RegisterListBox in einer separaten Unit unterbringen, die ich immer wieder verwenden kann: Ich füge in einem Projekt einfach den Namen meiner Unit hinzu, und schon funktioniert alles, ich muss dann TListBoxDrawer.RegisterListBox nicht jedesmal extra aufrufen.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
procedure [...](var aListBox: TListBox);
begin
  TListBoxDrawer.RegisterListBox(aListBox);
  try
    //Hier folgt der Code, der Items in aListBox einliest
    //...
  finally
    TListBoxDrawer.UnRegisterListBox(aListBox);  //<- Hier wird TListBoxDrawer.DoOnDrawItem deaktiviert
  end;
end;

Meine Frage: Wenn ich TListBoxDrawer.UnRegisterListBox(aListBox); einfach weglasse, funktioniert alles. Aber produziere ich dann ein Speicherleck?


jaenicke - Mi 25.07.18 18:58

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
Meine Frage: Wenn ich TListBoxDrawer.UnRegisterListBox(aListBox); einfach weglasse, funktioniert alles. Aber produziere ich dann ein Speicherleck?
Nein, das ist kein Speicherleck. Wichtig ist nur, dass das Event nicht mehr aufgerufen wird, wenn die behandelnde Klasse nicht mehr existiert. Das sollte der Fall sein. Sauberer finde ich allerdings generell bei solchen globalen Klassen, wenn man sie auch immer sauber wieder deinitialisiert, egal was sie machen. Einfach, damit man sich nicht darauf verlassen muss, dass das an genau der Stelle nicht nötig ist. Was, wenn diese globale Klasse noch mehr Funktionen bekäme, die eine Deinitialisierung nötig machen würden?

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:

Delphi-Quelltext
1:
procedure [...](var aListBox: TListBox);                    
Das var irritiert dabei vollkommen. Möchtest du in der anderen Prozedur wirklich eine andere Listbox Instanz dort hineinlegen und an den Aufrufer übergeben? :shock:


galagher - Mi 25.07.18 21:57

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:

Delphi-Quelltext
1:
procedure [...](var aListBox: TListBox);                    
Das var irritiert dabei vollkommen. Möchtest du in der anderen Prozedur wirklich eine andere Listbox Instanz dort hineinlegen und an den Aufrufer übergeben? :shock:

Das ist noch ein Relikt von vorherigen Versuchen, ich denke, const ist besser. Oder?

Der grundsätzliche Gedanke ist, ich möchte in meinem Projekt keinen Aufruf von RegisterListBox und UnregisterListBox, das soll alles automatisch gemacht werden.
Eine Unit macht das RegisterListBox und befüllt dann die ListBox, welche gleich alles ganz automatisch wie gewünscht zeichnet.
In meinem aktuellen und weiteren Projekten brauche ich dann nichts weiter zu machen, als in der uses-Klausel den Unitnamen einzufügen.
Nur: Wo setze ich nun das UnregisterListBox? Gleich anschliessend, ist die Darstellung im DoOnDrawItem klarerweise weg. Im finalization der Unit? Geht nicht, da kann ich ja die ListBox nicht als Parameter übergeben. Gar nicht? Ja, würde gehen, wie ich jetzt weiss.


galagher - Mi 25.07.18 22:42

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
Nur: Wo setze ich nun das UnregisterListBox?

Naja, ich habe immerhin eine Möglichkeit gefunden:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
unit ListBoxDrawer;
//...
type
  TListBoxDrawer = class
  private
    class var aLBx: TListBox;  //<- Hier eine private Variable
//...

Dann weise ich aLBx die ListBox zu und kann sie im finalization verwenden:

Delphi-Quelltext
1:
2:
3:
finalization
  if Assigned(TListBoxDrawer.aLBx) then
    TListBoxDrawer.UnregisterListBox(TListBoxDrawer.aLBx);

Zwar nicht so elegant, klappt aber.


jaenicke - Do 26.07.18 06:40

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
Der grundsätzliche Gedanke ist, ich möchte in meinem Projekt keinen Aufruf von RegisterListBox und UnregisterListBox, das soll alles automatisch gemacht werden.
Du könntest (z.B. mit Hilfe der Delphi Detours Library) einen Hook auf die Methode TCustomListBox.DrawItem setzen und somit deine Änderung direkt in jede TListBox deiner Anwendung injizieren.

Vielleicht ließe sich ja auch mit den VCL Styles etwas erreichen, je nachdem was du machen möchtest?


galagher - Do 26.07.18 19:59

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Du könntest (z.B. mit Hilfe der Delphi Detours Library) einen Hook auf die Methode TCustomListBox.DrawItem setzen und somit deine Änderung direkt in jede TListBox deiner Anwendung injizieren.
Was auch immer du damit meinst :nixweiss: , es geht lediglich darum, aus den Items eine voranstehende "Zahl" optisch zu entfernen. Die Zahl soll vorhanden bleiben, aber nicht sichtbar sein.

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Vielleicht ließe sich ja auch mit den VCL Styles etwas erreichen, je nachdem was du machen möchtest?
Meinst du "Benutzerdefinierte Stile"? Brauche ich hier auch nicht!


jaenicke - Do 26.07.18 23:18

user profile icongalagher hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Du könntest (z.B. mit Hilfe der Delphi Detours Library) einen Hook auf die Methode TCustomListBox.DrawItem setzen und somit deine Änderung direkt in jede TListBox deiner Anwendung injizieren.
Was auch immer du damit meinst :nixweiss: , es geht lediglich darum, aus den Items eine voranstehende "Zahl" optisch zu entfernen. Die Zahl soll vorhanden bleiben, aber nicht sichtbar sein.
Das könnte man damit machen. Aber das hört sich eher so an als ob du die Listbox als Datenspeicher missbrauchst.

Wenn das der Fall sein sollte: Dafür wäre es viel einfacher die Datenhaltung von der Anzeige schlicht zu trennen... dann sparst du dir die Verrenkungen.


galagher - Fr 27.07.18 08:05

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Aber das hört sich eher so an als ob du die Listbox als Datenspeicher missbrauchst.
Nein, die Daten werden aus einer Datei in eine TStringList eingelesen und diese benutze ich, um darin nach Text(teilen) zu suchen. Die Funde werden dann in der ListBox dargestellt, aber ohne die Zahl.
Das alles klappt mittlerweile perfekt!