Bitmaps und Speicher-DCs

In dieser Lektion wird nur die Darstellung von Bitmaps aus Dateien behandelt. Später im Kurs lernen Sie auch noch Bitmaps kennen die in Ressourcen abgelegt sind. Außerdem wird auf Grund der am Markt vorhandenen Grafikkarten davon ausgegangen, dass für die Anzeige mindestens die High-Color Darstellung (65535 Farben) eingestellt ist. Bei niedriger Farbauflösung (<= 256 Farben) sind zusätzliche Schritte notwendig um eine Bitmap soweit wie möglich farbgetreu wiedergeben zu können da hier die Bitmap-Farben in einer Farbpalette abgelegt sind.

Sehen wir uns zuerst einmal an, was Bitmaps sind. Bitmaps sind Dateien in denen Bilder in digitalisierter Form abgespeichert sind. Jedes Pixel (Punkt) der Bitmap besteht aus einem (monochrome Bitmap) oder 4, 8, 16, 24 oder 32 Bits, je nach Farbtiefe der Bitmap. Bitmap-Dateien haben meistens die Extension BMP oder auch DIP. WINDOWS unterstützt direkt die Darstellung von Bitmaps.

Wollen Sie andere Grafikdateien wie z.B. GIF- oder JPG-Dateien darstellen, so müssen Sie die dafür notwendigen Funktionen selbst schreiben; WINDOWS unterstützt diese Grafikformate nicht von Haus aus.

Wenn wir in dieser Lektionen von Bitmaps sprechen, dann sind damit immer so genannte device independend bitmaps (DIB) gemeint. Es gibt noch ein älteres Format für Bitmaps, die device dependend bitmaps (DDB), das aber fast nicht mehr im Einsatz ist. Einer der Hauptunterschiede zwischen dem DIB- und DBB-Format besteht darin, dass bei DIBs die Farbinformationen mit in der Datei abgespeichert ist. Dadurch ist jedes GDI-fähige Ausgabegerät in der Lage, DIBs farbrichtig darzustellen.

Fast alle BMP-Dateien sind DIB-Dateien. DIB-Dateien besitzen in der ersten beiden Bytes die Kennung 'BM'. Sie können dies nachprüfen, indem Sie im Editor des VC++ eine BMP-Datei in binärer Darstellung einmal öffnen.

Kommen wir nun zur Darstellung von Bitmaps. Leider besitzt WINDOWS keine Funktion wie z.B. DrawBitmap(...) um eine Bitmap darzustellen. Die Darstellung einer Bitmap muss in mehreren Schritten erfolgen.

Zuerst muss die Bitmap natürlich geladen werden. Dazu stellt WINDOWS die API-Funktion LoadImage(...) zur Verfügung. Die MFC enthält leider keine äquivalente CDC-Methode. Da LoadImage(...) nicht nur zum Laden von Bitmaps verwendet werden kann, müssen beim Laden einer Bitmap mehrere Parameter entsprechend gesetzt sein. Der erste Parameter ist das Instanzen-Handle des Moduls, in dem die Bitmap abgelegt ist. Wollen Sie die Bitmap aus einer Datei laden, so geben Sie hier den Wert '0' an. Mit Hilfe dieses Parameter wäre es aber auch möglich, Bitmaps aus DLLs zu laden. Danach folgt der Name der zu ladenden Bitmap. Den nächsten Parameter für den zu ladenden Image-Type müssen Sie auf den den Wert IMAGE_BITMAP setzen. Die dann folgenden Parameter für die Bestimmung der Auflösung sind mit 0 anzugeben. Der letzte Parameter der Funktion bestimmt dann noch, wie das Image zu laden ist. Für das Laden einer Bitmap aus einer Datei ist hier die Konstanten LR_LOADFROMFILE anzugeben. Wollen Sie die Bitmap schwarz/weiß darstellen, können Sie zusätzlich noch die Konstante LR_MONOCHROME mit angeben. Die weiteren Konstanten entnehmen Sie bitte wieder der Online-Hilfe. Als Returnwert liefert die Funktion ein allgemeines Handle. Sie müssen diese Handle in der Regel auf den gewünschten Typ konvertierten; in unserem Fall auf den Typ HBITMAP.

