neuer Image-Typ


HarryXVI

Erfahrenes Mitglied
#1
Ich habe einen neuen Typen deklariert:
Code:
type TSpielfigur = class(TImage)
     private
     pos: array[1..819,1..396] of integer;
     end;
Jetzt gibt es zwei Probleme:
1. pos ist ein zweidimensionales Array, ich möchte einer Speicherposition zwei Werte gleichzeitig zuweisen (z.B. pos[5,8] := 32,36 o.ä.). Wie könnte man das machen?

2.
Code:
Figur := TSpielfigur.Create(Spielfeld);
Figur.Picture.Icon.LoadFromFile('C:\Delphi-Programme\Icons\Smiley2.ico');
Figur.Picture.Icon.Height := 33;
Figur.Picture.Icon.Width := 33;
Figur.Left := 32; Figur.Top := 176;
Figur.Visible := true;
Ich erstelle eine Komponente vom Typ TSpielfigur und lade ein Bild vom Typ TIcon. Höhe, Breite und Position auf dem Formular werden festgelegt. Trotzdem wird das Icon auf dem Formular nicht sichtbar. Was ist da los und wie kann man es beheben?
 
#2
Ich habe einen neuen Typen deklariert:
1. pos ist ein zweidimensionales Array, ich möchte einer Speicherposition zwei Werte gleichzeitig zuweisen (z.B. pos[5,8] := 32,36 o.ä.). Wie könnte man das machen?
Gar nicht. Denn dein Array ist ein Array (wenn auch zweidimensional) von Integern und ein Integer kann per definitionem nur einen (Ganzzahl-) Wert aufnehmen. Ich habe keine Ahnung, was du mit deinem Array bezweckst, aber eventuell kann dir TPoint bei dem helfen, was du vorhast.

Ich erstelle eine Komponente vom Typ TSpielfigur und lade ein Bild vom Typ TIcon. Höhe, Breite und Position auf dem Formular werden festgelegt. Trotzdem wird das Icon auf dem Formular nicht sichtbar. Was ist da los und wie kann man es beheben?
Wow. Also mit TIcon innerhalb von TPictures (innerhalb von Timages) habe ich noch nicht gearbeitet, aber ich hatte schon ziemlich oft den Fall, daß das "Image" nicht angezeigt wird. Der Grund lag zu 99% darin, daß die TBitmap des TPicture des TImage entweder nil war oder Width, Height auf 0,0 standen.
Von daher habe ich mir die direkte Arbeit mit TBitmaps angewöhnt und verwende TImages normalerweise nur noch als Viewports (insbesondere für schnelle Skalierungen). Seitdem hatte ich keinen Ärger mehr mit nicht angezeigten Bildern.

Als schnelle Lösung kannst du mal folgendes probieren:
Code:
Figur.Picture.Bitmap.Width := Figur.Picture.Icon.Width;
Figur.Picture.Bitmap.Height := Figur.Picture.Icon.Height;
Figur.Picture.Bitmap.Canvas.Draw(0, 0, Figur.Picture.Icon);
Ist allerdings ungetestet. Bin nicht mehr am Arbeitsrechner. ;)
 

HarryXVI

Erfahrenes Mitglied
#3
Also, ich habe jetzt auf Bitmap umgestellt (auch das Bildchen ist eine Bitmap).

Code:
Figur.Picture.Bitmap.LoadFromFile('C:\Delphi-Programme\Icons\Smiley2.bmp');
Figur.Picture.Bitmap.Height := 33;
Figur.Picture.Bitmap.Width := 33;
if Figur.Picture.Bitmap.Empty = true 
   then ShowMessage('Bitmap leer') 
else  Figur.Picture.Bitmap.Canvas.Draw(32, 176, Figur.Picture.Bitmap);
Laut if-Abfrage ist die Bitmap nicht leer, aber es wird trotzdem nichts gezeichnet. Ich dachte erst, es könnte an der Zeichenfläche liegen, deshalb habe ich das Formular die Bitmap zeichnen lassen:

Code:
Spielfeld.Canvas.Draw(32,176,Figur.Picture.Bitmap);
Jetzt sagt der Compiler, das wäre eine Adressverletzung. Warum?
 
#4
Jetzt sagt der Compiler, das wäre eine Adressverletzung. Warum?
Keine Ahnung.
Zeichnest du im OnCreate?
Referenzierst du ein self im OnCreate?
Ist Spielfeld nicht nur definiert sondern auch instantiiert?
Ist Figur nicht nur definiert sondern auch instantiiert?
Ist Figur.Picture nicht nur definiert sondern auch instantiiert?
Ist Figur.Picture.Bitmap nicht nur definiert sondern auch instantiiert?

All dies kann dir der Debugger beantworten.
 

HarryXVI

Erfahrenes Mitglied
#5
habe jetzt den Typ TSpielfigur von TBitmap abgeleitet
Code:
 type TSpielfigur = class(TBitmap)
     private
      pos: TPoint;
     end;
Dann habe ich eine Prozedur geschrieben, die das Bild erstellen soll (damit FormCreate nicht zu voll wird).
Code:
procedure TSpielfeld.bild_einrichtung;
begin
Figur := TSpielfigur.Create;
Figur.pos.X := 32;
Figur.pos.Y := 176;
Figur.LoadFromFile('C:\Delphi-Programme\Icons\Smiley2.bmp');
Figur.Height := 33;
Figur.Width := 33;
Figur.Canvas.Draw(32, 176, Figur);
end;
Sieht nach Augenwischerei aus, ist es vielleicht auch, aber selbst mit Spielfigur als direkte Bitmap funktioniert es nicht. CSANecromancer, ich weiß, du gibst ungern Sourcecodes, aber wie machst du es generell, wenn du ein Bild von dem Programm erstellen und anzeigen lassen willst?
 
