[FAQ] ListView im Report-Modus



  • Vorwort

    Ich möchte mit diesem Beitrag die ListView-Controls jedem ein bisschen näher bringen. Ich werde ersteinmal nur die Erstellung von ListViews im Report-Modus behandeln. Dieser Artikel ist nun mehr 3 Jahre alt und sollte ursprünglich im Magazin veröffentlicht werden. Allerdings bin ich bisher noch nicht zur Fertigstellung gekommen. Es fehlen die Behandlung von mehreren Columns, Screenshots und weitere Kleinigkeiten, alles in allem sollte dieser Artikel doch eine gute Übersicht zum Einstieg bieten.

    Einteilung

    • 1. Erstellung
    • 2. Konfiguration
    • 2.1 Hinzufügen von Columns
    • 2.2 Hinzufügen von Einträgen
    • 2.3 Farben
    • 2.3.1 einfache Text- und Hintergrundfarbe
    • 2.3.2 erweiterte Text- und Hintergrundfarbe
    • 2.4 Größen und Sortierung
    • 2.4.1 Größenänderung von Columns verhindern
    • 2.4.2 Sortierung nach Columns

    --------------------------------------------------------------------------

    1. Erstellung

    Da ein ListView-Control kein Standard-Control aus der "windows.h" ist, muss vorher noch der passende Header inkludiert werden. In diesem Fall ist es "commctrl.h". Diese Datei enthält die Definitionen von dem ListView-Control. Außerdem beinhaltet "commctrl.h" noch TreeView-, Trackbar-, Progress-, Tab-, UpDown- und einige weitere Controls. Um diese Controls nutzen zu können, muss noch die Funktion InitCommonControlsEx () aufgerufen werden. Die Funktion stellt sicher, dass die benötigte Programmbibliothek für die Controls geladen wurde und benötigt als Paramter einen Zeiger der Strukur INITCOMMONCONTROLSEX. So ist die Strukur in "commctrl.h" definiert:

    typedef struct tagINITCOMMONCONTROLSEX {
        DWORD dwSize;             // size of this structure
        DWORD dwICC;              // flags indicating which classes to be initialized
    } INITCOMMONCONTROLSEX, *LPINITCOMMONCONTROLSEX;
    

    Hier die Erklärung der Elemente:

    • dwSize: Größe der Struktur
    • dwICC: Flags, die angeben, welche Controls alle initialisiert werden sollen; möglich sind:
      ICC_LISTVIEW_CLASSES, ICC_TREEVIEW_CLASSES, ICC_BAR_CLASSES, ICC_TAB_CLASSES, ICC_UPDOWN_CLASS, ICC_PROGRESS_CLASS, ICC_HOTKEY_CLASS, ICC_ANIMATE_CLASS, ICC_WIN95_CLASSES, ICC_DATE_CLASSES, ICC_USEREX_CLASSES, ICC_COOL_CLASSES, ICC_INTERNET_CLASSES, ICC_PAGESCROLLER_CLASS und ICC_NATIVEFNTCTL_CLASS

    Für ListView-Controls wird nur das erste Flag aus der Liste benötigt. Nun zum Einbinden von ListView-Controls in ein Programm. Das Control wird unter der Fenster-Message WM_CREATE wie ein Standard-Control erstellt:

    case WM_CREATE:
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof (INITCOMMONCONTROLSEX);
        icc.dwICC  = ICC_LISTVIEW_CLASSES;
    
        InitCommonControlsEx (&icc);
        hListView = CreateWindow (WC_LISTVIEW, "",
                                  WS_CHILD | WS_VISIBLE,
                                  5, 5, 300, 300,
                                  hWnd, (HMENU) LISTVIEW,
                                  g_hInst, NULL);
    return 0;
    

    Als Name der Fensterklasse wird WC_LISTVIEW angegeben. Ansonsten unterscheidet sich die restliche CreateWindow ()-Funktion nicht, von z.B. der eines Buttons. Vor dem ListView-Control wird die oben beschriebene Funktion InitCommonControlsEx () aufgerufen, damit nichts schief geht. Diese Funktion kann auch bereits in der WinMain aufgerufen werden. Damit wurde ein ListView-Control erstellt, wie es auch z.B. in der Symbolansicht des Explorers verwendet wird. Um ein ListView-Control im Report-Modus zu erstellen, muss noch LVS_REPORT als Style mit angegeben werden. Fertig ist das ListView-Control im Report-Modus!

    2. Konfiguration

    Nachdem das ListView-Control erfolgreich erstellt wurde, beschäftige ich mich jetzt mit der Konfiguration dessen. D.h.: Columns, Einträge, Farben, Größen.

    2.1 Hinzufügen von Columns

    Um ein Column zu einem ListView-Control hinzuzufügen, muss die gefüllte Struktur LV_COLUMN via SendMessage () bzw. ListView_InsertColumn () an das Control gesendet werden. Ich werde hier nur mit der 2. Variante arbeiten, da diese die kürzere ist und zumal diese Variante intern auch nur mit SendMessage () arbeitet. LV_COLUMN ist in "commctrl.h" wie folgt definiert:

    typedef struct tagLVCOLUMNA
    {
        UINT mask;
        int fmt;
        int cx;
        LPSTR pszText;
        int cchTextMax;
        int iSubItem;
    #if (_WIN32_IE >= 0x0300)
        int iImage;
        int iOrder;
    #endif
    } LVCOLUMNA, FAR* LPLVCOLUMNA;
    

    In diesem Zusammenhang werde ich nur die ersten 6 Elemente benutzen. Nachfolgend die Beschreibung der Elemente:

    • mask: definiert, welche Elemente benutzt werden sollen über: LVCF_FMT, LVCF_WIDTH, LVCF_TEXT, LVCF_SUBITEM, LVCF_IMAGE, LVCF_ORDER
    • fmt: Formatierungen für das Column: LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_CENTER, LVCFMT_JUSTIFYMASK, LVCFMT_IMAGE, LVCFMT_BITMAP_ON_RIGHT, LVCFMT_COL_HAS_IMAGES (benötigt, wenn LVCF_FMT in mask definiert ist)
    • cx: Breite des Columns (benötigt, wenn LVCF_WIDTH in mask definiert ist)
    • pszText: Text/Name des Columns (benötigt, wenn LVCF_TEXT in mask definiert ist)
    • cchTextMax: Maximallänge von pszText (benötigt, wenn LVCF_TEXT in mask definiert ist)
    • iSubItem: Nummer des Columns (benötigt, wenn LVCF_SUBITEM in mask definiert ist)
    • iImage und iOrder sind hier außen vor gelassen

    Nachdem das alles geklärt ist, nun eine Funktion, in der das unter 2.1 Beschriebene zusammen getragen ist:

    bool InsertListViewColumn (HWND hListView,
                               LPTSTR lpColumnName,
                               int iWidth,
                               int iSubItem,
                               DWORD dwStyle = NULL)
    {
        int iStatus;
    	LV_COLUMN lvc;
    
    	lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
    	lvc.fmt = LVCFMT_LEFT | dwStyle;
    	lvc.cx = iWidth;
    	lvc.pszText = lpColumnName;
    	lvc.cchTextMax = 100;
    	lvc.iSubItem = iSubItem;
    
    	iStatus = ListView_InsertColumn (hListView, iSubItem, &lvc);
    
        return (iStatus != -1);
    }
    

    Diese Funktion kann an einer beliebigen Position im Code aufgerufen werden, vorausgesetzt das ListView-Control wurde bereits erstellt. Wenn sie erfolgreich war, liefert sie true zurück, ansonsten false. Eingesetzt wird die Funktion zum Beispiel so:

    case WM_CREATE:
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof (INITCOMMONCONTROLSEX);
        icc.dwICC  = ICC_LISTVIEW_CLASSES;
    
        InitCommonControlsEx (&icc);
        hListView = CreateWindow (WC_LISTVIEW, "",
                                  WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                  5, 5, 300, 300,
                                  hWnd, (HMENU) LISTVIEW,
                                  g_hInst, NULL);
    
        InsertListViewColumn (hListView, "1. Spalte", 150, 0);
        InsertListViewColumn (hListView, "2. Spalte", 150, 0, LVCFMT_CENTER);
    return 0;
    

    Das war auch schon alles. Es ist also nicht ganz so schwer, wie man am Anfang denkt.

    2.2 Hinzufügen von Einträgen

    Das Hinzufügen von Einträgen ist ähnlich. Um ein Eintrag in ein ListView-Control unter eine Spalte zu bekommen, muss man mit SendMessage () bzw. mit ListView_InsertItem () eine gefüllte LV_ITEM-Struktur an das Control senden. Wie auch bei 2.1, hier ersteinmal die Strukur:

    typedef struct tagLVITEMA
    {
        UINT mask;
        int iItem;
        int iSubItem;
        UINT state;
        UINT stateMask;
        LPSTR pszText;
        int cchTextMax;
        int iImage;
        LPARAM lParam;
    #if (_WIN32_IE >= 0x0300)
        int iIndent;
    #endif
    } LVITEMA, FAR* LPLVITEMA;
    

    Und hier die Erklärung der Elemente:

    • mask: definiert, welche Elemente benutzt werden sollen über: LVIF_TEXT, LVIF_IMAGE,
      LVIF_PARAM(, LVIF_STATE)
    • iItem: Zeile, in der der Eintrag eingefügt werden soll
    • iSubItem: Spalte, in die der Eintrag eingefügt werden soll
    • pszText: Eintrag (benötigt, wenn LVIF_TEXT in mask definiert ist)
    • cchTextMax: Maximallänge von pszText (benötigt, wenn LVIF_TEXT in mask definiert ist)
    • lParam: Parameter, um den Eintrag zu identifizieren, z.B für das Sortieren (benötigt, wenn LVIF_PARAM in mask definiert ist)
    • iImage, state und stateMask sind hier außen vor gelassen

    Diese Struktur muss nur noch via ListView_InsertItem () an das Control gesendet werden, und schon ist ein Eintrag drin.
    Nun aber zu einem Problem: ListView_InsertItem () kann nur für die erste (!) Spalte benutzt werden. Für die anderen Columns muss ListView_SetItem verwendet werden. Nachdem auch das beachtet ist, kann man sich daraus ja wieder eine Funktion basteln, um die Geschichte zu erleichtern:

    bool InsertListViewEntry (HWND hListView,
                              LPTSTR lpEntryValue,
                              int iRowID,
                              int iColID)
    {
        int iStatus;
    	LV_ITEM lvi;
    
    	lvi.mask = LVIF_TEXT;
    	lvi.iItem = iRowID;
        lvi.iSubItem = iColID;
        lvi.pszText = lpEntryValue;
    	lvc.cchTextMax = MAX_PATH;
    
    	if (iColID == 0)
    		iStatus = ListView_InsertItem (hListView, &lvi);
    	else
    		iStatus = ListView_SetItem (hListView, &lvi);
    
        return (iStatus != -1 || iStatus != false);
    }
    

    Auch diese Funktion gibt bei einem Misserfolg false zurück und andernfalls true.
    Hier das Beispiel, wie diese Funktion zusammen mit InsertListViewColumn () funktionieren kann:

    case WM_CREATE:
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof (INITCOMMONCONTROLSEX);
        icc.dwICC  = ICC_LISTVIEW_CLASSES;
    
        InitCommonControlsEx (&icc);
        hListView = CreateWindow (WC_LISTVIEW, "",
                                  WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                  5, 5, 300, 300,
                                  hWnd, (HMENU) LISTVIEW,
                                  g_hInst, NULL);
    
        InsertListViewColumn (hListView, "1. Spalte", 150, 0);
        InsertListViewColumn (hListView, "2. Spalte", 150, 0, LVCFMT_CENTER);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 0", 0, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 0", 0, 1);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 1", 1, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 1", 1, 1);
    return 0;
    

    So, das war auch schon alles zu den Einträgen.

    2.3 Farben

    In diesem Bereich beschäftige ich mich mit der Farbgebung von ListView-Controls.

    2.3.1 einfache Text- und Hintergrundfarbe

    Dieser Teil ist sehr schnell abgehandelt. Um die Hintergrundfarbe von einem ListView-Control zu ändern, benutzt man das Makro ListView_SetBkColor (), für die Texthintergrundfarbe das Makro ListView_SetTextBkColor () und für die Textfarbe das Makro ListView_SetTextColor (). Ein schwarzes ListView-Control mit weißen Text und schwarzen Texthintergrund könnte also so aussehen:

    case WM_CREATE:
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof (INITCOMMONCONTROLSEX);
        icc.dwICC  = ICC_LISTVIEW_CLASSES;
    
        InitCommonControlsEx (&icc);
        hListView = CreateWindow (WC_LISTVIEW, "",
                                  WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                  5, 5, 300, 300,
                                  hWnd, (HMENU) LISTVIEW,
                                  g_hInst, NULL);
    
    	ListView_SetBkColor (hListView, RGB (0, 0, 0));
    	ListView_SetTextBkColor (hListView, RGB (0, 0, 0));
    	ListView_SetTextColor (hListView, RGB (255, 255, 255));
    
        InsertListViewColumn (hListView, "1. Spalte", 150, 0);
        InsertListViewColumn (hListView, "2. Spalte", 150, 0, LVCFMT_CENTER);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 0", 0, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 0", 0, 1);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 1", 1, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 1", 1, 1);
    return 0;
    

    2.3.2 erweiterte Text- und Hintergrundfarbe

    Um die Zeilen z.B. zweifarbig im Wechsel zu färben, muss man selbst zeichnen. Um das zu können, muss das ListView-Control beim Erstellen noch LVS_OWNERDRAWFIXED als Style hinzubekommen. Danach wird die vordefinierte Zeichenoperation ausgesetzt und man kann über WM_DRAWITEM selber zeichnen. WM_DRAWITEM bekommt über den lParam-Wert eine Struktur, in der alle wichtigen Daten (Größe, Device Kontext, ID, etc.) über das zu zeichnende Element enthalten sind. Diese Struktur nennt sich DRAWITEMSTRUCT, allerdings werde ich nicht näher auf die Elemente eingehen. Damit man nun zeichnen kann, muss man erstmal wissen, ob das zu zeichnende Objekt auch wirklich das ListView-Control ist. Das findet man über das in der Struktur enthaltene Element hwndItem heraus. Wenn es das Control ist, muss man überprüfen, ob das Item irgendeinen Status hat, also ob es zum Beispiel selektiert ist. Weil wenn, dann soll es ja eine andere Farbe bekommen. Das passende Element dafür ist itemState und der Wert ist ODS_SELECTED, falls es selektiert ist. Wenn das Item nicht diesen Status hat, soll es auch nicht die Farbe bekommen, als wäre es selektiert. Im Element itemID von DRAWITEMSTRUCT steht die ID des Items. Wenn es eine gerade Zahl ist, soll das Item eine andere Farbe bekommen, als wenn die Zahl gerade ist. Mit den Farben kann man nun einen Brush erstellen und diesen über FillRect () auf das Rechteck des Items anwenden. der Device Context ist in hDC gespeichert und das Rechteck in rcItem. Anschließend nicht vergessen den Brush über DeleteObject zu löschen. Fertig. Aber Moment! Wo bleibt der Text? Der muss ja schließlich auch irgendwie gezeichnet werden! Also ran an die Arbeit!
    Zuerst wird ein Puffer angelegt, der den Text aufnehmen kann. Anschließend wird das Makro ListView_GetItemText () dazu verwendet, über die ID itemID den Text in den Puffer zu schreiben. Über SetTextBkColor () und SetTextColor () kann man noch die Farben für den Hintergrund des Textes und die Farbe für den Text ändern. Alles was jetzt noch fehlt, ist den Text auf den Device Context zu pappen. Über TextOut () ist auch das schnell erledigt und fertig ist das selbstgezeichnete ListView-Control!
    Hier nocheinmal ein Beispiel:

    case WM_CREATE:
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof (INITCOMMONCONTROLSEX);
        icc.dwICC  = ICC_LISTVIEW_CLASSES;
    
        InitCommonControlsEx (&icc);
        hListView = CreateWindow (WC_LISTVIEW, "",
                                  WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_OWNERDRAWFIXED,
                                  5, 5, 300, 300,
                                  hWnd, (HMENU) LISTVIEW,
                                  g_hInst, NULL);
    
        InsertListViewColumn (hListView, "1. Spalte", 150, 0);
        InsertListViewColumn (hListView, "2. Spalte", 150, 0, LVCFMT_CENTER);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 0", 0, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 0", 0, 1);
    
        InsertListViewEntry (hListView, "Spalte 0, Zeile 1", 1, 0);
        InsertListViewEntry (hListView, "Spalte 1, Zeile 1", 1, 1);
    return 0;
    
    case WM_DRAWITEM:
        dis = (DRAWITEMSTRUCT*) lParam;
        if (dis->hwndItem == hListView)
        {
            COLORREF bgColor;
    
            if (dis->itemState & ODS_SELECTED)
                bgColor = RGB (200, 240, 200);
            else
            {
                if (dis->itemID % 2 == 0)
                    bgColor = RGB (245, 245, 245);
                else
                    bgColor = RGB (230, 230, 250);
            }
    
            HBRUSH hbr = CreateSolidBrush (bgColor);
            FillRect (dis->hDC, &dis->rcItem, hbr);
    
            DeleteObject (hbr);
    
            char buf[100];
            ListView_GetItemText (hClientListView, dis->itemID, 0, buf, 100);
    
            TextOut (dis->hDC, dis->rcItem.left, dis->rcItem.top, buf, strlen (buf));
        }
    return 0;
    

    Viel Text, viel Erklärung, wenig Code, viel Ergebnis.

    2.4 Größen und Sortierung

    2.4.1 Größenänderung von Columns verhindern

    Dieser Teil ist auch wieder schnell abgehandelt. Um zu verhindern, dass der User die Columns kleiner bzw. größer machen kann, muss nur die Message HDN_ITEMCHANGING unter WM_NOTIFY abgefangen werden. Diese Message wird immer dann gesendet, wenn eine Änderung an einer Eigenschaft eines Headers bzw. Columns vorgenommen werden soll. In dieser Message überprüft man dann noch, ob es sich um ein Column aus dem ListView-Control handelt und gibt dann einfach
    1 zurück, falls ja.

    case WM_NOTIFY:
        switch (((LPNMHDR) lParam)->code)
        {
            case HDN_ITEMCHANGING:
                #define phn     ((HD_NOTIFY FAR *) lParam)
                if ((int) phn->iItem == 0)
                {
                    if ((int) phn->hdr.hwndFrom == SendMessage (hClientListView, LVM_GETHEADER, 0, 0))
                        return 1;
                }
                #undef phn
            break;
        }
    return 0;
    

    So einfach kann das sein. Nun der vorerst letzte Teil.

    2.4.2 Sortierung nach Columns

    Um die Einträge bei einen Klick auf ein Column zu sortieren, wird auch hier wieder WM_NOTIFY benötigt. Diesmal ist allerdings LVN_COLUMNCLICK verantwortlich. Das Makro ListView_SortItem () dient zum Sortieren von Einträgen. Allerdings benötigt es eine Funktion, in der die Regel, nach der sortiert werden soll, aufgeführt wird. Und hier kommt das vorhin erwähnte Element lParam aus der LV_ITEM-Struktur zum Einsatz. Das Makro übergibt an die Sortier-Funktion nähmlich nur 3 Werte: 2 lParam-Werte von Einträgen und einen lParam Wert, der über das Makro überliefert wird. Natürlich könnte auch hier wieder SendMessage () benutzt werden. Diese Sortier-Funktion übernimmt und verarbeitet dann die Werte. Hier ein Beispiel:

    LRESULT CALLBACK ListViewSorting (LPARAM, LPARAM, LPARAM);
    .
    .
    .
    case WM_NOTIFY:
        switch (((LPNMHDR) lParam)->code)
        {
            case LVN_COLUMNCLICK:
                #define pnm ((NM_LISTVIEW *) lParam)
                ListView_SortItems (pnm->hdr.hwndFrom, ListViewSorting, (LPARAM) (pnm->iSubItem));
                #undef pnm
            break;
        }
    return 0;
    .
    .
    .
    LRESULT CALLBACK ListViewSorting (LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
    {
        UNREFERENCED_PARAMETER (lParamSort);
        if(lParam1 < lParam2)
            return -1;
        else if(lParam1 > lParam2)
            return 1;
        else
            return 0;
    }
    

    Das Makro UNREFERENCED_PARAMTER () dient nur dazu, die Warnung vom Compiler zu unterdrücken, dass lParamSort nicht benutzt wird.

    Das war es vorerst von ListView-Controls.

    Fake oder Echt.


Anmelden zum Antworten