<<
Demo "ReduceToPresentationDataPicture"
Dateninhalte von OLE-Feldern in Microsoft Access
(Demo-MDB)

In dieser Demo-MDB wird das Präsentationsbild eines OLE-Objekts isoliert und in Form eines statischen Bildobjekts in einem OLE-Feld gespeichert. Mit diesem Beispiel soll gezeigt werden, welche unterschiedlichen Formate Microsoft Access für OLE-Felder kennt und wie die zugehörigen Daten aufgebaut sind. 

Diese Dokumentation behandelt die Microsoft Object Linking and Embedding (OLE) Data Structures nach OLE1. Das OLE-Format ist hervorragend von Microsoft dokumentiert. Um eine vollständige Darstellung dieses Formats zu erhalten, ist es ratsam, sich mit der originalen Dokumentation von Microsoft zu beschäftigen. Diese Dokumentation hier dient nur dem besseren Verständnis der Dateninhalte der OLE-Felder von Microsoft Access. 

Auf Ebene der Microsoft Jet-Engine sind OLE-Felder einfache Tabellenfelder vom Typ Long-Binary. Die höhere Funktionalität der OLE-Felder ist Bestandteil von Microsoft Access und stellt einen Teil der implementierten OLE-Schnittstelle dar. Mit Hilfe der OLE-Felder können Daten gespeichert werden, die von anderen Programmen erstellt wurden. Diese Daten werden OLE-Objekte genannt. Der Aufbau der OLE-Objekte wird durch das OLE-Format vorgegeben. MS Access stellt als Container-Anwendung nur den Speicherplatz zur Verfügung, die Daten der OLE-Objekte selber müssen von den zugehörigen Fremdprogrammen bereitgestellt werden. Das Fremdprogramm muss dafür seine eigene OLE-Schnittstelle implementiert haben und in der Windows-Registry als OLE-Server registriert sein. Über die Registry können dann die OLE-Objekte den zugehörigen Anwendungen zugeordnet werden. Soll ein OLE-Objekt eingefügt werden, dann kann die Container-Anwendung nachsehen, welches Fremdprogramm es beauftragen muss, die nötigen Daten zur Verfügung zu stellen. Zu einem gespeicherten OLE-Objekt gehört dann ein eindeutiger Klassenname, der die Ersteller-Anwendung identifiziert. 

Ich möchte hier nicht auf die Technik der OLE-Schnittstelle eingehen, sondern nur auf den Datenaufbau der OLE-Objekte selber. Im Prinzip geht es bei einer solchen OLE-Schnittstelle darum, dass zwei Programme Daten in Form eines OLE-Objekts austauschen können. 

Access fasst den Inhalt eines OLE-Felds zunächst als einfachen Long-Binary-Wert auf, kann dann aber anhand der Datenstruktur des Bytestreams spezielle Formate erkennen. Neben den OLE-Objekten gibt es auch noch verschiedene Varianten zum Speichern von Bildern. 

Die unterschiedlichen Formate werden von Access durch eine Header-Struktur identifiziert, die sich am Anfang des Bytestreams befinden muss. In der Terminologie des OLE1-Formats nennt man einen solchen Header die Container-Application-Data und sein Aufbau wird ausschließlich von der Container-Anwendung vorgegeben. Access benutzt immer denselben Container-Application-Header, auch wenn das gespeicherte Format vom OLE-Standard abweicht. 

OLE-Objekte werden von Access im OLE1-Format gespeichert. Der Standard für OLE-Objekte ist aber das OLE2-Format. Bevor OLE2-Objekte gespeichert werden können, müssen sie in das OLE1-Format "konvertiert" werden. Wirkliche OLE-Objekte im OLE1-Format sind aber nur die sogenannten Pakete. Alle anderen OLE-Objekte benutzen selber das OLE2-Format und werden dann, wie ein Paket in einen OLE1Stream eingebettet. Auch verknüpfte OLE2-Objekte werden auf diese Weise gespeichert. 