#6
ich weiß, du gibst ungern Sourcecodes
Das liegt in 9 von 10 Fällen daran, daß derjenige, der nach dem Source fragt, mit an Sicherheit grenzender Wahrscheinlichkeit nichts damit anfangen kann, weil eine 1-Zeilen-Lösung für komplexe Probleme erwartet wird.

aber wie machst du es generell, wenn du ein Bild von dem Programm erstellen und anzeigen lassen willst?
Ich denke, an der Stelle sollte ich mal ein paar Takte darüber ablassen, wie ich die graphischen Komponenten von Delphi betrachte und kennen gelernt habe.

Der unterste Level ist der TCanvas. So ziemlich jede visuelle und/oder graphische Komponente hat einen TCanvas. Egal ob TImage, TPicture, TBitmap, TShape, TPaintBox, TForm, TButton etc.pp., überall gibt es einen TCanvas, mittels dem gezeichnet werden kann.
Einige Leute zeichnen direkt auf den TCanvas des Hauptformulars, doch ich war noch nie ein Fan davon. Denn zwischen Zeichnen und Anzeigen liegt ein deutlicher Unterschied, wie du ja auch schon festgestellt hast.

Ok, wozu sind dann die anderen graphischen Komponenten gut?

TBitmap ermöglicht die einfachsten Lade- und Speicheroptionen für die einfachste Bildart überhaupt, eine unkomprimierte Bitmap. D.h. jeder Pixel der Bitmap wird im Idealfall (24 Bit Farbtiefe) durch 3 Byte repräsentiert: Rot, Grün, Blau. Klar gibt's da noch Feinheiten, aber um die nachzulesen gibt's ja die Onlinehilfe.
TPicture kapselt die TBitmap und ermöglicht erweiterte Lade- und Speicheroptionen. Schau dir auch das mal in der Onlinehilfe an, da ist sehr schön beschrieben, welche Eigenschaften und Methoden TPicture selbst mitbringt und welche von TBitmap oder TCanvas stammen.
TImage wiederum kapselt ein TPicture und bietet v.a. folgende Möglichkeiten: Bildproportionen verankern, Bild zentriert in einem Bereich anzeigen, Bild skalieren (und das sehr schnell). Den Rest von TImage habe ich noch nicht so intensiv verwendet.

Am Rande noch: Mit TShape lassen sich einfachste geometrische Figuren darstellen und TPaintBox soll einfach nur dafür sorgen, daß nicht unmittelbar auf den Canvas der TForm gepinselt wird sondern auf einen festgelegten Bereich - nämlich die TPaintBox.

Dann werfen wir mal einen genaueren Blick auf TImage. Timage beinhaltet TPicture - und einen TCanvas. Jup, das TImage hat einen eigenen(!) TCanvas. Und auch das TPicture hat einen eigenen TCanvas. Und die TBitmap im TPicture im TImage hat auch einen eigenen TCanvas! Was soll denn der Blödsinn?

TImage
|
+---> TCanvas
|
+--->TPicture
|
+---> TCanvas
|
+---> TBitmap
|
+---> TCanvas

Welches ist jetzt der richtige?
Du kannst dir das grob vereinfacht so vorstellen:
Den TCanvas von TPicture kannst du vergessen. Der hat keinen echten Wert. Der TCanvas von TBitmap ist das, was an TImage zur Darstellung weitergereicht wird. Bevor TImage aber das Bild in TBitmap anzeigt, wird es noch umgewandelt (skaliert, zentriert, proportioniert usw., was TImage halt so gemäß seiner Einstellungen mit dem Bild macht). Wenn TImage dann endlich fertig ist mit dem Verwursten der Bilddaten, dann werden die an den TCanvas von TImage durchgereicht und auf den Bildschirm gezeichnet.

Klar soweit? (im Ton von Cpt. Jack Sparrow)

Nachdem ich mich mal durch diesen Wust durchgewühlt hatte, habe ich zwei Dinge beschlossen:
1. Soweit und wann immer möglich, bearbeite ich Bilddaten als TBitmap.
2. Ich zeichne niemals direkt auf einen TForm.Canvas, sondern verwende als Anzeigefläche immer ein TImage. Denn mit diesem TImage kann ich die Anzeige noch ein wenig steuern und sehr viel bequemer erweitern als wenn alles im TForm in einem Haufen vorliegt.

Damit ist der allererste Teile deiner Frage beantwortet.