Wird die geladene Bitmap nicht mehr benötigt, so sollte sie mit der API-Funktion DeleteObject(...) aus dem Speicher entfernt werden. Als Parameter geben Sie der Funktion das von LoadImage(...) erhaltene Handle mit.

Beginn wir die Übung zu diesem Kapitel damit, eine beliebige Bitmap zu laden. Erstellen Sie sich dazu zunächst eine Standard SDI-Anwendung. Geben Sie dem Projekt den Namen DispBmp. Damit Sie später im Datei-Öffnen Dialog auch nur BMP-Dateien angezeigt bekommen, klicken Sie bitte im Schritt 4 des Anwendungs-Assistenten den Button Weitere Optionen... an. Geben Sie dann, wie im Bild unten dargestellt, im Feld Dateierweiterung die Extension bmp ein. Die restlichen Schritte führen Sie wie gewohnt durch.

Dateierweiterung auf BMP umstellen

Nach dem der Anwendungsrahmen erstellt ist, fügen Sie der Klasse CDispBmpDoc eine privat Membervariable m_hBitmap vom Typ HBITMAP hinzu. Initialisieren Sie die Membervariable im Konstruktor der Klasse mit dem Wert NULL.

Nun kann die Bitmap geladen werden. Versuchen Sie es einmal selbst. Sie wissen doch hoffentlich noch, in welcher Methode Sie die Bitmap laden müssen? Die Methode Serialize(...) des Dokuments erhält eine Referenz auf das mit der zu ladenden Datei verknüpfte CArchive-Objekt.

Lösung zum Laden einer Bitmap

CDispBmpDoc::~CDispBmpDoc()
{
    // Falls noch Bitmap geladen ist,
    // diese auch wieder freigeben
    if (m_hBitmap != NULL)
        ::DeleteObject(m_hBitmap);
}
....
void CDispBmpDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // ZU ERLEDIGEN: Hier Code zum Speichern einfügen
    }
    else
    {
        // ZU ERLEDIGEN: Hier Code zum Laden einfügen
        // Falls eine Bitmap bereits geladen wurde
        // dann Bitmap loeschen
        if (m_hBitmap != NULL)
            ::DeleteObject(m_hBitmap);
        // Neue Bitmap laden und auch Fehler abfangen
        m_hBitmap = (HBITMAP)::LoadImage(0,ar.m_strFileName,
                                        IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
        if (m_hBitmap==NULL)
            AfxMessageBox("Kann Bitmap nicht öffnen!",MB_OK);
    }
}

Haben Sie bei Ihrer Lösung im Destruktor auch die geladene Bitmap wieder gelöscht?

Das eigentliche Laden der Bitmap sollte nicht besonders schwer gewesen sein, wenn Sie den Namen der Bitmap im CArchive-Objekt gefunden haben. Und denken Sie immer daran, eine bereits geladene Bitmap vor dem Laden einer neuen Bitmap auch wieder zu löschen.

Ende der Lösung

Sie können das Programm nun auch einmal starten. Selbstverständlich wird im Augenblick noch keine Bitmap dargestellt, den der Teil fehlt uns noch, aber im Datei-Öffnen Dialog sollte als Standard-Dateityp nun BMP stehen.

Als nächstes wird die Dokumentenklasse noch um eine public-Methode erweitert, die dem Ansichtsobjekt den Zugriff auf die geladene Bitmap ermöglicht. Fügen Sie zur Dokumentenklasse noch die nachfolgende Methode hinzu:

HBITMAP& CDispBmpDoc::GetBitmap()
{
    return m_hBitmap;
}