Der Unterschied zwischen OLE1 und OLE2 ist vor allem der, dass OLE2 das Compound File Binary Format verwendet. Die Daten werden bei OLE2 in einem Structured Storage gespeichert, das OLE1-Format verwendet dagegen nur einen einfach aufgebauten Bytestream. OLE2 kann mehr zusätzliche Daten speichern, verbraucht dafür aber auch mehr Speicherplatz. Im OLE2-Format sind z.B. die Präsentationsdaten (Presentation Data) oft redundant im WMF- und im EMF-Format vorhanden. Die Präsentationsdaten machen den größten Teil der zusätzlichen Daten eines OLE-Objekts aus. Wenn solche Daten doppelt vorhanden sind, dann hat das natürlich einen erheblichen Einfluss auf die Gesamtgröße des Datenstreams. Bei Access kommt es aber noch arger, da das OLE2-Format ja noch in das OLE1-Format gewandelt werden muss und dieses auch wiederum eigene Präsentationsdaten besitzt. Das Drama wird deutlich, wenn z.B. ein Bild als OLE-Objekt gespeichert wird. Einmal werden die originalen Bilddaten gespeichert, dann das Bild als WMF und als EMF im OLE2-Format und dann nochmal als WMF im OLE1-Format. Ein solches Bild wird also gleich 4-mal gespeichert und der Speicherverbrauch ist entsprechend hoch. Besonders eklatant sichtbar wird das, wenn ein Bildobjekt ursprünglich in einem komprimierten Bildformat vorliegt, wie z.B. in einer JPG-Datei. Da ein komprimiertes Bildformat nicht auf dem Bildschirm existiert, wird das JPG-Format erst in ein Bitmap gewandelt, das dann 4-fach gespeichert wird. Einmal nativ und dann im Metafile-Format. Im Metafile wird dafür ein BITBLT-Record with Bitmap verwendet, was bedeutet, dass dort auch die gesamten Bitmapdaten gespeichert werden müssen. 

Nun möchte ich aber nicht weiter auf das OLE2-Format eingehen, sondern mich dem OLE1Stream zuwenden, da Microsoft Access selber die OLE-Objekte gemäß OLE1 interpretiert. Das bedeutet im Wesentlichen, dass die Präsentationsdaten aus dem OLE1Stream verwendet werden und nicht die aus dem OLE2-Storage. 

Ein OLE-Objekt nach OLE1 ist ein Datenstream der aus vier Bereichen besteht: 

Als Erstes kommen die Container-Application-Data. Dieser Bereich ist für die Container-Anwendung reserviert, also für das Programm, das das OLE-Objekt speichert. Der Aufbau und die Größe dieses Bereichs sind nicht vorgegeben und alleinige Angelegenheit der Container-Anwendung. Die Container-Anwendungs-Daten werden nicht über die OLE-Schnittstelle ausgetauscht. 

Als Zweites folgen die Creating-Application-Data. Dieser Bereich beinhaltet Angaben über die gespeicherten Daten. Der Aufbau wird durch das OLE-Format vorgegeben. Hier befindet sich z.B. der Creating-Application-Identifier, also der Klassenname, über den das OLE-Objekt seiner Ersteller-Anwendung zugeordnet wird. 

Als Drittes kommen nun die tatsächlichen Daten. Bei reinen OLE1-Objekten, also den Paketen, stehen hier die nativen Daten der Ersteller-Anwendung. Die Größe dieses Bereichs ist in den Creating-Application-Data angegeben. Wenn man bei einem Paket diesen Bereich aus dem Bytestream isoliert und als Datei speichert, dann ist das genau die Datei, die als OLE-Objekt eingefügt wurde. Wenn es sich aber um ein OLE2-Objekt handelt, das nur in das OLE1-Format gewandelt wurde, dann befindet sich in diesem Bereich das Compound File des OLE2-Objekts. 

