Visual C++ .NET - Programmiertipps und Codebeispiele:

Hinweise:
Ich bin zwar kein Programmieranfänger, aber absolut neu sowohl in C++ als auch in Visual C++. Folgende Tipps und Codebeispiele habe ich mir für mein erstes Visual C++ .Net-Projekt RanDEM mühsam zusammengesucht und aus unzähligen Debugsessions herausgefiltert. Auf dieser Seite sammle ich meine ersten Erkenntnisse und Erleuchtungen, um später nicht wieder an den gleichen Problemen zu hängen. Vielleicht kann der ein oder andere hier noch etwas lernen oder nützlichen Quellcode finden.

Die Namen für die SDI-Anwendung ("Programm"), Variablen und so weiter sind von mir frei gewählt und müssen natürlich entsprechend angepasst werden.

Irgendwie ist es mir noch nicht gelungen, vom MainFrame aus einen gültigen Zeiger auf View bzw. Doc zu bekommen. Deswegen lege ich Membervariablen gerne im MainFrame an, da ich von überall mit

CMainFrame* pFrame = (CMainFrame*) AfxGetMainWnd();

einen Zeiger dorthin bekomme. Dieser Workaround ist sicher nicht ideal, bitte nicht verinnerlichen, sondern mir stattdessen eine bessere Lösung mailen.

Für Lob, Kritik, Verbesserungsvorschläge oder Korrekturen bitte bei mir melden: niki@nikis.de

- Allgemeines:

In Visual C++ ist es leider sehr aufwändig, Variablen, Funktionen etc. nachträglich zu verschieben. Deshalb sollte man sich vor dem Programmieren schon Gedanken machen, wo man alles anlegt.

Ein (zumindest bei mir) häufiger und schwer zu erkennender Fehler ist das fehlende Einbinden einer erforderlichen include-Datei wie z.B. Mainfrm.h . Am besten bei so wundersamen Fehlermeldungen wie 'CMainFrame': nicht deklarierter Bezeichner oder Ungültig, da der Operand vom Typ "unknown-type" ist, gleich die include-Dateien überprüfen.

Normalerweise nutzt die Release-Version eines Programms die MFC-Bibliotheken von Windows. Dadurch wird die Datei zwar kleiner, wenn man das Programm aber an andere weitergeben möchte, muss man sicherstellen, dass diese alle erforderlichen Bibliotheken (z.B. MFC70.DLL) installiert haben. Wenn man allerdings im Assistenten für neue Projekte unter "Anwendungstyp", "Verwendung von MFC" "MFC in einer statischen Bibliothek verwenden" auswählt, werden die erforderlichen Dateien in die Programmdatei in der Release-Konfiguration integriert.

- Fenstertitel bei SDI-Anwendungen:

In vom Assistenten erstellten SDI-Anwendungen wird üblicherweise als Fenstertitel "Programmname - Dokumentname" angezeigt (wobei der Dokumentname anfangs "unbekannt" ist). Den Fenstertitel kann man aber mit der SetWindowText-Methode wie folgt den eigenen Wünschen anpassen:

BOOL CProgrammApp::InitInstance()
{
   ...
   m_pMainWnd->SetWindowText("neuer Titel");
   m_pMainWnd->ShowWindow(SW_SHOW);
   m_pMainWnd->UpdateWindow();
   ...
}

Wer nur den Dokumentnamen von "unbekannt" auf etwas anderes setzen möchte:

BOOL CProgrammDoc::OnNewDocument()
{
   SetTitle("neuer Dokumentname");
   ...
}


- nichtmodalen Dialog als eine Art Toolbox im Vordergrund anzeigen:

gewünschten Dialog (IDD_MEINDIALOG) im Ressourceneditor anlegen,
die Eigenschaften Topmost und Toolfenster auf True setzen,
neue zugehörige Klasse (CMeinDialog) mit Basisklasse CDialog hinzufügen,
in CMainFrame eine Membervariable (m_MeinDialog) diesen Typs (CMeinDialog) anlegen,

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   // Das neue Dialogfeld erzeugen
   m_MeinDialog.Create(IDD_MEINDIALOG, NULL);
   // Das neue Dialogfeld gleich zu Beginn anzeigen (stattdessen kann auch im
   // Ressourceneditor die Eigenschaft Sichtbar des Dialogfelds auf True gesetzt werden)
   m_MeinDialog.ShowWindow(SW_SHOW);
   ...
}