Damit ist die Anpassung der Dokumentenklasse abgeschlossen und wir kommen zur Darstellung der geladenen Bitmap. Wie alle anderen Ausgaben erfolgt auch die Darstellung der Bitmap in der OnDraw(...) Methode des Ansichtsobjektes. Zuerst holen wir uns in dieser Methode das Handle die geladene Bitmap.

Erweitern Sie die Methode OnDraw(...) der Ansichtsklasse CDispBmpView wie folgt:
void CDispBmpView::OnDraw(CDC* pDC)
{
    CDispBmpDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    // ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten hinzufügen

    BITMAP BitmapParam;
    HBITMAP hBitmap;

    // Handle auf geladene Bitmap holen
    hBitmap = pDoc->GetBitmap();
    if (hBitmap == 0)
        return;
// keine Bitmap geladen!

}

Was es mit dem ebenfalls eingefügten Datum BitmapParam auf sich erfahren Sie gleich noch.

Da WINDOWS, wie Sie bereits erfahren haben, leider keine Funktion DrawBitmap(...) besitzt muss zur Darstellung der geladenen Bitmap ein 'kleiner' Umweg gegangen werden. Zur Darstellung einer Bitmap benötigen Sie immer einen so genannten Speicher-DC. Ein Speicher-DC erlaubt es anstelle auf einem Ausgabegerät, wie z.B. dem Bildschirm, in den Speicher 'zu zeichnen'. Doch warum sollte man überhaupt in den Speicher zeichnen und nicht gleich auf dem Bildschirm? Stellen Sie sich dazu einmal vor, Sie haben eine relativ komplexe Ausgabe die aus vielen einzelnen Zeichenanweisungen besteht. Muss nun der Inhalt des Fensters neu darstellt werden, so werden die einzelnen Zeichenanweisungen der Reihe nach abgearbeitet und der Anwender sieht quasi den Aufbau des Fensters. Anders dagegen wenn Sie im Speicher zeichnen. Da der Anwender das Zeichnen im Speicher nicht auf dem Bildschirm sieht, können Sie dort den Fensterinhalt sozusagen versteckt aufbauen. Erst wenn der gesamte 'Fensterinhalt' gezeichnet wurde, kopieren Sie diesen auf den Bildschirm. Für den Anwender hat dies dann den Anschein, als ob das gesamte Fenster auf einmal gezeichnet wird. Wie der Speicherinhalt ins Fensters der Anwendung gelangt erfahren Sie gleich. Mehr zu dem Speicher-DC erfahren Sie auf den Tipps&Tricks Seiten zu diesem Kapitel bei den Animationen; dort spielt er eine wichtige Rolle.

Doch sehen wir uns nun an wie ein Speicher-DC erstellt wird. Ein Speicher-DC wird immer als Kopie eines bestehenden DC erstellt. Dazu wird zuerst wie gewohnt ein neues CDC-Objekt erstellt und dann dessen Methode CreateCompatibleDC(...) aufgerufen. Diese Methode erhält als Parameter ein Zeiger auf das CDC-Objekt, dessen Einstellungen für den Speicher-DC verwendet sollen. Sie erinnern sich doch noch: ein DC ist nicht etwa der Speicher in den gezeichnet wird, sondern er enthält nur die zum Zeichnen verwendeten Einstellungen.

Fügen wir die Erstellung des Speicher-DC in unser Beispiel ein.
void CDispBmpView::OnDraw(CDC* pDC)
{
    ....
    if (hBitmap == 0)
        return; // keine Bitmap geladen!
    // Neuen DC im Speicher erstellen
    CDC *pMemDC = new CDC;
    BOOL bRetCode = pMemDC->CreateCompatibleDC(pDC);
    ASSERT(bRetCode);

}

