Die Community zu .NET und Classic VB.
Menü

Safearrays + Bitmaps

 von 

Übersicht 

Die oft einzig bekannte Technik unter VB Bitmaps pixelgenau zu manipulieren, sind die beiden sehr langsamen Methoden .Pset und .Point.

Dabei steht uns auch in VB eine relativ einfache Möglichkeit, die das Einlesen von Bitmaps in ein Array innerhalb von Mikrosekunden bewerkstelligt, zur Verfügung. Das dürfte selbst in Assembler nur unwesentlich fixer gehen.

Ist die Grafik ersteinmal im Feld gespeichert, lassen sich mit dieser Technik fast in Echtzeit, komplexere Operationen auf die Bitmap anwenden.

Dieser Artikel vermittelt das hierfür relevante Wissen, als auch die speichertechnischen Hintergründe.

Mit freundlichen Grüßen
Götz Reinecke

Was sind SafeArrays?  

Arrays unter Visual Basic besitzen genauso wie Strings einen Deskriptor. Dieser wird benötigt um die, im Vergleich zu anderen Programmiersprachen, vorherrschende hohe Dynamik von Feldern verwaltbar zu machen. So ist das dynamische Verschieben von Felderober- und Untergrenzen zur Laufzeit für den VB-Programmierer sehr praktisch und auch selbstverständlich, wie folgendes Beispiel zeigt:

Private Sub Command1_Click()
  Dim Feld() As Long

    ReDim Feld(0 To 20)
    ReDim Feld(-10 To 50)
End Sub

Listing 1

Dies wird nur möglich durch den besagten Deskriptor. Wir können uns ihn als einen Speicherbereich mit vordefiniertem Aufbau vorstellen. Am leichtesten läßt er sich als Kombination zweier Strukturen darstellen. Die folgende erste, ist Teil der zweiten und beschreibt die Arraygrenzen:

Private Type SAFEARRAYBOUND
  cElements As Long
  lLbound As Long
End Type

Listing 2

Beide Members der Struktur sind vom Typ Long und umfassen je 4 Byte. .cElements stellt dabei die Gesamtzahl der im Feld enthaltenen Elemente dar. lLbound beschreibt die Untergrenze, also den Index des ersten Feldelementes. Für die Zeile Dim Feld(-10 To 50) stände demnach in .lLbound -10 und in .cElements der Wert 61.

Schauen wir uns jetzt die zweite Struktur an, in der SAFEARRAYBOUND ein Mitglied neben anderen ist:

Private Type SAFEARRAY1D
  cDims As Integer
  fFeatures As Integer
  cbElements As Long
  cLocks As Long
  pvData As Long
  Bounds(0 To 0) As SAFEARRAYBOUND
End Type

Listing 3

  • .cDims umfaßt zwei Bytes und benennt die Dimensionen. Bei einem eindimensionalen Feld wie Dim Feld(0 To 9) enthält diese Variable den Wert 1, für eine Deklaration wie Dim Feld(0 To 9, 0 To 4) den Wert 2 usw.
  • .fFeatures ebenfalls 2 Byte lang, gibt Auskunft über den im Array verwendeten Datentyp. Für rein numerische Datentypen ist .fFeatures immer Null, interessant wird es bei anders gearteten Variablen. Für die verschiedensten Arraytypen gibt es je ein Flag in Form einer Konstante.

Konstanten Tabelle für .fFeature

Konstante Wert Beschreibung
FADF_AUTO &H1 Das Array liegt im Stack
FADF_STATIC &H2 Ein statisches Array
FADF_EMBEDDED &H4 Das Feld ist in einer Struktur eingebettet
FADF_FIXEDSIZE &H10 Die Grenzen des Arrays sind nicht änderbar
FADF_RECORD &H20 Das Array enthält Records
FADF_HAVEIID &H40 Array über eine IID-Schnittstelle identifizierbar ist
FADF_HAVEVARTYPE &H80 Feld des Typs VT
FADF_BSTR &H100 String-Array
FADF_UNKNOWN &H200 Array der Schnittstelle IUnknown
FADF_DISPATCH &H400 Array der Schnittstelle IDispatch
FADF_VARIANT &H800 Array ist vom Typ Variant