über die Meldung WM_ACTIVATEAPP kann man den Dialog ausblenden, wenn zu einer anderen Anwendung gewechselt wird:

void CMainFrame::OnActivateApp(BOOL bActive, DWORD dwThreadID)
{
   CFrameWnd::OnActivateApp(bActive, dwThreadID);
   // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein.
   if (bActive)
   {
      m_MeinDialog.ShowWindow(SW_SHOW);
   }
   else
   {
      m_MeinDialog.ShowWindow(SW_HIDE);
   }
}

- neuen Menüpunkt anlegen und diesen ankreuzbar machen (um z.B. oben erwähnten Dialog ein- und auszublenden):

in CMainFrame eine Membervariable (m_bMeinDialog) des Typs bool anlegen,
im Ressourceneditor einen neuen Menüpunkt (ID_ANSICHT_MEINDIALOG) z.B. unter Ansicht hinzufügen,
diesem Menüpunkt den Ereignishandler COMMAND in der Klasse CMainFrame hinzufügen:

void CMainFrame::OnAnsichtMeinDialog()
{
   // TODO: Fügen Sie hier Ihren Befehlsbehandlungscode ein.
   if (m_bMeinDialog)
   {
      m_bMeinDialog=FALSE;
      m_MeinDialog.ShowWindow(SW_HIDE);
   }
   else
   {
      m_bMeinDialog=TRUE;
      m_MeinDialog.ShowWindow(SW_SHOW);
   }
}

diesem Menüpunkt (ID_ANSICHT_MEINDIALOG) zusätzlich den Ereignishandler UPDATE_COMMAND_UI in der Klasse CMainFrame hinzufügen:

void CMainFrame::OnUpdateAnsichtMeinDialog(CCmdUI *pCmdUI)
{
   // TODO: Fügen Sie hier Ihren Befehlsaktualisierungs-UI-Behandlungscode ein.
   if (m_bMeinDialog)
   {
      pCmdUI->SetCheck(1);
   }
   else
   {
      pCmdUI->SetCheck(0);
   }
}

damit der Dialog auch beim Wechseln zwischen Anwendungen immer der Markierung des Menüpunktes entsprechend angezeigt wird, muß noch folgende Veränderung vorgenommen werden:

void CMainFrame::OnActivateApp(BOOL bActive, DWORD dwThreadID)
{
   CFrameWnd::OnActivateApp(bActive, dwThreadID);
   // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein.
   if (bActive && m_bMeinDialog)
   {
      m_MeinDialog.ShowWindow(SW_SHOW);
   }
   else
   {
      m_MeinDialog.ShowWindow(SW_HIDE);
   }
}

- Wertebeschränkungen von Steuerelementen ohne Popupwarnmeldung einhalten:

beim Hinzufügen der zugehörigen Membervariablen (m_iEingabe vom Typ int) keine Min. bzw. Max. Werte setzen,
stattdessen dem Steuerelement (IDC_EINGABE) einen Ereignishandler für die Meldung EN_KILLFOCUS hinzufügen:

void CMeinDialog::OnEnKillfocusEingabe()
{
   // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode für die Benachrichtigung ein.
   // Werte aus den Steuerelementen des Dialogfelds lesen
   UpdateData(TRUE);
   // Wertebeschränkungen setzen
   int minimal=1;
   int maximal=100;
   // Werte korrigieren
   m_iEingabe=(m_iEingabe<minimal?minimal:m_iEingabe);
   m_iEingabe=(m_iEingabe>maximal?maximal:m_iEingabe);
   // Steuerelemente im Dialogfeld mit neuen Werten aktualisieren
   UpdateData(FALSE);
}

- Bitmap aus Datei laden:

in CMainFrame eine Membervariable (m_bmpBitmap) vom Typ CBitmap erstellen,

// Pointer auf MainFrame holen, um dessen Membervariable zu erreichen
CMainFrame* pFrame = (CMainFrame*) AfxGetMainWnd();
HBITMAP hBitmap = (HBITMAP) ::LoadImage(AfxGetInstanceHandle(), "Bild.bmp", IMAGE_BITMAP,
   0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION);
