Kommunikation nach außen bei einem plattformunabhängigen Grafikframework
-
Mal angenommen, mal will ein kleines Grafik-Wrapper-Framework schreiben, das Schnittstellenmäßig sowohl plattformunabhängig, als auch unabhängig vom darunterliegenden eigentlichen Grafikframework sein soll. Irgendwas ganz einfaches, z.B.:
Pseudocode: class Bitmap { public void LoadFromFile(String file); public void LoadFromBitmap(Bitmap bitmap); public void DrawBitmap(Bitmap bitmap, int x, int y, Color bgColor); // Zeichnet ein fremdes Bitmap auf das eigene public void SetPixel(int x, int y, Color color); public Color GetPixel(int x, int y); // u.s.w. }
Aber wie realisiere ich jetzt die Kommunikation mit der Außenwelt? Wenn ich zum Beispiel das Bitmap auf ein Fenster zeichnen will. Unter Windows mit GDI hab ich in der Bitmap-Klasse eine private Variable vom Typ HBITMAP. Unter Linux hab ich irgendwas ganz anderes. Und wenn ich mich unter Windows entscheide, nicht mehr direkt die GDI zu verwenden, sondern irgendeine andere Grafiklib, dann hab ich auch einen ganz anderen Datentyp für meine private Variable. Wie kann meine Klasse also mit der Außenwelt kommunizieren?
HBITMAP GetBitmap();
wäre an sich die Lösung, wenn ich eine Windows-GDI-Wrapperklasse hätte programmieren wollen. Aber was schreib ich bei einer nach außen plattformunabhängigen Klasse?
-
Das PIMPL-Idiom ist die Lösung.
-
rüdiger schrieb:
Das PIMPL-Idiom ist die Lösung.
Wenn ich mir den Klassenauszug anschaue hat die Bibliothek noch ganz andere Design-Probleme.
Ganz einfach geht es so: biete nur abstrakte Klassen an und implementiere für jede Plattform eine konkrete Klasse. Objekte von diesen Klassen werden dann über Factories angelegt. Das verhindert z.B. diese extrem hässlichen Funktionen die du in deiner Klasse hast wie LoadFile *brrrr*
-
rüdiger schrieb:
PIMPL-Idiom
die c++ compilation firewall? *fg*
OP: such mal nach dem 'bridge' design pattern, das könnte vielleicht passen.
-
C++-Progger schrieb:
rüdiger schrieb:
Das PIMPL-Idiom ist die Lösung.
Wenn ich mir den Klassenauszug anschaue hat die Bibliothek noch ganz andere Design-Probleme.
[...] Das verhindert z.B. diese extrem hässlichen Funktionen die du in deiner Klasse hast wie LoadFile *brrrr*eigentlich ist es aber auch egal ob die klasse über
bitmap b; b.loadfile()
oderbitmap b = bitmap::loadfile()
angesprochen wird.;fricky schrieb:
rüdiger schrieb:
PIMPL-Idiom
die c++ compilation firewall? *fg*
die Trennung hat echt nichts mit C++ zu tun.
-
rüdiger schrieb:
Das PIMPL-Idiom ist die Lösung.
Was hat das mit meinem Problem zu tun? Bei dem Link geht's doch um was ganz anderes, nämlich um die Frage, wie ich private Details im Klassenheader vermeide, so dass bei Änderungen in der Implementierung nicht alle Dateien, die den Header einbinden, neu kompiliert werden müssen. Mein Problem war aber: Wie kann ich mit ein und derselben Klassendeklaration auf mehreren Plattformen mit der Außenwelt kommunizieren? Wie kann mit derselben Bitmapwrapperklasse sowohl einem Windows-, als auch einem Linux-Fenster die entsprechenden Daten geben, die es braucht, um das Bild darzustellen?
C++-Progger schrieb:
Wenn ich mir den Klassenauszug anschaue hat die Bibliothek noch ganz andere Design-Probleme.
Na, sag mal an, welche.
C++-Progger schrieb:
Ganz einfach geht es so: biete nur abstrakte Klassen an und implementiere für jede Plattform eine konkrete Klasse. Objekte von diesen Klassen werden dann über Factories angelegt.
Wie soll mir das helfen? Ich brauche in meiner Klasse eine Funktion
GetBitmapData
zum Weiterverarbeiten des Bildes auf einer entsprechenden Oberfläche. In Windows wäre dasHWND GetBitmapData()
, damit man das Bild auf einem Fenster anzeigen kann, unter Linux wäre es irgendwas anderes. Auch soll das Grafikframework ausgetauscht werden können, ohne dass die Implementierung geändert werden muss. Das heißt, wenn ich als Implementierung ein Framework benutze, das ebenfalls nur ein Wrapper um etwas anderes ist und wo man gar nicht an das Handle rankommt, weil das Bitmap seinerseits gewrappt ist, soll meine Schnittstelle trotzdem unverändert bleiben.C++-Progger schrieb:
Das verhindert z.B. diese extrem hässlichen Funktionen die du in deiner Klasse hast wie LoadFile *brrrr*
Es heißt nicht LoadFile, sondern LoadFromFile. Und warum ist das hässlich?
;fricky schrieb:
rüdiger schrieb:
PIMPL-Idiom
die c++ compilation firewall? *fg*
OP: such mal nach dem 'bridge' design pattern, das könnte vielleicht passen.
Ich hab mir das jetzt mal bei Wikipedia durchgelesen. Aber die verschiedenen APIs zum Zeichnen (in dem Beispiel) funktionieren nur, weil die Funktionsdeklaration jedesmal gleich ist. Doch ich würde ja jedesmal andere Deklarationen brauchen:
void DrawBitmap(Bitmap bmp, HWND hwnd) // fürs Zeichnen auf Windows-Fenster void DrawBitmap(Bitmap bmp, DXSurface durcface) // für DirectX-Zeug (keine Ahnung, ob der Code jetzt richtig ist) void DrawBitmap(Bitmap bmp, LinuxWindow window) // für Linux-Sachen
Mein Problem ist: Die Klasse Bitmap soll zwar plattformunabhängig sein, aber wenn ein Programm mit der Bitmapklasse arbeiten will, braucht es ja die Rohdaten des Bildes. Die sind in Windows ein HWND, in .NET sind sie als eigene Bitmap-Klasse repräsentiert und in Linux sind sie sonst was. Und die muss ich irgendwie übergeben, ohne die Plattformunabhängigkeit kaputtzumachen.
-
Nachtrag: Ich hab mir mal angesehen, wie die SDL das macht. Das ist ja u.a. auch ein Grafikwrapperframework. Und die SDL hat einen eigenen Datentyp SDL_Surface. Wenn man den initialisiert und sein Bild da reinkopiert, baut er sich ein eigenes Fenster. Das heißt, wenn man mit der SDL ein Spiel programmieren will, muß man sich um den Aufbau der Oberfläche auch mit der SDL kümmern, nicht mit der WinAPI oder irgendeinem GUI-Framework.
Aber so will ich das nicht machen. Meine Klassen sollen wirklich nur das ganze Bitmap- und Grafikzeug an sich behandeln. Wo das dann erscheint, ob auf einem Fenster, einem MFC-Dialog oder sonst wo, das soll dann von außen geregelt werden.
-
Dann kann es aber im Interface nimmer 100% plattformunabhängig sein, Freund Husky.
Grundsätzlich sehe ich drei Möglichkeiten:-
Das Interface per #ifdef auf jeder Plattform leicht anders machen - also auf Windows dann eben nen HBITMAP zurückgeben etc.
-
Abstrakte Basis-Klasse + mehrere konkrete Implementierungs-Klassen für verschiedene Plattformen. Die abstrakte Klasse ist dann 100% plattformunabhängig und bietet 90% der Funktionen an, die konkreten Implementierungs-Klassen sind nicht plattformunabhängig, und bieten den Rest an - eben das GetBitmapData() etc. Im Prinzip das was schon vorgeschlagen wurde, nur dass die Implementierungs-Klassen auch öffentlich sind (=ein öffentliches Interface haben). So arbeiten z.B. viele 3D Engines, vermutlich auch viele 2D Engines, und vielleicht sogar die eine oder andere Image-Library.
2.1) Du hast doch nur eine Image-Klasse, mit immer gleichem public Interface, deren "Innereien" aber je nach Plattform unterschiedlich sind. Dazu bastelst du dann ala GLU/GLUT Plattformabhängige Hilfsfunktionen/-klassen, mit deren Hilfe man die "Bitmap" Objekte z.B. auf Fenster zeichnen kann. Diese Hilfsfunktionen/-klassen haben dann z.B. einfach per "friend" Zugriff auf die Innereien der "Bitmap" Klasse.
- Du bietest einfach kein HBITMAP an, sondern nur einen void* oder char* der auf die Bilddaten zeigt (+ Infos darüber in welchem Format die vorliegen etc.). Der "aussen" Code der das anzeigen regeln soll muss dann einfach etwas mehr machen. Das wäre z.B. was die Boost/Adobe Image Library macht.
1, 2 & 3 kann man natürlich auch irgendwie kombinieren - die "void*" Funktionen landen dann z.B. mit in der Basis-Klasse. Ein Beispiel dafür wäre GDI+. Gibt zwar nur die Implementierung für Windows, aber der GDI+ "Kern" bzw. der grossteil vom Interface ist total von GDI entkoppelt. Genauso wie vermutlich ein Grossteil der Implementierung von GDI unabhängig ist (was auch der Grund dafür sein dürfte, dass GDI+ so elendiglich langsam ist). Zusätzlich werden aber "Verbindungen" zu GDI zur Verfügung gestellt, mit deren Hilfe man hübsch in DCs malen kann etc.
p.S.: statt "void* GetImageData()" kannst du natürlich auch Funktionen machen die Teile vom Bitmap Objekt in ein vom User verwaltetes Array kopieren, oder in der umgekehrten Richtung. Dadurch müsstest du keinen Zeiger auf die Innereien der Klasse hergeben. Und könntest evtl. beim Kopieren konvertieren - wodurch das interne Datenformat dann vom öffentlichen "Schnittstellen-Datenformat" entkoppelt würde. Natürlich wird dadurch auch alles langsamer.
p.p.S.: mit "andere" Probleme ist vermutlich gemeint, dass deine "LoadFromXxx" Funktionen wohl besser Konstruktoren/Factory-Methoden sein sollten, und nicht normale Mutatoren.
-
-
Husky schrieb:
;fricky schrieb:
rüdiger schrieb:
PIMPL-Idiom
die c++ compilation firewall? *fg*
OP: such mal nach dem 'bridge' design pattern, das könnte vielleicht passen.
Ich hab mir das jetzt mal bei Wikipedia durchgelesen. Aber die verschiedenen APIs zum Zeichnen (in dem Beispiel) funktionieren nur, weil die Funktionsdeklaration jedesmal gleich ist. Doch ich würde ja jedesmal andere Deklarationen brauchen:
void DrawBitmap(Bitmap bmp, HWND hwnd) // fürs Zeichnen auf Windows-Fenster void DrawBitmap(Bitmap bmp, DXSurface durcface) // für DirectX-Zeug (keine Ahnung, ob der Code jetzt richtig ist) void DrawBitmap(Bitmap bmp, LinuxWindow window) // für Linux-Sachen
nee, brauchste doch nicht. du kannst es z.b. mit einem sogenannten 'handle' machen, das deine lib selbst definiert, so wie in windoze z.b. ein 'HDC' oder in Java ein Graphics2D-objekt eine zeichenfläche abstrahieren. deine library weiss ja irgendwie, auf welchem host-system sie sich gerade befindet und kann dann so'n virtuelles zeichenblatt in was systemspezifisches umsetzten.
DrGreenthumb schrieb:
;fricky schrieb:
rüdiger schrieb:
PIMPL-Idiom
die c++ compilation firewall? *fg*
die Trennung hat echt nichts mit C++ zu tun.
die trennung nicht, aber dieses wörtchen 'pimpl' stammt doch aus der c++ bastelkiste. wie heisst es denn allgemein? müsste ja eigentlich ein bekanntes 'structural' design pattern sein, wenn ich mich nicht irre.
-
PIMPL ist kein Pattern, sondern eine "Spezialisierung" des Handle/Body Idioms.
-
hustbaer schrieb:
PIMPL ist kein Pattern, sondern eine "Spezialisierung" des Handle/Body Idioms.
dann hab ich ja gestern richtig geraten.
The interface object is the “handle” known and used by the client; while the implementation object, or “body”, is safely encapsulated to ensure that it may continue to evolve, or be entirely replaced (or shared at run-time.
-
nein, eine bridge ist was anderes fricky.
-
hustbaer schrieb:
nein, eine bridge ist was anderes fricky.
nun mach's nicht so spannend. was ist der entscheidende unterschied? die bridge macht doch genau das, nämlich ein abstraktes 'handle' anzubieten, hinter dem die funktionalität bzw. implementierung des 'body' versteckt ist.