Tabelle 1

Anzumerken ist, daß es bei der Verwendung von Stringarrays, auf Grund der Visual Basic internen Umwandlungen von BSTR in ANSI bzw. Unicode, nicht direkt möglich ist an den Arrayzeiger zu gelangen. Bei Verwendung der undokumentierten Funktion VarPtrArray ist lediglich ein Zeiger auf die jeweilige interne Kopie des temporären ANSI- bzw. Unicode-Strings zu erhalten. Zur Lösung des Problems muß eigens eine Type-Library erstellt werden. Dies soll hier aber nicht weiter erörtert werden.

  • .cbElements 4 Bytes, gibt Auskunft wieviel Speicherplatz ein einzelnes Element des Feldes benötigt. Für Long-Werte wären dies 4, für Byte 1, Variant 16 Byte etc.
  • .cLocks 4 Bytes, Gibt den aktuellen Wert des Sperrzählers eines Arrays an. Dieser Sperrzähler tritt in Erscheinung, wenn z.B. ein in einer Struktur befindliches Array, welches mit einem With MyStruct ... End With Konstrukt umschlossen ist, geändert werden soll. In diesem Zustand ist das Feld gesperrt, Versuche sich darüber hinwegzusetzen führen zu einem Laufzeitfehler.
  • .pvData Länge: 4 Bytes. Beinhaltet ebenfalls einen 4 Byte langen Zeiger der die eigentlichen Daten referenziert. Letztere sind losgelöst vom Deskriptor und befinden sich an beliebiger Stelle im Speicher. Dieser Umstand ist ein ganz besonderer Leckerbissen mit dem wir später noch ein paar nette Kunstückchen vollführen werden.
  • .Bounds(0 To 0) Ist eine Struktur vom Typ SAFEARRAYBOUND mit zwei Long-Werten, also insgesamt 8-Bytes, die wie oben bereits angeschnitten, die Größe und die Untergrenze des Arrays aufnehmen. Der Parameter (0 To 0) deutet uns auch hier an, daß es sich um ein eindimensionales Feld handelt. Käme eine weitere Dimension hinzu, würde dort entsprechend .Bounds(0 To 1), für drei Dimensionen .Bounds(0 To 2) usw. Vereinbarungsgemäß, hieße unsere Deskriptor-Struktur dann nicht mehr SAFEARRAY1D sondern SAFEARRAY2D, SAFEARRAY3D, SAFEARRAY4D ... SAFEARRAYnD.

Die Verteilung einer SAFEARRAY1D Struktur sähe als Speicherauszug bzw. -Belegung wie folgt aus:


Abbildung 1

Die dunkelgrünen Felder zeigen die namentliche Benennung des zugehörigen Speicherplatze, also werden hier die jeweiligen Member der Struktur dargestellt. Die leeren Kästchen dahinter bezeichnen je ein Byte. Daraus folgt, daß z.B. das Member .fFeatures 2 Byte und beispielsweise .cLocks hingegen 4-Bytes in Anspruch nimmt.

Schauen wir uns jetzt anhand einer einfachen Dimensionierung wie Dim MyArray(10 To 25) As Long den zugehörigen Deskriptor in hexadezimaler Darstellung im Speicher an:


Abbildung 2

.CDims beinhaltet wie zu erwarten den Wert 1, handelt es sich ja hier auch um ein eindimensionales Feld. .fFeatures steht auf Null, da es sich bei dem Feldtypen Long um einen rein numerischen handelt. Hätten wir anstatt dessen Variant gewählt, stände an dieser Stelle der Wert &H800 [s.a. Tabelle oben]. .cbElements sagt uns, wie zu erwarten war, daß der verwendete Datentyp eines Elements [Long] pro Eintrag 4 Byte benötigt. .cLocks steht auf Null, da wir bis jetzt ja nur deklariert haben und keine der Sperrung förderlichen Operationen vorgenommen haben. .pvData ist als Zeiger auf die eigentlichen Felddaten ebenfalls vorhanden..Bounds(0).cElements besitzt den hexadezimalen Wert &H10, entspricht 16, gleich bedeutend mit 16 Elementen [10 To 25]. Womit wir auch schon bei .lLbound wären, das die Untergrenze des Feldes korrekterweise auf 10 festlegt.

