Programmieren mit C++

Borland C++

Grafik

Bitmap als Hintergrund in Listbox

Wie kann eine Bitmap als Hintergrund in einer Listbox gezeichnet werden?

Frage

Das Anzeigen einer Bitmap als Hintergrund einer Listbox ist etwas komplexer als das einfache Setzen einer Hintergrundfarbe. Die Control muß dazu auf jeden Fall selbst gezeichnet werden, wobei die Bitmapanzeige sich über mehrere Zeilen der Listbox erstreckt, so daß die Bitmap für die Anzeige in einzelne horizontale Streifen zerlegt werden muß.

Bei der Auswahl der Bitmap ist darauf zu achten, daß der Text durch die dahinter liegende Bitmap nicht unlesbar oder zumindest erschwert lesbar wird.

Die nachfolgend gezeigte Lösung verwendet eine Bitmap mit 256 Farben, die aus den Ressourcen geladen wird. Ist die Bitmap kleiner als die Control, wird die Bitmap gekachelt angezeigt, so daß sie den gesamten Clientbereich der Control einnimmt.

Lösung

An die Listbox ist zunächst die Anforderung zu stellen, daß sie benutzerdefiniert zeichenbar sein muß. Hierzu ist das Flag LVS_OWNERDRAWFIXED im Ressourceneditor zu setzten. Konsequenterweise muß dann auch eine DrawItem()- Funktion implementiert werden, die alle Zeichenaktionen für die einzelnen Einträge der Listbox übernimmt.

Benutzerdefinierte Control

Damit das Laden der Bitmap und das Erzeugen der logischen Palette nicht bei jeder Zeichenaktion anfällt, werden die Bitmap und die Palette sowie die Größe der Bitmap in Membervariablen der speziellen Listboxklasse abgelegt.

protected:
 CPalette m_pal;
 CBitmap m_bitmap;
 int m_cxBitmap, b_cyBitmap;

Zusätzliche Membervariable

Zum Setzen der Hintergrundbitmap werden zwei überladene Funktionen verwendet, die als öffentliche Member deklariert sind. Die erste Variante übernimmt als Parameter eine Ressourcen-ID, während die zweite den Namen der Ressource übergeben bekommt. Zur Vereinfachung ruft die erste Variante die zweite auf.

BOOL CMyListCtrl::SetBkImage(
    UINT nIDResource)
{
  return SetBkImage( (LPCTSTR)nIDResource );
}

Diese Funktionen können aufgerufen werden, nachdem die Bitmap bereits spezifiziert wurde.

Als erstes zerstört die Funktion die Bitmap- und Paletten-Objekte, sofern solche bereits erzeugt wurden.

Memberfunktion zum Setzen der Hintergrundbitmap