Den Smily würde ich also (für's Erste) als Bitmap speichern.
Auf das Formular würde ich ein TImage knallen. Dieses heisst bei mir normalerweise "Image". Sehr phantasielos, ich weiß. Aber da ich in der Regel nur ein einziges Image verwende, ist es dann doch eindeutig.

Zum Laden und Anzeigen des Smilys würde ich dann so vorgehen:
Code:
prodecure ShowSmily;
var
  bmSmily: TBitmap;
begin
  bmSmily := TBitmap.Create;
  bmSmily.LoadFromFile('smily.bmp');
  // Nach dem Laden ist kein explizites Zuweisen von Width und Height erforderlich.
  // Diese werden implizit durch das Laden des bmp-Files gesetzt

  // Ich sitze jetzt gerade nicht am Compiler, deswegen kann ich nicht
  // ausprobieren, ob die TBitmap unten in TImage defaultmässig schon
  // existiert. Daher diese "Nummer Sicher"
  if Image.Picture = nil then
    Image.Picture := TPicture.Create;
  if Image.Picture.Bitmap := nil then
    Image.Picture.Bitmap := TBitmap.Create;

  // Ok, furzegal, was ist, ich habe jetzt mal meine Bitmap mit dem Smily
  // und es sind alle Ebenen von TImage eingerichtet. Auch wenn die Bitmap
  // von TImage nichts anzeigt, weil bislang weder eine Bitmap geladen noch
  // die Dimensionen der Bitmap eingerichtet worden sind.

  Image.Picture.Bitmap.Assign(bmSmily);
  // Tadaaa! Jetzt wäre schon mal der Smily in der Bitmap des Image.
  // Ich habe aber schlechte Erfahrungen mit Assign gemacht, vor allem, 
  // wenn das Ursprungsobjekt, wie in diesem Falle die bmSmily am
  // Prozedurende wieder aufgeräumt wird. 

  // Aus diesem Grund bevorzuge ich das explizite Kopieren der Bilddaten
  // und deren explizite Zuweisung.
  Image.Picture.Bitmap.Width := bmSmily.Width;
  Image.Picture.Bitmap.Height := bmSmily.Height;
  Image.Picture.Bitmap.Canvas.CopyRect(Rect(0, 0, bmSmily.Width - 1, bmSmily.Height - 1), bmSmily, Rect(0, 0, bmSmily.Width - 1, bmSmily.Height - 1));
  
  // Da jetzt die Bilddaten von bmSmily ganz ausdrücklich kopiert und damit 
  // vervielfältigt wurden, sind die Ursprungsdaten nicht mehr notwendig.
  FreeAndNil(bmSmily);

  // Kann aber immer noch sein, daß der Smily nicht korrekt angezeigt wird.
  // Das ist u.a. davon abhängig, was alles im Hintergrund auf dem Rechner
  // läuft, welche Prozeßpriorität dieses Programm hier hat usw.
  // Da mir das aber alles zu blöd ist, sage ich dem Programm mit Brachialgewalt
  // "Hallo, da wurde ein neues Bild geladen, zeige das gefälligst an!":
  Image.Invalidate;

  // Oder alternativ:
  Image.Refresh;
end;
Mit dem Source kannst du jetzt eine beliebige Bitmap laden und anzeigen. Bewegen ist aber wieder eine andere Baustelle.

Wenn's um Animation geht, kommen mir sehr viele Leute immer gleich mit OpenGL, DirectX und wasweissichnichtalles an. Und erklären mich dann für irre, wenn ich behaupte, daß sich am Prinzip der Animation seit 1906 nichts Grundlegendes geändert hat. :eek:
Das Prinzip ist bis heute das gleich:
- Hintergrund zeichnen
- Smily auf den Hintergrund zeichnen
- Hintergrund neu zeichnen
- Smily versetzt auf den Hintergrund zeichnen
- Hintergrund neu zeichnen
- Smily noch etwas weiter versetzt auf den Hintergrund zeichnen
usw.usf.
Und das Ganze richtig schnell. Je schneller es geht, desto flüssiger erscheint die Animation (genau das drückt der Begriff "frames per second" aus, der aber wiederum so ziemlich jedem Gamer bekannt ist...)

Mist, daß der Smily im Codestück weiter oben direkt in den Hintergrund (nämlich die TBitmap des TPicture des TImage) gezeichnet wurde.
Aus diesem Grunde arbeite ich normal mit mehreren Bitmaps: Eine Bitmap für den Hintergrund, eine Bitmap für den Smily, eine Bitmap zum Zusammenführen des Ganzen. Und dann wird das Zusammengeführte in die Bitmap des TImage geleitet und fertig ist der Käse.

Hier ein einfacher, unkommentierter Source, der dieses Geschehen veranschaulichen soll:
Code:
procedure Demo;
var
  bmBackground: TBitmap;
  bmSmily: TBitmap;
  bmBlitter: TBitmap;
  i: Integer;
begin
  if Image.Picture = nil then
    Image.Picture := TPicture.Create;
  if Image.Picture.Bitmap = nil then
    Image.Picture.Bitmap := TBitmap.Create;
  Image.Picture.Bitmap.Width := 800;
  Image.Picture.Bitmap.Height := 600;
  Image.Width := 800;
  Image.Height := 800;

  bmBackground := TBitmap.Create;
  bmBackground.LoadFromFile('background.bmp');

  bmSmily := TBitmap.Create;
  bmSmily.LoadFromFile('smily.bmp');

  bmBlitter := TBitmap.Create;
  bmBlitter.Width := Image.Picture.Bitmap.Width;
  bmBlitter.Height := Image.Picture.Bitmap.Height;

  for i := 0 to 700 do
  begin
    bmBlitter.Canvas.CopyRect(Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1), bmBackground, Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1);
    bmBlitter.Canvas.CopyRect(Rect(i, 50, i + bmSmily.Width, 50 + bmSmily.Height), bmSmily, Rect(0, 0, bmSmily.Width - 1, bmSmily.Height - 1));
    Image.Picture.Bitmap.Canvas.CopyRect(Rect(0, 0, bmBlitter.Width - 1, bmBlitter.Height - 1), bmBlitter, Rect(0, 0, bmBlitter.Width - 1, bmBlitter.Height - 1));
    Image.Refresh;
  end;

  FreeAndNil(bmBackground);
  FreeAndNil(bmSmily);
  FreeAndNil(bmBlitter);
end;
Wie im Sourcekommentar weiter oben erwähnt, habe ich gerade keinen Compiler zur Hand zum Austesten, aber ich bin mit ziemlich sicher, daß die Sources funktionieren oder wenn dann nur geringe Fehler drin sind (z.B. bin ich mir mit den -1 bei den Dimensionierungen der CopyRects gerade nicht 100% sicher).

Ich hoffe, das hilft jetzt etwas weiter. :)
 

HarryXVI

Erfahrenes Mitglied
#7
CSANecromancer hat gesagt.:
bmBlitter.Canvas.CopyRect(Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1), bmBackground, Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1);
da kannst du als zweiten Parameter, nicht die Bitmap nehmen, da CopyRect hier einen Canvas verlangt. Aber mit Assign klappts bei mir, deshalb ist das jetzt nicht das Problem.