Deskriptoren finden  

Jetzt beschleicht uns die Frage, wie ein so gearteter Deskriptor für uns zu nutzen ist. Als erstes ergibt sich hier das Problem, daß er über einen doppelten Zeiger referenziert wird. Dies stellt ersteinmal, auf Grund der normalerweise nicht vorhandenen Zeigerwerkzeuge unter VB, ein Problem dar. Zur Veranschaulichung hier der prinzipielle Aufbau des gesamten Arrangements als Speichermodell, mit seinen einzelnen Referenzen:


Abbildung 3

Um jetzt als ersten Schritt zu erfahrenen an welcher Stelle die Variable MyArray im Speicher abgelegt wurde, verwenden wir die in den Visual Basic Laufzeit-Bibliotheken vorhandene und durch Paul Wilde bekanntgemachte Funktion VarPtrArray. Ihre Deklaration sieht unter VB5 wie folgt aus:

Private Declare Function VarPtrArray Lib "msvbvm50.dll" _
Alias "VarPtr" (Ptr() As Any) As Long

Listing 4

In VB6 ist sie entsprechend auf das folgende abzuwandeln:

Private Declare Function VarPtrArray Lib "msvbvm60.dll" _
Alias "VarPtr" (Ptr() As Any) As Long

Listing 5

Angewandt auf MyArray erhalten wir den ersten Zeiger also mit:

PointerToPointer = VarPtrArray(MyArray)

Listing 6

Um dem zweiten, dem tatsächlich auf SAFEARRAY1D verweisenden, Zeiger habhaft zu werden., bemühen wir die CopyMemory-API, indem wir den Inhalt der durch den ersten Zeiger referenzierten 4 Bytes in eine weitere Long-Variable umkopieren:

Call CopyMemory(SafeArray, ByVal PointerToSafeArray, Len(SafeArray))

Listing 7

Jetzt stehen alle Türen offen um beliebige Manipulationen an einem Array vornehmen zu können. Das folgende Beispiel, zeigt das gesamte Vorgehen nochmals ausführlicher. Es liest im wesentlichen den Deskriptor eines angelegten Arrays ein und gibt ihn bennannt und formatiert in einer Textbox aus:

Option Explicit

Private Declare Function VarPtrArray Lib "msvbvm50.dll" _
Alias "VarPtr" (Ptr() As Any) As Long

'Private Declare Function VarPtrArray Lib "msvbvm60.dll" _
Alias "VarPtr" (Ptr() As Any) As Long

Private Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (pDst As Any, pSrc As Any, _
ByVal ByteLen As Long)

'Die Bounds für das SafeArray
Private Type SAFEARRAYBOUND
  cElements As Long
  lLbound As Long
End Type

'Das SafeArray, eindimensional
Private Type SAFEARRAY1D
  cDims As Integer
  fFeatures As Integer
  cbElements As Long
  cLocks As Long
  pvData As Long
  Bounds(0 To 0) As SAFEARRAYBOUND
End Type

'Konstanten für fFeatures, hier nur zur Anschauung
Const FADF_AUTO = &H1
Const FADF_STATIC = &H2
Const FADF_EMBEDDED = &H4
Const FADF_FIXEDSIZE = &H10
Const FADF_RECORD = &H20
Const FADF_HAVEIID = &H40
Const FADF_HAVEVARTYPE = &H80
Const FADF_BSTR = &H100
Const FADF_UNKNOWN = &H200
Const FADF_DISPATCH = &H400
Const FADF_VARIANT = &H800

Private Sub Command1_Click()
  Call GetSafeArray
End Sub