// Ist Handle für das geladene Bild gültig?
if (hBitmap)
{
   // Aktuelles Bitmap löschen
   if (pFrame->m_bmpBitmap.DeleteObject())
   {
      // war Bitmap vorhanden, lösen
      pFrame->m_bmpBitmap.Detach();
   }
   // Aktuell geladenes Bitmap mit Bitmap-Objekt verbinden
   pFrame->m_bmpBitmap.Attach(hBitmap);
   // Anzeigebereich für ungültig erklären, um Neuzeichnen zu veranlassen
   pFrame->Invalidate();
}

- geladenes Bitmap darstellen (normal, anpassen oder dehnen):

void CProgrammView::OnDraw(CDC* pDC)
{
   CProgrammDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
   // Pointer auf MainFrame holen, um dessen Membervariable zu erreichen
   CMainFrame* pFrame = (CMainFrame*) AfxGetMainWnd();
   // Geladenes Bitmap holen
   BITMAP bmp;
   pFrame->m_bmpBitmap.GetBitmap(&bmp);
   // zu pDC kompatiblen Gerätekontext erzeugen, in den Bitmap geladen wird
   CDC SpeicherDC;
   SpeicherDC.CreateCompatibleDC(pDC);
   // Bitmap in den kompatiblen Gerätekontext selektieren
   CBitmap* pAltesBitmap = (CBitmap*)SpeicherDC.SelectObject(pFrame->m_bmpBitmap);
   // Anzeigebereich verfügbar machen
   CRect lRect;
   GetClientRect(lRect);
   lRect.NormalizeRect();
   // Bitmap in pDC kopieren und evt. in Größe anpassen

   // Bitmap normal anzeigen
   pDC->StretchBlt(0, 0, bmp.bmWidth, bmp.bmHeight, &SpeicherDC,
      0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);

   // Bitmap an Anzeigebereich anpassen (festes Seitenverhältnis)
   // Breitenverhältnis zwischen Anzeigebereich und Bitmap ermitteln
   double bv=((lRect.Width())/static_cast<float>(bmp.bmpWidth));
   // Höhenverhältnis zwischen Anzeigebereich und Bitmap ermitteln
   double hv=(lRect.Height()/static_cast<float>(bmp.bmpHeight));
   // Anpassungsfaktor ermitteln
   double Anpassung=(bv<hv?bv:hv);
   pDC->StretchBlt(0, 0, static_cast<int>(bmp.bmpWidth*Anpassung), static_cast<int>
      (bmp.bmpHeight*Anpassung), &SpeicherDC, 0, 0, bmp.bmpWidth, bmp.bmpHeight, SRCCOPY);

   // Bitmap auf Anzeigebereich dehnen
   pDC->StretchBlt(0, 0, lRect.Width(), lRect.Height(), &SpeicherDC,
      0, 0, bmp.bmpWidth, bmp.bmpHeight, SRCCOPY);
}

Natürlich sollte man vom Ende des Quellcodes nur eine der drei Darstellungsvarianten (normal, anpassen, dehnen) übernehmen. Außerdem sollte man noch sicherstellen, dass bereits ein CBitmap in m_bmpBitmap geladen wurde.

- DDB- und DIB-Bitmaps:

Geräteabhängige Bitmaps (Device-dependent bitmaps, DDB) sind der Form, wie sie letztendlich vom zugehörigen Gerätetreiber dargestellt werden, sehr ähnlich. Aus diesem Grund, sind DDBs zu anderen Geräten in der Regel inkompatibel (z.B. wegen unterschiedlichen Farbtiefen).

Geräteunabhängige Bitmaps (Device-independent bitmaps, DIB) sind logisch strukturiert und können von jedem Gerätetreiber verwendet werden. Der Nachteil von DIBs ist, dass sie im Vergleich zu DDBs wesentlich langsamer auf den Anzeigebereich gerendert werden.

- Farbtiefe des Gerätekontextes ermitteln:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   CClientDC dc(this);
   int Farbtiefe = GetDeviceCaps(dc,BITSPIXEL);
   ...
}

oder

