Δευτέρα 26 Ιουλίου 2010

C++ Μια γλωσσά για console application; Μπααα!

Πολλοί που ασχολούνται με τον προγραμματισμό και κυρίως με γλωσσά C/C++ έχουν αρχίσει με το κλασικότατο console app
int main()
{
printf("Hello world!");
return 0;
}
και δύστυχος πολλοί έχουν μείνει εκεί και άλλοι το παράτησαν. Φυσικά και μιλάω για άτομα σα και έμενα , τα οποία δεν έχουν πάει να σπουδάσουν το αντικείμενο, ώστε να έχουν μια πλήρη εικόνα του. Λοιπών θα γράψω μια μικρή εισαγωγή για το πως θα φτιάξουμε ένα παράθυρο στα windows. Για να καταλάβετε αυτά που θα γράψω, πρέπει να ξέρετε μια από τις δυο γλώσσες (c++ ή c).



Γιατί native windows και όχι κάποιο "portable third party library"; Γιατί μια εφαρμογή εξαρτάτε άπω το OS και θεωρώ ότι είναι άσκοπο να ζητήσεις κάτι άπω το OS μέσου κάποιας τρίτης βιβλιοθήκης. Επίσης όταν είναι native δεν χρειάζεται να κουβαλάει διάφορα dll.

Γιατί θεωρείται δύσκολο το native win gui; Γιατί είναι event based, κάτι πολύ διαφορετικό από το consola, και από διάφορα third pary gui. Αλλά καθόλου δύσκολο!

Πρώτα πρώτα να δούμε το μαγικό τύπο των windows ο οποίος είναι το HANDLE. Το Handle μπορούμε να  το παρομοιάσουμε με pointer, ΠΡΟΣΟΧΉ δεν είναι pointer!!! Απλός η δουλεία του είναι παρόμοια με αυτή του pointer. Ένα handle μας δείχνει κάποιο object το οποίο έχουμε φτιάξει εμείς (ή κάποιο άλλο πρόγραμμα). Το object μπορεί να είναι ένα window, ένα κουμπί ή και ένα αρχείο το οποίο είναι έτοιμο για ανάγνωση, μέχρι και κάποιο block μνήμης που έχουμε δεσμεύσει. Για αυτό το λόγο τα handle έχουν κάποιες κατηγορίες που προσδιορίζουν το αντικειμένου που δείχνουν, έχουμε πχ το HFILE είναι handle για αρχεία, το HWND είναι για παράθυρα και controls κτλ.

Πλήρη λίστα των hande έχει εδώ, ο,τι αρχίζει από H είναι handle.

Ένα window app αρχίζει οπός και ένα κονσόλα. Δηλαδή έχει main function.

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
INT: Είναι τύπος integrate
WINAPI: Είναι calling convention που χρησιμοποιεί το windows api (__stdcall)
hInstance: Είναι ο handle του προγράμματος μας
hPrevInstance: Ξεπερασμένο
lpCmdLine: Το κλασικό argv
nCmdShow: Flag για την παρουσίαση του παραθύρου

Για να φτιάξουμε ένα παράθυρο θέλουμε μονό έναν header (windows.h), στο παρόν παράδειγμα θέλουμε και το tchar.h εφόσον γράφουμε ελληνικά.

Ας φτιάξουμε ένα παράθυρο
#include <windows.h>
#include <tchar.h>