'Ermittelt und kopiert ein SafeArray
Private Sub GetSafeArray()
  Dim MyArray() As Long
  Dim SafeArray As SAFEARRAY1D
  Dim PointerToPointer As Long
  Dim PointerToSafeArray As Long

    'Feld dimensionieren
    ReDim MyArray(10 To 25)

    'Ersten Zeiger ermitteln
    PointerToPointer = VarPtrArray(MyArray)
    
    'Zweiten Zeiger auslesen
    Call CopyMemory(PointerToSafeArray, _
    ByVal PointerToPointer, 4&)
    
    'SafeArray in vorgegebene Strukur kopieren
    Call CopyMemory(SafeArray, ByVal PointerToSafeArray, _
    Len(SafeArray))
    
    'Resultat ausgeben
    Call DisplaySafeArray(SafeArray)
End Sub

'Ausgabe der Struktur
Private Sub DisplaySafeArray(SA As SAFEARRAY1D)
  Dim H As String

    H = ".cDims = " & SA.cDims & vbCrLf
    H = H & ".fFeatures = " & SA.fFeatures & vbCrLf
    H = H & ".cbElements = " & SA.cbElements & vbCrLf
    H = H & ".cLocks = " & SA.cLocks & vbCrLf
    H = H & ".pvData = " & SA.pvData & vbCrLf

    H = H & ".Bounds(0).cElements = " & SA.Bounds(0).cElements & vbCrLf

    H = H & ".Bounds(0).lLbound = " & SA.Bounds(0).lLbound & vbCrLf

    Text1.Text = H
End Sub

Listing 8: Savearray ermitteln - Beispiel 1

Deskriptoren manipulieren  

Wie eben bereits schon angeschnitten, besteht die Möglichkeit Manipulationen am Deskriptor selbst vornehmen zu können. Wir können aber bereits schon auf einer viel früheren Ebene ansetzen. Normalerweise wird der Deskriptor durch einen Doppelzeiger referenziert. Denkbar wäre jetzt folgendes Szenario: Zwei Felder gleicher Größe, Grenzen und Datentyps aber verschiedenen Inhalts existieren friedlich nebeneinander her. Damit beide ihren Inhalt tauschen können, wäre für die reine VB-Lösung das Anlegen eines dritten, temporären Feldes gleicher Art nötig. Anschließend müßte in einer Schleife jedes Element einzeln umkopiert werden. Dieses Vorgehen ist recht mühselig und vor allem bei größeren Feldern sehr langsam. Eine Geschwindigkeitssteigerung ließe sich noch durch die Verwendung der CopyMemory-API erzielen, aber auch hier muß immer das gesamte Feld dreimal umgeschichtet werden.

Viel leichter und in erster Linie unschlagbar schneller ginge dies über das Tauschen der beiden auf die Deskriptoren zielenden Zeiger. Im Ausganszustand verweist Zeiger1 auf das SafeArray1 und Zeiger2 auf SafeArray2. Durch eine kleine Manipulation, könnte man aber auch ohne weiteres Zeiger1 auf SafeArray 2 und Zeiger2 auf SafeArray1 verweisen lassen.

Hierzu ist lediglich der erste Zeiger des einen Deskriptor-Doppelpointers in einen temporären zwischenzuspeichern, der zweite in den ersten zu kopieren um abschließend den temporären in den zweiten zu übertragen. Unter der Berücksichtigung, daß der Zeiger auf einen Deskriptor lediglich 4 Byte umfaßt, werden also genau 12 Byte bewegt. Daß bedeutet das Austauschen zweier Arrays, egal ob 10 Bytes oder 30 MB groß, bedarf lediglich der Zeit, die für das Kopieren von 3 x 4 Bytes benötigt wird. Das folgende Beispiel verwendet diese Methode des Zeigertauschens:

Option Explicit

Private Declare Function VarPtrArray Lib "msvbvm50.dll" _
Alias "VarPtr" (Ptr() As Any) As Long

'Private Declare Function VarPtrArray Lib "msvbvm60.dll"  _
Alias "VarPtr" (Ptr() As Any) As Long

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
    (pDst As Any, pSrc As Any, ByVal ByteLen As Long)