Als vierte Einheit kommen zuletzt noch die Presentation-Data. Dieser Bereich beinhaltet Daten, die zur Darstellung des OLE-Objekts in der Container-Anwendung verwendet werden. Dabei handelt es sich immer um ein Grafik-Format. Die Standardformate sind Metafile, DIB und Bitmap. Außerdem können auch generische Clipboard Formate verwendet werden. Die Struktur dieses Bereichs wird durch das OLE-Format vorgegeben. 

Alle vier Einheiten aneinander gereiht bilden den Bytestream der in einem OLE-Feld gespeichert ist, wenn dort ein OLE-Objekt eingefügt wurde. Es liegt natürlich auf der Hand, dass bei verknüpften OLE-Objekten keine nativen Daten der Ersteller-Anwendung gespeichert werden. Der Bereich der nativen Daten würde in so einem Fall fehlen, allerdings nur dann, wenn es sich um ein echtes OLE1-Objekt handelt. Bei einem verknüpften Paket würde das also zutreffen, da aber alle modernen Anwendungen das OLE2-Format verwenden und das OLE2-Objekt im OLE1Stream eingebettet wird, sind in den meisten Fällen auch alle vier Bereiche vorhanden. 

Neben den normalen OLE-Objekten findet man bei Access noch die Klasse Paint.Picture zum Speichern von Bitmaps. Diese Klasse hat den Creating-Application-Identifier PBrush und zeichnet sich im Wesentlichen dadurch aus, dass keine zusätzlichen Präsentationsdaten vorhanden sind. Es ist ein OLE1Stream, der als native Daten ein Bitmap-File beinhaltet. Die zugehörige Anwendung ist Microsoft Paint. 

Dann besteht noch die Möglichkeit, dass Bildobjekte statisch gespeichert werden können. Es handelt sich dann um einen OLE1Stream, der keine nativen Objektdaten beinhaltet und nur aus den Container-Application-Data und den Presentation-Data besteht. Da es keinen Bezug zu einer Ersteller-Anwendung gibt, können statische Objekte nicht bearbeitet werden. 

Um mit den Daten eines OLE-Felds arbeiten zu können, muss man zunächst wissen, ob dort überhaupt ein bekanntes Format gespeichert wurde. Damit man das feststellen kann, isoliert man zuerst den Container-Application-Header. Ist der nicht vorhanden, dann handelt es sich bei dem Inhalt des OLE-Felds um einen undefinierten Long-Binary-Wert. Ist der Access-Header vorhanden, dann weiß man anhand des dort gespeicherten Klassennamens, aus welchen einzelnen Einheiten der Rest des Bytestreams zusammengesetzt ist. Hat man die Einheiten getrennt, dann kann man sie den unterlegten Strukturen zuordnen und ihnen weitere Informationen entnehmen. 

Dieses Demo zeigt, wie man die Präsentationsdaten eines OLE-Objekts isoliert. Damit das möglich wird, muss man zuerst den OLE1Stream in seine Einzelteile zerlegen. Dann benötigt man Informationen aus dem Container-Application-Header und den Creating-Application-Data. Um schließlich die Presentation-Data zu isolieren, muss der Bereich der nativen Daten übersprungen werden. Mit den Präsentationsdaten wird danach ein neuer Bytestream erstellt, der dem Format eines statischen Bildobjekts entspricht. 

Dieses Beispiel geht davon aus, dass für die Präsentationsdaten das Metafile-Format verwendet wurde. Alle Klassen die ich in OLE-Feldern gesehen habe, benutzen ein MetaFilePresentationObject. 

Als Erstes braucht man den Container-Application-Header. Aufbau und Name des Container-Application-Header von Microsoft Access sind nicht wirklich bekannt, jedenfalls scheint keine Dokumentation von Microsoft verfügbar zu sein. Trotzdem findet man aber im Internet verschiedene Varianten von Definitionen dieser Struktur. Der Aufbau ist natürlich bei allen Varianten gleich, nur die Namensgebungen sind unterschiedlich. Ich verwende eine dieser bekannten Definitionen. Der Aufbau ist nicht sehr kompliziert. 