BOOL CMyListCtrl::SetBkImage(
    LPCTSTR lpszResourceName)
{
  if( m_bitmap.m_hObject != NULL )
    m_bitmap.DeleteObject();
  if( m_pal.m_hObject != NULL )
    m_pal.DeleteObject();

Anschließend lädt die Funktion die Bitmap und verbindet sie mit dem Cbitmap-Objekt. Hierzu wird die globale Funktion ::LoadImage() anstelle von CBitmap::LoadBitmap() verwendet. Der Grund liegt darin, daß ein Zugriff auf die DIBSECTION der Bitmap möglich sein soll, da eine logische Palette zu erzeugen ist, um die Farben korrekt wiederzugeben.

  HBITMAP hBmp = (HBITMAP)::LoadImage(AfxGetInstanceHandle(), 
                                      lpszResourceName, 
                                      IMAGE_BITMAP, 0,0, 
                                      LR_CREATEDIBSECTION );
if( hBmp == NULL ) 
    return FALSE;
 
  m_bitmap.Attach( hBmp );
  BITMAP bm;
  m_bitmap.GetBitmap( &bm );
  m_cxBitmap = bm.bmWidth;
  m_cyBitmap = bm.bmHeight;

Für das Erzeugen der logischen Palette wird zunächst die Anzahl der Farben bestimmt, indem über die DIBSECTION die Funktion Cbitmap::GetObject() aufgerufen wird.

  DIBSECTION ds;
  BITMAPINFOHEADER &bmInfo = ds.dsBmih;
  m_bitmap.GetObject( sizeof(ds), &ds );
 
  int nColors = bmInfo.biClrUsed ? bmInfo.biClrUsed
                                 : 1 << bmInfo.biBitCount;

Liegen mehr als 256 Farben vor, kommt eine Halbtonpalette zum Einsatz, die zum Ausgabekontext kompatibel ist. Eine Halbtonpalette ist in diesem Fall einfach eine Palette, die eine Mischung aus allen Farben enthält, was die einfachste, aber nicht unbedingt die beste Vorgehensweise ist.

  CClientDC dc(NULL); 
  if( nColors > 256 )
    m_pal.CreateHalftonePalette( &dc );
  else
 {

Besitzt die Bitmap maximal 256 Farben, wird eine logische Palette erzeugt. Dazu wird ausreichend Speicher allokiert und anschließend die Funktion ::GetDIBColorTable() aufgerufen, um die Palette aus der Bitmap zu ermitteln.

  RGBQUAD *pRGB = new RGBQUAD[nColors];
  CDC memDC;
  memDC.CreateCompatibleDC(&dc);
 
  memDC.SelectObject( &m_bitmap );
  ::GetDIBColorTable( memDC, 0, nColors, pRGB );
 
  UINT nSize = sizeof(LOGPALETTE) +
               (sizeof(PALETTEENTRY) * nColors);
  LOGPALETTE *pLP = (LOGPALETTE *) new BYTE[nSize];
 
  pLP->palVersion = 0x300;
  pLP->palNumEntries = nColors;

Die Farben der Palette ergeben sich anhand:

  for( int i=0; i < nColors; i++)
  {
    pLP->palPalEntry[i].peRed   = pRGB[i].rgbRed;
    pLP->palPalEntry[i].peGreen = pRGB[i].rgbGreen;
    pLP->palPalEntry[i].peBlue  = pRGB[i].rgbBlue;
    pLP->palPalEntry[i].peFlags = 0;
  }
 
  m_pal.CreatePalette( pLP );

Nach dem Erzeugen der Palette kann der verwendete Speicher wieder freigegeben werden.

  delete[] pLP;
  delete[] pRGB;
 }

Abschließend wird ein Neuzeichnen erzwungen.

 Invalidate();
 
 return TRUE;
}

Funktion SetBkImage

Die Funktion DrawItem() ist verantwortlich für das Zeichnen der einzelnen Einträge der Listbox und muß auch das Ausgeben des Bitmaphintergrunds übernehmen.

Das Ausgaberechteck, der Index des betroffenen Eintrags wie auch der Ausgabekontext selbst werden in lpDrawItemStruct übergeben.

void CMyListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
 CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
 CRect rcItem(lpDrawItemStruct->rcItem);
 int nItem = lpDrawItemStruct->itemID;
 CImageList* pImageList;
 
 int nSavedDC = pDC->SaveDC();

Das Item-Image und die Statusinformationen werden in einer LV_ITEM-Struktur abgelegt.

 LV_ITEM lvi;
 lvi.mask = LVIF_IMAGE | LVIF_STATE;
 lvi.iItem = nItem;
 lvi.iSubItem = 0;
 lvi.stateMask = 0xFFFF;
 GetItem(&lvi);

Ob der Eintrag hervorgehoben anzuzeigen ist, ergibt sich anhand der Auswertung.

 BOOL bHighlight = ((lvi.state & LVIS_DROPHILITED) ||
                    ((lvi.state & LVIS_SELECTED)   && 
                     ((GetFocus() == this) || 
                      (GetStyle() & LVS_SHOWSELALWAYS))));

Die Rechtecke für die Ausgabe und der Labeltext ergeben sich aus:

 CRect rcBounds, rcLabel, rcIcon;
 GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
 GetItemRect(nItem, rcLabel, LVIR_LABEL);
 GetItemRect(nItem, rcIcon, LVIR_ICON);
 CRect rcCol( rcBounds ); 
 
 CString sLabel = GetItemText( nItem, 0 );

Die Labeltexte werden um einen bestimmten Offset verschoben angezeigt, wobei sich der Offset aus der Breite eines Leerzeichens ergibt.

  int offset = pDC->GetTextExtent(_T(" "), 1).cx*2;

Hervorhebungen sind möglich für den Labelteil, das umhüllende Rechteck des Eintrags oder den gesamten Clientbereich des Eintrags.

 CRect rcHighlight, rcClient;
 int nExt;
 switch( m_nHighlight )
 {
 case 0: 
  nExt = pDC->GetOutputTextExtent(sLabel).cx + offset;
  rcHighlight = rcLabel;
  if( rcLabel.left + nExt < rcLabel.right )
   rcHighlight.right = rcLabel.left + nExt;
  break;
 case 1:
  rcHighlight = rcBounds;
  rcHighlight.left = rcLabel.left;
  break;
 case 2:
  GetClientRect(&rcClient);
  rcHighlight = rcBounds;
  rcHighlight.left = rcLabel.left;
  rcHighlight.right = rcClient.right;
  break;
 default:
  rcHighlight = rcLabel;
 }

