[WinAPI] Grafische Buttons erstellen

mh1001

Lt. Commander
Registriert
Nov. 2003
Beiträge
2.039
Hallo zusammen,

ich habe heute schon Stunden verbracht, irgendwas zu meinem Problem zu finden, doch scheint sich im Netz irgendwie nichts geeignetes zu finden.

Und zwar möchte ich für ein Programm, eigene grafische Button erstellen.
Dieses soll als Schalter fungieren.
Dazu müssten zu folgenden Button-Zuständen entsprechende Bitmaps auf das Button gelegt werden:
Zum einen der Standard-Zustand, sprich das Button ist nicht selektiert.
Als nächstes der Zustand, in dem das Button gedrückt ist - sprich in dem der Anwender seine linke Maustaste über dem Button-Bereich gedrückt hält und schließlich noch der aktivierte Zustand, der eintritt, wenn das Button vom Anwender angeklickt wurde.

Als ersten Ansatz habe ich schon einmal den entsprechenden Button-Style "BS_OWNERDRAW" gefunden. Jedoch darf dieser Style laut der MSDN-Dokumentation mit keinem anderen Style kombiniert werden, wodurch sich mir auch wieder die Frage stellt, wie ich den gedrückten Zustand ermitteln kann.

Gibt es vielleicht irgendwo im Netz ein Tutorial zu dieser Thematik oder hat irgend jemand zufällig einen entsprechenden Beispielcode parat?

MfG mh1001
 
Zuletzt bearbeitet:
Warum nimmst du nicht einen normalen Button aus der VCL/CLX/MFT (Was nutzt du?) und machst die Paint-Methode selbst?
 
Danke für deine Antwort. ;)
Ich schreibe meine Anwendung in C++ mit Dev-C++-Editor.
Somit greife ich auch nicht auf VCL oder CLX zurück.
Mit dem Begriff MFT kann ich jedoch nichts anfangen, den bringe ich nur mit "Master File Table" in Verbindung. ;)
Code:
[...]und machst die Paint-Methode selbst?
Leider beschäftige ich mich erst seit kurzem mit WinAPI.
Wenn du ein Beispiel oder ein Tutorial hättest, wie dies in der Praxis aussehen könnte, wäre ich dir sehr dankbar. ;)

MfG mh1001
 
Zuletzt bearbeitet:
Siehe hier:

http://msdn.microsoft.com/library/d...cc/platform/commctls/buttons/usingbuttons.asp

Code:
BOOL CALLBACK OwnDrawProc(HWND hDlg, UINT message, WPARAM wParam, 
                          LPARAM lParam) 
{ 
    HDC hdcMem; 
    LPDRAWITEMSTRUCT lpdis; 
 
    switch (message) 
    { 
        case WM_INITDIALOG: 
 
            // hinst, hbm1 and hbm2 are defined globally. 
            hbm1 = LoadBitmap((HANDLE) hinst, "OwnBit1"); 
            hbm2 = LoadBitmap((HANDLE) hinst, "OwnBit2"); 
            return TRUE; 
 
        case WM_DRAWITEM: 
            lpdis = (LPDRAWITEMSTRUCT) lParam; 
            hdcMem = CreateCompatibleDC(lpdis->hDC); 
 
            if (lpdis->itemState & ODS_SELECTED)  // if selected 
                SelectObject(hdcMem, hbm2); 
            else 
                SelectObject(hdcMem, hbm1); 
 
            // Destination 
            StretchBlt( 
                lpdis->hDC,         // destination DC 
                lpdis->rcItem.left, // x upper left 
                lpdis->rcItem.top,  // y upper left 
 
                // The next two lines specify the width and 
                // height. 
                lpdis->rcItem.right - lpdis->rcItem.left, 
                lpdis->rcItem.bottom - lpdis->rcItem.top, 
                hdcMem,    // source device context 
                0, 0,      // x and y upper left 
                32,        // source bitmap width 
                32,        // source bitmap height 
                SRCCOPY);  // raster operation 
 
            DeleteDC(hdcMem); 
            return TRUE; 
 
        case WM_COMMAND: 
            if (wParam == IDOK 
                || wParam == IDCANCEL) 
            { 
                EndDialog(hDlg, TRUE); 
                return TRUE; 
            } 
            if (HIWORD(wParam) == BN_CLICKED) 
            { 
                switch (LOWORD(wParam)) 
                { 
                    case IDC_OWNERDRAW: 
 
                        // application-defined processing 
 
                        break; 
                } 
            } 
            break; 
 
        case WM_DESTROY: 
            DeleteObject(hbm1);  // delete bitmaps 
            DeleteObject(hbm2); 
 
            break; 
 
    } 
    return FALSE; 
        UNREFERENCED_PARAMETER(lParam); 
}