Dein Prinzip mit der Bilderstellung zur Laufzeit habe ich verstanden, kann es auch gut anwenden. Aber ich verstehe nicht, wie das mit der Animation klappen soll. Könntest du deinen Sourcecode da vielleicht ein wenig kommentieren? Dann wäre ich einen Schritt weiter und könnte es selbstständig anwenden.
 
#8
da kannst du als zweiten Parameter, nicht die Bitmap nehmen, da CopyRect hier einen Canvas verlangt.
Ist ja kein Problem, dann gibst du halt statt bmBackground bmBackground.Canvas an. ;)

Aber ich verstehe nicht, wie das mit der Animation klappen soll. Könntest du deinen Sourcecode da vielleicht ein wenig kommentieren?
Ausnahmsweise, weil ich mir vorstellen kann, daß das alles etwas schnell war.

Code:
procedure Demo;
var
  bmBackground: TBitmap;
  bmSmily: TBitmap;
  bmBlitter: TBitmap;
  i: Integer;
begin
  // Zu Beginn des ganzen Gekrempels gehe ich mal davon aus, daß Image
  // noch überhaupt nicht richtig eingerichtet ist. Daher hier wieder die Instantierung
  // aller Ebenen von Image.
  if Image.Picture = nil then
    Image.Picture := TPicture.Create;
  if Image.Picture.Bitmap = nil then
    Image.Picture.Bitmap := TBitmap.Create;
  Image.Picture.Bitmap.Width := 800;
  Image.Picture.Bitmap.Height := 600;

  // Bloß weil die im Hintergrund liegende Bitmap von Image mit der Größe
  // 800x600 eingerichtet worden ist, heißt das noch lange nicht, daß die sichtbare
  // Fläche von Image auch schon ein bestimmtes Ausmaß hat.
  // Ich will aber nicht nur eine 800x600 Bitmap anzeigen, ich will sie auch im
  // Originalformat 800x600 anzeigen, weswegen das Image eine entsprechende
  // Größe braucht.
  Image.Width := 800;
  Image.Height := 800;

  // Jetzt lade ich mir ein x-beliebiges Hintergrundbild...
  bmBackground := TBitmap.Create;
  bmBackground.LoadFromFile('background.bmp');

  // ...und natürlich auch den Smily
  bmSmily := TBitmap.Create;
  bmSmily.LoadFromFile('smily.bmp');

  // Der Blitter ist das Bild (genauer: Die Bitmap), wo ich Hintergrund
  // und Smily miteinander verschmelze, um dann das Ergebnis von Image
  // anzeigen zu lassen. Logisch, daß der Blitter damit die gleiche Größe
  // braucht wie die Bitmap des Image.
  bmBlitter := TBitmap.Create;
  bmBlitter.Width := Image.Picture.Bitmap.Width;
  bmBlitter.Height := Image.Picture.Bitmap.Height;

  // Es sollen 700 "Animations"-Schritte durchlaufen werden, genauer formuliert
  // soll der Smily von links nach rechts 700 Pixel weit verschoben werden.
  for i := 0 to 700 do
  begin
    // Als allererstes muß ein sauberer Hintergrund aufgebaut werden.
    // Nochmal: Im Blitter wird alles zusammen gebaut. Deswegen wird hier
    // der Hintergrund auf die Bitmap des Blitters kopiert.
    bmBlitter.Canvas.CopyRect(Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1), bmBackground.Canvas, Rect(0, 0, bmBackground.Width - 1, bmBackground.Height - 1);

     // Nur ein leerer Hintergrund ist zu langweilig, deswegen wird hier noch der
     // Smily auf den Blitter kopiert. Wenn die komischen Koordinaten und das i
    // völlig unbekannt und nicht nachvollziehbar erscheinen:
    // Onlinehilfe, Suchbegriff CopyRect
    bmBlitter.Canvas.CopyRect(Rect(i, 50, i + bmSmily.Width, 50 + bmSmily.Height), bmSmily.Canvas, Rect(0, 0, bmSmily.Width - 1, bmSmily.Height - 1));

    // Der Hintergrund ist im Blitter gelandet, der Smily ist im Blitter gelandet,
    // das Bild ist aufgebaut und kann angezeigt werden. Dazu wird einfach der 
    // Blitter in die Bitmap des Image kopiert.
    Image.Picture.Bitmap.Canvas.CopyRect(Rect(0, 0, bmBlitter.Width - 1, bmBlitter.Height - 1), bmBlitter.Canvas, Rect(0, 0, bmBlitter.Width - 1, bmBlitter.Height - 1));

    // Abschließend muß dem Programm noch mitgeteilt werden, daß sich das
    // Image geändert hat.
    Image.Refresh;
  end;

  // Aufräumen der Resourcen nach Gebrauch nicht vergessen
  FreeAndNil(bmBackground);
  FreeAndNil(bmSmily);
  FreeAndNil(bmBlitter);
end;
 
#9
aber wie machst du es generell, wenn du ein Bild von dem Programm erstellen und anzeigen lassen willst?
Wie ich mit Bildern generell umgehe, habe ich dir ja bereits geschrieben. Aber ich fand das, was du da machen willst, ganz witzig und habe mir deswegen jetzt mal was gebastelt (was es sicher schon zigtausendfach und viel performanter und eleganter als fertige Komponenten gibt): Eine eigene Sprite-Klasse.

Und da es auch um Sourcecodes geht, hier einfach mal die Sources sowohl des Demo-Programms als auch des Sprites.

Demoprogramm, Hauptformular:
Name: Main
KeyPreview: true

Darauf gehört ein TImage mit folgenden Eigenschaften platziert:
Name: Main
Align: alClient
Picture: (irgendein Bild laden. Ich habe ein Wallpaper geladen).

