21. fejezet - WinAPI control-ok és resource-ok


Az elözö fejezetben megmutattam, hogyan kell csinálni egy üres ablakot. Most megmutatom hogyan lehet csinálni nem üres ablakot. Mindenki tükön ülve, körmeit rágva várja, hogy vajon hogy lehet olyat csinálni...

<dobpergés> <cintányér>

Nem kell izgulni, mindössze control-okat (gomb, cimke stb.) fogok hozzáadni az ablakhoz (phuhh...). A legfontosabb függvények amiket meg kell jegyezni:

  • CreateWindow
  • CreateWindowEx
  • CreateMenu
  • SendMessage
  • DialogBox
  • EndDialog
A vicc az, hogy a legtöbb control-t az elsö két függvénnyel lehet létrehozni, kivéve a menüt, mert azt a CreateMenu()-vel. Az elöbbieknél az lpClassName a control osztályának neve, pl. "BUTTON". Az osztály név nem case sensitive, azaz akárhogyan le lehet írni. Az egyes tulajdonságokat a dwStyle paraméterrel lehet megadni, viszont a WS_CHILD mindig legyen ezek között. A létrehozható control-okról egy lista található itt: Control Library. A példakódokban mindenhol ASCII karakterkódolást használok (állítólag bugos; érdemes lehet UNICODE-ot használni).

Bár ennek a tutoriálnak a célja a programászati oldalról való bemutatása a winapinak, kitérek egy kicsit a resource file-okra is és azt is megmutatom, hogyan lehet owner-drawn controlokat csinálni.


You are in control

Az elözö fejezetbeli RegisterClassEx() alapján a controlok osztályait is be kell regisztrálni, kivéve a standard controlokat. Ezeket létre lehet hozni simán a CreateWindow() függvénnyel, mert a winapi olyan rendes, hogy beregisztrálja öket. A bonyolultabb control-okhoz már kelleni fog a ComCtl32.lib és neked kell ezt megtenni az InitCommonControlsEx() függvénnyel. Tehát érdemes úgy elindulni, hogy ez már a kódban van, és ha kell valamilyen típus, akkor egyszerüen hozzáadod a megfelelö flag-et.

CODE
#pragma comment(lib, "comctl32.lib") #include <windows.h> #include <commctrl.h> // ... void CreateControls() { INITCOMMONCONTROLSEX iccs; // alap control-ok és a különféle bar-ok (pl. trackbar) iccs.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES; iccs.dwSize = sizeof(INITCOMMONCONTROLSEX); InitCommonControlsEx(&iccs); // ... }

Gondolom senki nem lepödik meg ezen; teljesen világos, hogy ha több controlt akarsz, akkor control szexet kell csinálni.

A control-ok üzeneteit ugyanúgy a föablak WndProc metódusában kapjuk meg, a WM_COMMAND üzenettel. A paraméterek között lehet a control azonosítója és/vagy a control ablakleírójára mutató pointer. A control-ok tulajdonságait késöbb is lehet állitgatni a SendMessage() függvénnyel.


Menü

A menüvel az a helyzet, hogy nem nagyon lehet beleszólni a kinézetébe, tehát hasonlóan az ablakhoz ez is olyan stílusú lesz amilyen be van állítva (Xp, Vista stb.). Ami nem is olyan nagy baj. A föablak létrehozása után az alábbi módon lehet egy menüt hozzácsatolni:

CODE
// ezek lesznek az azonosítók #define IDM_OPEN_ITEM 1001 #define IDM_SAVE_ITEM 1002 #define IDM_SAVEAS_ITEM 1003 #define IDM_EXIT_ITEM 1004 #define IDM_ABOUT_ITEM 1005 HMENU menu = CreateMenu(); HMENU submenu1 = CreatePopupMenu(); HMENU submenu2 = CreatePopupMenu(); AppendMenuA(submenu1, MF_STRING, IDM_OPEN_ITEM, "&Open"); AppendMenuA(submenu1, MF_STRING, IDM_SAVE_ITEM, "&Save"); AppendMenuA(submenu1, MF_STRING, IDM_SAVEAS_ITEM, "Save &As"); AppendMenuA(submenu1, MF_SEPARATOR, 0, 0); AppendMenuA(submenu1, MF_STRING, IDM_EXIT_ITEM, "&Exit"); AppendMenuA(submenu2, MF_STRING, IDM_ABOUT_ITEM, "&About"); AppendMenuA(menu, MF_STRING|MF_POPUP, (UINT)submenu1, "&File"); AppendMenuA(menu, MF_STRING|MF_POPUP, (UINT)submenu2, "&Help"); SetMenu(hwnd, menu);

Az elvétve fellelhetö & jelek accelerator-ok (értsd: shortcut), ami akkor fog aktiválódni, ha lenyomod az alt-ot. Ekkor a megjelölt betüt lenyomva megnyílik a hozzátartozó menü. Billentyüzet partizánoknak ajánlott.

Fontos, hogy nem lehet összevissza kavargatni ezeket a függvényhívásokat, tehát érdemes úgy kialakítani a rendszeredet, hogy ebben a sorrendben hívódjanak meg. Hasonlóan a menüelemek sorrendjét sem az azonosítóik határozzák meg, hanem a hívási sorrend. Az eredmény valami ilyesmi:

Menu image

Az egyes menüelemekhez tartozó akciókat a WndProc -ban lehet megadni. Például az About menüponthoz az alábbit kell csinálni:

CODE
case WM_COMMAND: switch( LOWORD(wParam) ) { case IDM_ABOUT_ITEM: MessageBoxA(hwnd, "WinAPI tutorial 2", "About", MB_OK); break; // ... default: break; }

Menükröl további info található itt: Menus


Gomb

Más nyelvekkel ellentétben a gombok közé tartozik minden ami gombhoz hasonló, tehát a checkbox, a radiobutton és a groupbox is. Mi az, hogy nem is hasonlít? Dehogyisnem:

Button vs. groupbox


Na ugye. Itt most a pushbutton-t fogom megmutatni, a többi hasonlóan kezelhetö. A létrehozás a CreateWindow() függvénnyel történik, például így:

CODE
#define IDC_BUTTON1 1020 button1 = CreateWindow( "BUTTON", "Click Me", WS_VISIBLE|WS_CHILD|BS_PUSHBUTTON, 300, starty, 120, 40, hwnd, (HMENU)IDC_BUTTON1, hinst, 0);

Ennyi. Nem kell hozzáadni az ablakhoz mint a menüt, automatikusan létrejön és felszabadul. Az egyetlen szépséghibája, hogy a Windows 95-ös idöket idézi, de majd késöbb megmutatom hogyan lehet átvariálni XP vagy Vista stílusúvá. A lenyomás kezelése teljesen hasonló a menüéhez:

CODE
case WM_COMMAND: switch( LOWORD(wParam) ) { case IDC_BUTTON1: MessageBoxA(hwnd, "LE nyomtad!", "A gombot", MB_OK); break; // ... }

Ha lusták lennétek azonosítót csinálni, akkor az lParam-ban ott a gomb HWND-je.


Label, picturebox

Ilyenek önmagukban nincsenek, ezekhez a static típusú control-t kell használni. Ami még rosszabb, hogy nem méretezödnek automatikusan (a szövegüktöl függöen), így elég sok mindent neked kell hozzá megirni. Alapvetöen tök ugyanugy kell létrehozni, mint a gombot, csak a "STATIC" osztálynévvel és nyilván az ehhez tartozó stílus flag-ekkel. Ki is használom a lehetöséget, hogy egy ilyen static controlba forgómorgót rajzoljak, de elöbb:

Trackbar

Ez nem standard control, tehát be kell regisztrálni (ICC_BAR_CLASSES), illetve a CreateWindowEx() függvénnyel kell létrehozni.

CODE
#define IDC_TRACKBAR 1022 trackbar = CreateWindowEx( 0, TRACKBAR_CLASS, "trackbar1", WS_CHILD|WS_VISIBLE|TBS_AUTOTICKS, 80, starty + 2, 200, 30, hwnd, (HMENU)IDC_TRACKBAR, hinst, NULL);

Mivel a winapi olyan nagyon következetes, a trackbar üzijeit nem a WM_COMMAND-on keresztül kapod meg, hanem a WM_VSCROLL és WM_HSCROLL-on keresztül. Söt itt már ID-t se kapsz, mert válság van meg minden. Az lParam-ból kell kiszedni a HWND-t.

CODE
case WM_VSCROLL: case WM_HSCROLL: if( (HWND)lParam == trackbar ) { switch( LOWORD(wParam) ) { case TB_LINEUP: case TB_LINEDOWN: case TB_PAGEUP: case TB_PAGEDOWN: // page up/down vagy nyilak break; case TB_THUMBTRACK: // egér break; } }

A thumb pozíciójának lekérését majd késöbb látni fogjátok. Bövebben ld. Trackbar


Resource fájlok

Tudom, hogy sokkal jobban szeretitek a kódból való programozást, de amikor a 734-edik dialogot programozod le, akkor nálad is kiborul a bili. Föleg ha ezt pure winapiban teszed (ne rontsuk már el mindenféle OOP bugyutasággal). Resource fájlt úgy lehet hozzáadni a projekthez (visual studio), hogy jobb klikk és Add -> Resource. És akkor meg is nyit...valamit, amit aztán nemtud bezárni, de ha sikerül valahogy megnyitni a kódját, akkor valami ilyen zagyvaság fogad:

CODE
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE 9, 1 #pragma code_page(1252) // ...

Bármennyire is viszket töle a tenyered, NE töröld ki, mert nem fog müködni. Keress egy neked tetszö helyet, és oda kapard a saját dolgaidat. Például egy dialogot így kell csinálni:

CODE
// resource.h-ba #define IDD_DIALOG1 100 #define IDC_RADIO1 10 #define IDC_RADIO2 11 #define IDC_RADIO3 12 #define IDC_GROUP1 13 // .rc fájlba IDD_DIALOG1 DIALOGEX 0, 0, 176, 102 STYLE DS_SETFONT|DS_MODALFRAME|DS_FIXEDSYS|WS_POPUP|WS_VISIBLE|WS_CAPTION|WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "Change indicator" FONT 8, "MS Shell Dlg", 0, 0 BEGIN DEFPUSHBUTTON "OK", IDOK, 64, 80, 50, 16 DEFPUSHBUTTON "Cancel", IDCANCEL, 120, 80, 50, 16 CONTROL "Type", IDC_GROUP1, "Button", BS_GROUPBOX, 6, 6, 100, 56 CONTROL "Circle", IDC_RADIO1, "Button", BS_AUTORADIOBUTTON, 20, 20, 38, 10 CONTROL "Square", IDC_RADIO2, "Button", BS_AUTORADIOBUTTON, 20, 32, 38, 10 CONTROL "Gear", IDC_RADIO3, "Button", BS_AUTORADIOBUTTON, 20, 44, 38, 10 END

Ezek az értékek nem pixelben értendöek. hanem logikai koordinátákban (így akkor is jól fog kinézni a cucc ha mondjuk kiprinteled). Tipikusan próba szerencse alapon kell belöni öket (amúgymeg vannak ilyen függvények, hogy DPtoLP meg fordítva).

A DEFPUSHBUTTON az olyan, hogy nem kell külön definiálni az ID-ját, mert egy beépitettet adhatsz meg neki, például IDOK. Ez azért van így, mert általában minden dialognak van OK és Cancel gombja. A dialog megnyitása így néz ki:

CODE
INT_PTR WINAPI DialogProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch( msg ) { case WM_CLOSE: EndDialog(hWnd, 1); break; case WM_COMMAND: switch( LOWORD(wParam) ) { case IDOK: // ... return TRUE; case IDCANCEL: SendMessage(hWnd, WM_CLOSE, 0, 0); return TRUE; } default: break; } return FALSE; } // valahol a kódban DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_DIALOG1), hWnd, &DialogProc);

Bármennyire is kívánkozik a DestroyWindow() meghívása, ne tedd, mert legyilkolja a föablakot is.


Forgómorgó

Szintén nincs. Illetve van homokóra, de nem sikerült még életre keltenem. Viszont van egy ilyen dolog amit úgy hívnak, hogy GDI+, és meglepö módon objektum orientált! Ennek segitségével lehet rajzolni animált gif-et. Elöször is egy hasznos oldal, amit nem szeretnék elfelejteni: Chimply. Itt lehet mindenféle forgómorgót csinálni, mindenféle színnel és méretben. Már csak le kéne programozni.

Úgy fog ez kinézni, hogy van egy static control, amibe GDI+-al belerajzoljuk az animált GIF-et. Rajzolni egy Graphics típusú objektummal lehet, aminek meg kell adni a control handle-jét (a háttérben ugyanúgy a control device context-jét használja, mint ahogy minden böcsületes winapi programozó csinálná).

CODE
#include <windows.h> #include <gdiplus.h> class AnimatedGIF : public Gdiplus::Image { protected: Gdiplus::Graphics* gfx; IStream* stream; HWND container; UINT numframes; UINT currentframe; // ... };

Elég fontos, hogy a GDI+ objektumokat nem szerencsés automatikus változóként létrehozni, mert szanaszéjjel szállhat a program. Ezért mivel úgyis leszármazok belöle, egy kötelezöen meghívandó Dispose() metódussal szabadítom fel, amiben viszont el kell végezni az ösosztály destruktorának dolgait, mert szintén szanaszét fog szállni:

CODE
void AnimatedGIF::Dispose() { delete gfx; stream->Release(); if( nativeImage ) { Gdiplus::DllExports::GdipDisposeImage(nativeImage); nativeImage = 0; } }

Kitérö: akkor mi a francért hozom létre automatikusan? Nem hozom. De esetleg te majd igen, és nem fogod érteni, hogy mi a bánatért száll el ;)

Maga a GIF betöltés nem egyértelmü, de mindenki leszedi a tutorial kódját és megnézi. Amire oda kell még figyelni, hogy rajzoláskor elöbb egy bitmapba rajzolj (és rögtön méretezd is át a képet) és utána rakd ki a controlba, különben villogni fog.

Maga az animálás úgy történik, hogy egy timer-re rákötöd az alábbi metódust:

CODE
void AnimatedGIF::NextFrame() { if( numframes > 0 ) { currentframe = (currentframe + 1) % numframes; if (nativeImage) SelectActiveFrame(&Gdiplus::FrameDimensionTime, currentframe); } }

Ilyen timert hozzá lehet csapni az ablakhoz a SetTimer() függvénnyel, aminek vagy megadsz egy függvénypointert, vagy a WM_TIMER üzenetben hajtod végre amit szeretnél. Egy létezö timert úgy lehet módosítani, hogy úgy csinálsz, mintha nem is létezne. Például a trackbar-hoz így lehet hozzákötni:

CODE
case TB_THUMBTRACK: { unsigned short pos = HIWORD(wParam); SetTimer(hWnd, IDC_TIMER, (6 - pos) * 20, 0); } break;

Ugyanezt el lehet játszani a többi üzenetnél is. Maga a rajzolás pedig a WM_DRAWITEM üzenetben történik.

Ne felejtsd el felinicializálni a GDI+-t a GdiplusStartup() függvénnyel, különben nem rosszabb helyen fog elszállni a program, mint minden new hívásnál.


Owner-drawn combo box

Elég sok fejfájást okozott, hogy a comboboxban nem lehet letiltani elemet. Végül hosszas guglizás és hajtépés után sikerült valamit összeszenvednem, ami elfogadhatónak mondható.

Elöször is az ilyen comboboxoknak saját WndProc-ot csináltam, amihez kell egy táblázat, ami megmondja, hogy melyik handle-hez melyik combobox tartozik. Ez egyébként is hasznos dolog, mert egy értelmesebb libraryban minden control ilyen.

A lényege a dolognak az, hogy a combobox itemek rajzolását is a WM_DRAWITEM üzenet csinálja meg. Ha az item elsö karaktere @, akkor az az item le van tiltva és szürkével tolom ki. A CBN_SELCHANGE üzenet pedig, ha mégis ilyen elem lett kiszelektálva, akkor visszaállitja az elözöt. Ehhez a föablaknak meg kell hivni a custom combo ProcessCommands() metódusát.

CODE
void CustomCombo::RenderItem(LPDRAWITEMSTRUCT ds) { TCHAR itemtext[256]; COLORREF fg = 0, bg = 0; // ... SendMessage(ds->hwndItem, CB_GETLBTEXT, ds->itemID, (LPARAM)itemtext); textlen = strlen(itemtext); if( itemtext[0] == '@' ) { fg = SetTextColor(ds->hDC, GetSysColor(COLOR_GRAYTEXT)); --textlen; } else { bg = SetBkColor( ds->hDC, GetSysColor((ds->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_WINDOW)); fg = SetTextColor( ds->hDC, GetSysColor((ds->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)); } ExtTextOut( ds->hDC, 2 * x, y, ETO_CLIPPED|ETO_OPAQUE, &ds->rcItem, &itemtext[i], (UINT)textlen, NULL); // ... }

CODE
LRESULT CustomCombo::ProcessCommands(WPARAM wparam, LPARAM lparam) { HWND ctrl = (HWND)lparam; WORD msg = HIWORD(wparam); if( msg == CBN_SELCHANGE ) { TCHAR itemtext[256]; DWORD ind = SendMessage(ctrl, CB_GETCURSEL, 0, 0); SendMessage(ctrl, CB_GETLBTEXT, ind, (LPARAM)itemtext); if( itemtext[0] == '@' ) SendMessage(ctrl, CB_SETCURSEL, cursel, 0); else cursel = ind; } return 0; }

Nagyjából ennyi, a többi már csak körítés. Amit sehogyan nem sikerült megcsinálnom, hogy Vista stílusú legyen, ugyanis ezt a winapisok "elfelejtették" leimplementálni. Azaz csak úgy tudtam volna megcsinálni, ha teljesen én rajzolom az egész controlt, amihez már nem volt kedvem. Egyébként ezt az uxtheme nevü csodát kellene használni.


XP és Vista stílusok

A módszer részletesen le van írva az alábbi linken: Enabling visual styles. Neked elég az alábbi direktívát hozzáadni a kódhoz (mindenféle #include elött):

CODE
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")

Ezzel a XP-s control library-t linkeli hozzá a kódhoz, így ha a célgépen megtalálható az a DLL, akkor azt fogja használni. Ezen kivül a control-okhoz tartozó szövegek köverségét ki lehet kapcsolni az alábbi módon:

CODE
HFONT font = (HFONT)GetStockObject(DEFAULT_GUI_FONT); SendMessage(button1, WM_SETFONT, (WPARAM)font, MAKELPARAM(TRUE, 0));

Saját fontot létrehozhatsz a CreateFont() függvénnyel.


Summarum

Nem túl részletesen megmutattam hogyan lehet a WinAPI-val szórakozni. Ha használható szintre akarod hozni ezt az egész rendszert, akkor érdemes valami OOP interfészt ráhúzni. Erre lehet találni példát a DummyFramework-ben (föoldal), vagy a Win32++ libraryban (ugyanaz mint az MFC, és amúgy elég hasznos).

Kód itt.

Result



Höfö:
  • Csinálj nyalóka control-t (olyan progressbar, ami nem jelez semmit, csak balról jobbra megy a csíkja, mint a Knight Rider-ben)!
  • Találd ki, hogyan lehet azt is megtiltani a comboboxban, hogy rákattintsanak az elemre!

back to homepage

Valid HTML 4.01 Transitional Valid CSS!