void CProgrammView::OnDraw(CDC* pDC)
{
   CtestDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
   CDC SpeicherDC;
   SpeicherDC.CreateCompatibleDC(pDC);
   int Farbtiefe = GetDeviceCaps(HDC(SpeicherDC),BITSPIXEL);
}

- selbstdefiniertes DDB erzeugen und darstellen:

Die Methode CreateCompatibleBitmap erzeugt ein zum angegebenen Gerätekontext (pDC) passendes Bitmap. Wenn wir allerdings die Bitmap-Bits selbst setzen wollen (mit der Methode SetBitmapBits), müssen wir die Farbtiefe des Gerätekontextes herausfinden und die Bitmap-Bits entsprechend strukturieren. Das erstellte Bitmap kann in einem Gerätekontext mit anderer Farbtiefe nicht dargestellt werden.

Alternativ können wir auch mit der Methode CreateBitmap eine bestimmte Farbtiefe vorgeben und gleich die Bitmap-Bits (entsprechend der vorgegebenen Farbtiefe strukturiert) übergeben. Das erstellte Bitmap kann in einem Gerätekontext mit anderer Farbtiefe auch nicht dargestellt werden.

Um das Bitmap von der Farbtiefe unabhängig zu machen, kann man DIBs verwenden. Diese sind aber aufgrund der langsamen Rendergeschwindigkeit selten zu empfehlen.

void CProgrammView::OnDraw(CDC* pDC)
{
   CProgrammDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
   CBitmap bitmap;
   CDC SpeicherDC;
   SpeicherDC.CreateCompatibleDC(pDC);
   int Breite = 4;
   int Hoehe = 2;

   // Farbtiefe ermitteln in Bits/Pixel: 8, 16 oder 32
   int Farbtiefe = GetDeviceCaps(HDC(SpeicherDC),BITSPIXEL);

   // Bitmap-Bits in BYTE-Array festlegen:

   // für 8 Bit-Farbtiefe, je Pixel 1 Byte, Format: Indexwert
   BYTE bits8[8]={
     0,  249,  250,  252,
   253,  254,  251,  255};

   // für 16 Bit-Farbtiefe, je Pixel 2 Byte, Format: 16 Bit-RGB565
   BYTE bits16[16] = {
     0,  0,     0,248,   224,  7,    31,  0,
    31,248,   255,  7,   224,255,   255,255};

   // für 32 Bit-Farbtiefe, je Pixel 4 Byte, Format: B,G,R,0
   BYTE bits32[32] = {
     0,  0,  0,  0,     0,  0,255,  0,     0,255,  0,  0,   255,  0,  0,  0,
   255,  0,255,  0,   255,255,  0,  0,     0,255,255,  0,   255,255,255,  0};

   // Bitmap erstellen mit CreateBitmap
   switch (Farbtiefe)
   {
      case 8: bitmap.CreateBitmap(Breite,Hoehe,1,Farbtiefe,&bits8);break;
      case 16: bitmap.CreateBitmap(Breite,Hoehe,1,Farbtiefe,&bits16);break;
      case 32: bitmap.CreateBitmap(Breite,Hoehe,1,Farbtiefe,&bits32);break;
      default: TRACE("Fehler: Farbtiefe von %i nicht vorgesehen!",Farbtiefe);
   }

   // oder alternativ Bitmap erstellen mit CreateCompatibleBitmap und SetBitmapBits
   bitmap.CreateCompatibleBitmap(pDC,Breite,Hoehe);
   switch (Farbtiefe)
   {
      case 8: bitmap.SetBitmapBits(8,&bits8);break;
      case 16: bitmap.SetBitmapBits(16,&bits16);break;
      case 32: bitmap.SetBitmapBits(32,&bits32);break;
      default: TRACE("Fehler: Farbtiefe von %i nicht vorgesehen!",Farbtiefe);
   }

   // Bitmap anzeigen
   CBitmap* pAltesBitmap = (CBitmap*)SpeicherDC.SelectObject(bitmap);
   RECT rect;
   GetClientRect(&rect);
   pDC->StretchBlt(0, 0, rect.right-rect.left, rect.bottom-rect.top, &SpeicherDC,
      0, 0, Breite, Hoehe, SRCCOPY);
}