Ist eine Bitmap gesetzt, wird diese als Hintergrund gezeichnet. Hierzu ist ein kompatibler Ausgabekontext notwendig. Der Clientbereich wird auch gleich vorweg bestimmt.

 if( m_bitmap.m_hObject != NULL )
 {
  CDC tempDC;
  tempDC.CreateCompatibleDC(pDC);
  tempDC.SelectObject( &m_bitmap );
 
  GetClientRect(&rcClient);
 
  CRgn rgnBitmap;
  CRect rcTmpBmp( rcItem );
  
  rcTmpBmp.right = rcClient.right;

Falls es sich um den letzten Eintrag der Listbox handelt, muß die Updateregion bis zum unteren Rand erweitert werden.

  if( nItem == GetItemCount() - 1 )
   rcTmpBmp.bottom = rcClient.bottom;
 
  rgnBitmap.CreateRectRgnIndirect(&rcTmpBmp);
  pDC->SelectClipRgn(&rgnBitmap);
  rgnBitmap.DeleteObject();

Für Ausgabegeräte, die eine logische Palette unterstützen, wird eine Palette realisiert, sofern eine solche vorhanden ist.

  if( pDC->GetDeviceCaps(RASTERCAPS) & RC_PALETTE && 
      m_pal.m_hObject != NULL )
  {
   pDC->SelectPalette( &m_pal, FALSE );
   pDC->RealizePalette();
  }

Für die gesamte Ausgabefläche wird die Bitmap so oft neben- und untereinander plaziert, bis der gesamte Bereich überdeckt ist.

  CRect rcFirstItem;
  GetItemRect(0, rcFirstItem, LVIR_BOUNDS);
  for( int i = rcFirstItem.left; i < rcClient.right; 
       i += m_cxBitmap )
    for( int j = rcFirstItem.top; j < rcClient.bottom; 
         j += m_cyBitmap )
      pDC->BitBlt( i, j, m_cxBitmap, m_cyBitmap, &tempDC, 
                   0, 0, SRCCOPY );
 }

Anschließend kann die Hintergrundfarbe gezeichnet werden:

 if( bHighlight )
 {
  pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
  pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
 
  pDC->FillRect(rcHighlight, 
                &CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
 }
 else if( m_bitmap.m_hObject == NULL )
   pDC->FillRect(rcHighlight, 
                 &CBrush(::GetSysColor(COLOR_WINDOW)));

Als Clippingregion ergibt sich:

 rcCol.right = rcCol.left +GetColumnWidth(0);
 CRgn rgn;
 rgn.CreateRectRgnIndirect(&rcCol);
 pDC->SelectClipRgn(&rgn);
 rgn.DeleteObject();

Nachdem die Ausgabe des Hintergrunds fertig ist, kann der Vordergrund gezeichnet werden, der aus den einzelnen Bestandteilen der Listview besteht. Zunächst wird das Symbol, sofern vorhanden, links plaziert.

 if (lvi.state & LVIS_STATEIMAGEMASK)
 {
  int nImage = ((lvi.state & LVIS_STATEIMAGEMASK)>>12) - 1;
  pImageList = GetImageList(LVSIL_STATE);
  if (pImageList)
  {
   pImageList->Draw(pDC, nImage,
    CPoint(rcCol.left, rcCol.top), ILD_TRANSPARENT);
  }
 }

Anschließend kann das normale Symbol wie auch das Overlay-Symbol gezeichnet werden.

 pImageList = GetImageList(LVSIL_SMALL);
 if (pImageList)
 {
  UINT nOvlImageMask=lvi.state & LVIS_OVERLAYMASK;
  pImageList->Draw(pDC, lvi.iImage, 
   CPoint(rcIcon.left, rcIcon.top),
   (bHighlight?ILD_BLEND50:0) | ILD_TRANSPARENT | nOvlImageMask );
 }

Die Labeltexte werden über die Methode DrawText() des DC-Objekts ausgegeben. Das Label für die erste Spalte besitzt die Attribute:

 rcLabel.left  += offset/2;
 rcLabel.right -= offset;
 
 pDC->DrawText(sLabel, -1, rcLabel,
               DT_LEFT | DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP | 
               DT_VCENTER | DT_END_ELLIPSIS);

Die Label der übrigen Spalten werden ohne Hervorhebung gezeichnet.

 LV_COLUMN lvc;
 lvc.mask = LVCF_FMT | LVCF_WIDTH;
 
 if( m_nHighlight == 0 ) 
 {
  pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
  pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));
 }
 
 rcBounds.right = rcHighlight.right > rcBounds.right ? 
                     rcHighlight.right : rcBounds.right;
 
 rgn.CreateRectRgnIndirect(&rcBounds);
 pDC->SelectClipRgn(&rgn);
       
 for(int nColumn = 1; GetColumn(nColumn, &lvc); nColumn++)
 {
  rcCol.left = rcCol.right;
  rcCol.right += lvc.cx;

Falls benötigt wird noch der Hintergrund gezeichnet.

if(m_bitmap.m_hObject == NULL && m_nHighlight == HIGHLIGHT_NORMAL)
  pDC->FillRect(rcCol, &CBrush(::GetSysColor(COLOR_WINDOW)));
sLabel = GetItemText(nItem, nColumn);
 if (sLabel.GetLength() == 0)
   continue;

Die Textausrichtung für die Ausgabe der Texte via DrawText() wird wie folgt umgesetzt:

  UINT nJustify = DT_LEFT;
  switch(lvc.fmt & LVCFMT_JUSTIFYMASK)
  {
  case LVCFMT_RIGHT:
    nJustify = DT_RIGHT;
    break;
  case LVCFMT_CENTER:
    nJustify = DT_CENTER;
    break;
  default:
    break;
  }

Anschließend kann die Textausgabe erfolgen.

  rcLabel = rcCol;
  rcLabel.left += offset;
  rcLabel.right -= offset;
pDC->DrawText(sLabel, -1, rcLabel, 
                nJustify | DT_SINGLELINE | DT_NOPREFIX | 
                DT_VCENTER | DT_END_ELLIPSIS);
 }