Dim MyArray1() As Long
Dim MyArray2() As Long
Dim at() As Long

Private Sub Form_Load()
  Dim x As Long

    ReDim MyArray1(0 To 24)
    ReDim MyArray2(0 To 24)

    For x = 0 To 24
      MyArray1(x) = x
      MyArray2(x) = x * 100
    Next x

    Call DisplayArrays
End Sub

Private Sub Command1_Click()
  Call SwapDescriptors
End Sub

Private Sub SwapDescriptors()
  Dim a1 As Long
  Dim a2 As Long
  Dim a3 As Long

    a1 = VarPtrArray(MyArray1)
    a2 = VarPtrArray(MyArray2)
    a3 = VarPtrArray(at)

    Call CopyMemory(ByVal a3, ByVal a1, 4)
    Call CopyMemory(ByVal a1, ByVal a2, 4)
    Call CopyMemory(ByVal a2, ByVal a3, 4)

    Call DisplayArrays
End Sub

Private Sub DisplayArrays()
  Dim x As Long

     List1.Clear
     List2.Clear
     For x = 0 To UBound(MyArray1)
       List1.AddItem MyArray1(x)
       List2.AddItem MyArray2(x)
     Next x
End Sub

Listing 9: SafeArray-Pointer tauschen - Beispiel 2

Bitmaps als Objekte  

Behalten wir die Sache mit den Deskriptoren in Erinnerung und kommen jetzt kurz auf eine andere Thematik zu sprechen. Später werden wir dann beide Bereiche mühelos zusammenführen können.

Wird in einen Speicherbereich eine Bitmap als Datei eingeladen, so ist sie damit im Windowssinne ein Objekt und verfügt über ein Handle, das sogenannte Bitmaphandle. Dieses kann an die API-Funktion GetObject übergeben werden, um dadurch weitere Informationen des Objekts einholen zu können. Da sich in VB hinter dem Wort GetObject bereits ein Befehl zur Ermittlung von ActiveX-Verweisen verbirgt, hat sich für die API-Funktion der Name eingebürgert.

In VB ist es möglich der Eigenschaft .Picture über den Befehl LoadPicture ein Bitmap-Objekt zuzuführen und damit beispielsweise dem Objekt PictureBox ein gültiges Objekthandle zuzuweisen. Somit kann dieses auch in der besagten Funktion zur Anwendung kommen.

Die Deklaration der Funktion lautet wie folgt:

Private Declare Function GetObjectAPI Lib "gdi32" Alias _
        "GetObjectA" (ByVal hObject As Long, ByVal nCount As _
        Long, lpObject As Any) As Long

Listing 10

  • hObject Das Objekthandle, in unserem Falle die in eine PictureBox eingeladene Bitmap, referenziert durch den dem Objekt innewohnenden Long-Wert selbst.
  • lpObject Zeiger auf einen Puffer der nach Aufruf der Funktion Informationen über das durch hObject bezeichnete Objekt zurückgibt.
  • nCount Die Größe des mit lpObject festgelegten Puffers in Byte.

In der Win32.hlp ist zu lesen, daß für unsere Bitmap die Information in Form einer Struktur namens BITMAP zurückgegeben wird. Diese hat folgenden Aufbau:

Private Type BITMAP
  bmType As Long
  bmWidth As Long
  bmHeight As Long
  bmWidthBytes As Long
  bmPlanes As Integer
  bmBitsPixel As Integer
  bmBits As Long
End Type

Listing 11

Als Beschreibung der einzelnen Mitglieder ist zu entnehmen:

  • bmType Spezifiziert den Bitmaptypen, dieses Member muß Null sein.
  • bmWidth Die Breite der Bitmap in Pixel. Die Weite muß immer größer Null sein.
  • bmHeight Die Höhe der Bitmap in Pixel. Die Höhe muß immer größer Null sein.
  • bmWidthBytes Spezifiziert die Anzahl der Bytes pro ScanLine. Dieser Wert muß durch zwei teilbar sein, damit Windows Werte aus einem Array mit word-Ausrichtung annehmen kann.
  • bmPlanes Anzahl der Farbebenen.
  • bmBitsPixel Anzahl der Bits die benötigt werden um ein Pixel farblich anzeigen zu können.
  • bmBits Ein Zeiger auf die Position der Bits einer Bitmap. bmBits muß ein 4-Bytelanger Pointer auf ein Feld mit Bytewerten sein.

