winapi

WinApi 32 - Resourcen, Icons und Menus
Resourcen

Im Anhang ist ein ausfürlicheres Kapitel zum Thema Resourcen im Zusammenhang mit VC++ und Borland vorgesehen.

Alle Code Beispiele in diesem und den folgeden Anschnitten sind nur zu Demonstrationszwecken und müssen nicht unbedingt compiliert werden, sie dienen vielmehr der Anschaung, als der Übersetzung mit einem Compiler.

Resourcen sind vordefinierte kleine Daten-Stückchen, die in binärer Form in einem kleinen Anhang mit in der ausführbaren Exe gespeichert werden. Sie legen das Aussehen von Dialogen, Menus und Buttons fest. Resourcen können entweder mit einem Script (Endung .rc) oder mit einem Visuellen Edior erstellt werden. Viele der vorhandenen Visuellen Editoren (Visual Studio und so weiter) haben viele Funktionen, speichern aber keine Skriptdatei, die nachbearbeitet werden könnte. So können oft nicht alle Funkrionen ausgeschöpft werden weil der Visuelle Editor diese nicht unterstützt. Ausdiesem Grunde wird es im Folgenden um die Resourcen Scripte gehen, die (fast) jeder Compiler verarbetein kann.

Jedoch haben sich mit der Zeit unglückicher Weise unterscheidliche Standarts entwichlet. Viele große Compiler Herstellen haben eigene Resorcen Scripts und Dateieformate entwickelt. Im Folgenden will ich versuchen so viele Gemeinsamkeiten wie möglich darzustellen.

Der Resourcen Editor von MSVC++ (Microsoft Visual C++) funktioniert komplett anders als das Editieren der Resourcen per Hand. Der MSVC++ Editor zwingt die Daten in ein MS eigenes Format und behandelt alle Daten selbst. Normaler weise muß man sich nicht damit abgeben diese .rc Datei per Hand zu editieren oder gar neu zu erstellen, vielmehr kommt es darauf an zu wissen wie man die entsprechenden Stellen ändern muß. Eine weniger angenehme Eigenschaft ist, dass der MSVC++ Editor immer den Namen "resource.h" für das Resourcen Header File vergibt. Das ist besonders schlecht, wenn man es anders nennen möchte. (Weitere Hinweise dazu finden ich im Anhang.)

Als alleresrtes Beispiel nehmen wir mal folgedes Beispiel mit einem ICON.

#include "resource.h"

IDI_MYICON ICON "my_icon.ico"

Das ist die ganze Datei, die notwendig ist ein ICON zu definieren. IDI_MYICON ist der Identifier der Resource, ICON ist der Type und "my_icon.ico" ist der Name der externen Datei, in der sich das ICON befindet. Dieses Beispiel sollte mit jedem Compiler funktionieren.

Wozu wird die Datei #include "resource.h" eingezogen? Das Program braucht einen Weg, die ICONS voneinander zu unterscheiden. Der beste Weg dafür ist eine eindeutige ID (IDI_MYICON). Diese ID's werden in der Datei "resource.h" für die Resource und den Quelltext geleichermaßen bereit gestellt (um an einer gemeinsamen Stelle gehalten zu werden). So braucht nur eine Stelle geändert werden, wenn sich eine ID ändert, dazukommt oder eine ganz wegfällt.

#define IDI_MYICON  101

Wie man in diesem Beispiel sieht wird dem Icon ein Name zugeordnet: IDI_MYICON mit dem Wert: 101. Der Name ist nur eine leichter zu lesende Ersetzung für 101. Wann immer diese ICON gemeint ist reicht es 101 als ID anzugeben, aber IDI_MYICON ist besser zu lesen und zu merken.

Nehmen wir an, es soll noch ein kleines MENU hinzugefügt werden:

#include "resource.h"

IDI_MYICON ICON "my_icon.ico"

IDR_MYMENU MENU
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "E&xit", ID_FILE_EXIT
    END
END
Wieder ist IDR_MYMENU der Name der neuen Resource und MENU ist der Type. Ein wichtiger Punkt ist das BEGIN und END. Manche Resourcen Compiler verwenden auch ein { und } wie in der Sprache C üblich. Die größt mögliche Kompartibilität erreicht man mit BEGIN und END. Sollte der Compiler auch {} unterstützen, ist es jedem selbst überlassen diese alternative Variante zu verwenden.