' Die Container-Application-Data von MS-Access
 
Private Type POINTS
X As Integer
Y As Integer
End Type
 
' Diesem Header folgen dann noch die Ansi-Strings Name und Class
Private Type OLEOBJHDR
typ As Integer ' Identifier des OLE-Streams / Der Wert 7189 kennzeichnet die vordefinierten Formate
cbHdr As Integer ' Größe der Container-Application-Data (Dazu gehören auch die Strings Class u. Name)
lObjTyp As Long ' FormatID / OLE-Objekt ist 1=verknüpft, 2=eingebettet, 3=statisches Bildobjekt
cchName As Integer ' Länge des AnsiStrings Name (Anzahl der Zeichen)
cchClass As Integer ' Länge des AnsiStrings Class (Anzahl der Zeichen)
ibName As Integer ' Offset zum AnsiString Name (vom Anfang des Bereichs)
ibClass As Integer ' Offset zum AnsiString Class (vom Anfang des Bereichs)
ptSize As POINTS ' ? / Werte sind immer -1
End Type
 
 

Der Bereich beginnt mit einem Integer-Wert, der die Anwesenheit eines definierten Formats identifiziert. Findet man hier den Dezimalwert 7189, dann ist davon auszugehen, dass die Container-Application-Data vorhanden sind. Der zweite Integer-Wert beinhaltet dann die Gesamtgröße dieses Bereichs. Der Bereich endet mit den null-terminierten Ansi Strings 'Name' und 'Class', die nicht in der Type-Definition vorhanden sind. Es ist eine übliche Vorgehensweise, einen Wert innerhalb eines Bytestreams zu referenzieren, indem man seinen Offset im Stream und seine Größe hinterlegt. In der Type-Definition befinden sich je zwei Integer-Werte, die auf den internen Objektnamen und den Klassennamen verweisen. Ein Offset wird immer in Bytes angegeben. Die Größenangabe bezieht sich hier auf die Zeichenanzahl eines null-terminierten Ansi Strings. Da ein solcher String nur ein Byte pro Zeichen verwendet, entspricht diese Angabe der Anzahl der Bytes, wobei das letzte Byte den Wert Null hat. Vor diesen Referenzangaben befindet sich noch ein Long-Wert, der die Speicherart der nativen OLE-Daten kennzeichnet. Hier wird angegeben, ob es sich um ein verknüpftes oder ein eingebettetes OLE-Objekt handelt. Ein dritter Wert zeigt an, dass ein statisches Bildobjekt vorhanden ist. 

Handelt es sich um ein OLE-Objekt, dann braucht man als nächstes die Creating-Application-Data. Der Aufbau dieses Bereichs wird durch das OLE-Format vorgegeben und ist davon abhängig, ob das OLE-Objekt verknüpft oder eingebettet ist. In beiden Fällen beginnt der Bereich mit dem ObjectHeader. Hier findet man den Klassennamen und kann über die FormatID feststellen, ob das Objekt verknüpft oder eingebettet ist. Bei verknüpften Objekten wird im TopicName der Pfad zum Objekt und im ItemName der Verweis zu einer bestimmten Stelle innerhalb des Objekts angegeben. Bei eingebetteten Objekten werden diese Werte ignoriert und sollten leer sein. Diese Werte sind vom Typ LengthPrefixedAnsiSting, was bedeutet, dass zuerst immer ein Long-Wert kommt, der die Anzahl der Bytes angibt, aus denen der Ansi-String besteht, der dem Long-Wert folgt. Ist ein solcher String leer, dann besteht er nur aus einem Long-Wert 0. Wie bereits erwähnt, würde man hier nur dann ein verlinktes Objekt finden, wenn es sich um ein Paket handelt. Handelt es sich aber um ein OLE2-Objekt, dann ist das OLE2-Objekt in jedem Fall im OLE1Stream eingebettet. Man kann also davon ausgehen, dass man im OLE1Stream meistens ein eingebettetes Objekte vorfindet. Aus diesem Grund gehe ich auch nicht weiter auf verknüpfte OLE1-Objekt ein und behandle nur die Variante für eingebettete Objekte. 