Ebenfalls auf das Formular gehört noch ein TButton mit folgenden Eigenschaften:
Name: btDemo
Caption: Demo starten

Wenn du es dir noch etwas einfacher machen willst, dann richtest du schon mal folgende Ereignismethoden ein:
Main.OnCreate
Main.OnClose
Main.OnKeyDown
Main.Demo_OnClick (die Buttonmethode)

Hier der Source der Main:
Code:
unit Dialogs.Main;

interface uses
  Classes,
  Controls,
  Dialogs,
  ExtCtrls,
  Forms,
  Graphics,
  Jpeg,
  Messages,
  StdCtrls,
  SysUtils,
  Variants,
  Windows,

  Data.Sprite;


type
  TMain = class(TForm)
    btDemo: TButton;
    Image: TImage;
    procedure btDemoClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

  private
    Sprite: TSprite;

  end;

var
  Main: TMain;

implementation

{$R *.dfm}

procedure TMain.FormCreate(Sender: TObject);
begin
  // Damit die Animation etwas flackerfreier läuft
  self.DoubleBuffered := true;

  Sprite := TSprite.Create;
end;


procedure TMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  FreeAndNil(Sprite);
end;


procedure TMain.btDemoClick(Sender: TObject);
begin
  Sprite.Load('sprite.bmp');
  Sprite.Width := 32;
  Sprite.Height := 32;
  Sprite.Position.X := Round(ClientWidth / 2);
  Sprite.Position.Y := Round(ClientHeight / 2);

  // Das Sprite wird direkt auf dem Canvas der Form angezeigt
  Sprite.Canvas := self.Canvas;
  Sprite.SetSpeed(100);

  // Die einzelnen Frames des Sprites festlegen
  Sprite.SetFrame(0, 0, 0);
  Sprite.SetFrame(1, 32, 0);
  Sprite.SetFrame(2, 64, 0);
  Sprite.SetFrame(3, 96, 0);

  // Transparenz ist Magenta (so definiert durch 0/0 der Spritebitmap)
  Sprite.SetTransparency(Point(0, 0));

  Sprite.Show;
end;


procedure TMain.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  // Q
  if Key =  81 then
  begin
    Sprite.Left(3);
    Key := 0;
  end;

  // W
  if Key =  87 then
  begin
    Sprite.Right(3);
    Key := 0;
  end;

  // A
  if Key =  65 then
  begin
    Sprite.Up(3);
    Key := 0;
  end;

  // Y
  if Key =  89 then
  begin
    Sprite.Down(3);
    Key := 0;
  end;

  // Space blendet das Sprite ein und aus.
  if Key =  32 then
  begin
    Key := 0;

    if Sprite.Visible = true then
      Sprite.Hide
    else
      Sprite.Show;
  end;
end;


end.
Und hier der Source der Spriteklasse ("Data.Sprite.pas"):
Code:
{*-----------------------------------------------------------------------------
  Klasse zur Darstellung von Sprites
-----------------------------------------------------------------------------*}
unit Data.Sprite;

interface uses

  Controls,
  ExtCtrls,
  Graphics,
  SysUtils,
  Types,
  Windows;


type
{*-----------------------------------------------------------------------------
  Klassendefinition eines Sprites
-----------------------------------------------------------------------------*}
  TSprite = class(TObject)

    private
      m_Bitmap: Graphics.TBitmap;                         /// Bitmap mit allen Frames des Sprites
      m_Background: Graphics.TBitmap;                     /// Buffer für den Hintergrund des Sprites
      m_Mask: Graphics.TBitmap;                           /// Bildmaske für alle Frames zur transparenten Anzeige
      m_Blitter: Graphics.TBitmap;                        /// Buffer zum Zusammenbauen des Sprites
      m_Transparency: TColor;                             /// Transparenzfarbe des Sprites

      m_FramePos: array[0..15] of TPoint;                 /// Positionen der einzelnen Frames in der Spritebitmap
      m_FrameIndex: Byte;                                 /// Index des aktuell angezeigten Frames
      m_FrameMax: Byte;                                   /// Index des maximalen Animationsframes

      m_Timer: TTimer;                                    /// Animationstimer
      m_Ascending: boolean;                               /// Animationsrichtungsflag (aufsteigend, absteigend)

      m_Visible: boolean;                                 /// Sichtbarkeitsflag des Sprites

      m_Width: Byte;                                      /// Breite des Sprites in Pixeln
      m_Height: Byte;                                     /// Höhe des Sprites in Pixeln

      procedure ShowFrame;                                /// Anzeige eines einzelnen Frames
      procedure SpriteTimer(Sender: TObject);             /// Ereignismethode für den Animationstimer
      procedure MoveBy(p_nX: Integer; p_nY: Integer);     /// Bewegen des Sprites

      procedure SetWidth(p_nValue: Byte);                 /// Festlegen der Spritebreite
      procedure SetHeight(p_nValue: Byte);                /// Festlegen der Spritehöhe

    public
      Position: TPoint;                                   /// Position des Sprites auf dem Darstellungs-Canvas
      Canvas: TCanvas;                                    /// Handle auf den Canvas, auf dem das Sprite dargestellt werden soll

      constructor Create;
      destructor Free;

      procedure Load(const p_strFileName: String);
      procedure SetFrame(const p_nIndex: Integer; const p_nX: Integer; const p_nY: Integer);
      procedure SetSpeed(const p_nSpeed: Integer);
      procedure SetTransparency(const p_Point: TPoint);
      procedure Show;
      procedure Hide;
      procedure Left(p_nValue: Integer);
      procedure Right(p_nValue: Integer);
      procedure Up(p_nValue: Integer);
      procedure Down(p_nValue: Integer);

      property Visible: boolean read m_Visible;           /// Lesezugriff auf das Visibility-Flag
      property Width: Byte read m_Width write SetWidth;   /// Angepasster Schreibzugriff auf die Breite
      property Height: Byte read m_Height write SetHeight;/// Angepasster Schreibzugriff auf die Höhe
  end;