HINSTANCE hInst;
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{

    MSG         Msg;
HWND        hWnd;
    WNDCLASSEX  cw;
hInst            = hInstance;

    cw.cbSize        = sizeof(WNDCLASSEX);
    cw.style         = CS_HREDRAW | CS_VREDRAW;
    cw.lpfnWndProc   = WndProc;
    cw.cbClsExtra    = NULL;
    cw.cbWndExtra    = NULL;
    cw.hInstance     = hInstance;
    cw.hIcon         = LoadIcon(hInstance, IDI_APPLICATION);
    cw.hCursor       = LoadCursor(NULL, IDC_ARROW);
    cw.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    cw.lpszMenuName  = NULL;
    cw.lpszClassName = L"noname";
    cw.hIconSm       = LoadIcon(hInstance, IDI_APPLICATION);

    RegisterClassEx(&cw);

    hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
                          L"noname",
                          L"Τιτλος",
                          WS_OVERLAPPEDWINDOW,
                          100,
                          100,
                          800,
                          600,
                          NULL,
                          NULL,
                          hInstance,
                          NULL);

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    while( GetMessage(&Msg, NULL, 0, 0) )
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return 0;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    switch(Msg)
    {
    case WM_DESTROY:
        PostQuitMessage(WM_QUIT);
        break;
    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

Αυτή είναι η διαδικασία για να φτιάξουμε ένα απλό κενό παράθυρο.
Πάνω βλέπουμε μια global μεταβλητή που είναι το handle του προγράμματος μας. Μέσα στη main έχουμε 3 μεταβλητές.

WNDCLASSEX : Περιέχει διάφορα στοιχεία σχετικά με το window που έχουμε, οπός menubar, application icon, cursor, background, και φυσικά το class name και winproc

  • lpszClassName Εδώ βάζουμε ένα όνομα στο πρότυπο μας, έτσι ώστε όποτε θέλετε ένα καινούριο παράθυρο, να μην γράφετε άλλη WNDCLASSEX. 
  • lpfnWndProc Εδώ μπαίνει ο pointer της winproc, θα το δούμε πιο κάτω.


HWND: Το handle του παράθυρου (όχι του προγράμματος)
Και τέλος είναι το MSG. Τι είναι αυτό; Είναι message (μήνυμα), και στην ουσία όλο το ζουμί. Είπα και πιο πάνω ότι τα windows είναι event based OS.
 Πως δουλεύει; Λοιπόν, λεμέ στο OS πχ να φτιάξει ένα παράθυρο, αυτό όμως δεν εξυπηρετεί μονό εμάς, άλλα παράλληλα και άλλες εφαρμογές, έτσι βάζει την αίτηση μας στη σειρά με τις άλλες αιτήσεις  που έχουν κάνει άλλα προγράμματα. Και όταν φτάσει η σειρά μας, τότε αυτό μας στέλνει ένα μήνυμα, να μας πει αν είναι οκ ή ο,τι άλλο θέλει να μας πει. Αυτή η διαδικασία γίνεται πάρα πολύ γρήγορα.

Ωραία εφόσον έχουμε φτιάξει το WNDCLASSEX τώρα πρέπει να τον "στείλουμε" στο OS. Αυτό γίνεται με το κάλεσμα της RegisterClassEx. Αν γίνει το regist τότε μας επιστρέφει 0 σε άλλη περίπτωση μας επιστρέφει το index (αν δε κάνω λάθος)

Τώρα που έχουμε το πρότυπο, μας απομένει να στείλουμε μια αίτηση στο OS να μας φτιάξει το παράθυρο. Αυτό γίνεται με τη συνάρτηση CreateWindowEx η οποία παίρνει τα παρακάτω ως παραμέτρους
HWND WINAPI CreateWindowEx(
  __in      DWORD dwExStyle,
  __in_opt  LPCTSTR lpClassName,
  __in_opt  LPCTSTR lpWindowName,
  __in      DWORD dwStyle,
  __in      int x,
  __in      int y,
  __in      int nWidth,
  __in      int nHeight,
  __in_opt  HWND hWndParent,
  __in_opt  HMENU hMenu,
  __in_opt  HINSTANCE hInstance,
  __in_opt  LPVOID lpParam
);

  • dwExStyle Δεν θυμάμαι τι ακριβός είναι
  • lpClassName Το όνομα του WNDCLASSEX που δώσαμε. Προσοχή, αν βάλουμε λάθος όνομα δεν θα δημιουργηθεί κανένα παράθυρο.
  • lpWindowName Ο τίτλος
  • dwStyle Διάφορα flags οπός να έχει κουμπί minimize close etc...
  • x,y Το μέρος στο οποίο θα εμφανιστεί το παράθυρο.
  • nWidth/nHeigt Το μέγεθος του client
  • HWND/HMENU Είναι για MDI. Parent/children window etc..
  • hInstance Το Handle του προγράμματος
  • lpParam και αυτό για MDI

Αν αποτύχει η κατασκευή, μας επιστρέφει NULL σε άλλη περίπτωση μας επιστρέφει το handle του παραθύρου, το οποίο και κρατάμε σε μια local var γιατί το θέλουμε για μετά.

Έπειτα καλούμε την ShowWindow με παράμετρο το handle που πήραμε από το CreateWindow. Υπάρχει και το UpdateWindow το οποίο δεν μας ενδιαφέρει αυτή τη στιγμή, και για το παρόν παράδειγμα, δεν μας χρειάζεται, είτε το βάλετε είτε όχι δεν θα δείτε καμιά διαφορά.

Και τέλος αν πάνε όλα καλά, φτάνουμε στο σημείο στο οποίο θα περιμένουμε τα μηνύματα από το OS. Αυτό γίνεται με την βοήθεια της συνάρτησης GetMessage

BOOL WINAPI GetMessage(
  __out     LPMSG lpMsg,
  __in_opt  HWND hWnd,
  __in      UINT wMsgFilterMin,
  __in      UINT wMsgFilterMax
);


  • lpMsg Το μονό που μας ενδιαφέρει, είναι το μήνυμα που λάβαμε. Το περνούμε by ref (by pointer λογού οτι το api είναι σε C)
  • hWnd Το Handle απτό οποίο θέλουμε να πάρουμε κάποιο μήνυμα (βάζουμε NULL για να πάρει μηνύματα απο το current thread)
  • Φίλτρα.
Εφόσον λάβουμε κάποιο μήνυμα πρέπει να το στείλουμε στη winproc του παραθύρου. Αυτο γινετε καλοτας την συνάρτηση  DispatchMessage η οποία στέλνει το μήνυμα στη winproc.

Τι είναι η winproc; Είναι η συνάρτηση στην οποία αξιολογούμε τα μηνύματα που έρχονται. 
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    switch(Msg)
    {
    case WM_DESTROY:
        PostQuitMessage(WM_QUIT);
        break;
    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

Αυτή είναι μια απλή winproc, το μονό που κάνει η παραπάνω είναι να περιμένει το πάτημα του X για να τερματίσει το πρόγραμμα. Με το παραπάνω παράδειγμα δεν μπορώ να εξηγήσω παραπάνω πράματα για τη winproc. Για αυτό θα γράψω πως φτιάχνουμε το κάθε control ξεχωριστά.

Όλα τα controls τα φτιάχνουμε με τη συνάρτηση CreateWindow (ή CreateWindowEx), όλα τα αποθηκεύουμε σε handle τύπου HWND. Αν γίνει οποιαδήποτε αλλαγή από τον χρήστη, περνούμε message που μας λέει την αλλαγή που έγινε. Τέλος δεν φτιάχνουμε WNDCLASSEX διότι υπάρχουν στο σύστημα, και όλα τα φτιάχνουμε στη winproc στο μήνυμα WM_CREATE.

Button 
Το πιο κλασικό control σε ένα πρόγραμμα είναι το κουμπί. Για να μην κάνω όλη την ώρα copy-paste όλο το κωδικά που χρειάζεται να τρέξει το πρόγραμμα. Θα ποστάρω μονό τον κωδικά που υπάρχει στη winproc. Στην ουσία μονό εκεί αλλάζει.
HWND button1;
HWND button2;
#define BTN1_EVENT 101
#define BTN2_EVENT 102
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{

    switch(Msg)
    {
case WM_DESTROY:
PostQuitMessage(WM_QUIT);
break;
case WM_CREATE:
{
button1= CreateWindowEx(NULL,L"Button",L"Πατα με!",WS_CHILD | WS_VISIBLE,10,30,80,20,hWnd,(HMENU)BTN1_EVENT,hInst,NULL);
button2= CreateWindowEx(NULL,L"Button",L"Κουμπακι",WS_CHILD | WS_VISIBLE,10,70,80,20,hWnd,(HMENU)BTN2_EVENT,hInst,NULL);
}
break;
case WM_COMMAND:
{
switch(wParam)
{

case BTN1_EVENT:
MessageBox(0,L"Με πατησες!!",0,0);
break;
case BTN2_EVENT:
MessageBox(0,L"Κουμπακι",0,0);
break;
default:
break;
}
}
break;


default:
return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

Πρώτα πρώτα φτιάχνουμε 2 globals handle για τα κουμπιά μας, αν δεν θέλουμε να αλλάξουμε κάτι στα κουμπιά μέσου προγράμματος, τότε δεν υπάρχει λόγος να κρατήσουμε τα HANDLE τους. Δεύτερον βάζουμε  δυο define με κάποιους αριθμούς που αντιπροσωπεύουν τα κουμπιά μας, μπορούμε να βάλουμε και enum αλλά αυτά είναι γούστα. Τέλος καλούμε την CreateWindow για να φτιάξουμε τα κουμπιά μας, αυτό που μας ενδιαφέρει από τους παραμέτρους της CreateWindow είναι το classname οπού βάζουμε το "button" και menu όπου βάζουμε το id που έχουμε για το κάθε κουμπί. Από flags βάζουμε WS_VISIBLE και WS_CHILD για να είναι ορατό και να ανήκει στο παράθυρο που βάλαμε στο hWnd παράμετρο. Τα άλλα είναι το μέγεθος και location.

Το κουμπί το φτιάξαμε. Τώρα πρέπει να φιλτράρουμε τα μηνύματα που έρχονται ώστε να κάνουμε αυτό που θέλουμε στο πάτημα του κουμπιού. Στη περίπτωση μας να πεταχτεί ένα παραθυράκι το οποίο θα έχει κάποιο μηνυματάκι.

Όταν ο χρήστης πατήσει ένα κουμπί (ο,τι και να'ναι αυτό), το λειτουργικό στέλνει το μήνυμα WM_COMMAND. Αυτό το μήνυμα είναι γενικό, εμείς θέλουμε το event από το X κουμπί, για αυτό φιλτράρουμε το wParam το οποίο κουβαλάει το id του κουμπιού (που ορίσαμε εμείς). Αυτό ήταν.

Σημείωση Αν θέλουμε το κείμενο το οποίο έχει το κουμπί καλούμε την συνάρτηση GetWindowText είναι απλή στην σύνταξη.
Σημείωση2 Για να βάλετε κάποιο κείμενο καλούμε την SetWindowText .


EditBox


HWND edit1;
HWND edit2;
#define EDIT1_EVENT 101
#define EDIT2_EVENT 102
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{

    switch(Msg)
    {
case WM_DESTROY:
PostQuitMessage(WM_QUIT);
break;
case WM_CREATE:
{
edit1 = CreateWindowEx(NULL,L"Edit",NULL,WS_CHILD | WS_VISIBLE,10,30,80,20,hWnd,(HMENU)EDIT1_EVENT,hInst,NULL);
edit2 = CreateWindowEx(NULL,L"Edit",L"ReadOnly",WS_CHILD | WS_VISIBLE| ES_READONLY , 10,70,80,20,hWnd,(HMENU)EDIT2_EVENT,hInst,NULL);
}
break;
case WM_COMMAND:
{
switch(HIWORD(wParam))
{
case EN_CHANGE:
{
switch(LOWORD(wParam))
{
case EDIT1_EVENT:
{
MessageBox(0,0,0,0);
}break;
default: break;
}
}
break;
default:break;
}
}
break;


default:
return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

Εδώ φτιάξαμε δυο Editbox, το ένα κανονικό και το άλλο μονό για διάβασμα. Το classname είναι "Edit" και από flags το πιο σημαντικό είναι το ES_READONLY που δηλώνει οτι είναι μονό για διάβασμα. Επίσης βλέπουμε και το HIWORD/LOWORD, αυτά απλός περνούν ένα μέρος από το μήνυμα δηλαδή το HIWORD παίρνει τα πρώτα 16bit από ένα int που είναι 32 και το άλλο παίρνει τα 16 τελευταία bits. Προσοχή το word εξαρτάτε από το σύστημα.

Στο HIWORD είναι ο τύπος του event και στο LOWORD είναι το id του edit που ορίσαμε εμείς. Στη περίπτωση μου πιάνω μονό το event που μας λέει ότι έγινε κάποιο αλλαγή.

Για να βάλουμε/πάρουμε κείμενο καλούμε την SetWindowText/GetWindowText.



5 σχόλια:

  1. Κάτι δε μου πάει καλά με το message handler που έγραψες για τα buttons,δεν θα έπρεπε να είναι:
    switch(LOWORD(wParam))
    αντί για
    switch(wParam);
    Επίσης να επισημάνω ότι σε όλα τα WCOMMAND μηνύματα που στέλνουν τα controls η lParam είναι το handle του control,οπότε και να μην χρησιμοποιήσει κανείς identifier για ένα control μπορεί να το ξεχωρίσει από τα υπόλοιπα από αυτήν την παράμετρο.

    ΑπάντησηΔιαγραφή
  2. Εχεις δικαιο! Απλος επειδη (αν δε κανω λαθος) το HIWORD ενος button ειναι παντα 0, ειπα να μην το φιλτραρω. Τεσπα, μαλλον θα το αλλαξω, και θα το κανω οπος και το Edit για να μην μπερδευει.
    Για την δευτερη παρατηρηση, θελω να το κανω οσο πιο ευκολο γινεται.


    Σε ευχαριστω που μπηκες σττο κοπο να το διαβασεις. :)

    ΑπάντησηΔιαγραφή
  3. Έχεις δίκιο στο ότι το notification BN_CLICKED που περιέχεται στο HIWORD(wParam)έχει την τιμή 0.Αν όμως το button σου στείλει οποιοδήποτε άλλο notification εκτός από το κλικ (π.χ ο χρήστης μπορεί να κάνει διπλό κλικ) ο κώδικάς σου όχι μόνο θα το θεωρήσει ως απλό κλικ αλλά το χειρότερο δε θα εξάγει τον identifier του κουμπιού σωστά.
    Καλή συνέχεια στη συγγραφή των άρθρων!

    ΑπάντησηΔιαγραφή
  4. Παιδιά έπεσα κατά τύχη στο blog και επειδή ασχολούμαι σε αρχικά στάδια με C++ λόγο σχολής ( Πινάκες , Συνάρτησης , Δομητες-Αποδομητες , Κλάσης )ήθελα να ρωτήσω γιατί δεν βάλατε Iostream και Stdlib στης βιβλιοθηκες εχω την εντύπωση πως μας είχαν πει ότι μπαίνουν πάντα η κάνω λάθος ???

    ΑπάντησηΔιαγραφή
  5. Εξαρταται τι θελεις να χρησιμοποιησεις, πχ για τα παραπανω θελεις μονο windows.h και τιποτα αλλο.

    ΑπάντησηΔιαγραφή