' Die Creating-Application-Data (OLE1.0)
 
' Erster Teil des ObjectHeaders (für linked und embedded)
Private Type ole1ObjectHeaderShort
lOLEVersion As Long ' Versions-Nr. (ist nur für die Ersteller-Anwendung interessant)
lFormatID As Long ' Das OLE1-Objekt ist 1 = verknüpft, 2 = eingebettet
lClassNameLength As Long ' Long-Wert des LengthPrefixedAnsiStings ClassName (Anzahl der Bytes in ClassName)
End Type
 
' Zweiter Teil des ObjectHeaders (optimiert für eingebettete Objekte)
Private Type ole1ObjectHeaderEmbd2
lTopicNameLength As Long ' Anzahl der Zeichen im TopicName (erwarteter Wert ist 0)
lItemNameLength As Long ' Anzahl der Zeichen im ItemName (erwarteter Wert ist 0)
lNativeDataSize As Long ' Größe der nativen Daten die im OLE1Stream eingebettet sind
End Type
 
 

Der erste Teil des ObjectHeaders ist bei verknüpften und eingebetteten Objekten gleich. Er beginnt mit einer Variable, in der die Ersteller-Anwendung einen beliebigen Long-Wert hinterlegen kann. Danach folgen die FormatID und der Klassenname. Der ClassName ist ein LengthPrefixedAnsiString und beginnt mit der Angabe der Zeichenanzahl. Diese drei Werte habe ich in einem Typen zusammengefasst, der sich bequem füllen lässt. Wenn der Klassennamen nicht gebraucht wird, genügt die Angabe der Zeichenanzahl, um ihn zu überspringen. Nach dem Klassennamen folgen nun bei eingebetteten Objekten drei weitere Long-Werte, die ich in einem Typen zusammengefasst habe. Die Variable lNativeDataSize gehört dabei zu der EmbeddedObject-Struktur. Mein Type ist einfach eine Optimierung für den Zugriff auf eingebettete Objekte. Ich gehe dabei davon aus, dass TopicName und ItemName Null-Strings enthalten und darum nur aus einem Long-Wert bestehen, da sie vom Typ LengthPrefixedAnsiString sind. Die Variable lNativeDataSize enthält dann die Größe des Bereichs der nativen Daten, der direkt nach diesem Wert beginnt. 

Hinter den nativen Daten folgt abschließend der Bereich der Präsentationsdaten. Sein Aufbau wird durch das OLE-Format vorgegeben und ist davon abhängig, in welchem Format die Grafikdaten vorliegen. Grundsätzlich wird zwischen Standardformat und Clipboardformat unterschieden. Es gibt drei Standardformate, die Clipboardformate unterteilen sich in vier Standardformate und einer unbegrenzten Anzahl von registrierten Formaten. Registrierte Clipboardformate müssen dann natürlich auch auf dem Container-Application-System vorhanden sein. 

Alle Klassen die ich in den OLE-Feldern von Microsoft Access gesehen habe, verwenden in den Präsentationsdaten das Standardformat METAFILEPICT. Ich nehme an, dass man dort immer ein MetaFilePresentationObject vorfindet. Aus diesem Grund gehe ich hier auch nur auf diese Variante ein. 

' Die Presentation-Data (OLE1.0/MetaFilePresentationObject)
 
' Der erste Teil des PresentationObjectHeaders ist identisch mit dem erste Teil des ObjectHeaders.
' Der Type ole1ObjectHeaderShort kann auch bei den Präsentationsdaten verwendet werden.
 
' Zweiter Teil des PresentationObjectHeaders (optimiert für die METAFILEPICT-Klasse)
Private Type ole1MFPresentationObjectHeader2
lWidth As Long ' Breite der Metafile-Grafik (in logischen Einheiten/MM_Anisotropic)
lHeight As Long ' Höhe der Metafile-Grafik (in logischen Einheiten/MM_Anisotropic)
lPresentationDataSize As Long ' Größe der Grafikdaten in Bytes
End Type
 
 