Nur die Paint Methode zu überladen ist nicht 100% da man nicht bei jedem Event benachrichtigt wird. Das führt dann auf die Dauer zu "zerstörten" oder nicht aktuellen Buttons.

MfG

Arnd
 
Zuletzt bearbeitet:
Vielen Dank für deine Antwort. :daumen:
Auf die Idee, in der MSDN-Dokumentation noch weiter zu suchen, wäre ich nie gekommen - vor allem da ich imho schon einmal auf dieser Seite war und diese aber nach der Überschrift "Using Buttons That Are Not Owner Drawn" wohl gleich beiseite gelegt habe, ohne auf die Idee zu kommen, herunterzuscrollen. ;)
Nach langem umherprobieren ist es mir nun auch gelungen, obigen Code entsprechend für meinen Anwendungszweck anzupassen und zum Laufen zu bringen. ;)

MfG mh1001

//Edit:

Nun stehe ich gleich schon wieder vor einem neuen Problem. Die Buttons funktionieren nun zwar mittlerweile perfekt, doch nun steht das nächste Problem an: die Trackbars.
Eigentlich geht es mir nur darum, diese auszublenden und nur den Schieberegler in Form eines Bitmaps anzuzeigen.
Viel mehr als diese kleine Auflistung konnte ich dazu in der MSDN nicht finden.
Doch bekomme ich damit irgendwie nichts auf die Reihe. ;)
Wenn auch hierzu jemand einen Tipp - oder noch besser ein Codebeispiel - hätte wäre ich sehr dankbar.
 
Zuletzt bearbeitet:
Hallo mh1001,

ich würde ja gerne was dazu schreiben, aber leider ist mir Dein Anliegen nicht ganz klar.
Was meinst Du mit ausblenden und nur Bitmap anzeigen?

Wenn wirklich nichts sichtbar sein soll ausser einem Schieberegler, dann geht das nicht.

Eventuell kannst Du mit dem Fensterstil Ownerdraw arbeiten.
Aber einen Support der Trackbar dafür gibt es nicht.

D.h. am einfachsten programmierst Du so ein Control selber.
Was schliesslich auch nicht allzu schwierig ist.

MfG

Arnd
 
[...] Wenn wirklich nichts sichtbar sein soll ausser einem Schieberegler, dann geht das nicht. [...]
Ja, genau so war es eigentlich gemeint. Eigentlich dachte ich, dass das vorhaben dadurcheinfacher wird, wenn nur ein einziger Teil, in dem Fall der Schieberegler, als Bitmap angezeigt werden soll und der Rest gleich ganz ausgeblendet werden soll.
Natürlich wäre es aber auch denkbar, einfach ein Bitmap mit der Textur des Hintergrunds den Teile, die eigentlich unsichtbar sein sollten, zuzuweisen.
Oder ist dies auch so nicht realisierbar?