implementation


{*-----------------------------------------------------------------------------
  Konstruktor
-----------------------------------------------------------------------------*}
constructor TSprite.Create;
var
  i: Integer;
begin
  // Erstellen des Timers für eine evtl. Spriteanimation
  m_Timer := TTimer.Create(nil);
  m_Timer.Enabled := false;
  m_Timer.OnTimer := self.SpriteTimer;

  // Zyklusreihenfolge für eine Animation
  m_Ascending := true;

  // Startindex des Frames für die Animation
  m_FrameIndex := 0;

  // Anzahl der Spriteanimationsschritte
  m_FrameMax := 0;

  // Erstellen der Spritebitmap
  m_Bitmap := Graphics.TBitmap.Create;
  m_Bitmap.Width := 0;
  m_Bitmap.Height := 0;

  // Erstellen der Maskenbitmap
  m_Mask := Graphics.TBitmap.Create;
  m_Mask.Width := 0;
  m_Mask.Height := 0;

  // Speicher für den Hintergrund, vor dem das Sprite dargestellt wird
  m_BackGround := Graphics.TBitmap.Create;
  m_BackGround.Width := 0;
  m_BackGround.Height := 0;

  // Speicher für den Aufbau des Sprites (v.a. für Transparaenz)
  m_Blitter := Graphics.TBitmap.Create;
  m_Blitter.Width := 0;
  m_Blitter.Height := 0;

  // Startposition des Sprites
  Position := Point(0, 0);

  // Extractionkoordinaten für die einzelnen Frames
  for i := 0 to 15 do
    m_FramePos[i] := Point(-1, -1);

  // Standardmässig ist das Sprite nicht sichtbar
  m_Visible := false;
end;


{*-----------------------------------------------------------------------------
  Destruktor

  Auflösen verwendeter Resourcen
-----------------------------------------------------------------------------*}
destructor TSprite.Free;
begin
  // Deaktivieren und Löschen des Timers
  m_Timer.Enabled := false;
  FreeAndNil(m_Timer);

  // Löschen aller Bitmaps
  FreeAndNil(m_Bitmap);
  FreeAndNil(m_Mask);
  FreeAndNil(m_Background);
  FreeAndNil(m_Blitter);
end;


{*-----------------------------------------------------------------------------
  Laden des Sprites
-----------------------------------------------------------------------------*}
procedure TSprite.Load(const p_strFileName: String);
begin
    if FileExists(p_strFileName) then
      m_Bitmap.LoadFromFile(p_strFileName);
end;


{*-----------------------------------------------------------------------------
  Wenn die Spritebreite festgelegt wird (Property), dann wird auch die
  Blitterbreite entsprechend angepasst.
-----------------------------------------------------------------------------*}
procedure TSprite.SetWidth(p_nValue: Byte);
begin
  if (p_nValue > 0) then
  begin
    m_Width := p_nValue;
    m_Blitter.Width := p_nValue;
  end;
end;


{*-----------------------------------------------------------------------------
  Wenn die Spritehöhe festgelegt wird (Property), dann wird auch die
  Blitterhöhe entsprechend angepasst.
-----------------------------------------------------------------------------*}
procedure TSprite.SetHeight(p_nValue: Byte);
begin
  if (p_nValue > 0) then
  begin
    m_Height := p_nValue;
    m_Blitter.Height := p_nValue;
  end;
end;


{*-----------------------------------------------------------------------------
  Festlegen der Framepositionen in der Spritebitmap
-----------------------------------------------------------------------------*}
procedure TSprite.SetFrame(const p_nIndex: Integer; const p_nX: Integer; const p_nY: Integer);
var
  i: Integer;
begin
  if (p_nIndex >= 0) or (p_nIndex <= 15) then
    // Die Frameposition wird festgelegt
    m_FramePos[p_nIndex] := Point(p_nX, p_nY);

  // Automatische Berechnung des Maximalframes
  m_FrameMax := 0;
  for i := 0 to 15 do
    if m_FramePos[i].X = -1 then
      break;
  m_FrameMax := i - 1;
end;


{*-----------------------------------------------------------------------------
  Animationsgeschiwndigkeit ändern
-----------------------------------------------------------------------------*}
procedure TSprite.SetSpeed(const p_nSpeed: Integer);
begin
  m_Timer.Enabled := false;
  m_Timer.Interval := p_nSpeed;
  if m_Timer.Interval > 0 then
    m_Timer.Enabled := true;
end;


{*-----------------------------------------------------------------------------
  Transparenz einstellen
-----------------------------------------------------------------------------*}
procedure TSprite.SetTransparency(const p_Point: TPoint);
var
  x, y: Integer;