Besitzt der Eintrag den Fokus, muß letztlich noch ein Fokusrechteck eingesetzt werden.

if (lvi.state & LVIS_FOCUSED && (GetFocus() == this))
   pDC->DrawFocusRect(rcHighlight);

Abschließend wird der Ausgabekontext wiederhergestellt und die Ausgabe ist beendet.

 pDC->RestoreDC( nSavedDC );
}

DrawItem() modifizieren

Wenn eine Bitmap als Hintergrund verwendet wird, macht es keinen Sinn, den Hintergrund zu löschen, da die Bitmap auf jeden Fall wieder als Hintergrund angezeigt wird. Ein Löschen bewirkt somit lediglich ein Flickern, so daß der Handler der entsprechenden Benachrichtigung TRUE zurückliefert, falls eine Bitmap vorhanden ist.

BOOL CMyListCtrl::OnEraseBkgnd(CDC* pDC) 
{
  if (m_bitmap.m_hObject != NULL )
    return TRUE;
  return CListCtrl::OnEraseBkgnd(pDC);
}

Handler für WM_ERASEBKGND

Wenn eine Spalte bezüglich ihrer Breite geändert wird, so muß die neue Fläche im Falle einer Vergrößerung neu gefüllt werden. Dadurch entsteht wie bei einer Verkleinerung ein unattraktiver Bruch in der Hintergrundbitmap, so daß es angebracht ist, ab der betroffenen Spalte die gesamte rechte Seite bis zum Rand der Control neu zu zeichnen. Es wird daher jedesmal die rechte Seite der Control invalidiert, wenn eine Größenänderung in OnNotify() angezeigt wird. Da die Nachricht HDN_TRACK nicht erzeugt wird, testet die Methode statt dessen auf HDN_ITEMCHANGING..

BOOL CMyListCtrl::OnNotify(WPARAM wParam, LPARAM lParam, 
                           LRESULT* pResult) 
{
 HD_NOTIFY *pHDN = (HD_NOTIFY*)lParam;
 
 if(pHDN->hdr.code == HDN_ITEMCHANGINGW || 
    pHDN->hdr.code == HDN_ITEMCHANGINGA)
 {
  if( m_bitmap.m_hObject != NULL )
  {
    CRect rcClient;
    GetClientRect( &rcClient );
    DWORD dwPos = GetMessagePos();
    CPoint pt( LOWORD(dwPos), HIWORD(dwPos) );
    ScreenToClient( &pt );
    rcClient.left = pt.x;
    InvalidateRect( &rcClient );
  }
 }
 return CListCtrl::OnNotify(wParam, lParam, pResult);
}

Handler OnNotify()

Die Nachricht WM_QUERYNEWPALETTE wird an ein Fenster gesendet, wenn dieses Fenster den Eingabefokus erhalten soll. Damit erhält das Fenster die Möglichkeit, seine logische Palette zu realisieren, um seine Objekte möglichst optimal darzustellen. Die Nachricht WM_PALETTECHANGED wird dagegen an alle Fenster gesendet, wenn die Systempalette geändert wurde. Wird in diesem Fall nicht auf die Änderung reagiert, erscheinen die Farben der Hintergrundbitmap falsch umgesetzt.