[...] D.h. am einfachsten programmierst Du so ein Control selber.
Was schliesslich auch nicht allzu schwierig ist. [...]
Wenn dies möglich ist wäre das sicher auch keine schlechte Lösung.
Das Problem ist nur, dass ich mich mit WinAPI noch absolut kaum auskenne und auch nicht wüsste, wie ich soetwas am besten angehen könnte.
Doch gerade bei WinAPI habe ich im Gegensatz zu sämtlichen anderen Schnittstellen/Programmiersprachen immer größte Mühe, auch nur irgendwas brauchbares im Netz zu finden. :(

MfG mh1001
 
Zuletzt bearbeitet:
Wie wäre es mit der MFC?
Das ganze geht auch in reinem WinAPI ist aber dann halt umständlicher.

Mal allgemein formuliert:

- Ein Fenster anlegen mit CreateWindow das als Tracker dienen soll.
Es muss gross genug sein um die Bitmap anzeigen zu können.
- In WM_CREATE eine Bitmap anlegen, die später gezeigt werden soll.
- in WM_ERASEBACKGROUND z.B. die Ticks anzeigen.
- IN WM_PAINT die aktuelle Position der Bitmap anzeigen
- In WM_LBUTTONDOWN den aktuellen Punkt merken
- In WM_MOUSEMOVE und bei gedrückter linker Maustaste die aktuelle Position verschieben.
Dann die alte und die neue Position als invalid markieren (InvalidateRect), damit diese neu gezeichnet werden.
- Bei WM_LBUTTONUP die aktuelle Position merken und die aktuelle Position als invalid markieren (InvalidateRect), damit sie neu gezeichnet wird.

Das einzige was Du brauchst ist eine POINT Variable um die aktuelle Position der Bitmap zu merken. Sowie ein Handle auf die Bitmap, die Ausmasse der Bitmap, des Fensters, sowie den Wertebereich und den aktuellen Wert Deines Ownerdraw Trackers.

Den Ansatz Teile des Controls mit einer Textur auszublenden, mag verführerisch sein wird aber nie 100% funktionieren. Lass das ganze mal auf einem anderen OS laufen. Angenommen du entwickelst unter Windows 2000 und lässt Dein Programm dann unter XP laufen. Dort hat sich der Standard Hintergrund geändert, dann wird man sofort sehen das dort gefummelt wird. Unter XP ist der Hintergrund auch keine einfache Farbe mehr sondern ein Farbverlauf, dort musst Du dann mit Themes arbeiten. D.h es wäre schon mal Code für 2 BS erforderlich. UNd es gibt sicher noch mehr Szenarien in denen es auffällt.
Ausserdem wird es eventuell deutlich sichtbar flackern oder, .....

Also wenn es ein bisschen professionell aussehen soll, mach es am besten selber oder benutze dokumentierte Schnittstellen in der ihnen zugedachten Weise.

MfG

Arnd
 
Zuletzt bearbeitet:
Besten Dank für deine Mühe und den guten Ansatz. :)
Dann werde ich mich nun einmal daran machen, die einzelnen Schritte in die Tat umzusetzen.
Sollte es dabei zu Problemen kommen werde ich mich hier nochmals melden. ;)

MfG mh1001

//Edit:

Das mit dem Hintergrund wäre nicht so schlimm, da dieser ebenfalls ein Bitmap ist, und somit fest definiert ist.
Aber du hast mich sowieso schon mit deinem anderem Ansatz überzeugt, womit ich mir dies wohl besser trotzdem gleich aus dem Kopf schlage. ;)

//Edit 2:

Ich habe nun einmal etwas Zeit gefunden und nach deiner Anleitung jetzt auch schon etwas ganz brauchbares machen können.

In WM_CREATE erstelle ich den Schieberegler mit folgendem Code:

Code:
schieberegler = CreateWindow("STATIC", 0, WS_VISIBLE | WS_CHILD | SS_BITMAP , (ll_geschwindigkeit / 10) + 42, 51, 28, 19, hwnd, (HMENU)LL_GESCHWINDIGKEIT, (HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE), NULL);

SendMessage(schieberegler, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)schieber);
Darauf folgen dann folgende Teile in der Nachrichtenschleife:

Code:
case WM_LBUTTONDOWN:
{
  if(LOWORD(lParam) >= (ll_geschwindigkeit / 10) + 42 && LOWORD(lParam) <= (ll_geschwindigkeit / 10) + 70 && HIWORD(lParam) >= 51 && HIWORD(lParam) <= 60)
  {
    regler_aktiv = 1;
  }
}
break;

case WM_LBUTTONUP:
{
  regler_aktiv = 0;
}
break;