Der so erstellte Speicher-DC hat jedoch noch einen 'kleinen' Nachteil: er besteht nur aus einem monochromen Pixel. Um den zum Zeichnen notwendigen Speicher zu reservieren, müssen Sie zuerst eine entsprechende Speicher-Bitmap erstellen. Da wir aber schon eine Bitmap in den Speicher geladen haben, brauchen wir nur diese mit dem Speicher-DC verbinden. Und hier kommt nun wieder die MFC ins Spiel. Die MFC-Klasse CBitmap, die erst später beim Laden von Bitmap-Ressourcen ausführlicher behandelt wird, enthält die statische Methode FromHandle(...), die ein WINDOWS Bitmap-Handle (HBITMAP) in ein CBitmap-Objekt konvertiert. Als Parameter erhält die Methode das Handle auf die zu 'konvertierende' Bitmap und liefert als Returnwert einen Zeiger auf das damit assoziierte CBitmap-Objekt.

Dieses CBitmap-Objekt ist nur solange gültig, bis die Anwendung das nächste Mal in die Idle-Verarbeitung eintritt. Gehen Sie unter keinen Umständen davon aus, dass das CBitmap-Objekt zwischen zwei Nachrichten immer noch gültig ist.

Anschließend muss nur noch das CBitmap-Objekt über die CDC-Methode SelectObject(...) des Speicher-DC selektiert werden. Durch den Aufruf von SelectObject(...) wird der für die Bitmap reservierte Speicher mit dem Speicher-DC verknüpft.

Bauen wir die eben beschriebenen Schritte wieder ins Beispiel ein.
void CDispBmpView::OnDraw(CDC* pDC)
{
    ....
    ASSERT(bRetCode);

    // temp. MFC Bitmap-Objekt fuer geladene Bitmap erstellen
    CBitmap* pCBitmap = CBitmap::FromHandle(hBitmap);
    // Groesse und Anzahl der darstellbaren Farben des Speicher-DCs
    // auf Bitmap-Aufloesung einstellen
    pMemDC->SelectObject(pCBitmap);
    ....
}

Und damit sind wir auch fast schon fertig. Um die im Speicher befindliche Bitmap jetzt noch ins Fenster zu kopieren wird die CDC-Methode BitBlt(...) aufgerufen. Die BitBlt(...) Methode (sprich: Bit-Blit) ist in der Lage, einen beliebig großen, rechteckigen Bereich innerhalb eines DC oder auch zwischen zwei DCs zu kopieren. BitBlt ist übrigens die Abkürzung für Bit-Block-Transfer. Damit Sie die Arbeitsweise dieser Methode besser verstehen, ist nachfolgend ausnahmsweise einmal das Prototyping der Methode wiedergeben:

BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC,
             int xSrc, int ySrc, DWORD dwRop );

Die Parameter x/y geben hierbei die linke obere Koordinate des Zielbereichs an und nWidth/nHeight die Breite und Höhe des zu kopierenden Bereichs. pSrcDC ist ein CDC-Zeiger auf das CDC-Objekt, aus dem kopiert wird. xSrc und ySrc geben die linke obere Koordinate der Bitmap im Quellbereich an. dwRop bestimmt wie der Kopiervorgang erfolgen soll. Hierfür sind diverse Konstanten definiert. In den Tipps&Tricks zu diesem Kapitel erfahren Sie mehr darüber. Um die Bitmap unverändert vom Quell- in den Zielbereich zu kopieren geben Sie hier die Konstante SRCCOPY an.

Bis auf zwei Parameter könnten Sie alle Parameter der Methode bestimmen. Soll z.B. die Bitmap links oben im Fenster dargestellt werden, so ist für x und y der Wert '0' anzugeben. Die beiden folgenden Parameter nWidth und nHeight können Sie im Augenblick (noch) nicht bestimmen, da Sie ja die Größe der geladenen Bitmap nicht kennen. Da die Bitmap vom Speicher-DC ins Fenster kopiert werden soll, ist für pSrcDC der vorhin erstellte Speicher-DC einzusetzen. Für xSrc und ySrc ist ebenfalls der Wert '0' einzusetzen da ja die komplette Bitmap kopiert werden soll. Für dwRop geben Sie, wie erwähnt, SRCCOPY an.