Was hat das mit dem & auf sich ? Dieses Zeichen steht vor dem Hotkey. In unserem Beispiel sind das ALT-F für File und ALT-X für EXIT. Diese Hotkeys sind nur dann aktiv, wenn das Menu entsprechen sichtbat ist.

Ferner muss ein weiterer Identifier in der Header Datei hinzugefügt werden, ID_FILE_EXIT, um später im Programm verwendet werden zu können. Grundsätzlich kann jeder Name vergeben werden, aus Gründen der leichteren Lesbarkeit ist es immer von Vorteil möglichst aussagekräftige Namen zu vergeben. ID steht für Identifier, FILE für Menu File und EXIT für Exit. Auf diese Art kann man dem Identifier gleich an sehen, welchem Ereingins er zugeordnet ist.

#define IDI_MYICON  101

#define ID_FILE_EXIT	4001

Den Überblick über alle IDs zu behalten, kann besonders bei großen Projekten etwas umfangreich werden, aus diesem Grund verwenden die meißten Programierer visual resource editors, welche sich darum kümmern neue IDs zu erzeugen und die vorhanden zu verwalten. Es kann vorkommen, das auch ein solcher Editor aus dem Tritt kommt, dann vergibt er die eine oder andere ID schon mal doppelt. Aus diesem Grund ist es ganz gut zu wissen wie man ein sloches Problem per Hand lösen kann.

Hier ein Beispiel für die Verwendung einer Resource in einem eingenen Programm.

    HICON hMyIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_MYICON));

Der erste Parameter of LoadIcon() (ist wie bei vielen anderen Resourcefunktionen auch) ein Handle auf die current instance (diese wurde in WinMain() vergeben und kann mit der Funktion GetModuleHandle() wie in diesem Beispiel gezeigt ermittelt werden). Der zweite Parameter ist der Identifier der Resource.

Hinweis: Das Macro MAKEINTRESOURCE() konvertiert eine Integer Resource ID in einen and possibly wondering LPCTSTR von UINT. Darüber kommen wir zu der zweiten Möglichkeit eine Resource zu identifizieren, mit Strings. Jedoch wird diese Methode nicht mehr verwendet, deshalb will ich mich an dieser Stelle auch nicht weiter mit Deteils aufhalten. Aber grundsätzlich ist es so, dass man auch mit einem #define einen Namen der Integer Resource zuweisen kann. Dieser wird dann im Programm wie folgt verwendet.

    HICON hMyIcon = LoadIcon(hInstance, "MYICON");

LoadIcon() und andere Resource-Lade-API-Funktinen erkennen den Unterschied zwischen einem übergebenen Integer und einem Pointer auf einen String mittels einer Überprüfung des High Word des übergebenen Wertes. Wenn das High Word 0 ist handelt es sich um einen Integer und es wird angenommen, dass es sich um eine Resource ID handelt. Anderenfalls wird der Wert als Pointer interpretiert, der auf einen String mit dem Namen der Resource zeugt. Dieser impliziten Umwandlung sollte man jedoch besser nie vertrauen und immer die entsprechenden Macros verwenden.

Zum Beispiel finktioniert das nicht mit Menu Komandos wie ID_FILE_EXIT, denn diese gibt es nur in Form eines Integers.

Menus und Icons

Example: menu_one

[images/menu_one.gif]

Dies ist nur ein kurzer Abschnitt um zu zeigen, wie man elementare Menus einfügt. Normalerweise benutzt man eine vorgefertigte Menu Resource. Diese befindet sich in der .rc Datei und wird beim Compilieren / Linken mit der .exe verbunden. Das ist sehr Compiler abhänig, komerzielle Compiler haben (fast) immer einen Resource-Editor mit dem die Menus visuel bearbeitet werden können.

In diesem Beispiel soll es um den Text in der .rc Datei gehen ohne Verwendung eines Resource-Editors. Normalerweise hat man eine .h Datei die sowohl in der .rc Datei und in der .c Source Datei eingezogen wird (INCLUDE). In dieser Datei werden die Identifier definiert für die Controls und Menuitems usw.

In diesem Beispiel dient der Window-code von dem simple_window als Grundlage mit folgender Ergänzung.