Oha, sehr verdächtig, vor allem des letzte Member bmBits, läßt uns plötzlich wieder ein SafeArray denken. Zuerst einmal müssen wir aber die GetObject-API erfolgreich zur Anwendung bringen:

Private Sub GetBitmapStruct()
  Dim Bmp As BITMAP

    Picture1.Picture = LoadPicture(App.Path & "\Bild.jpg")
    Call GetObject(Picture1.Picture, Len(Bmp), Bmp)
End Sub

Listing 12

Betrachten wir nochmals das letzte Member der Bitmap-Struktur, dort steht:

Ein Zeiger auf die Position der Bits einer Bitmap. bmBits muß ein 4-Bytelanger Pointer auf ein Feld mit Bytewerten sein.

Was soviel bedeutet, daß hier ein Pointer abgelegt wird der auf den Datenblock eines Arrays vom Typ Byte zeigt. Wir haben also eine Struktur, die ein Member enthält welches auf einen reinen Datenabschnitt im Speicher verweist. Die selbe Situation kennen wir bereits von den SafeArrays. Auch dort zeigt .pvData lediglich auf einen Speicherblock. Wäre es jetzt nicht denkbar in Visual Basic ein Bytearray zu initialisieren und im Deskriptor das Mitglied .pvData so zu ändern, daß es genau auf den Speicherbereich der Bitmap verweist? Natürlich ist das denkbar, und zudem sogar auch machbar.

Aufbau von 24-Bit Bitmaps  

Damit wir genau wissen wie das Bytearray anzulegen ist, muß vorab exakt bekannt sein, wie die einzelnen Pixel einer Bitmap im Speicher arrangiert sind. Wir beschränken uns vorerst auf 24-Bit Bitmaps. Diese haben den entscheidenden Vorteil, daß für die Darstellung eines ihrer farbigen Pixel 3 x 8 Bit, also 24 Bit benötigt werden. Jedes Byte eines solchen Pixels bildet einen Farbanteil: Der zugehörige 24-Bit-Farbwert ist daher eine RGB-Zahl, die mit je einem Byte in Rot, Grün und Blau zerfällt.


Abbildung 4: Ein Beispiel

Der obere Blau-Grauton [linke Abbildung] besitzt den 24-Bitwert &H92ABAF. Anhand der hexadezimalen Darstellung lassen sich gut die einzelnen Anteile erkennen, nämlich &H92 für Rot, &HAB für Grün, sowie &HAF für Blau. Die rechte Abbildung beweist, daß sich durch Mischen der ermittelten Anteile wieder der originale Blau-Grauton ergibt.

In unserer Bitmap liegen also jeweils immer 3 Byte nebeneinander, die nach dem oben genannten Schema je ein Farbpixel bilden.

Würden wir in einem Grafikprogramm eine 24 Bit-Bitmap der Farbe weiß mit 4 x 4 Pixel kreieren, ließe sich ihr visueller Aufbau wie folgt vorstellen. Jedes Kästchen steht dabei für ein Byte, so daß sich pro Zeile ein Bedarf von 12 Byte, also insgesamt 48, ergibt:


Abbildung 5

Umgewandelt in ein zweidimensionales Bytearray wäre das erste sichtbare Pixel in der oberen linken Ecke mit B(0,0), B(0,1) und B(0,2) zu adressieren. Das zweite Pixel hätte dann die Indizes B(0,3), B(0,4) und B(0,5) usw.