Sehen wir uns zum Schluss noch an, wie Sie an die Kenngrößen einer Bitmap gelangen. Um an die Kenngrößen einer mit einem CBitmap-Objekt verknüpften Bitmap zu gelangen, ist die CBitmap-Methode GetBitmap(...) aufzurufen. Als Parameter erhält die Methode eine Zeiger auf eine Struktur vom Typ BITMAP die folgenden Aufbau besitzt:

typedef struct tagBITMAP
{
    int bmType;
    int bmWidth;
    int bmHeight;
    int bmWidthBytes;
    BYTE bmPlanes;
    BYTE bmBitsPixel;
    LPVOID bmBits;
} BITMAP;

Wir wollen uns hier nicht alle Elemente der Struktur ansehen. Worauf es uns ankommt sind die Elemente bmWidth und bmHeight. Sie enthalten die uns noch fehlende Breite und Höhe der Bitmap.

Versuchen jetzt wieder selbst einmal, das Beispiel zu vervollständigen. Das einzige was Sie 'nur' noch tun müssen, ist die geladene Bitmap mit der erwähnten Methode BitBlt(...) aus dem Speicher ins Fenster zu kopieren.

Lösung zum Anzeigen einer Bitmap

void CDispBmpView::OnDraw(CDC* pDC)
{
    ....
    // Groesse und Anzahl der darstellbaren Farben des Speicher-DCs
    // auf Bitmap-Aufloesung einstellen
    pMemDC->SelectObject(pCBitmap);

    // Parameter der Bitmap auslesen
    pCBitmap->GetBitmap(&BitmapParam);
    // Bitmap-Daten nun aus dem Speicher auf den Bildschirm kopieren
    pDC->BitBlt(0,0,BitmapParam.bmWidth,BitmapParam.bmHeight,pMemDC,0,0,SRCCOPY);
    delete pMemDC;
}

Haben Sie vielleicht bei einem Ihrer Versuch folgende Anzeige im Debuggerfenster erhalten:

Detected memory leaks!
Dumping objects ->
D:\MFCKurs\Work\DispBmp\DispBmpView.cpp(66) : {111} client block at 0x00771560, subtype 0, 16 bytes long.
a CDC object at $00771560, 16 bytes long
Object dump complete.
Thread 0xFFFAEDCB wurde mit Code 0 (0x0) beendet.
Das Programm "D:\MFCKURS\WORK\DispBmp\Debug\DispBmp.exe" wurde mit Code 0 (0x0) beendet.

Dann haben Sie vergessen, den in der OnDraw(...) Methode erstellten Speicher-DC auch wieder zu löschen! Sie können übrigens durch einen Doppelklick auf die Zeile mit der Angabe der Zeilennummer im Source-Code zu Stelle gelangen, an der der nicht mehr freigegebene Speicher reserviert wurde. In unserem Fall sollten Sie also dann auf der Zeile landen, in der der Speicher-DC erstellt wird.

Ende der Lösung

Sie können das Programm nun starten und dann beliebige Bitmaps laden. Aber Achtung! Speicher Sie eine geladene Bitmap-Datei im Beispiel niemals ab, da sie sonst danach leer ist.

Das fertig Beispiel finden Sie wieder unter 05GDI\DispBmp.

Damit beenden wir die Einführung in die Darstellung von Bitmap. Bei den Tipps&Tricks zu diesem Kapitel können Sie sich dann noch einige zusätzlichen Anwendungen mit Bitmaps ansehen. Ansonsten geht's weiter mit einem weiteren Grafikformat, dem WMF-Format.



Copyright © 2004

Senden Sie Emails mit Fragen oder Kommentaren zu dieser Website an: mailto:info@cpp-tutor.de
 Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein. Tel: +49 7129 6470