Zuerst in der .h Datei. Diese heißt gewöhnlich "resource.h".

#define IDR_MYMENU 101
#define IDI_MYICON 201

#define ID_FILE_EXIT 9001
#define ID_STUFF_GO 9002
Das ist nicht besonders viel, aber es ist ja auch ein ganz einfaches Menu ;-) Die Namen und Werte kenn jeder wählen wie er möchte. (nur keine Doppelt vergeben) Kommen wir jetzt zu der .rc Datei.
#include "resource.h"

IDR_MYMENU MENU
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "E&xit", ID_FILE_EXIT
    END

    POPUP "&Stuff"
    BEGIN
        MENUITEM "&Go", ID_STUFF_GO
        MENUITEM "G&o somewhere else", 0, GRAYED
    END
END

IDI_MYICON ICON "menu_one.ico"

Als nächstes wird die .rc Datei dem Projekt hinzugefügt oder auch dem makefile, je nach dem mit welchem Compiler am arbeitet. Und natürlich auch in der c. Quellcode-Datei muß #include "resource.h" eingefügt werden, dass die Menuresource id definiert ist.

Der einfachste Weg das Menu und das Icon ;-) einem Fenster zuzuordnen ist, diese wie hier gleich beim registrieren der Klasse mitanzugeben:

    wc.lpszMenuName  = MAKEINTRESOURCE(IDR_MYMENU);
    wc.hIcon  = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_MYICON));
    wc.hIconSm  = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_MYICON), IMAGE_ICON, 16, 16, 0);

Wenn jetzt alles geklappt hat sollt das Fenster nach einem erneuten Compilerlauf ein Menu und ein Icon haben. Drückt man die ALT-TAB Tastenkombination sollte die große Variante vom ICON zum Vorschein kommen. Sollte das nicht der Fall sein, liegt wahrscheinlich ein Compiler Fehler vor. (Deteils zur Compilierung stehen auch in den compiler notes.)

In dem Beispiel wird die Funktion LoadIcon() verwendet um das große ICOn zu laden. Das ist einfacher als mit der Funktion LoadImage(), kann aber nur für ICON mit 32x32 verwendet werden. Icon Dateien und Resourcen können mehrere Bilder in einer Datei enthelten. In diesem Fall handelt es sich oft um die unterschiedlichen Auflösungen.

Beispiel: menu_two

Alternativ kann mann ein Menu auch on the fly (wenn das Programm schon läuft) erzeugen. Das ist zwar etwas aufwendiger, aber es bietet auch mehr Flexibilität, die ab und an bebraucht wird.

Auf die gleiche Art können Icons geladen werden die nicht in der Resource gespeichert sind. Es könnte ja sein, dass ein Icon ein einer Extra-Datei liegen soll. Diese kann dann z.B. vom Anwender ausgetauscht werden um dem Programm ein individuelles Aussehen zu geben, Stichwort Themes.

Als Ausgang dient wieder das Beispiel simple_window ohne .h or .rc Datei. Als Nächstes muß ein Handler für die WM_CREATE Nachricht eingefügt werden, dort wird das Menu erzeugt für das Fenster.

#define ID_FILE_EXIT 9001
#define ID_STUFF_GO 9002

Diese zwei IDs sollten ganz am Anfang der .c Datei stehen, möglichst unterhalb der #includes. Dann wird folgender Code für den WM_CREATE Handler eingefügt.

    case WM_CREATE:
    {
        HMENU hMenu, hSubMenu;
        HICON hIcon, hIconSm;

        hMenu = CreateMenu();

        hSubMenu = CreatePopupMenu();
        AppendMenu(hSubMenu, MF_STRING, ID_FILE_EXIT, "E&xit");
        AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu, "&File");

        hSubMenu = CreatePopupMenu();
        AppendMenu(hSubMenu, MF_STRING, ID_STUFF_GO, "&Go");
        AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu, "&Stuff");

        SetMenu(hwnd, hMenu);


        hIcon = LoadImage(NULL, "menu_two.ico", IMAGE_ICON, 32, 32, LR_LOADFROMFILE);
        if(hIcon)
            SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
        else
            MessageBox(hwnd, "Could not load large icon!", "Error", MB_OK | MB_ICONERROR);

        hIconSm = LoadImage(NULL, "menu_two.ico", IMAGE_ICON, 16, 16, LR_LOADFROMFILE);
        if(hIconSm)
            SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIconSm);
        else
            MessageBox(hwnd, "Could not load small icon!", "Error", MB_OK | MB_ICONERROR);
    }
    break;