Der Bereich der Präsentationsdaten beginnt mit demselben Aufbau wie der Bereich der Creating-Application-Data. Nach dem beliebigen Long-Wert der Ersteller-Anwendung folgt die FormatID, die angibt, ob es sich um ein Standardformat oder um ein Clipboardformat handelt. Liegt ein Standardformat vor, dann folgt danach der Klassenname als LengthPrefixedAnsiString. Nach dem Klassennamen, der den Wert METAFILEPICT enthält, folgen die Long-Werte Width und Height, die Breite und Höhe der Metafile-Grafik beinhalten. Diese zwei Variablen gehören zur StandardPresentationObject-Struktur, die für alle drei Standardformate gilt. Die dritte Variable in meiner Typen-Definition, die den Wert PresentationDataSize aufnimmt, gehört zum MetaFilePresentationObject. Diese Größenangabe bezieht sich auf die reinen Grafikdaten, die direkt nach diesem Wert beginnen. 

Damit sind alle benötigten Strukturen definiert und es fehlt im Deklarationsbereich nur noch eine API-Funktion mit der man auf einen OLEStream im Arbeitsspeicher zugreifen kann. 

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal cbLength As Long)
 

Die nun folgende Funktion reduziert ein OLE-Objekt auf seine Präsentationsdaten. Dafür wird aus dem aktuellen Datensatz eines Recordsets der Inhalt eines OLE-Feldes ausgelesen und die Präsentationsdaten isoliert. Danach wird mit diesen Daten ein Bytestream im Format eines statischen Bildobjektes erstellt und an Stelle des OLE-Objekts im OLE-Feld gespeichert. Als Parameter erwartet die Funktion das Recordset mit dem aktuellen Datensatz und den Namen des OLE-Feldes. 

Public Function ReduceToPresentationDataPicture(oRS As DAO.Recordset, sFieldName As String) As Long
Dim tOLE1OHS As ole1ObjectHeaderShort, tOLE1OHE2 As ole1ObjectHeaderEmbd2
Dim tOH As OLEOBJHDR, tOLE1OHE2 As ole1MFPresentationObjectHeader2
Dim bClassName() As Byte, bAccClass() As Byte, bAccName() As Byte
Dim sCName As String, lPointer1 As Long, lLength As Long
Dim bOLEStream() As Byte, bWMF() As Byte
 
' Container-Application-Data laden
If GetAccOLEInfo(oRS(sFieldName), sCName, "", tOH) = False Then
Exit Function
End If
 
' Statische Bildobjekte ignorieren.
If tOH.lobjTyp = 3 Then
Exit Function
End If
 
' Nur bestimmte OLE-Objekt-Klassen berücksichtigen
Select Case Left(sCName, InStr(sCName & ".", ".") - 1)
Case "Excel", "Word", "PowerPoint", "AcroExch", "MSPhotoEd", "SnapshotFile", "Package"
' OLE-Objekt als Bytestream laden
lLength = oRS(sFieldName).FieldSize
ReDim bOLEStream(lLength - 1)
bOLEStream() = oRS(sFieldName).GetChunk(0, lLength)
 
' Objekt-Header der Creating-Application-Data füllen (1.Teil)
CopyMemory ByVal VarPtr(tOLE1OHS), bOLEStream(tOH.cbHdr), Len(tOLE1OHS)
 
' Verknüpfte OLE1-Objekte ignorieren
If tOLE1OHS.lFormatID = 1 Then
ReduceToPresentationDataPicture = 1
Exit Function
End If
 
' Objekt-Header der Creating-Application-Data füllen (2.Teil)
CopyMemory ByVal VarPtr(tOLE1OHE2), bOLEStream(tOH.cbHdr + Len(tOLE1OHS) + _
tOLE1OHS.lClassNameLength), Len(tOLE1OHE2)
 