Im Speicher sieht Organisation ein kleines bißchen anders aus. So liegen die Farbanteile nicht in der Reihenfolge R -> G -> B, sondern genau umgekehrt, nämlich in der Kette B -> G -> R vor. Weiterhin ist die gesamte Bitmap um die x-Achse gespiegelt, so daß das erste Byte der ersten Zeile das erste Byte der letzten Zeile einnimmt:


Abbildung 6

Möchten wir das erste sichtbare Pixel, wie oben, indizieren, geht der Ansatz diesmal über die Position der drei Byte in der unteren linken Ecke, also B(3,0), B(3,1) und B(3,2). Damit es später nicht zu Überraschungen kommt, sollte dieser Aufbau stets präsent sein.

Der Clou  

Zurück zur ursprünglichen Idee. Wir wollten ein Bytearray vorbereiten, dessen Deskriptor auf den in der BITMAP-Struktur referenzierten Speicherbereich verweist. Dafür muß in erster Linie das Feld richtig dimensioniert werden. Da nicht nur die Arraygrenze sondern auch der Zeiger .pvData geändert werden muß, kann die BASIC-Funktion ReDim nicht zum Einsatz kommen. Der Arraydeskriptor ist selbst zu konstruieren. Weil ein zweidimensionales Feld notwendig ist, sollte jetzt die Struktur SAFEARRAY2D verwendet werden:

Private Type SAFEARRAY2D
  cDims As Integer
  fFeatures As Integer
  cbElements As Long
  cLocks As Long
  pvData As Long
  Bounds(0 To 1) As SAFEARRAYBOUND
End Type

Listing 13

Durch

Call GetObject(Picture1.Picture, Len(Bmp), Bmp)

Listing 14

ist die BITMAP-Struktur zu erhalten. Nun wird der Deskriptor entsprechend zusammengesetzt:

With SafeArray
  .cDims = 2
  .cbElements = 1
  .fFeatures = 0
  .cLocks = 0
  .pvData = Bmp.bmBits

  .Bounds(0).lLbound = 0
  .Bounds(0).cElements = Bmp.bmHeight

  .Bounds(1).lLbound = 0
  .Bounds(1).cElements = Bmp.bmWidthBytes
End With

Listing 15

.cDims erhält den Wert 2, da es sich ja um ein zweidimensionales Feld handeln soll. Der Datentyp der Feldelemente ist Byte. Dieser benötigt pro Element nur je eine Speicherzelle, daher ist .cbElements auf 1 zu setzten. Der Datentyp Byte ist ein numerischer, woraus folgt, daß .fFeatures logischerweise den Wert 0 erhält. Sperrungen gibt es bisher keine, weshalb .cLocks auch gleich 0 ist.

Jetzt kommt der eigentliche Trick:

.pvData = Bmp.bmBits

.pvData würde normalerweise auf den von Visual Basic zugewiesenen Speicherblock des Bytearrays verweisen. Bmp.bmBits referenziert hingegen ebenfalls ein Bytearray, nämlich das der Bitmap. Indem .pvData jetzt Bmp.bmBits zugewiesen wird, ist erreicht, daß unser Array gleich der in der PictureBox befindlichen Bitmap ist. Also kurzum: Wenn der neue Deskriptor zugewiesen wird, befindet sich die gesamte Bitmap auf einen Schlag in einem Visual Basic Bytearray!

Doch weiter. Was jetzt noch fehlt sind die Arraygrenzen. Da ja ein zweidimensionales Feld konstruiert werden soll, sind auch zwei Bounds zu berücksichtigen. Damit die Sache zu Beginn so einfach wie möglich gehalten wird, erstellen wir ein nullbasierters Array. Deshalb sind .Bounds(1).lLbound und .Bounds(1).lLbound auf Null zu setzen.

Das Array muß die Bedingung BMP-Höhe_in_Pixel * (BMP-Breite_in_Pixel * 3) erfüllen. Die Breite ist mit 3 zu multiplizieren, da eine 24-Bit Bitmap vorliegt und dort jedes Pixel 3 Byte benötigt. Die Höhe ist leicht ermittelt, liegt sie doch im Member Bmp.bmHeight der BITMAP-Struktur vor. .BmWidth ist zwar ebenfalls vorhanden, doch können die Operationen der Umrechungen erspart bleiben, wenn wir uns die Beschreibung des Members Bmp.bmWidthBytes nochmals anschauen:

bmWidthBytes Spezifiziert die Anzahl der Bytes pro ScanLine. Dieser Wert muß durch zwei teilbar sein, damit Windows Werte aus einem Array mit word-Ausrichtung annehmen kann.

Die Scanline ist in unserem Falle eine Zeile der Bitmap, also genau das was benötigt wird, daher können wir dieses Member zuweisen. Es ergibt sich für die Bounds:

.Bounds(0).lLbound = 0
.Bounds(0).cElements = Bmp.bmHeight

.Bounds(1).lLbound = 0
.Bounds(1).cElements = Bmp.bmWidthBytes

Listing 16

Jetzt gilt es nur noch den fertigen, neuen Deskriptor wie folgt anzubringen:

Call CopyMemory(ByVal VarPtrArray(MyArray), VarPtr(SafeArray), 4&)

Listing 17

Der Zeiger auf unsere gerade erst definierte Struktur, wird auf den ersten Zeiger des Doppelpointer kopiert. Das war alles. Durch das Umkopieren eines 4 Byte langen Pointers kann in Mikrosekunden eine Bitmap beliebiger Größe in ein Bytearray kopiert werden. Schneller geht es nicht. Aber es kommt noch besser.

Wir haben jetzt die Bitmap in ein Bytearray eingebettet und daher wirkt sich jede Änderungen eines Wertes im Feld direkt auf die Bitmap aus. Das heißt, ändern wir drei nebeneinander liegende Bytes mit je dem Wert &HFF auf den Wert &H00, wechselt auch das angesprochene Pixel seine Farbe unmittelbar von ehemals weiß in schwarz. Da wir mit der beschriebenen Technik an der .Image- und nicht der .Picture-Property der PictureBox werkeln, ist die Visualisierung der angewendeten Manipulationen mit Picture1.Refresh in den Vordergrund zu bringen.

Einmal als Array vorliegend, lassen sich mit dieser Methode in erstaunlichen Geschwindigkeiten Bilder in fast [abhängig von der Bildgröße] Echtzeit beliebig manipulieren. Es sei noch der Hinweis gegeben, daß nach Beendigung der grafischen Operationen mit der Zeile

Call CopyMemory(ByVal VarPtrArray(MyArray), 0&, 4&)

Listing 18

der Deskriptor des generierten Bytearray auf Null gesetzt werden sollte. Unter Win9x ist dies zwar überflüssig, bei NT-Systemen hingegen kann die nicht Verwendung dieser Zeile zu unangenehmen Nebenwirkungen führen.

Um die verschiedensten Effekte, wie Emboss, Ripple, Schärfen, Rotieren, sowie Ändern von Kontrast, Helligkeit und RGB-Verhältnissen, zu erzielen, schauen Sie bitte in die diesem Artikel beiliegende Beispiel-Sammlung. Die verwendete Methodik ist bei allen die gleiche, nämlich die hier besprochene. Es variieren lediglich die grafischen Algorithmen zwecks Erzielung der verschiedenen Effekte.

Nachteil dieser durch Matthew Curland und Francesco Balena erstmalig 1998 vorgestellten Zeigermethode, ist, daß nur Grafiken manipuliert werden können, die auch ein Bitmaphandle besitzen und das ist leider nicht immer gegeben. Ausweichmöglichkeit bietet das Arrangieren einer Grafik in einer DIB [Device Independent Bitmap]. Als Ergebnis liegt die Bitmap zwar wieder als Bytearray vor, hat aber nicht den harten und daher schnellen Kontakt zur Grafik. Für statische Bilder mag das keine Rolle spielen, bei raschem dynamischen Wechsel, sprich einer Animation, kann es aber schnell zu einer unflüssigen Darstellung kommen.

8 Beispiele plus dem Artikel als Download [247000 Bytes]

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.