begin
  // Die Transparenzfarbe ist diejenige in der Spritebitmap, auf die mit der
  // Methode gezeigt wird. Alle entsprechend gefärbten Pixel der Spritebitmap
  // gelten daraufhin als transparent.
  m_Transparency := m_Bitmap.Canvas.Pixels[p_Point.X, p_Point.Y];
  m_Mask.Width := m_Bitmap.Width;
  m_Mask.Height := m_Bitmap.Height;

  // Jedes reine Schwarz (RGB 0/0/0) wird durch ein "Fast-Schwarz" (RGB 0, 0, 1)
  // ersetzt und jedes Transparenzpixel durch ein reines Schwarz
  for y := 0 to m_Bitmap.Height do
  begin
    for x := 0 to m_Bitmap.Width do
    begin
      if m_Bitmap.Canvas.Pixels[x, y] = clBlack then
        m_Bitmap.Canvas.Pixels[x, y] := RGB(0, 0, 1);

      if m_Bitmap.Canvas.Pixels[x, y] = m_Transparency then
        m_Bitmap.Canvas.Pixels[x, y] := clBlack;
    end;
  end;

  // Jetzt wird die Maske aufgebaut: Jedes sichtbare Pixel wird schwarz,
  // jedes transparente Pixel wird Weiß.
  for y := 0 to m_Bitmap.Height do
  begin
    for x := 0 to m_Bitmap.Width do
    begin
      if m_Bitmap.Canvas.Pixels[x, y] <> clBlack then
        m_Mask.Canvas.Pixels[x, y] := clBlack
      else
        m_Mask.Canvas.Pixels[x, y] := clWhite;
    end;
  end;
end;


{*-----------------------------------------------------------------------------
  Spriteanimation über den Timer
-----------------------------------------------------------------------------*}
procedure TSprite.SpriteTimer(Sender: TObject);
begin
  if m_FrameMax > 0 then
  begin
    // Aufsteigende Animationsfolge
    if m_Ascending then
    begin
      if m_FrameIndex < m_FrameMax then
        Inc(m_FrameIndex)
      else
        m_Ascending := false;
    end
    // Abteigende Animationsfolge
    else
    begin
      if m_FrameIndex > 0 then
        Dec(m_FrameIndex)
      else
        m_Ascending := true;
    end;
  end;
  
  ShowFrame;
end;


{*-----------------------------------------------------------------------------
  Anzeige eines einzelnen Spriteframes
-----------------------------------------------------------------------------*}
procedure TSprite.ShowFrame;
var
  rtScreen: TRect;
  rtSprite: TRect;
  rtFrame: TRect;
begin
  // Die Anzeige erfolgt nur, wenn ein Zielcanvas vorgegeben wurde
  if Canvas <> nil then
  begin
    // Rectangle für Position auf dem Zielcanvas
    rtScreen := Rect(Position.X, Position.Y, (Position.X + m_Width), (Position.Y + m_Height));

    // Rectangle für einfache Spriteposition
    rtSprite := Rect(0, 0, Width, Height);

    // Rectangle für Frame innerhalb der Spriebitmap
    rtFrame := Rect(m_FramePos[m_FrameIndex].X , m_FramePos[m_FrameIndex].Y, m_FramePos[m_FrameIndex].X + m_Width, m_FramePos[m_FrameIndex].Y + m_Height);

    // Falls bis jetzt noch kein Hintergrund angelegt wurde, so ist jetzt der
    // letztmögliche Zeitpunkt dafür. Dies kann z.B. beim Bewegen des Sprites
    // geschehen, wenn es verschoben wird.
    if (m_Background.Width = 0) then
    begin
      m_Background.Width := m_Width;
      m_Background.Height := m_Height;
      m_Background.Canvas.CopyMode := cmSrcCopy;
      m_Background.Canvas.CopyRect(rtSprite, Canvas, rtScreen);
    end;

    // Das Sprite wird gelöscht, indem der Hintergrund wieder an die Position
    // eingeblendet wird, wo sich das Sprite befand.
    m_Blitter.Canvas.CopyMode := cmSrcCopy;
    m_Blitter.Canvas.CopyRect(rtSprite, m_Background.Canvas, rtSprite);

    // Wenn eine Maske existiert, dann soll ein transparentes Sprite angezeigt
    // werden. Dies geschieht durch eine Maske:
    // Das Sprite selbst ist entsprechend in seiner Bitmap bearbeitet worden.
    // Alle auszublendenden Pixel sind Schwarz gefärbt worden und die Maske
    // beinhaltet für alle nicht darzustellenden Pixel die Farbe Weiß.
    // Durch entsprechende booelsche Zeichenoperation (AND-Verknüpfung mit der
    // Maske) werden nur die nicht ausgeblendeten Pixel in den Blitter übertragen.
    if m_Mask.Width > 0 then
    begin
      m_Blitter.Canvas.CopyMode := cmSrcAnd;
      m_Blitter.Canvas.CopyRect(rtSprite, m_Mask.Canvas, rtFrame);
      m_Blitter.Canvas.CopyMode := cmSrcPaint;
    end
    else
      m_Blitter.Canvas.CopyMode := cmSrcCopy;

    // Die Zeichenoperationen (Transparenz oder nicht Transparanz, Maske oder nicht Maske)
    // sind bereits eingestellt, es muß nur noch der richtige Frame in den Blitter kopiert werden.
    m_Blitter.Canvas.CopyRect(rtSprite, m_Bitmap.Canvas, rtFrame);

    // Der Blitter enthält jetzt das Hintergrundbild und den richtigen Spriteframe, ggf. mit
    // entsprechender Transparenz. Also ab damit auf den Anzeige-Canvas.
    Canvas.CopyRect(rtScreen, m_Blitter.Canvas, rtSprite);
  end;
end;


{*-----------------------------------------------------------------------------
  Generelle Anzeige des Sprites
-----------------------------------------------------------------------------*}
procedure TSprite.Show;
begin

  // Wie gehabt: Anzeige nur, wenn auch ein Anzeige-Canvas zugewiesen wurde
  if (Canvas <> nil) then
  begin
    // Falls der Hintergrund noch nicht gespeichert wurde (z.B. bei Erstanzeige des
    // Sprites), dann wird er gespeichert.
    if m_Background.Width = 0 then
    begin
      m_Background.Width := Width;
      m_Background.Height := Height;
      Canvas.CopyMode := cmSrcCopy;
      m_Background.Canvas.CopyRect(Rect(0, 0, Width, Height), Canvas, Rect(Position.X, Position.Y, (Position.X + Width), (Position.Y + Height)));
    end;

    // Anzeige des Sprites und aktivieren einer evtl. vorhandenen Animation
    ShowFrame;
    m_Visible := true;
    m_Timer.Enabled := true;
  end;