' Pointer auf den Presentation-Data Bereich
lPointer1 = tOH.cbHdr + Len(tOLE1OHS) + tOLE1OHS.lClassNameLength + Len(tOLE1OHE2) + _
tOLE1OHE2.lNativeDataSize
 
' PresentationObjektHeader füllen (1.Teil)
CopyMemory ByVal VarPtr(tOLE1OHS), bOLEStream(lPointer1), Len(tOLE1OHS)
' PresentationObjektHeader füllen (2.Teil)
CopyMemory ByVal VarPtr(tOLE1POH2), bOLEStream(lPointer1 + Len(tOLE1OHS) + _
tOLE1OHS.lClassNameLength), Len(tOLE1POH2)
 
' Pointer auf die Metafile-Daten
lPointer1 = lPointer1 + Len(tOLE1OHS) + tOLE1OHS.lClassNameLength + Len(tOLE1POH2)
 
' Metafile als Bytestream laden
ReDim bWMF(tOLE1POH2.lPresentationDataSize - 1)
CopyMemory bWMF(0), bOLEStream(lPointer1), tOLE1POH2.lPresentationDataSize
 
' Die Präsentationsdaten wurden isoliert und das statische Bildobjekt kann nun erstellt werden.
 
' Klassenname des StandardPresentationObjects
bClassName = StrConv("METAFILEPICT" & Chr(0), vbFromUnicode)
 
tOLE1OHS.lFormatID = 3
tOLE1OHS.lClassNameLength = UBound(bClassName) + 1
 
' Keine OLE-Klasse
ReDim bAccClass(0)
' Interner Name Bild (oder Picture)
bAccName = StrConv("Bild" & Chr(0), vbFromUnicode)
 
tOH.typ = 7189
tOH.lobjTyp = 3 ' statisches Objekt
tOH.ptSize.X = -1
tOH.ptSize.Y = -1
tOH.ibName = Len(tOH)
tOH.cchName = UBound(bAccName) + 1
tOH.cchClass = UBound(bAccClass) + 1
tOH.ibClass = tOH.ibName + tOH.cchName
tOH.cbHdr = 20 + tOH.cchClass + tOH.cchName
 
' Bildobjekt erstellen
ReDim bOLEStream(tOH.cbHdr + Len(tOLE1OHS) + tOLE1OHS.lClassNameLength + Len(tOLE1POH2) + _
tOLE1POH2.lPresentationDataSize + 4 - 1) ' + 4 Füllbytes
 
' Container-Application-Data
CopyMemory bOLEStream(0), ByVal VarPtr(tOH), Len(tOH)
lPointer1 = Len(tOH)
 
CopyMemory bOLEStream(lPointer1), bAccName(0), UBound(bAccName) + 1
lPointer1 = lPointer1 + UBound(bAccName) + 1
 
CopyMemory bOLEStream(lPointer1), bAccClass(0), UBound(bAccClass) + 1
lPointer1 = lPointer1 + UBound(bAccClass) + 1
 
' Presentation-Data
CopyMemory bOLEStream(lPointer1), ByVal VarPtr(tOLE1OHS), Len(tOLE1OHS)
lPointer1 = lPointer1 + Len(tOLE1OHS)
 
CopyMemory bOLEStream(lPointer1), bClassName(0), UBound(bClassName) + 1
lPointer1 = lPointer1 + UBound(bClassName) + 1
 
CopyMemory bOLEStream(lPointer1), ByVal VarPtr(tOLE1POH2), Len(tOLE1POH2)
lPointer1 = lPointer1 + Len(tOLE1POH2)
 
' Metafile-Daten
CopyMemory bOLEStream(lPointer1), bWMF(0), UBound(bWMF) + 1
lPointer1 = lPointer1 + UBound(bWMF) + 1
 
' Bildobjekt im OLE-Feld speichern
oRS.Edit
oRS.Fields(sFieldName).AppendChunk bOLEStream
oRS.Update
 
ReduceToPresentationDataPicture = 2
 