Auf diese Art wird das gleiche Menu erzeugt, welches auch in der Resource (im vorherigen Beispiel) war. Ein Menu, welches einem Fenster zugewiesen ist wird automatisch gelöcht, wenn das Fenster zuerstört wird. Deshalb müssen wir uns auch später nicht mehr um das Menu kümmern. Wenn wir es trotzdem wollten gibt es dafür GetMenu() und DestroyMenu().

Der Code für die Icons ist sehr einfach, mit der Funktion LoadImage() wird jeweils das Icon für 16x16 und 32x32 geladen. An dieser Stelle kann LoadIcon() nicht verwendet werden, denn damit können nur Resourcen und keine Dateien geladen werden. Der Parameter instance handle wird auf NULL gesetzt, weil wir keine Resource von unserem Modul laden und anstelle einer Resource ID wird ein Dateiname übergeben. Zu guter Letzt übergeben wir das LR_LOADFROMFILE Flag um der Funktion mitzuteilen, das es sich bei dem übegebenen string um ein Dateinamen handelt.

Nachdem jeder dieser Funktionsaufrufe erfogreich war können wir das Icon-handle dem Fenster zu weisen mit der Nachricht WM_SETICON und sollte das schief gehen, wird eine MessageBox angezeigt die über den Fehler informiert.

Anmerkung: Der Funktionsaufruf LoadImage() wird schief gehen, wenn sich das Icon nicht im current working directory des Programms befindet. Bei der Verwendung von VC++ und einem Start aus der IDE, ist das current working directory das gleiche indem sich auch das Projektfile befindet. Sollte die Funktion auch nach dem kopieren der Datei in das entsprechede Verzeichnis schief gehen, muß der ganze Path angegeben werden wie z.B. "C:\\Path\\To\\Icon.ico".

Gut, jetzt haben wir das Menu, müssen wir es nur noch dazu kiegen auch was zu machen. das ist nich weiter schwer. Dazu wird unter der WM_COMMAND Message ein Handler eingefügt. Danach sollte unsere WinProc etwa wie folgt aussehen.

LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam)
{
    switch(Message)
    {
        case WM_CREATE:
        {
            HMENU hMenu, hSubMenu;

            hMenu = CreateMenu();

            hSubMenu = CreatePopupMenu();
            AppendMenu(hSubMenu, MF_STRING, ID_FILE_EXIT, "E&xit");
            AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu, "&File");

            hSubMenu = CreatePopupMenu();
            AppendMenu(hSubMenu, MF_STRING, ID_STUFF_GO, "&Go");
            AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT)hSubMenu, "&Stuff");

            SetMenu(hwnd, hMenu);


			hIcon = LoadImage(NULL, "menu_two.ico", IMAGE_ICON, 32, 32, LR_LOADFROMFILE);
			if(hIcon)
				SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
			else
				MessageBox(hwnd, "Could not load large icon!", "Error", MB_OK | MB_ICONERROR);

			hIconSm = LoadImage(NULL, "menu_two.ico", IMAGE_ICON, 16, 16, LR_LOADFROMFILE);
			if(hIconSm)
				SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIconSm);
			else
				MessageBox(hwnd, "Could not load small icon!", "Error", MB_OK | MB_ICONERROR);
        }
        break;
        case WM_COMMAND:
            switch(LOWORD(wParam))
            {
                case ID_FILE_EXIT:

                break;
                case ID_STUFF_GO:

                break;
            }
        break;
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, Message, wParam, lParam);
    }
    return 0;
Die Nachricht WM_COMMAND wird nochmals mit einem weiteren switch unterteilt. Der LOWORD Anteil des Parameter wParam der WM_COMMAND Nachricht enthällt die ID des Controls, welches die nachricht gesendet hat.

Natürlich soll der Menueintrag Exit das Programm schließen. Dazu muß unter WM_COMMAND, ID_FILE_EXIT mit dem folgenden Code ausgestattet werden.

PostMessage(hwnd, WM_CLOSE, 0, 0);