case WM_MOUSEMOVE:
{
  if(regler_aktiv)
  {
    if(LOWORD(lParam) < 42)
    {
      SetWindowPos(schieberegler, 0, 42, 51, 0, 0, SWP_NOZORDER | SWP_NOSIZE);

      ll_geschwindigkeit = 10;
    }
    else if(LOWORD(lParam) > 142)
    {
      SetWindowPos(schieberegler, 0, 142, 51, 0, 0, SWP_NOZORDER | SWP_NOSIZE);

      ll_geschwindigkeit = 1000;
    }
    else
    {
      SetWindowPos(schieberegler, 0, LOWORD(lParam), 51, 0, 0, SWP_NOZORDER | SWP_NOSIZE);

      ll_geschwindigkeit = (LOWORD(lParam) - 42) * 10;
    }
  }
}
break;
Die entsprechenden Variablen sehen so aus:

Code:
HWND schieberegler;

int ll_geschwindigkeit = 375;
bool regler_aktiv = 0;
Jetzt habe ich nur zwei Probleme:

Das erste Problem ist, dass wenn man die Maus über dem Schieberegler oder einem anderen Control loslässt, die WM_LBUTTONUP-Nachricht nicht gesendet wird.
Diese benötige ich aber, um den Schieberegler wieder "loszulassen". ;)

Das zweite Problem äußert sich in so fern, dass beim schnellen ziehen unschöne graue "Spuren" (graue Pixel im vorherigen Schieber-Bereich, die für etwa 1/10 Sekunde sichtbar bleiben) hinter dem Regler auftauchen.
Ich denke, dass dieses Problem mit dem von dir angesprochenem "InvalidateRect" zusammenhängt, jedoch weis ich leider nicht genau, wie ich dies am besten anwende.

Sollteet ihr noch ein paar ansonsten noch ein paar Verbesserungsvorschläge für den sicher nicht gerade einwandfreien Code haben, nehme ich diese natürlich auch gerne entgegen. ;)

MfG mh1001
 
Zuletzt bearbeitet:
Hallo mh1001,

den Buttonup Event bekommst Du wenn bei Button Down ein SetCapture und bei up ein ReleaseCapture machst. Das sollte aber zusätzlich auch bei WM_KILLFOCUS passieren.

Dein Ansatz ist immer noch zu aktiv orientiert. Mir ist nicht klar was Du mit dem SetWindowPos willst. Ich würde den Schieberegler nicht als Fenster realisieren der dann mittels SetWindowPos verschoben wird. Sondern ich würde ein Fenster anlegen in dem eine Bitmap gezeichnet wird die den Schieberegler darstellt.

In den Zeichenevents WM_PAINT und WM_ERASEBACKGROUND wird dann die Bitmap gezeichnet.

Wenn Du das zeichnen der Bitmap zusätzlich in eine Offscreen Bitmap machst bekommst Du dadurch eine absolut flackerfreie Darstellung hin. Eine Offscreen Bitmap bekommst Du mit CreateCompatibleBitmap() und CreateCompatibleDC(). Die OffscreenBitmap wird dann mit BitBlt() am Bildschirm angezeigt.

Im Create würde ich auch das WS_VISIBLE erst mal weglassen. Im WM_CREATE wird dann die Bitmap angelegt, werden alle benötigten Variablen gesetzt. Das zeichnen vorbereitet und dann wenn alles fertig ist wird das Fenster sichtbar.

PseudoCode:
Code:
  CreateWindow()
...
case WM_CREATE:
  createBitmap()
  createOffscreenBitmap();
  initRangeValues();
break;
case WM_ERASEBACKGROUND
  paintControlBackgroundToOffscreenBitmap();
  displayOffscreenBitmap();
return TRUE;
case WM_PAINT:
  drawBitmapAtCurrentPositionToOffscreenBitmap();
  displayOffscreenBitmap();
break;
case WM_LBUTTONDOWN:
  saveCurrentPosition();
  SetCapture();
  InvalidateRect(0);
break;
case WM_KILLFOCUS:
case WM_LBUTTONUP:
case WM_DESTROY:
  ReleaseCapture();
break;
case WM_MOUSEMOVE:
  if( LButtonDown() )
  {
    saveCurrentPosition();
    InvalidateRect(0);
  }
break;

MfG

Arnd
 
Zuletzt bearbeitet:
Hallo Arnd,

ich habe nun noch einmal alles nach deinen Tipps etwas umgestaltet und nun läuft scheinbar auch alles perfekt.
Nochmals besten Dank für Bemühungen. :)
Ohne dich hätte ich das wohl nie zum Laufen bekommen. ;)

MfG mh1001
 
Zurück
Oben