Case Else
ReduceToPresentationDataPicture = 1 ' Klasse wird nicht behandelt
 
End Select
 
End Function
 
 

Mit dieser Unterfunktion werden die Container-Application-Data von Microsoft Access geladen. Klassenname und Objektname liegen als Ansi-Strings vor und müssen zu Unicode-Strings konvertiert werden. Konnten die Container-Application-Data erfolgreich geladen werden, dann liefert die Funktion den Wert True zurück. 

Private Function GetAccOLEInfo(oField As DAO.Field, sOClass As String, _
sOName As String, tOH As OLEOBJHDR) As Boolean
Dim bStream() As Byte
 
If Nz(oField.FieldSize, 0) < Len(tOH) Then
Exit Function
End If
 
' Container-Application-Header füllen
ReDim bStream(Len(tOH) - 1)
bStream() = oField.GetChunk(0, Len(tOH))
CopyMemory ByVal VarPtr(tOH), bStream(0), Len(tOH)
 
If tOH.typ <> 7189 Then
Exit Function
End If
 
If oField.FieldSize < tOH.ibName + tOH.cchName Then
Exit Function
End If
 
' Klassennamen laden wenn vorhanden (null-terminierter Ansi-String)
If tOH.cchClass > 1 Then
sOClass = StrConv(oField.GetChunk(tOH.ibClass, tOH.cchClass - 1), vbUnicode)
End If
 
' Objektnamen laden wenn vorhanden (null-terminierter Ansi-String)
If tOH.cchName > 1 Then
sOName = StrConv(oField.GetChunk(tOH.ibName, tOH.cchName - 1), vbUnicode)
End If
 
GetAccOLEInfo = True
 
End Function
 
 

Das Demo soll in erster Linie den Aufbau der Daten in OLE-Feldern beleuchten, ob die Funktionalität als solches zweckmäßig ist, sei mal dahingestellt. Statische Bildobjekte im Metafile-Format werden langsamer angezeigt, als die Präsentationsbilder der OLE-Objekte. Das zeigt sich insbesondere, wenn Bitmaps im Metafile vorhanden sind. Das Metafile-Format eignet sich nicht sonderlich zum Speichern von Bitmaps. Möchte man aber z.B. den formatierten Text eines Word-Dokuments anzeigen, dann könnte man mit statischen Bildobjekten einiges an Speicherplatz einsparen. Laden Sie sich das Demo herunter, dann steht Ihnen ein lauffähiges Beispiel zur Verfügung. 

Damit komme ich zum Ende und möchte noch eine Anmerkungen anschließen. Bei dem, was ich über den Aufbau der Daten in OLE-Feldern geschrieben habe, muss man zwei Dinge unterscheiden. Die OLE-Objekte gehören zum Open Specification Promise (OSP), was bedeutet, dass das Format detailiert von Microsoft dokumentiert wurde und diese Dokumentation öffentlich zugänglich ist. Microsoft Access dagegen gehört nicht zum OSP und es gibt keine öffentliche Dokumentation zu den Details. Aus diesem Grund sind nur meine Angaben zu den OLE-Objekten fundiert. Alle Access-spezifischen Angaben basieren auf empirischen Erfahrungswerten (reverse engineering), bzw. inoffiziellen Dokumentationen aus dem Netz. Die meisten Informationen zum Thema OLE-Felder findet man bei Stephen Lebans. Den Open Specification Promise darf man übrigens nicht mit Open Source verwechseln, die Formate unterliegen dem Patentschutz. Microsoft gestattet aber eine Implementierung, so lange sie den Regeln der OSP entspricht. 

Download für Access 2002/2003
ReduceToPresentationPicture-Demo (Access 2002)
Download für Access 2000
ReduceToPresentationPicture-Demo (Access 2000)
(downgrade)
Download für Access 97
ReduceToPresentationPicture-Demo (Access 97)
(downgrade)
Installation:
Einfach runterladen und anschauen.
©   Oliver Straub  -  Fliegenstr. 6  -  80337 München