Your WM_COMMAND handler should now look like this:

    case WM_COMMAND:
        switch(LOWORD(wParam))
        {
            case ID_FILE_EXIT:
                PostMessage(hwnd, WM_CLOSE, 0, 0);
            break;
            case ID_STUFF_GO:

            break;
        }
    break;
Jeder kann sich an dieser Stelle selbst ausdenken, was die restlichen Menupunkte machen sollen, oder diese einfach offen lassen.

Das Datei-Icon

Sicher ist Euch aufgefallen, dass die menu_one.exe ein eigenes Programmsymbol zeigt, welches in der Resource hinzugefügt wurde. Wogegen menu_two.exe kein derartiges Symbol zeigt, weil wir eine extra Datei Laden. Der Windows Explorer nimmt einfach das erste Icon (Numerisch nach ID sortiert) welches er in der Resorce des Programms findet. Für den Fall das ein besonderes Icon gezeigt werden soll, muß diese die kleineste ID haben, am besten gleich eins.

Anhang D: Hinweise zum Resourcefile

Argh!

Das einzige was mir wirklich überhaupt nicht gefällt nach dem Umstieg von Borland C++ auf MS Visual C++, ist die Art in der VC++ Resourcen Scripte (.rc files) behandelt.

In BC++ war es frei möglich Aussehen und Inhalt der .rc Dateien zu bestimmen, und nach der Verwendung des Resorcen Editors, waren nur die Dinge geändert die auch wirklich geändert werden sollten. Aber zu meiner Bestürtzung ändert der VC++ Resource Editor die ganze .rc datei, indem sie komplett neu geschrieben wird. Manchmal werden somit Änderungen entfernt oder Teile der Datei zerstört, die man mühsam per Hand gemacht hat.

Am Anfang war das furchtbar frustrierend, aber mit der Zeit gewöhnt man sich daran und lernt damit umzugehen. Dann ist es gar nicht mehr SO schlecht, meistens schreibt man dann die Resource Dateien sowieso nicht per Hand.

Kompartibilität

Eine kleine Herausforderung war/ist es die Resourcefiles so zu gestsalten, dass sie in beiden VC++ und BC++ ohne jegliche Ändrungen funktionieren. Ursprünglich habe ich die Borland naming convention für den Resource Header verwendet, welche project_name.rh war. Warum auch immer werden diese in VC++ resource.h genannt, und der Einfachhaeit halber habe ich diesen Namen übernommen.

Für besonders neugirige Leser :-), Ja es ist möglich den Namen der Resource-Datei auch unter VC++ zu ändern. Dazu editiert man das .rc File per Hand und ändert den Namen an zwei Stellen, einmal hier #included, zum anderen hier TEXTINCLUDE resource.

Das nächste Problem war, dass VC++ defaultmäßig die Datei afxres.h benötigt im .rc File, wogegen BC++ alle notwendigen Macros intern abgebildet hat. Ein anderes Dummes Ding, ist es von VC++ , dass afxres.h nur dann mitinstaliert wird, wenn MFC instaliert wird. Das macht aber lange nicht jeder, der nur API nutzen will. In diesem Fall kann winres.h verwendet werden, welche immer mit instaliert wird.

Seit dem ich mit VC++ arbeite und den Resourcen Editor benutze habe ich das Problem mit folgender Alternativen Entscheidung gelöst. Einfach folgende Zeilen in jedes .rc File einbinden nach dem es erzeugt wurde:

#ifndef __BORLANDC__
#include "winres.h"
#endif
Welches vorher folgendes enthielt:
#include "afxres.h"
Für all diejenigen welche mit VC++ arbeiten. Unter "View > Resource Includes" findet Ihr eine option um diesen Text in der IDE ändern zu können, aber man kann es auch in der Datei eingeben.

Für alle anderen die mit BC++ arbeiten, es tut mir leid, das die Dateien der .rc Files um ein paar Zeilen "Schrott" für VC++ länger sein müssen.

Übersetzen der Resourcen unter BC++

Versuchts wie ich auch, bisher konnte ich keinen einfachen Weg finden, den ich hätte hier beschreiben, können, sollte mir inzwischen noch was einfallen, werde ich es auf diesen Seiten hinzufügen.
Weiter mit : Dialoge und andere Fensterarten Zurück : Nachrichten - Details zur Nachrichtenverarbeitung


---[ letzte Änderung : 19.04.2002 ]---