Palettenwechsel

Die beiden Nachrichten WM_QUERYNEWPALETTE und WM_PALETTECHANGED werden von Windows lediglich an Top-Level-Fenster gesendet, so daß eine Anwendung diese Nachrichten bearbeiten und an die ListView Control weiterreichen muß. Für eine Dialoganwendung könnte dies beispielhaft so aussehen:

void CListViewDlg::OnPaletteChanged(CWnd* pFocusWnd) 
{
 CDialog::OnPaletteChanged(pFocusWnd);
 
 m_listctrl.SendMessage(WM_PALETTECHANGED, 
                        (WPARAM)pFocusWnd->m_hWnd );
}
BOOL CListViewDlg::OnQueryNewPalette() 
{
 CDialog::OnQueryNewPalette();
return m_listctrl.SendMessage( WM_QUERYNEWPALETTE );
}

Die Funktion OnQueryNewPalette() prüft zunächst, ob sie eine Palette reselektieren muß. Ist eine logische Palette vorhanden, wird das Fenster invalidiert, sofern eine Farbe remappt wurde.

BOOL CMyListCtrl::OnQueryNewPalette() 
{
 CClientDC dc(this);
 if (dc.GetDeviceCaps(RASTERCAPS) & RC_PALETTE && 
     m_pal.m_hObject != NULL )
 {
  dc.SelectPalette( &m_pal, FALSE );
  BOOL result = dc.RealizePalette();
  if( result )
    Invalidate();
  return result;
 }
 return CListCtrl::OnQueryNewPalette();
}

Die Funktion OnPaletteChanged() kehrt ohne weitere Bearbeitungsschritte zurück, wenn die ListView selbst für die Nachricht verantwortlich war, weil sie selbst die Palette geändert hatte. Ansonsten wird die Palette über einen Aufruf von OnQueryNewPalette() realisiert.

void CMyListCtrl::OnPaletteChanged(CWnd* pFocusWnd) 
{
 CListCtrl::OnPaletteChanged(pFocusWnd);
if( pFocusWnd == this )
  return;
OnQueryNewPalette();
}

Nachrichten weiterleiten

Wie wird in einer Listbox ein individueller farbiger Hintergrund für einzelne Zeilen realisiert?

Frage

Soll nicht für alle Zeilen die gleiche Hintergrundfarbe gesetzt werden, muß eine zeilenspezifische Farbe gesetzt werden. Abgesehen davon, daß die Informationen über die jeweilige Farbe in geeigneten Datenstrukturen verwaltet werden müssen, ist die Methode DrawItem() zu implementieren. Der nachfolgend vorgestellte Code setzt der Einfachheit halber für alle hervorzuhebenden Zeilen die Farbe gelb als Hintergrund. Die entscheidende Codesequenz wird über die boolsche Variable bHighlight gesteuert, die zuvor - nach welchen Vorgaben auch immer - initialisiert wird. Der Ausgabekontext wird in lpDrawItemStruct geliefert.

void CMyListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
 CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);

Nach der Initialisierung von bHighlight erfolgt die Ausgabe:

if( bHighlight )
 {
  pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
  pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
pDC->FillRect(rcHighlight, 
                &CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
 }
 else
 {
  CRect rcClient, rcRow = rcItem;
  GetClientRect(&rcClient);
  rcRow.right = rcClient.right;
pDC->FillRect(rcRow, &CBrush(nItem%2 ? 
                       ::GetSysColor(COLOR_WINDOW) : 
                       RGB(255,255,0)));
 }

Soll statt der gelben Farbe eine andere verwendet werden, muß anstelle von RGB(255, 255, 0) aus einer geeigneten Datenstruktur die zuständige Farbe ermittelt und gesetzt werden. Wie dies zu erfolgen hat, ist anwendungsspezifisch und soll hier nicht weiter erläutert werden.

Lösung





Sachgebiet


© 2009-2012 by Alojado Publishing. Alle Rechte vorbehalten. Ausgewiesene Marken gehören ihren jeweiligen Eigentümern.
Mit der Benutzung dieser Seite erkennen Sie die Nutzungsbedingungen und die Datenschutzerklärung an. Der Betreiber übernimmt keine Haftung für den Inhalt verlinkter externer Internetseiten.
Seite erzeugt 2012-02-22 22:15:59 von textarchiv.alojado.de