Natürlich sollte nur eine der beiden Varianten (CreateBitmap oder CreateCompatibleBitmap mit SetBitmapBits) verwendet werden.

Das Ergebnis müßte ein 4 Pixel breites und 2 Pixel hohes Bitmap (auf die Größe des Anzeigebereichs gedehnt) nach folgendem Farbmuster sein:



Das Erstellen des Bitmaps kann natürlich auch außerhalb der OnDraw-Funktion erfolgen, dann kann das Bitmap über eine Membervariable (m_bmpBitmap) vom Typ CBitmap übergeben werden. Wenn allerdings die Farbtiefe geändert und OnDraw aufgerufen wird, ohne das Bitmap neu zu erstellen, kann es nicht angezeigt werden, da es ja für die ursprüngliche Farbtiefe erstellt wurde.

- 24-Bit RGB in 16-Bit RGB(565) umwandeln:

Bei der Umwandlung von 24-Bit RGB in 16-Bit RGB(565) werden zuerst die Einzelfarbwerte von 8 Bit auf 5 Bit (rot und blau) bzw. 6 Bit (grün) reduziert (z.B. mit Division durch 8 bzw. 4),
diese werden dann wieder wie folgt zusammengesetzt:

high-byte     low-byte
1111 1 000   000 1 1111
RRRR R GGG   GGG B BBBB

Im BYTE-Array für die 16 Bit-Bitmap-Bits kommt immer zuerst das low-, dann das high-byte.

Hier der Quellcode:

BYTE rot,gruen,blau,lowbyte,highbyte;
rot = 255;
gruen = 0;
blau = 255;
lowbyte = ( (gruen/4)*32 + (blau/8) ) %256;
highbyte = ( (rot/8)*8 + ((gruen/4)/8) ) %256;

Bei diesen vorgegebenen Farbwerten erhält man für highbyte = 248 und für lowbyte=31.

- selbstdefiniertes DIB erzeugen und darstellen:

void CProgrammView::OnDraw(CDC* pDC)
{
   CtestDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen
   BITMAPINFOHEADER bmpih;
   bmpih.biSize=40;
   // Breite des Bitmaps
   bmpih.biWidth=64;
   // Höhe des Bitmaps
   bmpih.biHeight=64;
   bmpih.biPlanes=1;
   bmpih.biBitCount=32;
   bmpih.biCompression=BI_RGB;
   bmpih.biSizeImage=0;
   bmpih.biXPelsPerMeter=0;
   bmpih.biYPelsPerMeter=0;
   bmpih.biClrUsed=0;
   bmpih.biClrImportant=0;

   BITMAPINFO bmpi;
   bmpi.bmiHeader=bmpih;

   RGBQUAD* pFarben;

   HBITMAP hbmp;
   hbmp = CreateDIBSection((HDC)pDC, &bmpi, DIB_RGB_COLORS,(void **) &pFarben,NULL,NULL);
   RGBQUAD rgbFarbe;
   for (int i=0;i<bmpih.biWidth*bmpih.biHeight;i++)
   {
      rgbFarbe.rgbRed = (BYTE) (i%256);
      rgbFarbe.rgbGreen = (BYTE) (i%256);
      rgbFarbe.rgbBlue = (BYTE) (i%256);
      pFarben[i]=rgbFarbe;
   }

   // Bitmap anzeigen
   CDC SpeicherDC;
   SpeicherDC.CreateCompatibleDC(pDC);
   SpeicherDC.SelectObject(hbmp);
   RECT rect;
   GetClientRect(&rect);
   pDC->StretchBlt(0, 0, rect.right-rect.left, rect.bottom-rect.top, &SpeicherDC,
      0, 0, bmpih.biWidth, bmpih.biHeight, SRCCOPY);
}

Die Breite, die Höhe, und die Farbwerte der einzelnen Pixel (pFarben[i], hier Graustufen) können natürlich beliebig verändert werden. Außerdem kann die Erstellung des Bitmaps woanders erfolgen und nur das Bitmap über Membervariablen an die OnDraw-Funktion übergeben werden.

Das einmal erstellte DIB-Bitmap lässt sich auch nach dem Verändern der Farbtiefe noch darstellen, allerdings ist die Rendergeschwindigkeit insgesamt geringer als bei DDBs.