end;


{*-----------------------------------------------------------------------------
  Sprite verstecken
-----------------------------------------------------------------------------*}
procedure TSprite.Hide;
begin
  // Das Verstecken klappt nur, wenn der Hintergrund gespeichert wurde
  if (m_Background.Width <> 0) and
     (Canvas <> nil) then
  begin
    // Zum Verstecken wird der Hintergrund wieder eingeblendet (ohne Sprite)
    // und die weitere Spriteanzeige unterdrückt.
    Canvas.CopyMode := cmSrcCopy;
    Canvas.CopyRect(Rect(Position.X, Position.Y, (Position.X + Width), (Position.Y + Height)), m_Background.Canvas, Rect(0, 0, Width, Height));
    m_Background.Width := 0;

    // Unterdrücken der weiteren Spriteanzeige
    m_Timer.Enabled := false;
    m_Visible := false;
  end;
end;


{*-----------------------------------------------------------------------------
  Spritebewegung
-----------------------------------------------------------------------------*}
procedure TSprite.MoveBy(p_nX: Integer; p_nY: Integer);
var
  ScreenPos: TRect;
begin
  // Derzeitige Position auf dem Anzeige-Canvas. An dieser Position muß das Sprite
  // gelöscht (also der Hintergrund wieder eingeblendet) werden,
  ScreenPos := Rect(Position.X, Position.Y, (Position.X + Width), (Position.Y + Height));

  if (m_Background.Width <> 0) then
  begin
    Canvas.CopyMode := cmSrcCopy;
    Canvas.CopyRect(ScreenPos, m_Background.Canvas, Rect(0, 0, Width, Height));
    m_Background.Width := 0;
  end;

  // Danach kann das Sprite passend verschoben und neu angezeigt werden
  Position.X := Position.X + p_nX;
  Position.Y := Position.Y + p_nY;
  ShowFrame;
end;


{*-----------------------------------------------------------------------------
  Sprite nach links bewegen
-----------------------------------------------------------------------------*}
procedure TSprite.Left(p_nValue: Integer);
begin
  MoveBy(-p_nValue, 0);
end;


{*-----------------------------------------------------------------------------
  Sprite nach rechts bewegen
-----------------------------------------------------------------------------*}
procedure TSprite.Right(p_nValue: Integer);
begin
  MoveBy(p_nValue, 0);
end;


{*-----------------------------------------------------------------------------
  Sprite nach oben bewegen
-----------------------------------------------------------------------------*}
procedure TSprite.Up(p_nValue: Integer);
begin
  MoveBy(0, -p_nValue);
end;


{*-----------------------------------------------------------------------------
  Sprite nach unten bewegen
-----------------------------------------------------------------------------*}
procedure TSprite.Down(p_nValue: Integer);
begin
  MoveBy(0, p_nValue);
end;


end.
Im Anhang findest du noch die von mir verwendete sprite.bmp.

Viel Spaß beim Basteln und Ausprobieren. ;)
 

Anhänge

HarryXVI

Erfahrenes Mitglied
#10
irgendwie wird die Bitmap-Datei, die ich in das Sprite lade, nicht angezeigt. Stattdessen erscheint auf dem Formular der Abschnitt aus dem Fenster, was dahinter liegt (Buchstaben aus dem Quellcode-Fenster). Wie kann das sein?
 
#11
Starte das Ganze mal außerhalb der IDE, ohne break points. Das von dir beschriebene Verhalten hatte ich teilweise auch, wenn ich zwischen IDE und Programm hin- und hergeschaltet habe. Liegt daran, daß die Formulardarstellung dann teilweise durch die IDE unterbrochen wird.
 

HarryXVI

Erfahrenes Mitglied
#12
also SetFrame legt die einzelnen Bilder fest, die hintereinander angezeigt werden...ok.
Aber wofür sind die Prozeduren "MoveBy", "Left", "Right", "Up", "Down"? Es bewegt sich doch auch, wenn man Frames + Speed festlegt und dann Show aufruft.
 
#13
Mit Speed legst du die Animationsgeschwindigkeit fest, also die Geschwindigkeit, mit der die einzelnen Frames des Sprites angezeigt werden.
MoveBy ist eine Prozedur in der ich sowohl horizontale als auch vertikale Bewegung zusammengefasst habe. Allerdings ist MoveBy von außen nicht sichtbar.
Das sind dann Left, Right, Up und Down. Und mit den Parametern gibst du an, wieviele Pixel das Sprite in die jeweilige Richtung bewegt werden soll.

In diesem Falle: Bewegung != Animation. ;)

Falls du dir den Source wirklich angeschaut und versucht hast, ihn nachzuvollziehen, dann wird dir nicht entgangen sein, daß du das Sprite in der Demo mit den Tasten Q, W, A und Y steuern und mit Space an- und ausschalten kannst...
 

HarryXVI

Erfahrenes Mitglied
#14
habe mich nun nochmal mit dem programm beschäftigt...ich habe den Smiley nun so als Bitmap eingebunden, wie wir dies am Anfang besprochen haben...Jetzt ist die Hintergrundfarbe des Rechtecks schwarz. Kann man die irgendwie ändern?
 

HarryXVI

Erfahrenes Mitglied
#15
Mir ist da noch was aufgefallen. Ich erstelle also 3 Objekte vom Typ TSprite auf dem Canvas einer Image-Komponente. Die Positionen wähle ich zufällig aus, das führt jedoch dazu, dass sich die Sprites oft überlappen. Wie kann ich überprüfen, ob sich auf einem Pixel schon ein Sprite befindet?