+ All Categories
Home > Documents > Programare Windows

Programare Windows

Date post: 15-Feb-2015
Category:
Upload: pcdd1
View: 129 times
Download: 6 times
Share this document with a friend
Description:
Programare Windows
126
1 Introducere Scopul acestui curs este de a familiariza cititorul cu o parte din problemele cu care se confruntă cel ce doreşte să înveţe să programeze aplicaţii de tip Windows. În acest sens am considerat necesar să prezint modul de abordare al construcţiei unei aplicaţii Windows, fără a detalia funcţiile din interfaţa de programare (API) şi mai ales fără a incerca o abordare căt de căt sistematică a acestora. Recomand cu caldură cartea lui Charles Petzold – “Programare Windows”, considerată de multi cititori biblia programării sub Windows, precum si cartea lui J. Prossie – “Programming Windows with MFC – Second Edition”. Aceste cărţi precum si MSDN, stau la baza întocmirii acestor note de curs. Totuşi nu uitaţi că Internet-ul. Site-uri precum codeguru sau codeproject merită a fi vizitate. Mediul de dezvoltare folosit în elaborarea exemplelor este VC++ 6.0 şi Visual Studio .Net 2003. Exemple si alte explicatii la aceste note de curs le gasiti pe pagina http://www.infoiasi.ro/~iasimi/IDD . Tot aici gasiti si cursul complet, ce poate fi descărcat. Ce aduce nou sistemul de operare Windows? 1. Interfata grafica cu utilizatorul GUI (mediu grafic cu ferestre); toate tipurile de interfeţe grafice cu utilizatorul folosesc elemente grafice afişate într-o imagine de tip bitmap. La început ecranul era folosit numai pentru afişarea informaţiilor pe care utilizatorul le introducea de la tastatură. Intr-o interfaţă grafică, ecranul devine chiar o sursă pentru celelalte intrari ale utilizatorului. Pe ecran sunt afişate diferite obiecte grafice (butoane, bare de derulare, casete de editare, etc.). Folosind tastatura şi/sau mouse-ul, utilizatorul poate gestiona aceste obiecte. In locul unui ciclu unidirectional al informatiilor de la tastatură la program şi de la program la ecran (SO DOS), utilizatorul interacţionează direct cu obiectele de pe ecran. 2. Consecventa privind interfata cu utilizatorul. 3. Multitasking-ul. Notiunea de proces si de fir de executie. 4. Gestionarea memoriei. Windows şi mesaje Windows este referit adesea ca un sistem de operare bazat pe mesaje. Fiecare eveniment (apasarea unei taste, clic de mouse, etc.) este transformat într-un mesaj. In mod obişnuit aplicaţiile sunt construite în jurul unei bucle de mesaje care regăseşte aceste mesaje şi apelează funcţia potrivită pentru a trata mesajul. Mesajele, deşi sunt trimise aplicaţiilor, nu se adresează acestora, ci unei alte componente fundamentale a SO, fereastra (windows). O fereastra este mai mult decât o zonă dreptunghiulară afişată pe ecran; aceasta reprezintă o entitate abstractă cu ajutorul căreia utilizatorul şi aplicaţia interacţionează reciproc. Aplicaţii, fire şi ferestre O aplicaţie Win32 constă din unul sau mai multe fire (threads), care sunt căi paralele de execuţie. Gândim firele ca fiind multitasking-ul din cadrul unei aplicaţii. Observaţie: Sub Win32s, poate rula o aplicaţie cu un singur fir de execuţie. O fereastră este totdeauna “gestionată de” un fir; un fir poate fi proprietarul uneia sau mai multor ferestre sau pentru nici una. In final, ferestrele sunt într-o relaţie ierarhică; unele sunt la nivelul cel mai de sus, altele sunt subordonate părinţilor lor, sunt ferestre descendente. Exista mai multe tipuri de ferestre in Windows; cele mai obişnuite sunt asociate cu o aplicaţie. Casetele de dialog (dialog boxes) din cadrul unei ferestre sunt de asemenea ferestre. Acelaşi lucru pentru butoane, controale de editatre, list box-uri, icoane, etc.
Transcript
Page 1: Programare Windows

1

Introducere Scopul acestui curs este de a familiariza cititorul cu o parte din problemele cu care se confruntă cel ce

doreşte să înveţe să programeze aplicaţii de tip Windows. În acest sens am considerat necesar să prezint modul de abordare al construcţiei unei aplicaţii Windows, fără a detalia funcţiile din interfaţa de programare (API) şi mai ales fără a incerca o abordare căt de căt sistematică a acestora. Recomand cu caldură cartea lui Charles Petzold – “Programare Windows”, considerată de multi cititori biblia programării sub Windows, precum si cartea lui J. Prossie – “Programming Windows with MFC – Second Edition”. Aceste cărţi precum si MSDN, stau la baza întocmirii acestor note de curs. Totuşi nu uitaţi că Internet-ul. Site-uri precum codeguru sau codeproject merită a fi vizitate.

Mediul de dezvoltare folosit în elaborarea exemplelor este VC++ 6.0 şi Visual Studio .Net 2003. Exemple si alte explicatii la aceste note de curs le gasiti pe pagina http://www.infoiasi.ro/~iasimi/IDD. Tot aici gasiti si cursul complet, ce poate fi descărcat.

Ce aduce nou sistemul de operare Windows? 1. Interfata grafica cu utilizatorul GUI (mediu grafic cu ferestre); toate tipurile de interfeţe grafice cu

utilizatorul folosesc elemente grafice afişate într-o imagine de tip bitmap. La început ecranul era folosit numai pentru afişarea informaţiilor pe care utilizatorul le introducea de la tastatură. Intr-o interfaţă grafică, ecranul devine chiar o sursă pentru celelalte intrari ale utilizatorului. Pe ecran sunt afişate diferite obiecte grafice (butoane, bare de derulare, casete de editare, etc.). Folosind tastatura şi/sau mouse-ul, utilizatorul poate gestiona aceste obiecte. In locul unui ciclu unidirectional al informatiilor de la tastatură la program şi de la program la ecran (SO DOS), utilizatorul interacţionează direct cu obiectele de pe ecran.

2. Consecventa privind interfata cu utilizatorul. 3. Multitasking-ul. Notiunea de proces si de fir de executie. 4. Gestionarea memoriei.

Windows şi mesaje Windows este referit adesea ca un sistem de operare bazat pe mesaje. Fiecare eveniment (apasarea unei taste, clic de mouse, etc.) este transformat într-un mesaj. In mod obişnuit aplicaţiile sunt construite în jurul unei bucle de mesaje care regăseşte aceste mesaje şi apelează funcţia potrivită pentru a trata mesajul.

Mesajele, deşi sunt trimise aplicaţiilor, nu se adresează acestora, ci unei alte componente fundamentale a SO, fereastra (windows). O fereastra este mai mult decât o zonă dreptunghiulară afişată pe ecran; aceasta reprezintă o entitate abstractă cu ajutorul căreia utilizatorul şi aplicaţia interacţionează reciproc.

Aplicaţii, fire şi ferestre O aplicaţie Win32 constă din unul sau mai multe fire (threads), care sunt căi paralele de execuţie. Gândim firele ca fiind multitasking-ul din cadrul unei aplicaţii.

Observaţie: Sub Win32s, poate rula o aplicaţie cu un singur fir de execuţie. O fereastră este totdeauna “gestionată de” un fir; un fir poate fi proprietarul uneia sau mai multor ferestre sau pentru nici una. In final, ferestrele sunt într-o relaţie ierarhică; unele sunt la nivelul cel mai de sus, altele sunt subordonate părinţilor lor, sunt ferestre descendente. Exista mai multe tipuri de ferestre in Windows; cele mai obişnuite sunt asociate cu o aplicaţie. Casetele de dialog (dialog boxes) din cadrul unei ferestre sunt de asemenea ferestre. Acelaşi lucru pentru butoane, controale de editatre, list box-uri, icoane, etc.

Page 2: Programare Windows

2

Clasa Window Comportarea unei ferestre este definita de clasa fereastră (window class). Clasa fereastră menţine informaţii despre modul de afişare iniţial, icoana implicită, cursor, resursele meniu şi cel mai important lucru, adresa funcţiei ataşată ferestrei – procedura fereastră – window procedure. Când o aplicaţie procesează mesaje, aceasta se face în mod obişnuit prin apelul funcţiei Windows DispatchMessage pentru fiecare mesaj primit; DispatchMessage la rândul ei apelează procedura fereastră corespunzătoare, identificând iniţial cărei ferestre îi este trimis mesajul. În continuare procedura fereastră va trata mesajul.

Există mai multe clase fereastră standard furnizate de Windows. Aceste clase sistem globale

implementează în general funcţionalitatea controalelor comune. Orice aplicaţie poate folosi aceste controale, de exemplu orice aplicaţie poate implementa controale de editare, utilizând clasa fereastra Edit. Aplicaţiile pot de asemeni să-şi definească propriile clase fereastră cu ajutorul funcţiei RegisterClass. Acest lucru se întâmplă în mod obişnuit pentru fereastra principală a aplicaţiei (icoana, resurse, etc.).

Windows permite de asemeni subclasarea sau superclasarea unei ferestre existente. Subclasarea substituie procedura fereastră pentru o clasă ferestră cu o altă procedură. Subclasarea se

realizează prin schimbarea adresei procedurii fereastră cu ajutorul funcţiei SetWindowLong (instance subclassing) sau SetClassLong (subclasare globală). Instance subclassing – înseamnă că se schimbă numai comportarea ferestrei specificate. Global subclassing – înseamnă că se schimbă comportarea tuturor ferestrelor de tipul specificat. Superclasarea crează o nouă clasă bazată pe o clasă existentă, reţinând numai procedura fereastră. Pentru a superclasa o clasă fereastră, o aplicaţie regăseşte informaţiile despre clasa fereastră utilizând funcţia GetClassInfo, modifică structura WNDCLASS astfel recepţionată şi foloseşte structura modificată într-un apel al funcţiei RegisterClass. GetClassInfo returneaza de asemenea şi adresa procedurii fereastră. Mesajele pe care noua fereastră nu le tratează trebuie trecute acestei proceduri.

Tipuri de mesaje Mesajele reprezintă în fapt evenimente la diferite nivele ale aplicaţiei. Există o clasificare a acestor mesaje (din păcate nu prea exactă): mesaje fereastră, mesaje de notificare şi mesaje de comandă, dar deocamdată nu ne interesează acest lucru.

Mesajele windows constau din mai multe părţi, descrise de structura MSG. typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; Descriere: hwnd identifică în mod unic fereastra la care a fost transmis acest mesaj. Fiecare fereastră în Windows are un asemenea identificator (tipul este HWND). message reprezintă identificatorul mesajului. Identificatorii mesajului sunt referiţi în mod obişnuit cu ajutorul constantelor simbolice şi nu prin valoarea lor numerică. Această descriere se găseşte în windows.h. Următoarele elemente pot fi privite ca parametrii ai mesajului, care au valori specifice funcţie de fiecare mesaj în parte. Marea majoritate a mesajelor încep cu WM_. De exemplu WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP, etc.

Page 3: Programare Windows

3

Aplicaţiile pot să-şi definească propriile mesaje. Cerinţa majoră este ca identificatorul mesajului să fie unic. Pentru a defini un mesaj în sistem folosim funcţia RegisterWindowMessage.

Mesaje şi multitasking

In Windows 3.1, bucla de mesaje are rol important în interacţiunea dintre aplicaţii şi sistemul de operare (SQ). Funcţionarea corectă a unui SO Windows 3.1 depinde de cooperarea dintre aplicaţii. Aplicaţiile sunt cele care permit SO să preia controlul. Acest neajuns este înlăturat în Windows 95/98, NT, 2000, XP şi cele ce vor urma. SO este cel care realizează programarea aplicaţiilor pentru cuantele de timp necesare pe procesor. Deşi această planificare a execuţiei este diferită pe Windows 95/98 faţă de NT, rolul primordial revine SO.

Cozi de mesaje

În Windows pe 16 biţi, SO menţine o singură coadă de mesaje. Când o aplicaţie încearcă să găsească următorul mesaj din coada de mesaje prin funcţiile GetMessage sau PeekMessage, SO poate efectua un context switch şi poate activa o altă aplicaţie pentru care mesajele aşteaptă în coadă. Mesajul din vârful cozii este extras şi returnat aplicaţiei via structura MSG. Dacă aplicaţia eşuează în apelul lui GetMessage, PeekMessage sau Yield, aceasta blochează sistemul. Coada de mesaje poate deveni plină!

În Win32 (95/98, NT, 2000, XP) mecanismul cozii de mesaje este mult mai complicat. O singură coadă de mesaje nu mai rezolvă problema. Aici sistemul de operare dă controlul unei aplicaţii şi tot SO permite multitasking-ul. Două sau mai multe fire pot accesa coada de mesaje în acelaşi timp şi nu există nici o garanţie că mesajele extrase sunt ale lor. Acesta este unul motivele pentru care coada de mesaje a fost separată în cozi de mesaje individuale pentru fiecare fir din sistem.

Procese şi Fire

Într-un SO non-multifir, de exemplu UNIX, unitatea cea mai mică de execuţie este task-ul sau procesul. Mecanismul de planificare (aparţine SO) al task-urilor va comuta între acestea; multitasking-ul se realizează între două sau mai multe procese (task-uri). Dacă o aplicaţie are nevoie să execute mai multe funcţii simultan, atunci aceasta se va divide în mai multe task-uri. Din această tehnică decurg anumite neajunsuri, consum de resurse, timp de lansare a unui nou task, spaţii de adrese diferite, probleme de sincronizare, etc.

În contrast, într-un sistem multifir (multifilar, multithreaded) unitatea cea mai mică de execuţie este firul, nu procesul. Un proces sau task poate conţine mai multe fire, dintre care unul singur este firul principal, firul primar. Lansarea în execuţie a unui nou fir cere mai puţine resurse din partea SO, firul rulează în cadrul aceluiaşi proces, problemele de sincronizare sunt şi nu sunt complicate. Oricum apar două sincronizări: sincronizare între procese şi sincronizare între fire.

Fire şi Mesaje

După cum am mai spus proprietarul ferestrei este firul de execuţie. Fiecare fir are coada proprie, privată, de mesaje în care SO depozitează mesajele adresate ferestrei. Aceasta nu înseamnă că un fir trebuie neapărat să aibă o fereastră proprie şi o coadă proprie de mesaje. Pot exista fire şi fără fereastră şi fără buclă de mesaje. În MFC, aceste fire se numesc worker threads (nu au ataşată o fereastră, nu prezintă interfaţă către utilizator), iar celelalte se numesc user-interface threads.

Apeluri de funcţii Windows

Windows oferă un mare număr de funcţii pentru a executa o mare varietate de task-uri, controlul proceselor, gestionarea ferestrelor, fişierelor, memoriei, servicii grafice, comunicaţii, etc. Apelurile sistem pot fi organizate în trei categorii: 1. servicii nucleu (apeluri sistem pentru controlul proceselor, firelor, gestiunea memoriei, etc.);

Page 4: Programare Windows

4

2. servicii utilizator (gestiunea elementelor de interfaţă ale utilizatorului cum ar fi ferestre, controale, dialoguri, etc.);

3. servicii GDI (Graphics Device Interface) (ieşirea grafică independentă de dispozitiv). Sistemul Windows include de asemenea funcţii API pentru alte funcţionalităţi – MAPI (Messaging API), TAPI (Telephony API) sau ODBC (Open Database Connectivity).

Servicii Nucleu

Serviciile nucleu cuprind de obicei: getionarea fişierelor, memoriei, proceselor, firelor, resurselor. Gestionarea fişierelor nu ar trebui să se mai facă cu funcţii din bibliotecile C sau prin iostream-urile din

C++. Aplicaţiile ar trebui să utilizeze conceptul Win32 de obiect fisier – file object – şi funcţiile asociate cu acesta. De exemplu există fişiere mapate în memorie care asigura comunicarea între task-uri.

Referitor la gestionarea memoriei pe lângă funcţiile cunoscute, SO Windows oferă funcţii care pot manipula spaţii de adrese de sute de MB alocându-le dar nefăcând commiting.

Cea mai importantă faţetă a proceselor şi firelor este gestiunea sincronizării. Problema este complet nouă şi nu a fost întâlnită în Windows 3.1. În Win32, sincronizarea se face cu ajutorul unor obiecte de sincronizare, pe care firele le pot utiliza pentru a informa alte fire despre starea lor, de a proteja zone senzitive de cod sau de a obtine informatii despre alte fire sau starea altor obiecte.

In Win32 multe resurse nucleu sunt reprezentate ca obiecte – obiecte nucleu: fişiere, fire, procese, obiecte de sincronizare, etc. Obiectele sunt referite prin manipulatori, identificatori (handlers); există funcţii pentru manipularea generică a obiectelor, pentru manipularea obiectelor de un anumit tip. Sub NT, obiectele au ataşate proprietăţi de securitate. De exemplu, un fir nu poate accesa un obiect fişier dacă nu are drepturile necesare care să coincidă cu proprietăţile de securitate.

Modulul nucleu furnizezză de asemeni funcţii pentru gestionarea resurselor interfaţă-utilizator. Aceste resurse includ icoane, cursoare, şabloane de dialog, resurse string, tabele de acceleratori, bitmap-uri, etc.

Nucleul NT furnizează printre altele: atribute de securitate pentru obiectele nucleu, backup, funcţionalitatea aplicaţiilor de tip consolă care pot utiliza funcţii pentru memoria virtuală sau pot utiliza mai multe fire de execuţie.

Servicii utilizator

Modulul utilizator furnizează apeluri sistem care gestionează aspecte şi elemente ale interfeţei utilizatorului; sunt incluse funcţii care manipulează ferestre, dialoguri, meniuri, controale, clipboard, etc. Se exemplifică tipurile de operaţii pentru fiecare resursă în parte (în general creare, modificare, ştergere, mutare, redimensionare, etc.).

Modulul utilizator furnizează funcţii pentru managementul mesajelor şi cozilor de mesaje. Aplicaţiile pot utiliza aceste apeluri pentru a controla conţinutul cozii de mesaje proprii, a regăsi şi a procesa mesajele, a crea noi mesaje. Noile mesaje pot fi trimise (sent) sau plasate (posted) la orice fereastră. Un mesaj plasat pentru o fereastră – funcţia PostMessage - înseamnă pur şi simplu intrarea acestuia în coada de mesaje nu şi procesarea imediată a acestuia. Trimiterea unui mesaj (sent) implică tratarea lui imediată sau mai corect spus funcţia SendMessage nu-şi termină execuţia pînă când mesajul nu a fost tratat.

Servicii GDI

Funcţiile din GDI sunt utilizate în mod obişnuit pentru a executa operaţii grafice primitive independente de dispozitiv, pe contexte de dispozitiv. Un context de dispozitiv este o interfaţă la un periferic grafic specific (în fapt este o structură de date păstrată în memorie). Contextul de dispozitiv poate fi utilizat pentru a obţine informaţii despre periferic şi pentru a executa ieşirile grafice pe acest periferic. Informaţiile care pot fi obţinute printr-un context de dispozitiv, descriu în detaliu acest periferic.

Ieşirea grafică este executată printr-un context de dispozitiv prin pasarea (trecerea) unui manipulator (identificator) al contextului de dispozitiv funcţiilor grafice din GDI.

Page 5: Programare Windows

5

Contextele de dispozitiv pot descrie o varietate mare de periferice. Contextele de dispozitiv obişnuite includ: contexte de dispozitiv display, contexte de dispozitiv memorie (pentru ieşirea unui bitmap memorat în memorie) sau contexte de dispozitiv printer.

Un context de dispozitiv foarte special este contextul de dispozitiv metafile care permite aplicaţiilor de a înregistra permanent apelurile din GDI (fişierul păstrează o serie de primitive grafice) care sunt independente de dispozitiv. Metafişierele joacă un rol crucial în reperzentarea independentă de dispozitiv a obiectelor OLE înglobate.

Desenarea într-un context de dispozitiv se face cu ajutorul coordonatelor logice. Coordonatele logice descriu obiectele utilizând măsurători reale independente de dispozitiv, de exemplu, un dreptunghi poate fi descris ca fiind lat de 2 inch şi înalt de 1 inch. GDI furnizează funcţionalitatea necesară pentru maparea coordonatelor logice în coordonate fizice.

Diferenţe semnificative există în modul cum această mapare are loc în Win32s, Windows 95 şi Windows NT.

Win32s şi Windows 95 folosesc reprezentarea coordonatelor pe 16 biti. Windows NT, XP poate manipula coordonate pe 32 biţi.

Toatre cele trei sisteme suportă mapări (transformări) din coordonate logice în coordonate fizice. Aceste transformări sunt determinate (influenţate) de valorile ce specifică originea coordonatelor şi (signed extent) extensia cu semn a spaţiului logic şi al celui fizic. Originea coordonatelor specifică deplasarea pe orizontală şi verticală, iar extensia (extent) determina orientarea şi scara obiectelor după mapare (transformare). În plus, Windows NT oferă ceea ce se numeşte world transformation functions. Prin aceste funcţii, orice transformare liniară poate fi folosită pentru transformarea spaţiului de coordonate logice în spaţiul de coordonate fizice; în plus pentru translaţii şi scalare ieşirile pot fi rotite sau sheared. Exemple de funcţii grafice: Rectangle, Ellipse, Polygon, TextOut, etc. Alte funcţii de interes deosebit (bit blit functions: PatBlt, BitBlt, StechBlt) sunt cele legate de desenarea şi copierea bitmap-urilor. Contextele de dispozitiv pot fi create şi distruse, starea lor poate fi salvată şi reîncărcată. Un alt grup de funcţii gestionează transformările de coordonate. Funcţii comune tuturor platformelor pot fi utilizate pentru a seta sau regăsi originea şi extent-ul unei ferestre (spaţiul de coordonate logic) şi viewport-ului (spaţiul de coordonate al perifericului destinaţie). NT posedă funcţii specifice pentru transformări matriceale. Funcţiile GDI pot fi folosite de asemenea pentru gestionarea paletelor, aceasta înseamnă că prin gestionarea paletei de culori, aplicaţiile pot selecta o mulţime de culori care se potrivesc cel mai bine cu culorile din imaginea (gif, pcx.) care trebuie afişată. Gestionarea paletei poate fi utilizată şi în tehnici de animaţie. O altă trăsătură a GDI-ului este crearea şi gestionarea obiectelor GDI (pensoane, peniţe, fonturi, bitmap-uri, palete) precum şi a regiunilor şi a clipping-ului.

Alte API-uri

• Funcţii pentru controale comune; • Funcţii pentru dialoguri comune; • MAPI, (Messaging Applications Programming Interface); • MCI (Multimedia Control Interface); • OLE API; • TAPI (Telephony API).

Page 6: Programare Windows

6

Raportarea erorilor

Majoritatea funcţiilor Windows folosesc un mecanism pentru evidenţierea erorilor. Când apare o eroare, aceste funcţii setează o valoare a erorii pentru firul respectiv, valoare care poate fi regăsită cu funcţia GetLastError.

Valoarile pe 32 biţi, returnate de această funcţie sunt definite in winerror.h sau în fişierul header al bibliotecii specifice. Valoarea erorii poate fi setată şi din cadrul aplicaţiei cu ajutorul funcţiei SetLastError. Codurile de eroare trebuie să aibă setat bitul 29.

Din păcate tratarea erorilor nu a fost foarte bine standardizată in Windows. Din acest motiv trebuie să citim

documentaţia funcţiei pe care dorim să o folosim pentru a vedea ce tip de valoare returneză in cazul unei erori.

Folosirea funcţiilor din biblioteca C/C++

Aplicaţiile Win32 pot folosi setul standard al funcţiilor din biblioteca C/C++ cu anumite restricţii. Aplicaţiile Windows nu au acces în mod normal la stream-urile stdin, stdout, stderr sau obiectele iostream din C++. Numai aplicaţiile consolă pot utiliza aceste stream-uri. Funcţiile relative la fişiere pot fi folosite, dar acestea nu suportă toate facilităţile oferite de SO Windows – securitate, drepturi de acces. În locul funcţiilor din familia exec se va folosi CreateProcess.

În ceeea ce priveşte gestionarea memoriei se folosesc cu succes funcţiile din C sau C++. Funcţiile din biblioteca matematică, pentru gestionarea stringurilor, a bufferelor, a caracterelor, a conversiilor de date pot fi de asemenea folosite. Aplicaţiile Win32 nu trebuie să folosească întreruperea 21 sau funcţii IBM PC BIOS. Arhitectura unei aplicatii Windows. Forma vizuală a unei aplicaţii Windows

1. Bara de titlu 2. Bara de meniu 3. Toolbar 4. Meniul sistem 5. Barele de navigare (derulare) verticală si orizontală 6. Bara de stare 7. Zona client

Vom descrie în continuare elementele de care avem nevoie pentru a scrie prima aplicaţie Windows (cu SDK). Pentru acest lucru mai avem nevoie de căteva noţiuni.

Page 7: Programare Windows

7

Fereastra. Clasa fereastra. Fereastra se reprintă pe ecran sub forma unui dreptunghi. Sistemul de operare pastrează o serie de informaţii despre fereastră, informaţii ce sunt descrise de clasa fereastră, iar structura corespunzătoare are numele de WNDCLASS(EX). Inainte de a crea fereastra cu funcţia CreateWindow(Ex), trebuie să completăm structura pentru clasa fereastră. Structura WNDCLASSEX - informatii despre clasa fereastra

• UINT cbSize; // sizeof(WNDCLASSEX) • UINT style; // incep cu CS_ • WNDPROC lpfnWndProc; // procedura fereastra • int cbClsExtra; // bytes extra pt. clasa • int cbWndExtra; // bytes extra pt. fereastra • HANDLE hInstance; • HICON hIcon; • HCURSOR hCursor; • HBRUSH hbrBackground; • LPCTSTR lpszMenuName; • LPCTSTR lpszClassName; • HICON hIconSm; // numai in WNDCLASSEX;

Clasa fereastră menţine informaţii despre modul de afişare iniţial al ferestrei, icoana implicită, cursor, resursele meniu şi cel mai important lucru, adresa funcţiei ataşată ferestrei – procedura fereastră – (window procedure). In Windows avem de-a face cu clase ferestre deja definite şi clase ferestre ce pot fidefinite de utilizator. Clase fereastră standard - clase sistem globale (controale commune, EDIT, BUTTON, LISTBOX, COMBOBOX, etc.). Clase fereastră definite de utilizator, de obicei fereastra principala a aplicatiei, inregistrarea clasei se face cu ajutorul functiei RegisterClass(Ex). Exemplu de completare a unei structuri WNDCLASSEX: // … WNDCLASS wndClass; memset(&wndClass, 0, sizeof(wndClass)); // stiluri de fereastra wndClass.style = CS_HREDRAW | CS_VREDRAW; // procedura fereastra wndClass.lpfnWndProc = WndProc; // instanta aplicatiei wndClass.hInstance = hInstance; // resursa cursor wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // resursa penson wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // nume fereastra wndClass.lpszClassName = "HELLO"; iar inregistrarea clasei: RegisterClassEx(&wndClass); O fereastră în Windows este caracterizată prin două categorii de stiluri: stiluri de clasă şi stiluri de fereastră. Stilurile de clasă sunt prefixate de obicei cu CS_, iar cele de fereastra cu WS_. Stilurile de clasă sunt reţinute de data membru

Page 8: Programare Windows

8

style din WNDCLASS(EX) (mai multe stiluri de clasă se pot înregistra folosind operatorul OR (|) pe biţi). Stilurile ferestrei (WS_) se completează ca valori ale parametrilor funcţiei CreateWindowEx sau CreateWindow. Observaţie: CreateWindow şi CreateWindowEx crează o fereastră, returnează un HWND, dar cea de-a doua formă foloseşte stiluri extinse pentru fereastră. In general sufixul Ex provine de la Extended. Astfel există şi funcţiile RegisterClass şi RegsiterClassEx, prescurtat scrise sub forma RegisterClass(Ex). Stiluri de clasa (CS_) Pot fi combinate folosind operatorul OR (|) pe biţi. Funcţii folosite GetClassLong, SetClassLong Cele mai importante stiluri:

Value Action

CS_CLASSDC Alocă un context de dispozitiv ce poate fi partajat de toate ferestrele din clasă.

CS_DBLCLKS Trimite mesajul dublu clic procedurii ferestră.

CS_OWNDC Alocă un unic context de dispozitiv pentru fiecare fereastră din calasă.

Stiluri fereastra (WS_) Pot fi combinate folosind operatorul OR (|) pe biţi. Functii folosite: GetWindowLong, SetWindowLong Se furnizeaza ca parametri in functia CreateWindow(Ex). In continuare se descriu câteva stiluri de fereastră (lista completă poate consultată în MSDN).

• WS_BORDER Crează o fereastră ce are margini. • WS_CAPTION Crează o fereastră ce are o bară de titlu. Nu poate fi utilizat cu stilul WS_DLGFRAME.

Folosirea acestui stil implică şi folosirea stilului definit anterior. • WS_CHILD Crează o fereastră descentă (copil). Nu poate fi utilizat cu stilul WS_POPUP. • WS_DISABLED Crează o fereastră ce iniţial este disabled. • WS_DLGFRAME Crează o fereastră cu margini duble, dar fără titlu. • WS_GROUP Specifică primul conttrol al unui grup de contraole. • WS_HSCROLL Crează o fereastră ce are bara de navigare orizontală. • WS_MAXIMIZE Crează o fereastră de mărime maximă. • WS_MAXIMIZEBOX Crează o fereastră ce are butonul Maximize. • WS_MINIMIZE şi WS_MINIMIZEBOX asemănătoare cu cele două de mai sus. • WS_VISIBLE Crează o fereastră ce iniţial vizibilă.

O listă completă a stilurilor de fereastră poate fi consultată în MSDN. Crearea unei ferestre. Funcţia CreateWindow(Ex) În exemplul ce urmează se crează o fereastra cu stiluri extinse. HWND CreateWindowEx( DWORD dwExStyle, // extended window style LPCTSTR lpClassName, // pointer to registered class name LPCTSTR lpWindowName, // pointer to window name DWORD dwStyle, // window style int x, // horizontal position of window

Page 9: Programare Windows

9

int y, // vertical position of window int nWidth, // window width int nHeight, // window height HWND hWndParent, // handle to parent or owner window HMENU hMenu, // handle to menu, or child-window //iden HINSTANCE hInstance, // handle to application instance LPVOID lpParam // pointer to window-creation data ); lpClassName = numele clasei, vine din completarea structurii WNDCLASS(EX) si apoi inregistrarea clasei cu functia RegisterClass(Ex). HWND CreateWindow( LPCTSTR lpClassName, // pointer to registered class name LPCTSTR lpWindowName, // pointer to window name DWORD dwStyle, // window style int x, // horizontal position of window int y, // vertical position of window int nWidth, // window width int nHeight, // window height HWND hWndParent, // handle to parent or owner window HMENU hMenu, // handle to menu or child-window identifier HANDLE hInstance, // handle to application instance LPVOID lpParam // pointer to window-creation data ); Returneaza un handle (tip HWND) la fereastra creată. Cu acest handle este identificată fereastra în aplicaţie. Structura mesajului Mesajele reprezintă în fapt evenimente la diferite nivele ale aplicaţiei. Există o clasificare a acestor mesaje : mesaje fereastră, mesaje de notificare şi mesaje de comandă. Mesajele windows constau din urmatoarele părţi, descrise de structura MSG. typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; Explicatii. Exemplu de mesaj cu parametri: Mesaj cu doi parametri WM_LBUTTONDOWN fwKeys = wParam; // key flags xPos = LOWORD(lParam); // horizontal position of cursor yPos = HIWORD(lParam); // vertical position of cursor

Observaţie:

Page 10: Programare Windows

10

Acest mesaj este trimis când s-a făcut clic pe butonul stânga al mouse-ului. Parametrii acestui mesaj păstrează coordonatele mouse-ului unde s-a făcut clic. Într-o aplicaţie SDK, wParam şi lParam îi găsiţi ca fiind parametrii numărul 3 şi 4 al funcţiei ce tratează mesajele pentru o casetă de dialog, sau ai funcţiei procedură fereastră. Preferăm să nu traducem în acest caz descrierea mesajelor pentru a încerca să vă obişnuim cu MSDN.

Parameters

fwKeys Value of wParam. Indicates whether various virtual keys are down. This parameter can be any combination of the following values: Value Description

MK_CONTROL Set if the ctrl key is down.

MK_LBUTTON Set if the left mouse button is down.

MK_MBUTTON Set if the middle mouse button is down.

MK_RBUTTON Set if the right mouse button is down.

MK_SHIFT Set if the shift key is down.

xPos Value of the low-order word of lParam. Specifies the x-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area.

yPos

Value of the high-order word of lParam. Specifies the y-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area.

Return Values If an application processes this message, it should return zero. An application can use the MAKEPOINTS macro to convert the lParam parameter to a POINTS structure. Mesaj cu un parametru WM_QUIT The WM_QUIT message indicates a request to terminate an application and is generated when the application calls the PostQuitMessage function. It causes the GetMessage function to return zero. WM_QUIT nExitCode = (int) wParam; // exit code Parameters nExitCode

Value of wParam. Specifies the exit code given in the PostQuitMessage function. Return Values This message does not have a return value, because it causes the message loop to terminate before the message is sent to the application's window procedure. Mesaj fara parametri WM_CLOSE The WM_CLOSE message is sent as a signal that a window or an application should terminate. Parameters This message has no parameters. Return Values If an application processes this message, it should return zero. Default Action The DefWindowProc function calls the DestroyWindow function to destroy the window.

Page 11: Programare Windows

11

WM_DESTROY The WM_DESTROY message is sent when a window is being destroyed. It is sent to the window procedure of the window being destroyed after the window is removed from the screen. This message is sent first to the window being destroyed and then to the child windows (if any) as they are destroyed. During the processing of the message, it can be assumed that all child windows still exist. Parameters This message has no parameters. Return Values If an application processes this message, it should return zero. Exemplu de tratare a unui mesaj într-o procedură fereastră LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_PAINT: DeseneazaCeva(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; // trebuie sa intoarca totdeauna 0 (zero) }

Categorii de mesaje Exista trei categorii de mesaje: 1) Mesaje fereastră (Windows messages )

Aceste mesaje sunt prefixate cu WM_ , excepţie făcând mesajul WM_COMMAND. Mesajele fereastra sunt tratate de ferestre si vizualizari (în fapt vizualizarile sunt tot ferestre, diferenta există între fereastra principală – main frame - si vizualizări). Aceste mesaje, în general, au parametri.

Exemplu // cod in procedura fereastra case WM_LBUTTONDOWN: { // hDC = handle la Device Context

HDC hDC; RECT clientRect; hDC = GetDC(hwnd); if (hDC != NULL) {

GetClientRect(hwnd, &clientRect); DPtoLP(hDC, (LPPOINT)&clientRect, 2); DrawText(hDC, "Hello, World!", -1, &clientRect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE); ReleaseDC(hwnd, hDC);

} break; }

Page 12: Programare Windows

12

2) Notificări ale controlului (Control notifications )

Acestea includ mesajul de notificare WM_COMMAND din controale şi alte ferestre descendente, către fereastra părinte. Exemplu: Un control de editare trimite parintelui lui (in mod normal o caseta de dialog) un mesaj WM_COMMAND ce contine codul de notificare EN_CHANGE cind utilizatorul a alterat textul din cadrul controlului. Exceptie: Notificarea BN_CLICKED (un control push button a fost apasat) este tratata ca un mesaj de comanda. Cadrul de lucru (framework) ruteaza mesajele de notificare ale controalelor ca orice alt mesaj. Unele mesaje de notificare sunt trimise via mesajul WM_NOTIFY. Trebuie consultata documentatia Windows pentru a vedea notificarile suportate de controale via WM_NOTIFY (de exemplu consultati controlul list view). In SDK, codul se gaseste in procedura fereastra a casetei de dialog ce contine controlul. Exemplu: i) Creare caseta de dialog (din resurse) ce are ID-ul IDD_CURS

// Codul se poate gasi in procedura fereastrei // principale a aplicatiei DialogBox(hInst, MAKEINTRESOURCE(IDD_CURS),

hwndMain, (DLGPROC)DlgCurs);

// hwndMain = HANDLE pentru fereastra principala // a aplicatiei, // obtinut ca rezultat al crearii ferestrei: // vezi functia CreateWindow // sau CreateWindowEx // DlgCurs = procedura fereastra a casetei // de dialog identificata de IDD_CURS

ii) Prototipul pentru procedura fereastră (ataşată unui dialog) este:

BOOL CALLBACK DlgCurs(HWND hwnd, UINT msg,

WPARAM wParam, LPARAM lParam) { ... switch(msg) { ... case WM_COMMAND:

{ switch (LOWORD(wParam)) { case IDC_EDITCURS: // ID control de editare switch(HIWORD(wParam)) { case EN_CHANGE: // tratare notificare... break; ...

}

Page 13: Programare Windows

13

case IDC_PUSHBUTTON: // ID buton // cod pentru tratare mesaj ...

break; } ... } case WM_NOTIFY: { UINT code; UINT idCtrl; code = ((NMHDR*)lParam)->code; idCtrl = ((NMHDR*)lParam)->idFrom; switch(wParam) // wParam par. in procedura fereastra { case IDC_LISTVIEW: { UINT code = ((NMHDR*)lParam)->code; switch(code) { case NM_CLICK: { break; } } ...

} }

In MFC Mesajele de notificare sunt tratate de obicei de obiectele claselor derivate din CWnd (CFrameWnd, CMDIFrame, CMDIChildWnd, CView, CDialog) precum şi clasele proprii derivate din aceasta. Aceste obiecte sunt caracterizate de faptul ca au un HWND, identificator al ferestrei Windows. 3) Mesaje de comanda (Command messages)

Acestea includ mesajele de notificare WM_COMMAND din obiectele interfeţei cu utilizatorul: meniu-uri, butoane din toolbar şi taste acceleratoare. Cadrul de lucru proceseaza comenzile in mod diferit faţă de celelalte mesaje şi acestea pot fi tratate de mai multe tipuri de obiecte (documente, şabloane de documente – document templates -, obiectul aplicaţie, vizualizări).

A se vedea rutarea comenzilor si ierarhia de clase MFC. Recomand exersarea acestora intr-o aplicatie tip SDI si apoi puteti trece la MDI. Intr-o aplicatie bazata pe dialog, lucrurile sunt mai usoare. Exemplu // cod scris in procedura ferestrei principale a aplicatiei switch(nCode) { // ... nCode ID-ul optiunii din meniu

Page 14: Programare Windows

14

case IDM_WINDOWTILE: SendMessage(hwnd,WM_MDITILE,0,0); break; case IDM_WINDOWCASCADE: SendMessage(hwnd,WM_MDICASCADE,0,0); break; } Coada de mesaje Windows foloseşte două metode pentru a ruta mesajele la procedura fereastră:

(1) punând mesajele într-o coadă de aşteptare (principiul FIFO), numita coada de mesaje, care este un obiect sistem, gestionat in memorie şi pastreaza mesajele; aceste mesaje se numesc queued messages.

(2) trimiţând mesajele în mod direct procedurii fereastră. Acestea sunt numite nonqueued messages (ex. WM_ACTIVATE, WM_SETFOCUS, etc.). Aceste mesaje sunt trimise pentru a notifica o fereastra despre aparitia unui eveniment care o afectează.

Sistemul mentine o singura coada de mesaje numita coada de mesaje sistem şi un număr de cozi de mesaje pentru fiecare fir cu interfaţă. Driver-ul pentru mouse sau tastatură converteşte intrările (evenimentele) in mesaje şi le plasează în coada de mesaje a sistemului. Evenimentele generează mesaje. SO preia câte un singur mesaj la un moment dat din coada de mesaje a sistemului, determină cărei ferestre îi este adresat şi apoi pune mesajul în coada de mesaje a firului respectiv. O schemă simplificată a acestui mecanism este Windows recepţionează evenimentul (acţiunea utilizatorului, scurgerea unui interval de timp, etc)

Windows transformă acţiunea în mesaj

Fereastra programului primeşte mesajul

Programul execută o buclă care preia şi distribuie mesajele.

Firul preia mesajul (functia GetMessage) îl redirectează către sistem (functia RegisterClass înregistrează în sistem o clasă fereastră – o structură WNDCLASS / WNDCLASSEX – deci are informaţii despre adresa procedurii fereastra; CreateWindow / CreateWindowEx va folosi aceasta clasa inregistrată pentru a crea fereastra şi ca atare va avea informaţii despre adresa procedurii fereastră) iar acesta va apela funcţia fereastră corespunzătoare pentru a-l trata (funcţia DispatchMessage face acest lucru, dar nu completează informaţiile legate de timpul când a apărut mesajul sau de poziţia cursorului mouse-ului). In cadrul procedurii fereastră există codul pentru tratarea mesajului. Procedura fereastră este de tip CALLBACK, deci este o functie ce va fi apelata de catre sistemul de operare. Programatorul “poate” considera ca functia DispatchMessage apeleaza procedura fereastra furnizindu-i mesajul ce trebuie tratat (in realitate SO procedează un “pic” altfel). Exceptie: Mesajul WM_PAINT este mentinut in coada de mesaje si va fi procesat cind nu mai exista alte mesaje. Daca apar mai multe mesaje WM_PAINT, se retine ultimul mesaj de acest tip, celelalte mesaje WM_PAINT fiind

Page 15: Programare Windows

15

sterse din coada de mesaje. Acest algoritm reduce efectul neplacut datorat multiplilelor redesenari ale ferestrei precum si timpul procesor necesar redesenarii. Functii utile ce lucreaza cu mesaje (pentru o lista completa consultati MSDN) Un fir poate pune un mesaj in coada proprie de mesaje folosind funcţia PostMessage. Funcţia PostThreadMessage va pune un mesaj în coada de mesaje a altui fir. Un mesaj poate fi examinat, fără a fi eliminat din coada de mesaje, cu ajutorul funcţiei PeekMessage. Aceasta funţie completeaza o structură de tip MSG cu informaţii despre mesaj.

Informatiile legate de timpul cind a aparut mesajul şi pozitia cursorului mouse-ului pot fi obţinute cu ajutorul funcţiilor GetMessageTime, respectiv GetMessagePos. Un fir poate folosi funcţia WaitMessage pentru a da controlul altui fir când nu mai există mesaje în coada proprie de mesaje.

Un mesaj este insotit in sistem de două valori date de parametrii WPARAM si LPARAM ai functiei SendMessage. Daca analizati prototipul procedurii fereastra veti observa ca aceasta contine ID-ul mesajului si doi parametri de tipul indicat mai sus. Un mesaj poate contine informatii aditionale, informatii furnizate pe o valoare pe 32 de biti (poate fi pointer la o structura). Aceste informatii se completeaza cu functia SetMessageExtraInfo si pot fi regasite cu functia GetMessageExtraInfo. Informatia ataşată cozii de mesaje a firului este validă pâna la următorul apel al functiei GetMessage sau PeekMessage. Aproape toate ferestrele din Windows pot păstra informaţii suplimentare, informaţii păstrate pe o valoare pe 32 biti, deci putem pastra pointeri la structuri (listbox, combo box – vezi functiile SetItemData, SetItemDataPtr, GetItemData, GetItemDataPtr, etc.) Pentru trimiterea unui mesaj se pot utiliza functiile SendMessage, PostMessage, PostQuitMessage, SendDlgItemMessage, SendMessageCallback, SendNotifyMessage, PostMessage, PostThreadMessage.

Functia SendMessage blochează firul apelant aşteptând tratarea mesajului. Celelalte functii, enumerate mai sus, prezintă diverse variaţii de la aceasta tehnica generală. Observatie. Trebuie sa facem dictinctie intre coada de mesaje, harta de mesaje, bucla de mesaje. Notiunea de harta de mesaje exista in ierarhia de clase MFC.

Bucla de mesaje Bucla de mesaje cea mai simplă consta din apelul uneia din functiile: GetMessage, TranslateMessage, si DispatchMessage. Exemplu: MSG msg; while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } Acest cod se poate gasi in cadrul functiei WinMain. Functia GetMessage regăseşte un mesaj din coadă şi îl copie în structura de tip MSG. Această funcţie returnează valori diferite de zero atâta timp cât nu extrage din coada mesajul WM_QUIT, caz in care returneaza zero (FALSE) şi bucla de mesaje se termină. Intr-o aplicatie cu un singur fir, terminarea buclei de mesaje este echivalent cu inchiderea aplicatiei. O aplicatie poate forta terminarea buclei de mesaje utilizind functia PostQuitMessage, in mod obisnuit ca răspuns la mesajul WM_DESTROY a ferestrei principale a aplicatiei. Vezi mai jos descrierea functiei GetMessage.

Page 16: Programare Windows

16

Daca specificăm un handle la fereastra (al doilea parametru din GetMessage), vor fi extrase numai mesajele pentru fereastra identificata de acel handle. Ultimii doi parametri din GetMessage realizeaza o filtrare pentru mesaje (valaorea minima si valoarea maxima pentru mesaje). TranslateMessage. Daca firul primeste caractere de la tastatură, atunci trebuie folosita functia TranslateMessage. Sistemul genereaza mesajele WM_KEYDOWN si WM_KEYUP (mesaje de chei virtuale) ori de cite ori utilizatorul apasă o tastă. Aceste mesaje nu contin codul caracterului apasat. TranslateMessage traduce mesajul de cheie virtuala intr-un mesaj caracter (WM_CHAR) şi il pune înapoi în coada de mesaje (evident in procedura fereastra trebuie sa existe un handler (functie, portiune de cod) care sa trateze mesajul WM_CHAR)), iar la următoarea iteraţie a buclei de mesaje acesta va ajunge la DispatchMessage. DispatchMessage trimite un mesaj la procedura fereastră a ferestrei identificată de handle ferestrei din mesaj. Daca handle este HWND_TOPMOST, DispatchMessage trimite mesajul procedurii ferestrei de la nivelul cel mai de sus (top level) din sistem. Dacă handle este NULL, nu se intâmplă nimic. TranslateAccelerator

O aplicatie care foloseşte taste de accelerare trebuie să fie în stare să tanslateze (traducă) mesajele de la tastură în mesaje de comandă. Pentru a realiza acest lucru, bucla de mesaje trebuie sa includă un apel la funcţia TranslateAccelerator. IsDialogMessage

Daca un fir foloseşte casete de dialog amodale, bucla de mesaje trebuie să includă un apel la funcţia IsDialogMessage. In acest mod caseta de dialog poate primi intrări de la tastatură.

GetMessage Functia GetMessage regaseste un mesaj din coada de mesaje a firului apelant şi il plasează în structura specificată. Această funcţie poate regăsi mesaje asociate cu o anumită fereastră (identificată de HWND) şi mesaje "puse" cu ajutorul funcţiei PostThreadMessage. Funcţia regăseşte mesajele care se află într-un anumit interval de valori. Functia GetMessage nu regăseşte mesaje pentru ferestre ce aparţin altor fire sau aplicaţii. Sintaxa este: BOOL GetMessage( LPMSG lpMsg, // address of structure with message HWND hWnd, // handle of window UINT wMsgFilterMin, // first message UINT wMsgFilterMax // last message );

Parametrii

lpMsg = Pointer la o structură de tip MSG, structură ce va fi completată cu informaţiile despre mesajul extras din coada de mesaje a firului.

hWnd = Handle la fereastra ale carei mesaje vor fi extrase. O valoare are o semnificaţie speciala:

Valoare Semnificatie

NULL GetMessage regaseste mesajele pentru orice fereastra ce apartine firului din care este apelata si mesaje "puse" (in coada de mesaje) cu ajutorul functiei PostThreadMessage.

Page 17: Programare Windows

17

WinMain Un program sub windows începe cu funcţia WinMain (un program sub DOS incepe cu functia main()) care are patru parametri: primii doi sunt de tip HINSTANCE, al treilea reprezinta un pointer la un şir de caractere ce reprezintă linia de comandă, iar ultimul este un intreg şi reprezintă modul de afisare al ferestrei (minimizata, maximizata, normal, etc.). Al doilea parametru este păstrat pentru compatibilitate cu Windows pe 16 biti, deci in Windows pe 32 biti nu se foloseşte şi totdeauna va fi NULL. int WINAPI WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance, LPSTR lpCmdLine, UINT nCmdShow);

Clasele fereastră care se completează şi inregistrează se fac în cadrul acestei funcţii WinMain (direct sau indirect prin apelul altei funcţii). Data membru hInstance din WNDCLASS(EX) se va completa cu hInstance din WinMain. lpfnWndProc (adresa procedurii fereastra) se va completa cu adresa funcţiei definită în cadrul aplicaţiei. In acest mod “stabilim” funcţia unde vom scrie codul ce va trata mesajele ce vin la aceasta fereastră. Adresa acestei funcţii va fi inregistrată în sistem, iar apelul functiei DispatchMessage va identifica funcţia corectă care trebuie să trateze mesajul. Procedura fereastra este apelata de catre SO (“nu ma apela tu, te apelez eu”). (vezi tipul CALLBACK) Observatie: Trebuie facută distincţie între clasa fereastră şi o clasă din POO şi de asemenea între stil clasă si stil fereastră (de altfel stilurile clasei sunt prefixate cu CS_ iar stilurile ferestrei cu WS_). Exemplu complet in SDK. #include <windows.h> // ---------- Apelata pe mesajul WM_PAINT void Deseneaza(HWND hwnd) {

HDC hDC; PAINTSTRUCT paintStruct; RECT clientRect; hDC = BeginPaint(hwnd, &paintStruct); if (hDC != NULL) {

GetClientRect(hwnd, &clientRect); DPtoLP(hDC, (LPPOINT)&clientRect, 2); DrawText(hDC, "Hello, World!", -1, &clientRect,

DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hwnd, &paintStruct);

} } // --------- Procedura fereastra LRESULT CALLBACK WndProc(HWND hwnd,

UINT uMsg, WPARAM wParam, LPARAM lParam)

Page 18: Programare Windows

18

{ switch(uMsg) {

case WM_PAINT: Deseneaza(hwnd); break;

case WM_DESTROY: PostQuitMessage(0); break;

default: return DefWindowProc(hwnd, uMsg, wParam, lParam);

} return 0; // trebuie sa intoarca totdeauna 0 (zero) } // --------------- Programul principal int WINAPI WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance, LPSTR d3, int nCmdShow)

{ MSG msg; HWND hwnd; WNDCLASS wndClass; memset(&wndClass, 0, sizeof(wndClass)); // stiluri de fereastra wndClass.style = CS_HREDRAW | CS_VREDRAW; // procedura fereastra wndClass.lpfnWndProc = WndProc; wndClass.hInstance = hInstance; // instanta aplicatiei // resursa cursor wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // resursa penson wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // nume fereastra wndClass.lpszClassName = "HELLO"; // inregistrare fereastra if (!RegisterClass(&wndClass)) return FALSE; hwnd = CreateWindow("HELLO", "HELLO",

WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

// stabilire atribute pentru afisarea ferestrei ShowWindow(hwnd, nCmdShow); // desenarea ferestrei – se trimite mesajul WM_PAINT UpdateWindow(hwnd); // bucla de mesaje while (GetMessage(&msg, NULL, 0, 0))

DispatchMessage(&msg); return msg.wParam;

} Explicaţii: 1. se crează fereastra prin completarea structurii WNDCLASS;

Page 19: Programare Windows

19

2. se înregistrează fereastra RegisterClass(&wndClass); 3. se crează fereastra CreateWindow; 4. se stabileste modul de afişare al ferestrei ShowWindow(hwnd, nCmdShow); 5. se afişează fereastra propriu zisă UpdateWindow(hwnd); 6. urmează bucla de mesaje. Alte functii folositoare in gestiunea ferestrei GetWindowLong - Regăseşte informaţii despre fereastra specificată. LONG GetWindowLong( HWND hWnd, // handle of window int nIndex // offset of value to retrieve); Adresa procedurii fereastră poate fi obţinută cu funcţia GetWindowLong cu parametrul GWL_WNDPROC sau DWL_DLGPROC. Valori posibile pentru parametrul nIndex

Valoare Actiune

GWL_EXSTYLE Regăseşte stilurile extinse ale ferestrei.

GWL_STYLE Regăseşte stilurile ferestrei.

GWL_WNDPROC Regăseşte adresa procedurii fereastră, sau un handle ce reprezintă adresa procedurii fereastră. Trebuie să folosim CallWindowProc pentru a apela procedura fereastră.

GWL_HWNDPARENT Regăsşte handle la fereastra părinte, dacă există.

GWL_ID Regăseşte identificatorul ferestrei.

Mai multe detalii în MSDN.

Când hWnd identifica o caseta de dialog, pot fi folosite si următoarele valori:

Valoare Actiune

DWL_DLGPROC Regăseşte adresa procedurii casetei de dialog.

DWL_MSGRESULT Regăseşte valoare de retur a unui mesaj procesat in procedura ataşată casetei de dialog.

SetWindowLong - modifică atributele unei ferestre. Prototip LONG SetWindowLong( HWND hWnd, // handle of window int nIndex, // offset of value to set LONG dwNewLong // new value ); nIndex - Valori posibile sunt în general aceleaşi ca la funcţia anterioară, deosebirea fiind în aceea că aceste valori se vor modifica. CallWindowProc – (fără traducere)

Page 20: Programare Windows

20

Putem folosi functia CallWindowProc pentru a apela procedura fereastră. LRESULT CallWindowProc( WNDPROC lpPrevWndFunc, // pointer to previous procedure HWND hWnd, // handle to window UINT Msg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); Parameters lpPrevWndFunc

Pointer to the previous window procedure. If this value is obtained by calling the GetWindowLong function with the nIndex parameter set to GWL_WNDPROC or DWL_DLGPROC, it is actually either the address of a window or dialog box procedure, or a handle representing that address.

hWnd Handle to the window procedure to receive the message.

Msg Specifies the message.

wParam Specifies additional message-specific information. The contents of this parameter depend on the value of the Msg parameter.

lParam Specifies additional message-specific information. The contents of this parameter depend on the value of the Msg parameter.

Return Values

The return value specifies the result of the message processing and depends on the message sent.

Trimiterea unui mesaj SendMessage Trimite mesajul specificat la o fereastră sau la ferestre. Funcţia asteaptă terminarea execuţiei codului corespunzător mesajului transmis.

Functia PostMessage, plaseaza un mesaj in coada de mesaje a unui fir şi nu asteaptă terminarea prelucrarii. LRESULT SendMessage( HWND hWnd, // handle of destination window UINT Msg, // message to send WPARAM wParam, // first message parameter LPARAM lParam // second message parameter );

Page 21: Programare Windows

21

Observatie: Dacă hWnd are valoarea HWND_BROADCAST, mesajul este trimis tuturor ferestrelor top-level din sistem, incluzand ferestre disabled sau invizibile, ferestre pop-up, dar nu ferestrelor copil.

Valoarea returnata depinde de mesajul trimis. Observaţie Aplicaţiile ce doresc să comunice folosind HWND_BROADCAST ar trebui să folosească functia RegisterWindowMessage pentru a obţine un mesaj unic în vederea comunicării între aplicaţii.

Exemplu // int i; are o valoare corecta // HANDLE hwndCtl = handle la un control de editare // plasat intr-o caseta de dialog SendMessage(hwndctl, EM_SETSEL, (WPARAM)(INT)i, (LPARAM)(INT)(i+1)); SendMessage(hwndctl, EM_REPLACESEL, (WPARAM)(BOOL)FALSE, (LPARAM)(LPCTSTR)"");

Page 22: Programare Windows

22

În continuare sunt prezentate câteva aplicaţii Windows, ce par anormale la prima vedere din cauză că nu au toate elementele descrise până acum (bucla de mesaje, procedură fereastră, etc.). Urmăriţi-le! Sunt foarte interesante. Bucla de mesaje “ascunsă” #include <windows.h> int WINAPI WinMain(HINSTANCE d1, HINSTANCE d2, LPSTR d3, int d4) { MessageBox(NULL, "Hello, World!", "", MB_OK); } Bucla de mesaje şi procedura fereastră sunt ascunse. MessageBox afişează o casetă (boxă) de dialog care conţine procedura fereastră şi deoarece boxa de dialog este modală (nu poate fi părăsită fără a se da clic pe ...) practic se ciclează pe bucla de mesaje.

Bucla de mesaje există Un program windows obişnuit, în timpul iniţializării, înregistrează mai întâi clasa fereastră apoi crează fereastra principală utilizând noua clasă înregistrată. În exemplul ce urmează folosim deja clasa înregistrată, BUTTON.

#include <windows.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE d2, LPSTR d3, int d4) { MSG msg; HWND hwnd; hwnd = CreateWindow("BUTTON", "Hello, World!", WS_VISIBLE | BS_CENTER, 100, 100, 100, 80, NULL, NULL, hInstance, NULL); while (GetMessage(&msg, NULL, 0, 0)) { if (msg.message == WM_LBUTTONUP) { DestroyWindow(hwnd); PostQuitMessage(0); } DispatchMessage(&msg); } return msg.wParam; } Explicaţii:

După ce se creează fereastra, programul intră în bucla while, unde se apelează GetMessage. Când aplicaţia primeşte un mesaj, GetMessage întoarce acel mesaj; valoarea întoarsă este FALSE numai dacă mesajul primit a fost WM_QUIT.

La tratarea mesajului WM_LBUTTONDOWN se distruge fereastra aplicaţiei şi apoi se pune în coda de mesaje, mesajul WM_QUIT, pentru a se realiza terminarea buclei while. Orice alt mesaj diferit de WM_LBUTTONDOWN nu este tratat de aplicaţie, este preluat de DispatchMessage care va apela procedura fereastră a clasei BUTTON.

În marea majoritate a cazurilor procedura nu execută nimic special, unul din rolurile ei fiind acela de a goli coada de mesaje a aplicaţiei şi de a respecta principiul “în Windows nici un mesaj nu se pierde”.

Page 23: Programare Windows

23

În afară de GetMessage, mai existăşi funcţia PeekMessage care se utilizează de obicei când aplicaţia doreşte să execute anumite acţiuni şi nu are nici un mesaj de procesat.

Proceduri fereastră #include <windows.h> // ---------------- Apelata pe mesajul WM_PAINT void DrawHello(HWND hwnd) { HDC hDC; PAINTSTRUCT paintStruct; RECT clientRect; hDC = BeginPaint(hwnd, &paintStruct); if (hDC != NULL) { GetClientRect(hwnd, &clientRect); DPtoLP(hDC, (LPPOINT)&clientRect, 2); DrawText(hDC, "Hello, World!", -1, &clientRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hwnd, &paintStruct); } } // --------------------------- Procedura fereastra LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_PAINT: DrawHello(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; // trebuie sa intoarca totdeauna 0 (zero) } // --------------- Programul principal int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR d3, int nCmdShow) { MSG msg; HWND hwnd; WNDCLASS wndClass; if (hPrevInstance == NULL) // valabil numai pentru Windows 3.1 { memset(&wndClass, 0, sizeof(wndClass));

Page 24: Programare Windows

24

// stiluri de fereastra wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = WndProc; // procedura fereastra wndClass.hInstance = hInstance; // instanta aplicatiei wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // resursa cursor wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // resursa penson wndClass.lpszClassName = "HELLO"; // nume fereastra // inregistrare fereastra if (!RegisterClass(&wndClass)) return FALSE; } // terminat if hwnd = CreateWindow("HELLO", "HELLO", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); // Ar trebui testat daca CreateWindow s-a executat cu succes! ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) DispatchMessage(&msg); return msg.wParam; } Explicaţii: se crează fereastra prin completarea structurii WNDCLASS; se înregistrează fereastra RegisterClass(&wndClass); se crează fereastra CreateWindow; se stabileste modul de afişare al ferestrei ShowWindow(hwnd, nCmdShow); se afişează fereastra propriu zisă UpdateWindow(hwnd); urmează bucla de mesaje.

Codul ce trebuie urmărit este cel din WndProc, procedura fereastră. Ce mesaje sunt tratate? Care sunt răspunsurile aplicaţiei la aceste mesaje? WndProc tratează două mesaje: WM_PAINT şi WM_DESTROY. Alte mesaje decât cele indicate sunt tratate de către DefWindowProc. La mesajul WM_DESTROY se plasează în coada de mesaje, mesajul WM_QUIT care are ca efect terminarea buclei de mesaje, şi deci terminarea aplicaţiei. La mesajul WM_PAINT se apelează funcţia DrawHello. Dar când este trimis mesajul WM_PAINT şi de cine? Mesajul WM_PAINT este trimis prima dată de funcţia UpdateWindow, adică atunci când fereastra devine vizibilă prima dată. Încercaţi opţiunile Size şi Move din meniul sistem. Ce se întâmplă? Trebuie reţinut următorul lucru: dacă în coada de mesaje apar mai multe mesaje WM_PAINT, sistemul va trata numai ultimul mesaj. În fapt ultima redesenare a ferestrei rămâne vizibilă, restul afişărilor ar fi consumatoare de timp şi în plus ar crea şi un efect neplăcut datorat rdesenărilor succesive.

Page 25: Programare Windows

25

Să explicăm codul din DrawHello. hDC = BeginPaint(hwnd, &paintStruct); BeginPaint încearcă să completeze variabila paintStruct şi ca răspuns obţine un context de dispozitiv care va trebui folosit de funcţiile din GDI. Ieşirile grafice au nevoie de acest context de dispozitiv. GetClientRect(hwnd, &clientRect); Se obţin dimensiunile zonei client, completate în clientRect. Observaţi parametrii funcţiei: hwnd va indica pentru ce fereastră se doreşte acest lucru. DPtoLP(hDC, (LPPOINT)&clientRect, 2); Coordonatele fizice sunt transformate în coordonate logice. Primul parametru, hDC, indică pentru ce context de dispozitiv se face acest lucru. DrawText(hDC, "Hello, World!", -1, &clientRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); Se afişează în zona client, “Hello, World!”, folosind contextul de dispozitiv obţiunut de BeginPaint. EndPaint(hwnd, &paintStruct); Se termină desenarea, şi se distrug informaţiile din paintStruct. La terminarea funcţiei, hDC se distruge, fiind local. În fapt după EndPaint hDC-ul nu mai este valid. Procedura fereastră nu este nimic altceva decât o structura mare switch.

Mai multe bucle de mesaje şi proceduri fereastră Aplicaţiile pot avea câte bucle de mesaje doresc. De exemplu o aplicaţie care are propria buclă de mesaje şi face apel la MessageBox va avea cel puţin două bucle de mesaje.

Pentru exemplificare vom considera cazul desenării libere realizat cu o captură a mouse-lui. Aplicaţia trebuie să fie în stare să trateze mesajele WM_LBUTTONDOWN, WM_LBUTTONUP şi WM_MOUSEMOVE pentru a realiza această desenare. Vom avea tot timpul în minte faptul că un eveniment de mouse, în zona client, va genera un mesaj care va fi însoţit de coordonatele punctului unde acesta a avut loc. Logica aplicaţiei este următoarea: bucla de mesaje va trata mesajul WM_LBUTTONDOWN. În cadrul funcţiei ce tratează acest mesaj se va realiza capturarea mouse-lui, astfel aplicaţia este informată de orice mişcare a mouse-lui prin tratarea mesajelor WM_MOUSEMOVE şi WM_LBUTTONUP. Ieşirea din cea de-a doua buclă de mesaje se face la tratarea mesajului WM_LBUTTONUP, caz în care şi capturarea mouse-lui încetează. De reţinut că în cadrul acestei a doua bucle de mesaje controlăm mereu dacă mouse-ul este capturat pentru zona client. Acest lucru înseamnă că dacă facem clic stânga în afara zonei client şi ţinem butonul stâng al mouse-lui apăsat şi ne mişcam prin zona client nu se va desena nimic. Mouse-ul nu a fost capturat de această fereastră. Funcţii noi în acest cod. GetMessagePos() = obţine coordonatele punctului unde se află mouse-ul, coordonate relative la ecran. Coordonatele sunt obţinute într-un DWORD, care conţine în primii doi octeti valoarea lui x, iar în ultimii doi octeţi valoarea lui y. (Numărătoarea octeţilor se face de la stânga la dreapta.) Macro-ul MAKEPOINTS transformă valoarea unui DWORD într-o structură de tip POINTS. Cum zona client (fereastra) este plasată în cadrul ecranului, va trebui să translatăm aceste coordonate în zona client.

Page 26: Programare Windows

26

ScreenToClient() = transformă coordonate ecran în zona client. DPtoLP() = transformă coordonatele fizice de dispozitiv în coordonate logice, necesare pentru a desena în zona client. LineTo() = desenează un segment de la origine (sau punctul stabilit cu MoveTo, MoveToEx) până la punctul curent. GetCapture() = testează dacă mouse-ul a fost capturat de fereastra aplicaţiei. SetCapture(HWND ) = realizează capturarea mouse-ului pentru fereastra cu handler-ul specificat. ReleaseCapture() = eliberează capturarea mouse-ului. GetDC() = obţine un context de dispozitiv pentru a desena în fereastră (zona client). ReleaseDC() = eliberează contextul de dispozitiv obţinut cu GetDC. #include <windows.h> void AddSegmentAtMessagePos(HDC hDC, HWND hwnd, BOOL bDraw) { DWORD dwPos; POINTS points; POINT point; dwPos = GetMessagePos(); points = MAKEPOINTS(dwPos); point.x = points.x; point.y = points.y; ScreenToClient(hwnd, &point); DPtoLP(hDC, &point, 1); if (bDraw) LineTo(hDC, point.x, point.y); else MoveToEx(hDC, point.x, point.y, NULL); } void DrawHello(HWND hwnd) { HDC hDC; MSG msg; if (GetCapture() != NULL) return; hDC = GetDC(hwnd); if (hDC != NULL) { SetCapture(hwnd); AddSegmentAtMessagePos(hDC, hwnd, FALSE); while(GetMessage(&msg, NULL, 0, 0)) { if (GetCapture() != hwnd) break; switch (msg.message) { case WM_MOUSEMOVE: AddSegmentAtMessagePos(hDC, hwnd, TRUE); break; case WM_LBUTTONUP: goto ExitLoop; default: DispatchMessage(&msg); } } ExitLoop: ReleaseCapture();

Page 27: Programare Windows

27

ReleaseDC(hwnd, hDC); } } LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_LBUTTONDOWN: DrawHello(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR d3, int nCmdShow) { MSG msg; HWND hwnd; WNDCLASS wndClass; if (hPrevInstance == NULL) { memset(&wndClass, 0, sizeof(wndClass)); wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = WndProc; wndClass.hInstance = hInstance; wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wndClass.lpszClassName = "HELLO"; if (!RegisterClass(&wndClass)) return FALSE; } hwnd = CreateWindow("HELLO", "HELLO", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) DispatchMessage(&msg); return msg.wParam; } Observaţie. Mesajul WM_MOUSEMOVE poate fi tratat şi în bucla de mesaje din WinMain, dar pentru a realiza aceeaşi funcţionalitate codul şi logica aplicaţiei trebuiesc schimbate.

Page 28: Programare Windows

28

Concluzii Fiecare aplicaţie Windows este construită în jurul unei bucle de mesaje. O buclă de mesaje face apeluri

repetate la funcţiile GetMessage sau PeekMessage şi regăseşte mesajele pe care le dispecerează procedurilor fereastră prin funcţia DispatchMessage.

Procedurile fereastră sunt definite pentru clasele fereastră în momemntul când clasa fereastră a fost înregistrată prin RegisterClass.

Mesajele adresate aplicaţiei sunt tratate de procedura fereastră sau sunt trimise procedurii implicite DefWindowProc sau DefDlgProc în situaţia când nu sunt tratate de procedura fereastră.

Orice mesaj windows trebuie tratat, nu trebuie pierdut. Mesajele pot fi plasate sau trimise unei aplicaţii. Mesajele plasate sunt depozitate în coada de unde sunt regăsite

cu GetMessage sau PeekMessage. Faţă de un mesaj plasat, un mesaj trimis (SendMessage) implică imediat un apel al procedurii fereastră. Cu alte cuvinte nu se termină execuţia funcţiei SendMessage până când mesajul nu a fost tratat.

O aplicaţie poate avea mai multe bucle de mesaje.

Page 29: Programare Windows

29

Clase de bază in MFC

Programarea Windows pe care încercăm să o învăţăm în cadrul acestui curs se bazează pe biblioteca de clase MFC – Microsoft Foundation Classes. Fără a întelege însă mecanismul programării sub Windows, aceste clase nu ne ajută foarte mult. Deşi exemplele ce urmează vor fi cu MFC, tot vom face trimeteri la SDK şi în cele ce urmează.

Pentru început vom descrie cele mai importante clase din MFC (cu care ne întâlnim in exemplele noastre) şi chiar dacă nu vom reţine multe lucruri la prima lectură, cel puţin nu ne vom “speria” câd vom găsi prin cod referinţe la aceste clase sau clase derivate.

Observaţie: O fereastră în SDK este identificată de o variabilă de tip HWND. Acest lucru rămâne valabil şi

în MFC, dar în general vom lucra mai mult cu pointeri la structuri ce reprezintă ferestre. Data membru de tip HWND ce identifică fereastra (de exemplu) va fi înglobată în obiectul ce reprezintă fereastra, şi de aici rezultă că funcţiile ce aveau un parametru de tip HWND (in SDK) nu-l vor mai avea in MFC. In loc de UpdateWindow(hWnd, nCmdShow); vom întâlni un cod de forma m_pMainWnd->UpdateWindow(nCmdShow);.

CObject

#include <afx.h> CObject este clasa de bază principală pentru MFC. Majoritatea claselor din MFC sunt drivate din această clasă. CObject furnizează următoarele servicii: • suport de serializare; • informaţii despre clasă în timpul execuţiei; • diagnosticare obiecte; • compatibilitate cu clasele colecţie (CArray, CList, etc.). CObject nu suporta moştenirea multiplă şi CObject trebuie să fie cel mai din stanga ierarhiei în cazul derivării. Dacă folosim CObject atunci în clasele derivate putem beneficia de macro-urile: DECLARE_DYNAMIC şi IMPLEMENT_DYNAMIC, permite accesul in timpul execuţiei la numele clasei şi poziţia acesteia în ierarhie. DECLARE_SERIAL şi IMPLEMENT_SERIAL, includ toată funcţionalitatea macrourilor de mai sus, şi permit unui obiect să fie serializat (scris/citit în/din arhivă). Exemplu pentru serializare class XPersoana : public CObject {

pubic: // Interfata private: CString m_Nume; WORD m_Varsta; protected: virtual void Serialize(CArchive& ar); }; Implementarea funcţiei Serialization() pentru aceasta clasă ar putea fi: void XPersoana::Serialize(CArchive& ar)

{ if (ar.IsStoring()) ar << m_Nume << m_Varsta;

Page 30: Programare Windows

30

else ar >> m_Nume >> m_Varsta; }

Când avem nevoie să memorăm o instanţă a clasei XPersoana pe disc sau să o citim din disc, vom apela funcţia Serialize().

Exemplu

class CMyDocument : public CDocument

{... XPersoana m_persoane[100]; ... };

void CMyDocument::Serialize(CArchive& ar)

{ for (int i=0;i<100;i++) m_persoane[i].Serialize(ar); } Considerăm in continuare că printre aceste persoane există una care este manager şi deci are in subordine alte persoane. Vom construi clasa class XManager : public CPersoana { public: // Interface private: CList<XPersoana*, XPersoana*> m_subordonati; }; unde subordonaţii sunt constituiti intr-o listă simplu înlănţuită. Schimbăm şi implementarea clasei CMyDocument astfel: class CMyDocument : public CDocument { ... CList<XPersoana*, XPersoana*> m_persoane; ... }; In timpul execuţiei, lista înlănţuită ar putea arăta astfel: m_persoane -> XPersoana(“Popescu”, 20) -> XManager(“Zetu”,20) -> XPersoana(“Zoe”,12) -> etc. Deci nu mai avem un proces simplu de serializare. Adaugand macro-ul DECLARE_SERIAL in definitia clasei si IMPLEMENT_SERIAL in implementarea clasei, un pointer la o instanţă a clasei poate fi memorat şi realocat dintr-o arhivă. In concluzie implementarea completa pentru aceasta clasa este: class XPersoana : public CObject { public:

Page 31: Programare Windows

31

// Interfata private: CString m_Nume; WORD m_Varsta; protected: virtual void Serialize(CArchive& ar); DECLARE_SERIAL(XPersoana) }; class XManager : public XPersoana { public: // Interfata private: CList<XPersoana*, XPersoana*> m_subordonati; protected: void Serialize(CArchive& ar); DECLARE_SERIAL(XManager) }; IMPLEMENT_SERIAL(XPersoana, CObject, 1) IMPLEMENT_SERIAL(XManager, XPersoana, 1) // // Aceasta este o functie helper pentru clasa colectie // template CList si ne spune cum memoreaza obiecte // de tipul XPersoana* // void SerializeElements(CArchive& ar, XPersoana** pElemente, int nCount) { for (int i=0;i < nCount; i++) { if (ar.IsStoring()) ar << pElemente[i]; else ar >> pElemente[i]; } } void XPersoana::Serialize(CArchive& ar) { if (ar.IsStoring()) ar << m_Nume << m_Varsta; else ar >> m_Nume >> m_Varsta; } void XManager::Serialize(CArchive& ar) { XPersoana::Serialize(ar); m_subordonati.Serialize(ar); }

void CMyDocument::Serialize(CArchive& ar)

{ m_persoane.Serialize(ar); }

Page 32: Programare Windows

32

CCmdTarget CCmdTarget este clasa de bază pentru arhitectura de tratare a mesajelor din biblioteca MFC. Dacă se doreşte crearea unei noi clase ce trebuie să trateze mesaje, aceasta trebuie derivată din CCmdTarget sau dintr-un descendent al acesteia. Metoda OnCmdMsg ( ) este folosită pentru rutarea, distribuirea mesajelor şi tratarea acestora. În plus clasa CCmdTarget mai gestionează trecerea cursorului în starea de aşteptare (cursor cu clepsidră) şi ieşirea din această stare folosind metodele BeginWaitCursor ( ), EndWaitCursor ( ) şi RestoreWaitCursor ( ). Clase derivate din CCmdTarget: CView, CWinApp, CDocument, CWnd si CFrameWnd. Pentru a lucra cu comenzi va trebui sa completam harta de mesaje (se face corespondenta intre mesaj si functia ce-l trateaza) iar in majoritatea cazurilor acest lucru este facut de ClassWizard. Bine-nteles codul din functia ce trateaza mesajul trebuie scris de noi. In general mesajele sunt trimise ferestrei cadru principale, dar comenzile sunt rutate apoi catre alte obiecte. In mod normal o clasa ruteaza comenzile la alte obiecte pentru a le da sansa sa trateze comanda. Daca comanda nu este trata de nici un obiect atunci se cauta in harta de mesaje a clasei pentru a vedea daca mesajul are asociata o functie de tratare. In situatia cind clasa nu trateaza comanda, aceasta este rutata catre clasa de baza a clasei curente. Vom explica pe larg aceasta rutare intr-un curs viitor. Urmatorul exemplu este din MSDN. Se explica sintaxa metodei OnCmdMsg şi apoi se dă un exemplu.

CCmdTarget::OnCmdMsg virtual BOOL OnCmdMsg( UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo ); Return Value Nonzero if the message is handled; otherwise 0. Parameters nID Contains the command ID. nCode Identifies the command notification code. pExtra Used according to the value of nCode. pHandlerInfo If not NULL, OnCmdMsg fills in the pTarget and pmf members of the pHandlerInfo structure instead of dispatching the command. Typically, this parameter should be NULL. Remarks Called by the framework to route and dispatch command messages and to handle the update of command user-interface objects. This is the main implementation routine of the framework command architecture. At run time, OnCmdMsg dispatches a command to other objects or handles the command itself by calling the root class CCmdTarget::OnCmdMsg, which does the actual message-map lookup. Observatie: 1. In concluzie, această funcţie este apelata de cadrul de lucru (framework), asta însemnând că nu vom găsi în cod un apel explicit la această funcţie. Funcţia este folosită pentru a ruta mesajele de comandă. 2. Fiecare comandă are un ID (identificator) de exemplu IDM_FILE_NEW, poate fi ID-ul pentru comanda de meniu, “File->New”. Example // This example illustrates extending the framework's standard command // route from the view to objects managed by the view. This example // is from an object-oriented drawing application, similar to the // DRAWCLI sample application, which draws and edits "shapes".

Page 33: Programare Windows

33

BOOL CMyView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { // Extend the framework's command route from the view to // the application-specific CMyShape that is currently selected // in the view. m_pActiveShape is NULL if no shape object // is currently selected in the view. if ((m_pActiveShape != NULL) && m_pActiveShape->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE; // If the object(s) in the extended command route don't handle // the command, then let the base class OnCmdMsg handle it. return CView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); } // The command handler for ID_SHAPE_COLOR (menu command to change // the color of the currently selected shape) was added to // the message map of CMyShape (note, not CMyView) using ClassWizard. // The menu item will be automatically enabled or disabled, depending // on whether a CMyShape is currently selected in the view, that is, // depending on whether CMyView::m_pActiveView is NULL. It is not // necessary to implement an ON_UPDATE_COMMAND_UI handler to enable // or disable the menu item. BEGIN_MESSAGE_MAP(CMyShape, CCmdTarget) //{{AFX_MSG_MAP(CMyShape) ON_COMMAND(ID_SHAPE_COLOR, OnShapeColor) //}}AFX_MSG_MAP END_MESSAGE_MAP() CWinThread Un obiect din clasa CWinThread reprezintă un fir de execuţie dintr-o aplicaţie. Firul de execuţie principal este un obiect al clasei CWinApp ce este o clasă derivată din CWinThread. Pe lângă firul de execuţie principal se mai pot folosi şi alte fire de execuţie, folosind obiecte CWinThread. Exista doua tipuri de fire de executie suportate de CWinThread:

1. fire de lucru (fara interfata utilizator, deci nu au bucla de mesage); 2. fire cu interfata utilizator.

O problema importantă legată de firele de execuţie o constituie sincronizarea acestora (executie sincronizată).

Pentru o documentare asupra mebrilor clasei CWinThread consultati MSDN. CWinApp Derivată din CWinThread Clasa CWinApp este clasa de bază pentru obiectul aplicaţie. Clasa aplicaţie din MFC încapsulează iniţializarea, execuţia şi terminarea unei aplicaţii Windows.

Page 34: Programare Windows

34

Fiecare aplicaţie MFC poate conţine doar un singur obiect derivat din CWinApp. Acest obiect este global şi este construit înaintea construirii ferestrelor, fiind disponibil atunci când sistemul de operare Windows apelează funcţia WinMain, care este oferită de biblioteca MFC. Când se derivează o clasă aplicaţie din CWinApp, se suprascrie funcţia membru InitInstance ( ) pentru a se construi şi iniţializa noua aplicaţie. Pe lângă funcţiile membru ale lui CWinApp, MFC oferă funcţii globale pentru a obţine informaţii despre obiectul aplicaţie curent:

• AfxGetApp ( ) – returnează un pointer la obiectul aplicaţie curent; • AfxGetInstanceHandle ( ) – handle la instanţa aplicaţie curentă; • AfxGetResourceHandle ( ) – handle la resursele aplicaţiei; • AfxGetAppName ( ) – numele aplicaţiei.

Exemplu class Cmfc1App : public CWinApp { public: Cmfc1App(); // Overrides public: virtual BOOL InitInstance(); // Implementation afx_msg void OnAppAbout(); DECLARE_MESSAGE_MAP() }; iar in implementare BOOL Cmfc1App::InitInstance() { // InitCommonControls() is required on Windows XP if an application // manifest specifies use of ComCtl32.dll version 6 or later to enable // visual styles. Otherwise, any window creation will fail. InitCommonControls(); CWinApp::InitInstance(); // Initialize OLE libraries if (!AfxOleInit()) { AfxMessageBox(IDP_OLE_INIT_FAILED); return FALSE; } AfxEnableControlContainer(); // Standard initialization // If you are not using these features and wish to reduce the size // of your final executable, you should remove from the following // the specific initialization routines you do not need // Change the registry key under which our settings are stored // TODO: You should modify this string to be something appropriate

Page 35: Programare Windows

35

// such as the name of your company or organization SetRegistryKey(_T("Local AppWizard-Generated Applications")); LoadStdProfileSettings(4); // Load standard INI file options (including MRU) // Register the application's document templates. Document templates // serve as the connection between documents, frame windows and views CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(Cmfc1Doc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(Cmfc1View)); if (!pDocTemplate) return FALSE; AddDocTemplate(pDocTemplate); // Parse command line for standard shell commands, DDE, file open CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Dispatch commands specified on the command line. Will return FALSE if // app was launched with /RegServer, /Register, /Unregserver or /Unregister. if (!ProcessShellCommand(cmdInfo)) return FALSE; // The one and only window has been initialized, so show and update it m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); // call DragAcceptFiles only if there's a suffix // In an SDI app, this should occur after ProcessShellCommand return TRUE; } CWnd Clasa CWnd este clasa de bază pentru toate celelalte clase fereastră oferite de MFC. Această clasă are o mare parte din funcţiile necesare în obiectele frame, view, controale, etc. Obiectul fereastră este un obiect al clasei CWnd sau derivat din clasa CWnd care este creat direct de către program. Fereastra, pe de altă parte este un handle către o structură internă din Windows care conţine resursele ferestrei respective. CFrameWnd Clasa CFrameWnd conţine o implementare implicită pentru următoarele funcţii ale ferestrei principale dintr-o aplicaţie Windows:

• menţine ordinea cu ferestrele de vizualizare active; • comenzile şi mesajele de notificare le trimite ferestrei de vizualizare active; • modificarea textului ce apare pe bara de titlu a ferestrei în funcţie de fereastra de vizualizare activă; • se ocupă de poziţionarea barelor de control, a ferestrelor de vizualizare şi a altor ferestre copil, în

zona client a ferestrei principale; • bara de meniu; • meniul sistem al aplicaţiei;

Page 36: Programare Windows

36

• acceleratori (tratarea tastelor speciale); • starea iniţială a aplicaţiei (minimizată, maximizată); • help senzitiv la context; • bara de stare; • se ocupă de perechile document-view.

Exemplu class CMainFrame : public CFrameWnd { protected: // create from serialization only CMainFrame(); DECLARE_DYNCREATE(CMainFrame) // Attributes public: // Operations public: // Overrides public: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); // Implementation public: virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // control bar embedded members CStatusBar m_wndStatusBar; CToolBar m_wndToolBar; // Generated message map functions protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); DECLARE_MESSAGE_MAP() }; iar in implementare avem (cod partial pentru crearea ferestrei) int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))

Page 37: Programare Windows

37

{ TRACE0("Failed to create toolbar\n"); return -1; // fail to create } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } // TODO: Delete these three lines if you don't want // the toolbar to be dockable m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; } BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return TRUE; } Structura CREATESTRUCT este asemănătoare cu structura WNDCLASSEX. typedef struct tagCREATESTRUCT { LPVOID lpCreateParams; HANDLE hInstance; HMENU hMenu; HWND hwndParent; int cy; int cx; int y; int x; LONG style; LPCSTR lpszName; LPCSTR lpszClass; DWORD dwExStyle; } CREATESTRUCT;

Page 38: Programare Windows

38

Document templates - Şabloane de document

Aplicaţiile MFC folosesc implicit un model de programare ce separă datele programului de partea de afişare a acestor date şi de majoritatea interacţiunilor dintre utilizator şi date. În acest model, un obiect document citeşte şi scrie datele pe disc şi mai poate oferi nişte funcţii de lucru cu aceste date.

Un obiect distinct se ocupă de partea de vizualizare a datelor într-o anumită fereastră şi tratează interacţiunea utilizatorului cu acestea. Obiectul de vizualizare poate citi datele din document şi le poate modifica la acţiunea utilizatorului.

Modelul document/view este aplicabil şi în situaţia în care există mai multe ferestre de vizualizare pentru un document, lăsând libertatea fiecărui obiect de vizualizare să-şi afişeze datele, în timp ce partea de cod comună tuturor ferestrelor de vizualizare (cum ar fi partea de calcule) se poate implementa în document.

Documentul se mai ocupă şi de reactualizarea ferestrelor de vizualizare dacă datele au fost modificate (de către document sau de către una din ferestrele de vizualizare).

Arhitectura MFC document/view suportă o implementare uşoară a ferestrelor de vizualizare multiple, tipuri de documente diferite, ferestre împărţite (splitter windows), şi alte tipuri de caracteristici ale interfeţelor. La baza modelului document/view stau următoarele patru clase:

• CDocument (sau COleDocument); • CView; • CFrameWnd; • CDocTemplate.

Părţile din MFC cele mai vizibile, atât pentru utilizator cât şi pentru programator, sunt documentul şi ferestrele

de vizualizare. Cea mai mare parte din munca investită într-un proiect constă în scrierea claselor document şi view. Clasa CDocument oferă funcţiile de bază pentru clasele document specifice aplicaţiei. Un document

reprezintă un bloc de date pe care utilizatorul îl poate deschide cu comanda Open, salva cu comanda Save, etc. Clasa CView stă la baza claselor view ale aplicaţiei. Un view este ataşat unui document şi funcţionează ca

un intermediar între document şi utilizator: view-ul construieşte în fereastră o imagine a documentului şi interpretează acţiunile utilizatorului şi le transmite documentului. În figura următoare este prezentată relaţia dintre document şi view:

Documentele, ferestrele de vizualizare asociate şi ferestrele cadru care conţin ferestrele de vizualizare pentru un document sunt create de un template document. Acesta este responsabil de crearea şi gestionarea tuturor documentelor de acelaşi tip. Orice aplicaţie MFC SDI creată cu AppWizard are cel puţin un document template. Acest template crează şi defineşte relaţiile dintre document, fereastra cadru şi fereastra de vizualizare. Când este creat un nou element sau când este deschis un document, acest template este folosit pentru a crea cele trei elemente în următoarea ordine: documentul, fereastra cadru şi fereastra de vizualizare. Pentru o aplicaţie MDI mai apare un pas în plus faţă de aplicaţia SDI: crearea ferestrei cadru principale a aplicaţiei înaintea creării documentului. Template-ul document mapează documentul, fereastra cadru şi fereastra de vizualizare pe clasele proprii aplicaţiei. Crearea unui template (creat implicit de AppWizard):

CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSdiDoc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(CSdiView)); AddDocTemplate(pDocTemplate);

Secvenţa de apeluri la crearea documentului:

• CMyApp::InitInstance ( ); • dacă aplicaţia este MDI atunci se crează şi afişează fereastra cadru principală; • parcurge şi procesează linia de comandă (dacă nu există se apelează OnFileNew ( )); • se selectează template-ul document;

Page 39: Programare Windows

39

• se crează un document gol prin CDocTemplate::OpenDocumentFile ( ); • CDocTemplate::CreateNewDoc ( ); • constructorul pentru CMyDocument; • CMyDocument::OnNewDocument ( ).

Crearea ferestrei cadru: • CDocTemplate::CreateNewFrame ( ) • constructorul pentru CMainFrame; • CMainFrame::LoadFrame ( ); • CMainFrame::PreCreateWindow ( ); • CMainFrame::OnCreate ( ); • CMainFrame::OnCreateClient ( );

Crearea ferestrei de vizualizare: • CMainFrame::CreateView ( ); • constructorul pentru CMyView; • CMyView::PreCreateWindow ( ); • CMainFrame::InitialUpdateFrame ( ); • CMyView::OnInitialUpdate ( ); • CMyView::OnActivateFrame ( ); • CMainFrame::ActivateFrame ( ); • CMyView::OnActivateView ( );

CDocument Clasa CDocument furnizeaza functionalitatea de baza in arhitectura document-view implementata in MFC

si are ca scop gestionarea documentului aplicatiei. CDocument suporta operatii standard de creare, incarcare si salvare a documentului. O aplicatie poate suporta mai mult decat un document. Fiecare tip are asociat un document template

(sablon). Acest sablon specifica resursele utilizate pentru acel tip de document. Utilizatorul interactioneaza cu un document prin intermediul unui obiect CView. Un obiect CView

reprezinta o vizualizare a documentului in zona client. Un document poate avea asociate mai multe vizualizari. Un document primeste comenzi forward-ate de vizualizarea activa precum si comenzi din meniu (Open, Save).

CView Clasa CView (obiecte instantiate direct sau indirect) furnizeaza vizualizarea documentului. O vedere

actioneaza ca un intermediar intre document si utilizator. O vedere este o fereastra descendenta din fereastra cadru.O vedere este responsabila de tratarea mai multor tipuri de intrari: tastatura, mouse, operatii de drag & drop, comenzi din meniu, toobar sau bare de defilare (scroll bars).

Metoda cea mai importanta din aceasta clasa este OnDraw, responsabila pentru desenarea in zona client. O alta metoda folosita este cea care face legatura intre document si vizualizare: GetDocument(), functie ce

returneaza un pointer la obiectul de tip document. Alte clase de vizuallizare: CCtrlView, CDaoRecordView, CEditView, CFormView, CListView, CRecordView, CRichEditView, CScrollView, CTreeView.

Page 40: Programare Windows

40

Harta de mesaje

Hărţile de mesaje sunt părţi ale modelului MFC de programare Windows. În loc de a scrie funcţia WinMain() care trimite mesaje la procedura fereastră (funcţia) WindProc() şi apoi să controlăm ce mesaj a fost trimis pentru a activa funcţia corespunzătoare, vom scrie doar funcţia care tratează mesajul şi vom adăuga mesajul la harta de mesaje a clasei. Cadrul de lucru va face operaţiunile necesare pentru a ruta acest mesaj în mod corect. Construirea hărţii de mesaje

Hărţile de mesaje se construiesc în două etape. Declararea hărtii de mesaje (macro DECLARE_MESSAGE_MAP()) se face în fişierul .h al clasei, iar implementarea se face in fişierul .cpp al clasei (BEGIN_MESSAGE_MAP() ... END_MESSAGE_MAP()).

Exemplu //{{AFX_MSG(CShowStringApp) afx_msg void OnAppAbout(); //the ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG DECLARE_MESSAGE_MAP() Se declară funcţia OnAppAbout() care este prefixată cu afx_msg ce constituie un comentariu pentru compilatorul de C++, dar care indică vizual că această funcţie tratează un mesaj. Această funcţie o vom găsi şi în cadrul macro-ului BEGIN_MESSAGE_MAP(), ca un parametru al macro-ului ON_COMMAND(). Primul parametru al acestui din urmă macro este ID-ul mesajului (comenzii în acest caz), iar al doilea numele funcţiei ce tratează acest mesaj. Cod in .cpp BEGIN_MESSAGE_MAP(CShowStringApp, CWinApp) //{{AFX_MSG_MAP(CShowStringApp) ON_COMMAND(ID_APP_ABOUT, OnAppAbout) //DO NOT EDIT what you see in these blocks of generated code! //}}AFX_MSG_MAP // Standard file based document commands ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew) ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen) ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup) END_MESSAGE_MAP()

Macro-ul DECLARE_MESSAGE_MAP() adaugă date mebru şi funcţii la clasa respectivă. Practic se declară o tabelă cu un număr de intrări variabil (sfârşitul tabelei este marcat (completat) de END_MESSAGE_MAP()) şi funcţii care operează cu acest elementele acestui tabel.

Macro-uri pentru harta de mesaje BEGIN_MESSAGE_MAP şi END_MESSAGE_MAP sunt macro-uri care ca şi macro-ul DECLARE_MESSAGE_MAP din fişierul .h, declară anumite variabile membru şi funcţii pe care cadrul de lucru le va utiliza pentru a naviga prin hărţile de mesaje ale tuturor obiectelor din sistem. Dintre macro-urile folosite în hărţile de mesaje, enumerăm:

• DECLARE_MESSAGE_MAP—folosit în fişierul .h pentru a declara că va exista o hartăesaje in .cpp • BEGIN_MESSAGE_MAP—Marchează începutul hărţii de mesaje în fişierul sursă. • END_MESSAGE_MAP—Marchează sfârşitul hărţii de mesaje în fişierul sursă. • ON_COMMAND—Folosit pentru a face legătura între comenzi şi funcţiile care tratează aceste comenzi.

Page 41: Programare Windows

41

• ON_COMMAND_RANGE—Folosit pentru a face legătura între un grup de comenzi şi funcţia care le tratează.

• ON_CONTROL—Folosit pentru a face legătura între un mesaj de notificare al unui control şi funcţia ce-l tratează.

• ON_CONTROL_RANGE—Folosit pentru a face legătura între un grup de mesaje de notificare al unui control şi funcţia corespunzătoare.

• ON_MESSAGE—Folosit pentru a realiza legătura între un mesaj definit de utilizator şi funcţia care-l tratează.

• ON_REGISTERED_MESSAGE—Folosit pentru a realiza legătura între un mesaj defint de utilizator, dar înregistrat şi funcţia care-l tratează.

• ON_UPDATE_COMMAND_UI—Folosit pentru a indica funcţia care va face actualizarea pentru o comandă specifică.

• ON_COMMAND_UPDATE_UI_RANGE—Ca mai sus, dar pentru un grup de comenzi. • ON_NOTIFY—Folosit pentru a indica funcţia ce va adăuga informaţii suplimentare, pentru un mesaj de

notificare al unui control. • ON_NOTIFY_RANGE—Ca mai sus, dar pentru un grup de mesaje de notificare al unui control.

ON_NOTIFY_EX—Ca la ON_NOTIFY, dar funcţia va întoarce TRUE sau FALSE pentru a indica dacă notificarea poate fi trecută altui obiect pentru tratări suplimentare.

• ON_NOTIFY_EX_RANGE—Ca mai sus, dar se referă la un grup de comenzi de notificare. În plus la aceste macro-uri, există peste 100 de macro-uri, unul pentru fiecare din cele mai comune mesaje. De

exemplu macro-ul ON_CREATE pentru mesajul WM_CREATE, etc. În mod obişnuit aceste macro-uri sunt adăugate la clasă de către ClassWizard.

Cum lucrează harta de mesaje ? Fiecare aplicaţie are un obiect moştenit din clasa CWinApp şi o funcţie membru Run(). Această funcţie

apelează funcţia CWinThread::Run(), care apelează GetMessage(), TranslateMessage() şi DispatchMessage().

Funcţia fereastră (în SDK) ştie handler-ul ferestrei pentru care este trimis mesajul. Fiecare obiect fereastră foloseşte acelaşi stil al clasei Windows şi aceeaşi funcţie WindProc, numită AfxWndProc(). MFC menţine ceva asemănător, numit handle map, o tabelă cu handler-ii ferestrelor şi pointeri la obiecte, şi framework-ul foloseşte aceasta pentru a trimite un pointer la obiectul C++, un CWnd*. În continuare el apelează WindowProc(), o funcţie virtuală a acestui obiect. Datorită polimorfismului, indiferent că este vorba de un Button sau o vizualizare se va apela funcţia corectă. WindowProc() apelează OnCmdMsg(), funcţia C++ care efectiv manipulează mesajul. Mai întâi caută dacă acesta este un mesaj, o comandă sau o notificare. Presupunând că este un mesaj. caută în harta de mesage a clasei, folosind funcţiile şi variabilele membru adăugate la clasă de DECLARE_MESSAGE_MAP, BEGIN_MESSAGE_MAP şi END_MESSAGE_MAP. Modul de organizare al acestei tabele permite căutarea mesajului, dacă este nevoie, în toată arborescenţa clasei. AfxWndProc()->WindowProc()->OnCmdMsg()

Înţelegerea comenzilor O comandă este un tip special de mesaj. Windows generează comenzi când utilizatorul alege un articol de

meniu, apasă un buton, sau altfel spune sistemului să facă ceva. Pentru un articol de meniu se primeşte mesajul WM_COMMAND iar pentru notificarea unui control WM_NOTIFY, cum ar fi selectarea dintr-un list box.

Comenzile şi notificările sunt trecute prin SO ca orice alt mesaj, până când acestea ajung la OnWndMsg(). În acest punct pasarea mesajului windows încetează şi se startează rutarea comenzilor în MFC. Mesajele de comandă au ca prim parametru, ID-ul articolului din meniu care a fost selectat sau a butonului care a fost apăsat.

Rutarea comenzilor este mecanismul pe care OnWndMsg() îl foloseşte pentru a trimite comanda (sau notificarea) la obiectele care pot trata acest mesaj. Numai obiectele care sunt moştenite din CWnd pot primi mesaje, dar toate obiectele care sunt moştenite din CCmdTarget, incluzând CWnd şi CDocument, pot primi

Page 42: Programare Windows

42

comenzi sau notificări. Aceasta însemană că o clasă moştenită din CDocument poate avea o hartă de mesaje. Pot să nu existe mesaje în această hartă ci numai pentru comenzi şi notificări, dar tot hartă de mesaje se numeşte.

Comenzile şi notificările ajung la clasă prin mecanismul de rutare al comenzilor. OnWndMsg() apelează CWnd::OnCommand() sau CWnd::OnNotify(). OnCommand() apelează OnCmdMsg(). OnNotify() apelează de asemenea OnCmdMsg(). Binenţeles că ambele funcţii efectuează anumite controale înainte de a face aceste apeluri. OnCmdMsg() este virtuală, ceea ce înseamnă că diferite comenzi au implementări diferite. Implementarea pentru fereastra cadru trimite comanda vizualizărilor şi documentelor pe care le conţine. Comanda pentru actualizări

Folosit în special pentru actualizarea articolelor din meniu. De exemplu când se selectează text in vizualizare şi opţiunile de Copy, Cut, Paste, Undo sunt implementate aceste articole de menu vor fi afişate în starea enable sau disable (funcţie de logica programului). Există două posibilităţi de a face acest lucru: continuous update approach şi update-on-demand approach.

Continuous update approach presupune existenţa unei tabele mari ce conţine câte o intrare pentru fiecare meniu şi un flag care va indica dacă opţiunea este disponibilă sau nu.

Cea de-a doua posibilitate presupune controlarea tuturor condiţiilor asupra unui articol de meniu înainte ca meniul să fie afişat. În acest caz obiectul care are meniul va şti mai multe despre acesta, în schimb nu toată aplicaţia va avea acces la aceste informaţii.

Tehinca MFC-ului este de a utiliza un obiect numit CCmdUI, o comandă a interfeţei utilizatorului, şi de a da acest obiect când se trimite mesajul CN_UPDATE_COMMAND_UI. În harta de mesaje va apărea macro-ul ON_UPDATE_COMMAND_UI. Ce se întâmplă în realitate? SO trimite mesajul WM_INITMENUPOPUP; clasa CFrameWnd va construi un obiect CCmdUI, setează variabilele membru ce corespund primului articol din meniu şi apelează funcţia membru DoUpdate(). DoUpdate() trimite mesajul CN_COMMAND_UPDATE_UI la ea însăşi, cu un pointer la obiectul CCmdUI. Se vor seta variabilele membru ale obiectului CCmdUI cu articolul doi din meniu şi procesul continuă până când este parcurs tot meniul. Obiectul CCmdUI este folosit pentru a valida (enable) sau invalida (disable) [gray(disable) sau ungray(enable) ] articolele din meniu sau butoane. CCmdUI are următoarele funcţii membru:

• Enable() — Are un parametru care poate lua valorile TRUE sau FALSE (implicit TRUE). • SetCheck() — Marchează sau demarchează articolul. • SetRadio() – Setează sau nu unul din butoanele radio al unui grup. • SetText()—Setează textul unui meniu sau buton. • DoUpdate()—Generează mesajul.

Exemplu: BEGIN_MESSAGE_MAP(CWhoisView, CFormView) ... ON_UPDATE_COMMAND_UI(ID_EDIT_PASTE, OnUpdateEditPaste) ... END_MESSAGE_MAP() void CWhoisView::OnUpdateEditPaste(CCmdUI* pCmdUI) { pCmdUI->Enable(::IsClipboardFormatAvailable(CF_TEXT)); } Exemplu complet de aplicatie (SDI) generata de AppWizard îl găsiti pe pagina http://www.infoiasi.ro/~iasimin/IDD Urmăriţi codul colorat în roşu, cod adaugat cu ajutorul classwizard-ului.

Page 43: Programare Windows

43

Incercaţi să generaţi acest proiect pe calculatoarele dv. Inserati cu classwizard-ul codul scris cu roşu sau introduceţi-l manual. Observaţi ce se întâmplă. Observaţie: Există ClassWizard şi AppWizard, wizard-uri diferite. Primul este pentru clase, al doilea pentru aplicaţii. Crearea unei aplicaţii Windows Pentru generarea unei aplicaţii se foloseşte AppWizard. În primul rând aici se lucrează cu proiecte. Un proiect poate conţine mai multe aplicaţii. Pentru a crea un proiect vom folosi comenzile File->New->Projects. Din lista prezentată în pagina (tab-ul) Projects vom selecta MFC AppWizard (EXE), selectăm locul unde va fi memorat pe HDD şi apoi completăm numele proiectului. După acest lucru va trebui să completăm o serie de informaţii grupate pe etape. Etapa 1. Tip aplicaţie

În etapa 1 (pas 1) vom selecta tipul aplicaţiei (se alege interfaţa cu utilizatorul). Avem următoarele posibilităţi:

• O aplicaţie single document interface (SDI), are numai un document deschis la un moment dat. Când

selectăm File->Open, fişierul existent şi care este deschis va fi închis înainte ca să se deschidă noul document.

• O aplicaţie multiple document interface (MDI), cum ar fi Excel sau Word, poate deschide mai multe documente odată. Dacă dorim vizualizări multiple pentru un document va trebui să construim o aplicaţie MDI.

• O aplicaţie dialog-based, cum ar fi utilitarul Character Map. Aplicaţiile nu au meniu.

OBSERVAŢIE:: Aplicaţiile bazate pe dialog sunt diferite de cele de tip SDI sau MDI. Vor fi tratate în mod separat.

Mai există un checkbox care ne dă posibilitatea de a indica dacă dorim suport pentru arhitectura Document/View. Opţiunea se foloseşte în special pentru portarea aplicaţiilor dintr-un alt sistem de dezvoltare. Nu o vom folosi.

Etapa 2. Baze de date În această etapă vom alege nivelul pentru suportul bazelor de date. Există patru posibilităţi:

• Pentru aplicaţii fără baze de date vom selecta None. • Dacă dorim să avem acces la baze de date dar nu dorim să derivăm vizualizarea din CFormView sau să nu

avem meniu Record vom selecta Header Files Only. • Dacă dorim să derivăm vizualizarea din CFormView şi să avem meniul Record dar nu dorim să serializăm

documentul, vom selecta Database View Without File Support. • Dacă dorim suport pentru baze de date şi în plus dorim şi salvarea documentului vom selecta Database

View With File Support.

Dacă selectăm ultima opţiune, va trebui să indicăm şî sursa de date – butonul Data Source.

Etapa 3. Suport pentru documente compuse Tehnologia ActiveX şi OLE este referită ca fiind tehnologia documentului compus (compound document technology). În această etapă există cinci posibilităţi:

• Dacă nu scriem o aplicaţie ActiveX, alegem None.

Page 44: Programare Windows

44

• Dacă dorim o aplicaţie care să conţină obiecte ActiveX înglobate sau legate, cum ar fi aplicaţia Word, alegem Container.

• Dacă dorim ca aplicaţia noastră să furnizeze obiecte, care pot fi înglobate, pentru alte aplicaţii, dar aplicaţia să nu poată fi executată separat (stand alone), vom alege Mini Server.

• Dacă dorim ca aplicaţia noastră să furnizeze obiecte, care pot fi înglobate, pentru alte aplicaţii, şi aplicaţia să poată fi executată separat (stand alone), vom alege Full Server.

• Dacă dorim ca aplicaţia să încorporeze opţiunile 3 şi 4 vom selecta Both Container and Server. Dacă alegem suport pentru documentele compuse vom avea şi suport pentru fişiere compuse (compound files). Fişierele compuse conţin unul sau mai multe obiecte ActiveX şi sunt salvate într-un mod special astfel încât un obiect poate fi modificat fără a rescrie întregul fişier. Pentru acest lucru facem o selecţie pe unul din butoanele radio Yes, Please, sau No, Thank You. Tot în această pagină ne hotărâm dacă aplicaţia suportă automatizare sau va folosi controale ActiveX.

OBSERVAŢIE: Dacă dorim ca aplicaţia să fie un control ActiveX, nu trebuie să creăm o aplicaţie .exe obişnuită. Crearea controlului ActiveX se face selectând o altă opţiune din Projects.

Etapa 4. Opţiuni pentru interfaţă. Alte Opţiuni Următoarele opţiuni afectează modul de afişare al interfeţei:

• Docking Toolbar. AppWizard pregăteşte un toolbar. Acesta poate fi editat (adăugare, modificare, ştergere). • Initial Status Bar. AppWizard crează o bară de stare care afişează mesajele ataşate comenzilor din meniu

sau alte mesaje specifice aplicaţiei. • Printing and Print Preview. Aplicaţia va avea opţiunile Print şi Print Preview din meniul File, şi o parte

din codul necesar va fi generat de AppWizard. • Context-Sensitive Help. Meniul Help va avea opţiunile Index şi Using Help, şi o parte din codul necesar

pentru a fi implementat Help va fi generat de AppWizard. Această decizie este dificil de luat mai târziu pentru că părţi din cod sunt generate în diverse locuri din cadrul aplicaţiei.

• 3D Controls. Aplicaţia va arăta ca o aplicaţie obişnuită Windows 95. Dacă nu selectăm această opţiune, boxele de dialog vor avea background alb şi nu vor fi umbre în jurul boxelor de editare, checkbox-urilor şi alte controale.

• MAPI(Messaging API). Aplicaţia va avea posibilitatea de trimite fax-uri, email-uri şi alte mesaje. • Windows Sockets. Aplicaţia va putea accesa Internet-ul în mod direct, folosind protocoale ca FTP şi HTTP

(protocolul World Wide Web).

Putem seta de asemenea numărul fişierelor din lista MRU. Implicit acest număr este 4. Butonul Advanced activează două tab-uri (Document Template Strings, Window Styles) unde putem schimba numele aplicaţiei, titlul ferestrei cadru, extensia fişierelor folosite în File->Open, etc. Prporpietăţile care pot fi setate pentru ferestrele cadru:

• Thick Frame. Dacă nu o selectăm se previne redimensionarea. • Minimize Box. • Maximize Box. • System Menu. • Minimized. Cadrul este minimizat când aplicaţia este lansată în execuţie. Pentru aplicaţiile SDI, această

opţiune va fi ignorată când aplicaţia rulează sub Windows 95. • Maximized. The frame is maximized when the application starts. For SDI applications, this option will be

ignored when the application is running under Windows 95.

Alte Opţiuni Dorim biblioteca MFC să fie “legată” ca un DLL partajabil sau în mod static? Un DLL este o colecţie de funcţii utilizate de diferite aplicaţii. Folosirea DLL-urilor face ca programul să fie mai mic dar mai greu de instalat. Dacă legătura este statică creşte mărimea programului dar nu mai sunt probleme deosebite cu instalarea.

Page 45: Programare Windows

45

Etapa 6. Numele fişierelor şi al claselor Ultima etapă stabileşte numele claselor şi fişierelor create de AppWizard. Putem schimba aceste nume. Dacă aplicaţia include o clasă pentru vizualizare, care în mod obişnuit este derivată din CView, putem schimba clasa de bază, de exemplu CScollView sau CEditView care oferă o funcţionalitate sporită vizualizării. Vom apăsa butonul Finish pentru terminarea generării aplicaţiei. Urmează exemple de creare aplicaţii pentru fiecare tip în parte.

Crearea DLL-urilor, Aplicaţiilor de tip Consolă Alte opţiuni ce se regăsesc în tab-ul Projects: • ATL COM AppWizard • Custom AppWizard • Database Project • DevStudio Add-In Wizard • Extended Stored Procedure AppWizard • ISAPI Extension Wizard • Makefile • MFC ActiveX ControlWizard • MFC AppWizard (dll) • Utility Project • Win32 Application • Win32 Console Application • Win32 Dynamic Link Library • Win32 Static Library

Înţelegerea codului generat de AppWizard Aplicaţie SDI O aplicaţie SDI are meniu pe care utilizatorul poate să-l folosească pentru a deschide documente şi să lucreze cu ele. AppWizard generează cinci clase. Numele proiectului este FirstSDI.

• CAboutDlg, o clasă de dialog pentru About • CFirstSDIApp, o clasă CWinApp pentru întraga aplicaţie (obiectul aplicaţie) application • CFirstSDIDoc, o clasa document • CFirstSDIView, o clasă vizualizare • CMainFrame, o clasă cadru

Fişierul FirstSDI.h – pentru aplicaţie // FirstSDI.h : main header file for the FIRSTSDI application // #if !defined(AFX_FIRSTSDI_H__CDF38D8A_8718_11D0_B02C_0080C81A3AA2__INCLUDED_) #define AFX_FIRSTSDI_H__CDF38D8A_8718_11D0_B02C_0080C81A3AA2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #ifndef __AFXWIN_H__ #error include `stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols ///////////////////////////////////////////////////////////////////////////// // CFirstSDIApp: // See FirstSDI.cpp for the implementation of this class // class CFirstSDIApp : public CWinApp

Page 46: Programare Windows

46

{ public: CFirstSDIApp(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CFirstSDIApp) public: virtual BOOL InitInstance(); //}}AFX_VIRTUAL // Implementation //{{AFX_MSG(CFirstSDIApp) afx_msg void OnAppAbout(); // NOTE - The ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code! //}}AFX_MSG DECLARE_MESSAGE_MAP() }; ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations // immediately before the previous line. #endif //!defined(AFX_FIRSTSDI_H__CDF38D8A_8718_11D0_B02C_0080C81A3AA2__INCLUDED_) #ifndef acţionează ca în codul care urmează,şi are ca efect includerea fişierului ce urmează o singură dată. #ifndef test_h #include "test.h" #define test_h #endif #pragma once este de asemenea pentru a preveni definiţiile multiple dacă acest fişier este inclus de două ori.

Clasa CFirstSDIApp derivată din CWinApp, furnizează cea mai mare funcţionalitate a aplicaţiei. Instanţa acestei clase constituie obiectul aplicaţie. AppWizard a generat anumite funcţii pentru această clasă care reacoperă funcţiile moştenite din clasa de bază. Această secţiune de cod începe cu //Overrides. De asemenea sunt generate annumite comentarii care ajută la înţelegerea codului. În această secţiune vom găsi declaraţia pentru funcţia InitInstance(). Următoarea secţiune de cod este pentru harta de mesaje. AppWizard generează cod pentru constructorul CFirstSDIApp, şi funcţiile InitInstance() şi OnAppAbout() în fişierul firstsdi.cpp. Codul generat pentru constructor arată astfel: CFirstSDIApp::CFirstSDIApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance } Ca regulă generală trebuie reţinuţ că Microsoft construieşte obiectele în doi paşi. Se crează obiectul unde se scrie cod care sigur se execută corect, iar iniţializările se fac cu ajutorul unei funcţii membru, care poate indica dacă acestea au fost efectuate cu succes sau nu. Constructorul nu întoarce nici o valore, construieşte obiectul.

În continuare prezentăm listingul pentru CFirstSDIApp::InitInstance() BOOL CFirstSDIApp::InitInstance() { AfxEnableControlContainer(); // Standard initialization // If you are not using these features and want to reduce the size

Page 47: Programare Windows

47

// of your final executable, you should remove from the following // the specific initialization routines you don't need. #ifdef _AFXDLL Enable3dControls(); // Call this when using MFC in a shared DLL #else Enable3dControlsStatic(); // Call this when linking to MFC statically #endif // Change the registry key under which our settings are stored. // You should modify this string to be something appropriate, // such as the name of your company or organization. SetRegistryKey(_T("Local AppWizard-Generated Applications")); LoadStdProfileSettings(); // Load standard INI file options (including // MRU) // Register the application's document templates. Document templates // serve as the connection between documents, frame windows, and views. CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CFirstSDIDoc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(CFirstSDIView)); AddDocTemplate(pDocTemplate); // Parse command line for standard shell commands, DDE, file open CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Dispatch commands specified on the command line if (!ProcessShellCommand(cmdInfo)) return FALSE; // The one and only window has been initialized, so show and update it. m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); return TRUE; } Comentariile sunt incluse de AppWizard. InitInstance este apelată prima la lansarea apliacţiei. AfxEnableControlContainer() permite aplicaţiei de a conţine controale ActiveX. Se permite afişarea controalelor cu aspect 3D. Se completează regiştrii sub care va fi înregistrată această aplicaţie. InitInstance() înregistrează în continuare şablonul de document care este SDI în acest caz. InitInstance() crează un obiect vid (neiniţializat) CCommandLineInfo pentru a menţine parametrii pasaţi aplicaţiei prin linia de comandă când aceasta a fost lansată în execuţie şi apoi apelează ParseCommandLine() pentru a completa acest obiect. În final se procesează linia de comandă printr-un apel la ProcessShellCommand(). De exemplu dacă lansăm aplicaţia din linia de comandă astfel: FirstSDI curs, aplicaţia va deschide fişierul curs. Parametrii din linia de comandă pe care ProcessShellCommand() îi suportă sunt următorii: Parameter Action None Start app and open new file. Filename Start app and open file. /p filename Start app and print file to default printer. /pt filename printer driver port Start app and print file to the specified printer.

Page 48: Programare Windows

48

/dde Start app and await DDE command. /Automation Start app as an OLE automation server. /Embedding Start app to edit an embedded OLE item. Dacă dorim să implementăm o altă comportare, vom construi o clasă derivată din CCommandLineInfo pentru a păstra linia de comandă, şi apoi rescriem funcţiile CWinApp:: ParseCommandLine() şi CWinApp::ProcessShellCommand(). Se completează variabila m_pMainWnd cu adresa obiectului aplicaţiei, variabilă ce este defintă în CWinThread, ce este clasă de bază pentru CWinApp. La sfârşit funcţia întoarce TRUE pentru a indica că restul aplicaţiei poate rula.

Harta de mesaje indică că funcţia OnAppAbout() va trata un mesaj. Care este? Va trata comanda de meniu care are ID-ul ID_APP_ABOUT. Acest ID corespunde comenzii de meniu Help->About. Descrierea hărţii de mesaje este: BEGIN_MESSAGE_MAP(CFirstSDIApp, CWinApp) //{{AFX_MSG_MAP(CFirstSDIApp) ON_COMMAND(ID_APP_ABOUT, OnAppAbout) // NOTE - The ClassWizard will add and remove mapping macros here. // DO NOT EDIT what you see in these blocks of generated code! //}}AFX_MSG_MAP // Standard file-based document commands ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew) ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen) // Standard print setup command ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup) END_MESSAGE_MAP() OnAppAbout() arată aşa: void CFirstSDIApp::OnAppAbout() { CAboutDlg aboutDlg; aboutDlg.DoModal(); } Se construieşte obiectul aboutDlg, care este o boxă de dialog, şi apoi se execută acest dialog.

Aplicaţia Multiple Document Interface O asemenea aplicaţie are meniu şi permite utilizatorului de a deschide mai multe documente odată. AppWizard a generat cinci clase. Numele proiectului este FirstMDI. Clasele generate sunt:

• CAboutDlg, o clasă de dialog pentru About • CFirstMDIApp, o clasă CWinApp pentru aplicaţie • CFirstMDIDoc, o clasă document • CFirstMDIView, o clasă pentru vizualizare • CMainFrame, o clasă cadru

Descărcati exemplul de pe pagina http://www.infoiasi.ro/~iasimin/IDD.

Componentele unei aplicaţii bazate pe dialog AppWizard generează trei clase:

• CAboutDlg, o clasă dialog pentru About • CFirstDialogApp, o clasă CWinApp pentru întreaga aplicaţie • CFirstDialogDlg, o clasă de dialog pentru întreaga aplicaţie.

Descărcati codul de pe aceeaşi pagină.

Page 49: Programare Windows

49

Controale clasice Windows pune la dispoziţie 6 (şase) controale clasice. Un control nu este altceva decât o fereastră cu stiluri speciale. Trebuie retinut faptul că a lucra cu un asemea control este ca şi cum am lucra cu o fereastră. Tipurile controalelor, structurile WNDCLASS si clasele corespondente MFC sunt date in tabela urmatoare: Controale clasice

Tip Control WNDCLASS Clasa MFC Butoane "BUTTON" CButton List box-uri "LISTBOX" CListBox Controale de editare "EDIT" CEdit Combo box-uri "COMBOBOX" CComboBox Scroll bar-uri "SCROLLBAR" CScrollBar Controale statice "STATIC" CStatic

Un control este creat prin instantierea clasei respective din MFC urmat apoi de apelul functiei Create din

acea clasa. MFC creaza un obiect in doi pasi (am mai spus acest lucru pâna acum?). Daca m_wndPushButton este un obiect CButton, instructiunea: m_wndPushButton.Create (_T ("Start"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, rect, this, IDC_BUTTON); creaza un control push button ce contine textul “Start”. Descriere parametrii pentru functia Create:

• primul parametru specifică textul controlului. • al doilea parametru este stilul ferestrei, ce reprezintă o combinaţie între stilurile ferestrei si stiluri specifice

controlului. Controlul creat este o fereastra descendent (copil) al ferestrei identificata de al patrulea parametru (in SDK se furnizeaza un HWND la fereastra părinte).

• al treilea parametru specifică marimea si pozitia (in pixeli) controlului, data printr-un obiect CRect; pozitia este relativa la coltul din stanga sus al ferestrei parinte,

• ultimul parametru este ID-ul butonului (controlului - un intreg), folosit in diverse functii pentru a avea access la el; acest ID trebuie sa aiba o valoare unica in interiorul ferestrei date pentru a putea identifica corect controlul si functiile care trateaza mesajele de notificare.

Unele controale (ListBox, Edit) pentru a se alinia la noile stiluri, au o noua functie membru CreateEx. Stilurile extinse se scriu numai in cadrul acestei functii (vezi CreateWindow si CreateWindowEx). Daca m_wndListBox este un obiect CListBox, urmatoarea instructiune creaza un control list box cu stilul extins WS_EX_CLIENTEDGE: m_wndListBox.CreateEx (WS_EX_CLIENTEDGE, _T ("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | LBS_STANDARD, rect, this, IDC_LISTBOX); Ca o alternativa, putem deriva clasa noastra din CListBox, si apoi rescriem functia PreCreateWindow in clasa derivata, si aplicam stilul de fereastra WS_EX_CLIENTEDGE: BOOL CMyListBox::PreCreateWindow (CREATESTRUCT& cs) { if (!CListBox::PreCreateWindow (cs)) return FALSE; cs.dwExStyle |= WS_EX_CLIENTEDGE;

return TRUE; }

Page 50: Programare Windows

50

Un control trimite notificari parintelui sub forma de mesaje WM_COMMAND. Tipurile de notificari depind de tipul controlului, dar in fiecare caz, informatia din parametrii mesajului, wParam si lParam, identifica controlul care trimite mesajul si actiunea ceruta de mesaj. De exemplu, cind un push button este apasat (clic), codul de notificare este BN_CLICKED care se regaseste in HIWORD(wParam) si ID-ul controlului este in LOWORD(wParam), iar handler-ul ferestrei controlului in lParam. Cadrul de lucru MFC, insereaza in harta de mesaje acest mesaj de notificare, ascunzand detaliile de implementare pentru tratarea mesajului WM_COMMAND (se face legatura intre ID-ul controlului si functia care trateaza mesajul de notificare): ON_BN_CLICKED (IDC_BUTTON, OnButtonClicked) ON_BN_CLICKED este un macrou, si asemenea macro-uri exista pentru fiecare mesaj de notificare de la fiecare control. Exista un macro generic ON_CONTROL, care manipuleaza toate notificarile si toate tipurile de controale, si ON_CONTROL_RANGE, care mapeaza notificari in mod identic de la doua sau mai multe controale la o functie comuna. Comunicarea intre controale si parinti (proprietarii controlului) este in ambele directii. De exemplu parintele poate trimite mesajul BM_SETCHECK unui control check box cu parametrul wParam setat pe BST_CHECKED. MFC simplifica interfata controlului bazata pe mesaje prin construirea de functii membru in clasa controlului care ascund in fapt mesajul BM_SETCHECK si alte mesaje ale controlului. De exemplu: m_wndCheckBox.SetCheck (BST_CHECKED); plaseaza un check mark in interiorul check box-ului reprezentat de un obiect CButton, numit m_wndCheckBox. Din cauza ca un control este o fereastra, anumite functii membru pe care controlul le mosteneste din CWnd sunt folositoare pentru controlul programarii. De exemplu aceeasi functie care modifica titlul unei ferestre, SetWindowText, modifica textul (eticheta) unui push button, sau schimba continutul unui control de editare (box edit). Alte functii: GetWindowText, EnableWindow, SetFont. Daca vrem sa facem ceva in control si nu gasim o functie corespunzatoare in clasa controlului va trebui sa cautam o asemenea functie in clasa CWnd din care sunt derivate toate controalele.

Clasa CButton

CButton reprezinta controale de tip button bazate pe clasa WNDCLASS "BUTTON". Controalele button exista in patru variante: push buttons, check boxes, radio buttons, si group boxes. Cand cream un control button, vom specifica tipul acestuia prin includerea unuia din urmatoarele flag-uri in stilul ferestrei butonului:

Style Description BS_PUSHBUTTON Creaza un control standard de tip push button BS_DEFPUSHBUTTON Creaza un buton implicit de tip push button; folosit in casetele de

dialog pentru a identifica un push button care primeste mesajul BN_CLICKED cand s-a apasat tasta Enter

BS_CHECKBOX Creaza un control de tip check box BS_AUTOCHECKBOX Creaza un buton de tip check box care se autoseteaza / reseteaza

atunci cand este facut clic pe el (este de tip On/Off) BS_3STATE Creaza un check box cu trei stari BS_RADIOBUTTON Creaza un control de tip radio button BS_AUTORADIOBUTTON Creaza un control de tip radio button control care se autoseteaza /

reseteaza atunci cand este facut clic pe el

Page 51: Programare Windows

51

BS_GROUPBOX Creaza un control de tip group box In plus, putem adauga urmatoarele valori (OR pe biti) la stilul ferestrei controlului privitoare la alinierea textului ce insoteste controlul:

Style Description BS_LEFT Aliniere text la stanga BS_CENTER Centrare text BS_RIGHT Aliniere text la dreapta

Exista si alte tipuri de stiluri de butoane, dar care sunt folosite mai putin. De ex., BS_NOTIFY, programeaza un buton sa trimita notificarile BN_DOUBLECLICKED, BN_KILLFOCUS, si BN_SETFOCUS. BS_OWNERDRAW creaza un buton owner-draw (desenat de proprietar – programatorul va scrie cod pentru acest lucru) — infatisarea (aparenta) butonului este gestionata de parintele butonului.

Butoane Push Un push button este un control de tip button creat cu stilul BS_PUSHBUTTON. Cand este apasat,

controlul trimite parintelui notificarea BN_CLICKED printr-un mesaj WM_COMMAND. In absenta stilului BS_NOTIFY, un asemenea control nu trimite nici un alt tip de notificare. Macroul ON_BN_CLICKED din MFC leaga notificarile BN_CLICKED de functia membru din clasa fereastra parinte: ON_BN_CLICKED(IDC_BUTTON, OnButtonClicked) Functiile pentru BN_CLICKED nu au parametri si nu intorc valori.

Check Boxes Check boxes sunt butoane create cu stilul BS_CHECKBOX, BS_AUTOCHECKBOX, BS_3STATE, sau

BS_AUTO3STATE. Stilurile BS_CHECKBOX si BS_AUTOCHECKBOX pot presupune doua stari: checked si unchecked. Un check box trece in starea checked sau unchecked cu CButton::SetCheck: m_wndCheckBox.SetCheck (BST_CHECKED); // Check m_wndCheckBox.SetCheck (BST_UNCHECKED); // Uncheck

Pentru a determina daca un check box este in starea checked, folosim CButton::GetCheck. O valoare de retur egala cu BST_CHECKED inseamna ca chcek box-ul este in starea checked, iar BST_UNCHECKED este pt unchecked. Check boxes trimit notificarile BN_CLICKED parintilor lor cand facem clic in zona lor. Stilul BS_AUTOCHECKBOX face ca acest control sa lucreze ca un switch on/off automatizat in raspuns la evenimentul click mouse. Stilul BS_CHECKBOX nu face acelasi lucru. Un exemplu de cod pentru un check box cu stilul BS_CHECKBOX si ce trebuie sa facem la BN_CLICKED: void CMainWindow::OnCheckBoxClicked () { m_wndCheckBox.SetCheck (m_wndCheckBox.GetCheck () == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED); }

Stilurile BS_3STATE si BS_AUTO3STATE creaza un check box care presupune o a treia stare numita nedeterminata (indeterminate), si controlul intra in aceasta stare cind facem clic pe un asemenea buton iar starea lui curenta este checked sau cind apelam SetCheck cu parametrul BST_INDETERMINATE: m_wndCheckBox.SetCheck (BST_INDETERMINATE); Un check box in starea “indeterminate” contine a grayed check mark.

Page 52: Programare Windows

52

Butoane Radio

Un buton radio este un control de tip buton cu stilul BS_RADIOBUTTON sau BS_AUTORADIOBUTTON. In mod normal butoanele radio lucreaza in grup, si reprezinta o lista de optiuni mutual exclusive. Cand selectam un buton radio cu stilul BS_AUTORADIOBUTTON va ramine activ numai butonul selectat, celelalte butoane din grup devenind inactive in mod automat. Daca folosim stilul BS_RADIOBUTTON, va trebui sa scriem noi cod pentru a dezactiva celelalte butoane, folosind functia CButton::SetCheck. Butoanele radio trimit notificarile BN_CLICKED parintilor lor, la fel ca mai sus. Urmatorul cod trateaza BN_CLICKED: void CMainWindow::OnRadioButton1Clicked () { m_wndRadioButton1.SetCheck (BST_CHECKED); m_wndRadioButton2.SetCheck (BST_UNCHECKED); m_wndRadioButton3.SetCheck (BST_UNCHECKED); m_wndRadioButton4.SetCheck (BST_UNCHECKED); } Pentru butoanele radio ce au stilul BS_AUTORADIOBUTTON nu este necesara scrierea unei functii ce trateaza mesajul BN_CLICKED. Pentru butoane radio cu stilul BS_AUTORADIOBUTTON pentru a deselecta corect alte butoane din grup, trebuie sa grupam butoanele in momentul crearii, astfel incat Windows sa stie care butoane apartin grupului. Pentru a crea un grup de butoane radio cu stilul BS_AUTORADIOBUTTON urmam urmatoarea procedura (tehnica):

1. In codul aplicatiei, cream butoanele unul dupa altul fara a intercala intre acestea alte controale de alt tip. 2. Pentru a marca inceputul grupului, atribuim stilul WS_GROUP primului buton radio pe care il cream. 3. Daca cream controale aditionale dupa ultimul buton radio din grup, atribuim stilul WS_GROUP primului

control aditional pe care il cream. Cream doua grupuri de cite 4 respectiv 3 butoane radio cu stilul BS_AUTORADIOBUTTON cu un control check box intre ele: m_wndRadioButton1.Create (_T ("COM1"), WS_CHILD ¦ WS_VISIBLE ¦ WS_GROUP ¦ BS_AUTORADIOBUTTON, rect1, this, IDC_COM1); m_wndRadioButton2.Create (_T ("COM2"), WS_CHILD ¦ WS_VISIBLE ¦ BS_AUTORADIOBUTTON, rect2, this, IDC_COM2); m_wndRadioButton3.Create (_T ("COM3"), WS_CHILD ¦ WS_VISIBLE ¦ BS_AUTORADIOBUTTON, rect3, this, IDC_COM3); m_wndRadioButton4.Create (_T ("COM4"), WS_CHILD ¦ WS_VISIBLE ¦ BS_AUTORADIOBUTTON, rect4, this, IDC_COM4); m_wndRadioButton1.SetCheck (BST_CHECKED); m_wndCheckBox.Create (_T ("Save settings on exit"), WS_CHILD ¦ WS_VISIBLE ¦ WS_GROUP ¦ BS_AUTOCHECKBOX, rectCheckBox, this, IDC_SAVESETTINGS); m_wndRadioButton5.Create (_T ("9600"), WS_CHILD ¦ WS_VISIBLE ¦ WS_GROUP ¦ BS_AUTORADIOBUTTON, rect5, this, IDC_9600); m_wndRadioButton6.Create (_T ("14400"), WS_CHILD ¦ WS_VISIBLE ¦ BS_AUTORADIOBUTTON, rect6, this, IDC_14400); m_wndRadioButton7.Create (_T ("28800"), WS_CHILD ¦ WS_VISIBLE ¦ BS_AUTORADIOBUTTON, rect7, this, IDC_28800); m_wndRadioButton5.SetCheck (BST_CHECKED); Butoanele radio nu sunt niciodata checked implicit. Este responsabilitatea programatorului.

Page 53: Programare Windows

53

Group Boxes Un control group box este creat cu stilul BS_GROUPBOX. Acest control nu primeste si nici nu trimite mesaje. Singurul rol al lor este de a grupa anumite controale in interfata destinata utilizatorului. Clasa CStatic CStatic, reprezinta un control static creat din "STATIC" WNDCLASS. Exista trei tipuri de controale statice: text (folosit pentru a eticheta alte controale), dreptunghiuri si imagini. Exemplu m_wndStatic.Create (_T ("Name"), WS_CHILD ¦ WS_VISIBLE ¦ SS_LEFT, rect, this, IDC_STATIC); SS_LEFT = aliniaza text in stanga. Daca textul nu incape se continua pe linia urmatoare. Pentru a preveni trecerea textului pe linia urmatoare putem folosi stilul SS_LEFTNOWORDWRAP in locul stilului SS_LEFT. Alte stiluri: SS_CENTER (centrare text) sau SS_RIGHT (text aliniat la dreapta). Stilul SS_SIMPLE este asemanator cu SS_LEFT dar creaza un control al carui text poate fi modificat cu CWnd::SetWindowText. Pentru a centra vertical textul facem OR pe flagul SS_CENTERIMAGE. Urmatorul cod m_wndStatic.Create (_T (""), WS_CHILD ¦ WS_VISIBLE ¦ SS_ETCHEDFRAME, rect, this, IDC_STATIC); creaza un control static asemanator cu un group box. Un control static de tip dreptunghi nu afiseaza text. Controale statice pentru imagini formate din bitmap-uri, icoane, cursoare sau metafisiere GDI. Stilurile folosite in acest caz sunt:

Style Description SS_BITMAP Afiseaza un bitmap SS_ENHMETAFILE Afiseaza un metafisier SS_ICON Afiseaza o icoana sau un cursor

Dupa ce se creaza un control static imagine, asociem bitmap, icoana sau cursor cu una din functiile SetBitmap, SetEnhMetaFile, SetIcon sau SetCursor. Urmatorul cod m_wndStatic.Create (_T (""), WS_CHILD ¦ WS_VISIBLE ¦ SS_ICON, rect, this, IDC_STATIC); m_wndStatic.SetIcon (hIcon); creaza un control static care afiseaza o icoana (atasam icoana cu ajutorul functiei SetIcon). Dreptughiul este marit automat pentru a cuprinde imaginea. Exista o serie de falg-uri care pot fi folosite pentru a controla modul de afisare al imaginii in control (SS_CENTERIMAGE = are urmatoarea cerinta majora: dreptunghiul de afisare trebuie sa fie destul de mare pentru a cuprinde imaginea). Implicit, un control static nu trimite mesaje de notificare. Daca se creaza un control static cu stilul SS_NOTIFY, atunci acesta trimite urmatoarele notificari:

Notificare Trimis cand Message-Map Macro STN_CLICKED S-a facut clic pe control. ON_STN_CLICKED STN_DBLCLK S-a facut dublu clic pe control ON_STN_DBLCLK STN_DISABLE Controlul este in starea disabled ON_STN_DISABLE STN_ENABLE Controlul este enabled ON_STN_ENABLE

Page 54: Programare Windows

54

Notificarile STN_CLICKED si STN_DBLCLK permit crearea de controale statice ce raspund la clic-uri de mouse, ca in exemplul urmator: // In harta de mesaje din CMainWindow ON_STN_CLICKED (IDC_STATIC, OnClicked) // In CMainWindow::OnCreate m_wndStatic.Create (_T ("Click me"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE | SS_NOTIFY | SS_SUNKEN, rect, this, IDC_STATIC); void CMainWindow::OnClicked() m_wndStatic.PostMessage(WM_CLOSE, 0, 0);

Clasa CEdit (CE) Clasa CEdit din MFC incapsuleaza functionalitatea unui control de editare folosit pentru a edita text, pe o singura linie sau pe mai multe linii. Zona client din Notepad este un control de editare multilinie.

Crearea unui control Edit Daca m_wndEdit este un obiect CEdit codul de mai jos m_wndEdit.Create (WS_CHILD ¦ WS_VISIBLE ¦ WS_BORDER ¦ ES_AUTOHSCROLL, rect, this, IDC_EDIT); creaza un control single line care face scroll orizontal automat, daca textul nu incape in zona de afisare. Incluzind stilul ES_MULTILINE vom crea un CE multilinie: m_wndEdit.Create (WS_CHILD ¦ WS_VISIBLE ¦ WS_BORDER ¦ WS_HSCROLL ¦ WS_VSCROLL ¦ ES_MULTILINE, rect, this, IDC_EDIT); WS_HSCROLL si WS_VSCROLL adauga barele de scroll vertical si orizontal. Putem folosi CEdit::SetRect sau CEdit::SetRectNP pentru a defini zona editabila a controlului, independent de marginile controlului. O utilizare pentru aceste functii este de a defini marimea paginii care ramine constanta chiar daca controlul este redimensionat. Putem folosi de asemenea CEdit::SetMargins pentru a specifica latimea (in pixeli) marginii din stanga si dreapta. Implicit latimile marginilor sunt 0. Cand este prima data creat, un CE va accepta numai 30,000 caractere. Putem modifica acest lucru cu ajutorul metodelor CEdit::LimitText sau CEdit::SetLimitText. Urmatoarea instructiune seteaza numarul maxim de caractere la 32: m_wndEdit.SetLimitText (32); Cand folosim un CE multilinie, SetLimitText limiteaza cantitatea totala de text din control, deci nu lungimea fiecarei linii. In acest caz putem controla lungimea liniei numai manual. O metoda este de a folosi SetFont pentru a comuta fontul CE la un font fixed-pitch si CEdit::SetRect pentru a specifica dreptunghiul de formatare a carui latime este un pic mai mare decit latimea caracterelor din font inmultita cu numarul de caractere dorit a se afisa pe o linie. Stiluri pentru controlul de editare

Style Description ES_LEFT Aliniere la stanga a textului din control ES_CENTER Centrarea textului in control ES_RIGHT Aliniere la dreapta a textului din control ES_AUTOHSCROLL Permite CE de a defila textul orizontal fara a atasa bara de navigare

orizontala. Bara de navigare orizontala se adauga folosind stilul WS_HSCROOL.

Page 55: Programare Windows

55

ES_AUTOVSCROLL Defilarea textului se face pe verticala fara a avea atasata bara de navigare. Bara de navigare se ataseaza folosind stilul WS_VSCROLL.

ES_MULTILINE Creaza un CE multilinie ES_LOWERCASE Afiseaza toate caracterele in “lowercase” ES_UPPERCASE Ca mai sus, dar uppercase ES_READONLY Creaza un CE read only

Pentru alte stiluri recomandam a consulta MSDN.

Inserarea si Regasirea Textului Textul se insereaza cu SetWindowText si se regaseste cu GetWindowText. CEdit mosteneste ambele functii din clasa de baza CWnd. Codul de mai jos m_wndEdit.SetWindowText (_T ("Hello, MFC")); insereaza textul "Hello, MFC" in controlul m_wndEdit, iar m_wndEdit.GetWindowText (string); regaseste textul intr-un obiect CString numit string. GetWindowText si SetWindowText lucreaza cu ambele tipuri de controale, single line si multiline. Textul inserat cu SetWindowText inlocuieste textul existent, iar GetWindowText returneaza tot textul din control, chiar daca acesta este pe mai multe linii. Pentru a sterge textul din control, apelam SetWindowText cu un sir nul: m_wndEdit.SetWindowText (_T ("")); Putem insera text fara a stege cel existent cu CEdit::ReplaceSel. Daca unul sau mai multe caractere sunt selectate cand apelam ReplaceSel, textul care se insereaza inlocuieste textul selectat, in caz contrar textul este inserat la pozitia curenta a cursorului (caret-ului). Un control multiline insereaza line break automat. Daca dorim sa determinam line break-urile dintr-un text folosim CEdit::FmtLines pentru a face enable soft line breaks inainte de apelul lui GetWindowText: m_wndEdit.FmtLines (TRUE); Cu soft line breaks enabled, fiecare linie este delimitata cu doua CR (0x13) urmat de un LF (0x10). Pentru a invalida soft line break folosim FmtLines( FALSE): m_wndEdit.FmtLines (FALSE); CR introdus in text la apasarea tastei <Enter> sunt semnificate de o pereche CR/LF. FmtLines nu afecteaza modul de afisare al textului intr-un CE multilinie, ci afecteaza numai modul cum este memorat intern textul si formateaza textul regasit cu GetWindowText. Pentru a citi exact o linie de text dintr-un control multilinie folosim CEdit::GetLine. GetLine copie continutul unei linii intr-un buffer pe care trebuie sa-l alocam si apoi furnizam functiei adresa acestuia. Linia este identificata de un index 0-based. Instructiunea: m_wndEdit.GetLine (0, pBuffer, nBufferSize);

Page 56: Programare Windows

56

copie prima linie din control in zona data de pBuffer, iar parametrul al 3 lea indica dimensiunea bufferului in bytes. GetLine returneaza numarul de octeti copiati in buffer. Putem determina dinaninte marimea necesara a bufferului cu ajutorul functiei CEdit::LineLength, iar numarul de linii din control il determinam cu ajutorul functiei CEdit::GetLineCount. GetLineCount nu returneaza niciodata 0, chiar daca nu exista text, valoarea returnata este 1.

Clear, Cut, Copy, Paste, and Undo CEdit furnizeaza functii pentru operatiile enumerate mai sus. m_wndEdit.Clear (); sterge textul selectat fara a afecta continutul clipboard-ului. m_wndEdit.Cut (); sterge textul selectat si il copie in clipboard. m_wndEdit.Copy (); copie textul selectat in clipboard fara a-l sterge. Putem interoga CE pentru selectia curenta cu un apel al functiei CEdit::GetSel, care returneza o valoare DWORD ce contine doi intregi pe 16 biti ce specifica indexul de inceput si indexul de sfarsit al selectiei. Daca indecsii sunt egali nu exista text selectat. Exista o forma a functiei GetSel care copie indecsii in doi intregi ale caror adrese sunt pasate ca parametrii prin referinta. Putem adauga urmatoare functie IsTextSelected, la clasa controlului de editare derivat din CEdit pentru a determina daca exista sau nu text selectat in control: BOOL CMyEdit::IsTextSelected () { int nStart, nEnd; GetSel (nStart, nEnd); return (nStart != nEnd); } CEdit::Cut and CEdit::Copy nu fac nimic daca nu este text selectat. Textul poate fi selectat prin program cu CEdit::SetSel. Instructiunea: m_wndEdit.SetSel (100, 150); selecteaza 50 de caractere incepind cu al 101-lea caracter si o face vizibila in view daca aceasta nu este vizibila (se face scroll automat). Pentru a preveni defilarea (scrolling), vom folosi si al 3-lea parametru al functiei cu valoarea TRUE. Cind facem selectii prin program intr-un control multilinie, este necesar adesea sa convertim un numar de linie si posibil un offset din interiorul acestei linii intr-un index pe care-l vom folosi in SetSel. Functia CEdit::LineIndex accepta un numar de linie 0-based si returneaza indexul primului caracter din acea linie. In exemplul ce urmeaza se determina index-ul primului caracter din linia 8 (LineIndex), apoi determinam lungimea liniei si selectam tot textul care se gaseste in acea linie (SetSel): int nStart = m_wndEdit.LineIndex (7); int nLength = m_wndEdit.LineLength (nStart); m_wndEdit.SetSel (nStart, nStart + nLength); CEdit furnizeaza functia LineFromChar pentru a calcula numarul liniei plecand de la index-ul unui caracter.

Page 57: Programare Windows

57

CEdit::Paste pastes text intr-un CE. m_wndEdit.Paste (); Daca clipboard-ul nu contine text, CEdit::Paste nu are efect. Daca exista text selectat cand se apeleaza Paste atunci se insereaza textul din clipboard la pozitia curenta a caret-ului. Daca exista o selectie, atunci textul din clipboard inlocuieste selectia existenta. Putem determina din timp daca exista text in clipboard printr-un apel al functiei. ::IsClipboardFormatAvailable. Codul de mai jos BOOL bCanPaste = ::IsClipboardFormatAvailable (CF_TEXT); seteaza bCanPaste la o valoare diferita de 0 daca exista text in clipboard sau 0 in caz contrar. O alta trasatura a unui CE este posibilitatea rollback-ului (undo) , reface ultima stergere: m_wndEdit.Undo (); Se poate determina din timp daca am putea apela Undo prin apelul functiei CEdit::CanUndo. O alta functie., CEdit::EmptyUndoBuffer, reseteaza manual flag-ul pentru undo, a.i., urmatoarele apeluri la Undo nu vor face nimic.

Notificarile Controlului de Editare In aplicatiile cu MFC, notificarile sunt mapate cu macro-uri de forma ON_EN in harta de mesaje a clasei. In exemplul urmator se trateaza notificarea (mesaj) EN_CHANGE a unui CE. Un control de tip push buton (m_wndPushButton) este facut enable/disable dupa cum exista/nu exista text in CE ce are ID egal cu IDC_EDIT si dat de obiectul (m_wndEdit) : // In harta de mesaje din CMainWindow ON_EN_CHANGE(IDC_EDIT, OnUpdatePushButton) void CMainWindow::OnUpdatePushButton () { m_wndPushButton.EnableWindow (m_wndEdit.LineLength ()); } Notificari ale controlului de editare

Notification Sent When Message-Map Macro EN_UPDATE Textul din control este pe cale sa se schimbe ON_EN_UPDATE EN_CHANGE Textul din control a fost schimbat ON_EN_CHANGE EN_KILLFOCUS Controlul de editare a pierdut focusul de intrare ON_EN_KILLFOCUS EN_SETFOCUS Controlul de editare a primit focusul de intrare ON_EN_SETFOCUS EN_HSCROLL The edit control is scrolled horizontally using a

scroll bar. ON_EN_HSCROLL

Pentru alte notificari ale CE consultati MSDN.

Clasa CListBox Clasa CListBox din MFC incapsuleaza controalele list box, care afiseaza o lista de stringuri numite articole. Optional, un list box poate sorta articolele pe care le contine. Cand pe un item (articol) facem clic sau dublu clic , list box-urile (care au stilul LBS_NOTIFY) notifica parintilor lor printr-un mesaj WM_COMMAND. MFC simplifica

Page 58: Programare Windows

58

procesarea acestor mesaje furnizind macro-ul ON_LBN in harta de mesaje, care ruteaza notificarile list box-ului la functii din clasa fereastra parinte.. Un list box standard afiseaza stringuri intr-o coloana verticala si permite ca un singur articol sa fie selectat la un moment dat. Articolul curent selectat este afisat in video invers cu culoarea sistem COLOR_HIGHLIGHT. Windows suporta un numar de variatii de la standardul initial, variatii ce permit selectii multiple, afisarea pe mai multe coloane, list box-uri desenate de proprietar, afisare de imagini in locul textului.

Crearea unui List Box Urmatoarea instructiune creaza un list box din obiectul CListBox numit m_wndListBox: m_wndListBox.Create(WS_CHILD | WS_VISIBLE | LBS_STANDARD, rect, this, IDC_LISTBOX); LBS_STANDARD combina stilurile WS_BORDER, WS_VSCROLL, LBS_NOTIFY, si LBS_SORT pentru a crea un list box care are margini, o bara de scroll verticala, care notifica parintilor sai cind selectia s-a schimbat sau s-a facut dublu clic pe un articol, si ca articolele vor fi sortate in ordine alfabetica. Implicit bara de scroll este vizibila numai cind articolele nu pot fi afisate in intregime in fereastra controlului. Pentru a face ca bara de scroll sa fie afisata tot timpul va trebui sa includem stilul LBS_DISABLENOSCROLL. Putem crea list box-uri care cuprind toata zona client. List box-urile au incapsulata interfata cu tastatura (tastele sageti, page up, down, apasarea unui caracter muta selectia pe articolul care incepe cu acel caracter). Apasarea barei de spatiu face sa avem selectie multipla sau nu (on/off). Putem programa interfata cu tastatura prin includerea stilului LBS_WANTKEYBOARDINPUT si procesarea mesajelor WM_VKEYTOITEM si WM_CHARTOITEM. O aplicatie MFC poate mapa aceste mesaje cu functiile OnVKeyToItem si OnCharToItem folosind macro-urile ON_WM_VKEYTOITEM si ON_WM_CHARTOITEM. O clasa list box derivata poate manipula aceste mesaje singura prin suprascrierea functiilor virtuale CListBox::VKeyToItem si CListBox::CharToItem. Stiluri pentru list box

Style Description LBS_STANDARD Creaza un listbox “standard” ce are margini si bara de

scroll vertical, notifica ferestrei parinte cand s-a schimbat selectia sau cand s-a facut dublu clic pe un articol si sorteza articolele.

LBS_SORT Sorteaza articolele ce sunt adaugate la list box. LBS_NOTIFY Creaza un list box ce notifica ferestrei parinte cand s-a

schimbat selectia sau s-a facut dublu clic pe un articol. LBS_HASSTRINGS Stil implicit. List box-ul pastreaza string-urile adaugate.

Pentru ca fontul implicit pe care Windows il foloseste pentru list box-uri este proportional spatiat, virtual este imposibil de a alinia coloanele prin spatii. O modalitate de a crea liste ce contin informatii pe mai multe coloane este sa folosim SetFont pentru a aplica un font fixed-pitch la un list box. O solutie mai buna este de a asigna list box-urilor stilul LBS_USETABSTOPS si de a separa coloanele de informatii cu tab. Un list box cu stilul LBS_USETABSTOPS trateaza caracterele tab ca un procesor de texte. Implicit tab este de marimea a 8 caractere pentru latime. Putem schimba acest lucru cu functia CListBox::SetTabStops. SetTabStops masoara distanta in unitati de dialog = o patrime din latimea unui caracter in fontul sistem si 1/8 din cel mai inalt caracter. Instructiunea: m_wndListBox.SetTabStops (64); pune spatiul dintre tab-uri la 64 unitati de dialog , si int nTabStops[] = { 32, 48, 64, 128 }; m_wndListBox.SetTabStops (4, nTabStops);

Page 59: Programare Windows

59

plaseaza stop tab-uri la 32, 48, 64, si 128 unitati de dialog fata de marginea din stanga. Implicit un list box se redeseneaza singur cind este adaugat/sters un articol. Pentru a impiedica acest lucru putem seta stilul LBS_NOREDRAW. O asemenea lista va fi redesenata cand zona ei client va fi invalidata. O alta alternativa este de a inhiba procesul de actualizare cu LBS_NOREDRAW si a-l reactiva dupa ce ultimul articol din list box a fost adaugat. Putem face redesenarea enable/disable prin trimiterea mesajului si nu mai este necesar Invalidate() // disable redesenarea m_wndListBox.SenMessage(WM_SETREDRAW, FALSE, 0); // permite desenarea m_wndListBox.SendMessage(WM_SETREDRAW, TRUE, 0); Stilul LBS_MULTIPLESEL este folosit pentru selectii multiple. Cele mai multe list box-uri sunt create cu stilul LBS_EXTENDEDSEL, care permite selectii extinse. Cu un asemenea stil se fac selectii cu ajutorul mouse-ului si a tastei Ctrl (pe sarite) sau Shift (selectie contigua) (se poate combina Ctrl si Shift). Stilul LBS_MULTICOLUMN creaza un list box cu mai multe coloane (implicit 16 caractere per articol), care in mod normal au si stilul WS_HSCROLL pentru defilare orizontala. List Box-urile multicoloana nu pot avea bara verticala pentru scroll. Latimea coloanei se ajusteaza cu functia CListBox::SetColumnWidth.

Adaugarea si Stergerea articolelor Articolele sunt adaugate cu functiile CListBox::AddString si CListBox::InsertString. Exemplu m_wndListBox.AddString (string); adauga un obiect CString la list box. Daca stilul include LBS_SORT, atunci articolul e pozitionat corespunzator ordinii de sortare alfabetice, altfel este adaugat la sfirsitul listei. InsertString adauga articolul la o pozitie indicata de primul parametru al functiei (zero-based index): m_wndListBox.InsertString (3, string); LBS_SORT nu are efect asupra stringurilor adaugate cu InsertString. Ambele functii AddString si InsertString intorc pozitia stringului din list box. In caz de esec se returneaza LB_ERRSPACE pentru a indica ca un list box este plin sau LB_ERR pentru a indica ca s-a intimplat altceva din diverse motive. Capacitatea unui list box este limitata numai de memoria disponibila. Functia CListBox::GetCount returneaza numarul art. dintr-un list box. Fct. CListBox::DeleteString elimina un articol dintr-un list box, articol identificat prin indexul sau. Intoarce numarul articolelor ramase in list box. Pentru a sterge toate articolele folosim functia CListBox::ResetContent. Daca dorim sa asociem un pointer pe 32 biti sau o valoare DWORD cu un articol din list box putem folosi functia CListBox::SetItemDataPtr sau CListBox::SetItemData. Un pointer sau un DWORD asociat cu un articol poate fi regasit cu ajutorul functiei CListBox::GetItemDataPtr sau CListBox::GetItemData. O folosire a acestei trasaturi este de exemplu de a asocia o structura de date –ce contine nr. de telefon – pentru persoanele dintr-un list box. Din cauza ca GetItemDataPtr intoarce un pointer la void trebuie facuta conversia. O alta tehnica este de a asocia extra date – in particular text – cu articolele dintr-un list box , sa cream un list box cu stilul LBS_USETABSTOPS, sa setam primul tab stop la o pozitie din afara marginii drepte a list box-ului si din a adauga stringuri ce contin caractere tab urmate de extra data (text). Textul de la dreapta tab-ului va fi invizibil, dar CListBox::GetText va returna intregul text, deci si cel extra.

Cautarea si regasirea articolelor CListBox::GetCurSel intoarce indexul (0-based) al articolului care este selectat. Daca valoarea returnata este LB_ERR inseamna ca nu s-a selectat nimic. GetCurSel este adesea apelata ca urmare a unei notificari ce semnifica ca selectia s-a schimbat sau a fost facut dublu clic pe un articol. Un program poate seta selectia curenta cu SetCurSel. Pasind valoarea –1 pt. SetCurSel vom deselecta toate articolele.

Page 60: Programare Windows

60

Pentru a gasi daca un articol particular este selectat putem folosi functia CListBox::GetSel. SetCurSel identifica un articol prin indexul sau, dar articolele pot fi selectate si dupa continut cu functia CListBox::SelectString care realizeaza o singura selectie pentru un articol ce incepe cu textul specificat si selecteaza articolul daca se gaseste unul care satisface conditia. Codul m_wndListBox.SelectString (-1, _T ("Times")); incepe cautarea cu primul articol din list box si va selecta primul articol care incepe cu Times. Cautarea nu este case senzitive. Primul parametru indica indexul de unde incepe cautarea; -1 inseamna de la inceput. Indiferent de unde incepe cautarea aceasta poate sa parcurga circular intreaga lista asociata list box-ului daca este necesar. Pentru a cauta pentru un articol particular fara a schimba selectia vom folosi CListBox::FindString sau CListBox::FindStringExact. FindString face cautare numai pe primele caractere din articol. Daca se intoarce LB_ERR inseamna ca nu s-a gasit acel articol, altfel se intoarce indexul articolului. FindStringExact adauga in plus cautarea exacta. Cu indexul obtinut anterior putem aobtine textul articolului cu CListBox::GetText. In exemplul urmator, se copie textul articolului in variabila string. CString string; int nIndex = m_wndListBox.GetCurSel (); if (nIndex != LB_ERR) m_wndListBox.GetText (nIndex, string); Al doilea parametru este un pointer la char. Putem folosi CListBox::GetTextLen pentru a determina marimea zonei necesare pentru a primi textul articolului inainte de a apela GetText. Selectiile multiple sunt tratate diferit. GetCurSel, SetCurSel, si SelectString nu pot fi folosite in acest caz. Articolele sunt selectate (deselectate) cu functiile SetSel si SelItemRange. In continuare se selecteaza articolele cu indecsii 0, 5, 6, 7, 8, si 9 si deselecteaza articolul cu indexul 3: m_wndListBox.SetSel (0); m_wndListBox.SelItemRange (TRUE, 5, 9); m_wndListBox.SetSel (3, FALSE); Alte functii: GetSelCount pt. determinarea numarului de articole selectate, GetSelItems pentru regasirea indecsilor articolelor selectate. Intr-un list box cu selectie multipla, dreptunghiul ce reprezinta articolul cu focus-ul asupra lui, poate fi mutat fara a schimba selectia curenta. Dreptunghiul care are focusul poate fi mutat sau obtinut cu ajutorul functiei SetCaretIndex si GetCaretIndex. Multe din functiile ce lucreaza cu o singura selectie sunt disponibile si pentru list box-urile cu selectie multipla: GetText, GetTextLength, FindString, si FindStringExact.

Notificarile List Box Notificarile sunt trimise via mesajul WM_COMMAND. In aplicatiile MFC, notificarile list box-urilor sunt mapate la functiile din clasa cu macro-ul ON_LBN….Vezi tabelul de mai jos. Notificările LBN_DBLCLK, LBN_SELCHANGE, si LBN_SELCANCEL sunt trimise numai daca list box-ul a fost creat cu stilul LBS_NOTIFY sau LBS_STANDARD. List Box Notifications

Notification Sent When Message-Map Macro LBS_NOTIFY Required?

LBN_SETFOCUS List box-ul obtine focusul de intrare

ON_LBN_SETFOCUS No

LBN_KILLFOCUS List box-ul pierde focusul de intrare

ON_LBN_KILLFOCUS No

LBN_DBLCLK S-a facut dublu clic pe un articol

ON_LBN_DBLCLK Yes

LBN_SELCHANGE S-a schimbat selectia ON_LBN_SELCHANGE Yes

Page 61: Programare Windows

61

LBN_SELCANCEL S-a anulat selectia ON_LBN_SELCANCEL Yes Notificarile cele mai folosite sunt: LBN_DBLCLK si LBN_SELCHANGE. Pentru a determina indexul articolului pe care s-a facut dublu clic intr-un list box cu o singura selectie folosim CListBox::GetCurSel. Urmariti exemplul urmator: // In harta de mesje din CMainWindow ON_LBN_DBLCLK(IDC_LISTBOX, OnItemDoubleClicked) void CMainWindow::OnItemDoubleClicked () { CString string; int nIndex = m_wndListBox.GetCurSel (); m_wndListBox.GetText (nIndex, string); MessageBox (string); } Pentru un LB cu selectie multipla folosim GetCaretIndex in locul functiei GetCurSel pentru a determina articolul pe care s-a facut dublu clic. Notificarea LBN_SELCHANGE este trimisa cind utilizatorul schimba selectia, dar nu si in cazul cind selectia este schimbata automat prin program. Un LB cu selectie simpla trimite notificarea LBN_SELCHANGE cind selectia se muta din cauza unui clic sau a apasarii unei taste. Intr-un LB cu selectie multipla notificarea LBN_SELCHANGE este trimisa cind se face clic pe un articol, cind starea selectiei articolului este modificata (on/off) si cind dreptunghiul care are focus-ul este mutat.

Clasa CComboBox (control combo box) Un CB este format dintr-un control de editare si un list box. Tipuri de combo box-uri: simple, drop-down, si drop-down list. CB simple (stil CBS_SIMPLE) sunt cele mai putin folosite. Trasatura principala a acestora este ca sunt permanent afisate. Cand un utilizator selecteaza un articol din lista, acest articol este automat copiat in CE. Utilizatorul poate tipari text direct in CE. Daca textul se potriveste cu un articol din lista, articolul este automat setat pe video invers si se executa scroll-ul . Un CB drop-down (stil CBS_DROPDOWN) difera de un CB simplu prin aceea ca lista este afisata numai la cererea utilizatorului si nu permite introducerea de text in CE asociat. Un CB drop-down list (stil CBS_DROPDOWNLIST) are in plus fata de CB drop-down bara de navigare verticala. Stilurile se dau in functia Create or CreateEx . Alte stiluri exista pentru cosmetizarea CB. Cind cream un CB trebuie sa punem stilul WS_VSCROLL daca dorim scroll vertical . Daca m_wndComboBox este un obiect CComboBox, instructiunea: m_wndComboBox.Create (WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | CBS_DROPDOWNLIST | CBS_SORT, rect, this, IDC_COMBOBOX); ceaza un CB drop-down list care contine bara pentru scroll vertical. Dimensiunea controlului (dreptunghiul) trebuie sa fie destul de mare pentru a afisa tot textul. Stiluri combo box

Stil Descriere CBS_DROPDOWN Creaza un combo box drop-down. CBS_DROPDOWNLIST Creaza un com box drop down list. CBS_HASSTRINGS Vezi list box. CBS_LOWERCASE Textul din combo box va fi lower case (vezi controlul de

Page 62: Programare Windows

62

editare). CBS_SIMPLE Creaza un combo box simplu. CBS_SORT Vezi LBS_SORT. CBS_UPPERCASE Vezi Controlul de editare.

Adaugare articolelor se face cu CComboBox::AddString si CComboBox::InsertString. Numarul maxim de caractere pentru CE al CB este setat cu CComboBox::LimitText. Functiile GetWindowText si SetWindowText lucreaza pentru CE al CB. Functii specifice: GetLBText, care regaseste textul unui articol identificat printr-un index 0-based. GetLBTextLen, returneaza lungimea unui articol, in caractere; ShowDropDown, afiseaza sau ascunde un CB drop-down list; GetDroppedState, returneaza o valoare ce indica daca CB drop-down list este afisat.

Notificari Combo Box Notificarea Message-Macro Map Sim

ple Drop-Down

Drop-Down List

CBN_DROPDOWN Trimis cand este afisat (CB drop-down list).

ON_CBN_DROPDOWN √ √

CBN_CLOSEUP Trimis cand CB drop-down list este nchis.

ON_CBN_CLOSEUP √ √

CBN_DBLCLK

ON_CBN_DBLCLK √

CBN_SELCHANGE Trimis cand s-a schimbat selectia.

ON_CBN_SELCHANGE √ √ √

CBN_SELENDOK Trimis cand s-a facut o selectie.

ON_CBN_SELENDOK √ √ √

Pentru alte notificari consultati MSDN. Nu toate notificarile se aplica la toate tipurile de CB. Notificarile CBN_DROPDOWN si CBN_CLOSEUP nu sunt trimise la un CB simplu (CBS_SIMPLE) pentru ca un asemenea CB este deschis tot timpul. CB cu stilurile CBS_DROPDOWN si CBS_DROPDOWNLIST-nu primesc notificarea CBN_DBLCLK pentru ca pe articolele din lista nu se poate face dublu clic. (LB asociat CB se inchide dupa primul clic). Notificarile CBN_EDITUPDATE si CBN_EDITCHANGE sunt echivalente cu EN_UPDATE si EN_CHANGE trime de CE, si CBN_SELCHANGE este la fel cu LBN_SELCHANGE pentru LB. Cind procesam notificarea CBN_SELCHANGE, CE asociat poate sa nu fie actualizat cu selectia din LB asociat. Va trebui sa folosim GetLBText pentru a regasi noul text selectat in loc de GetWindowText. Indexul articolului selectat il gasim cu CComboBox::GetCurSel. Exemplu de tratare a mesajului WM_DRAWITEM într-un list box şi construirea unui control list box cu atribut de culoare pentru text (item din listbox) îl găsiti pe pagina mentionată anterior.

Page 63: Programare Windows

63

GDI - Graphical Device Interface Biblioteca GDI32.DLL Sumar

Pentru a desena în Windows, avem nevoie să ştim cu ce scriem (peniţa, grosime, culoare, stil), cu ce desenăm fundalul (pensula, culoare, stil), ce foncturi folosim, de unde începem să desenăm,etc. Toate aceste elemente şi încă multe altele sunt descrise într-o structură ce formează aşa numitul “device context” sau în romănă, context de dispozitiv , prescurtat DC. Înainte de a desena, trebuie să creăm un asemenea DC, să-l utilizăm în cadrul desenării şi apoi să-l distrugem.

Să vedem care sunt problemele legate de desenare şi bineînţeles elementele constitutive ale unui context de dispozitiv. Vom încerca să ne familiarizăm cu modul de “scriere” în Windows. Trebuie reţinut de la început că orice afişare (indiferent de dispozitiv) este o desenare.

În MFC contextul de dispozitiv este descris în clasa CDC şi clasele derivate din aceasta. Pentru o mai bună documentare vedeţi MSDN şi cartea lui J. Prossie “Programming with MFC, Second edition”. GDI = permite manipularea elementelor grafice independente de dispozitiv, este un sistem de afisare static, ce permite numai animatii simple. Dipozitive grafice de iesire:

4. dispozitive rastru = reprezentare imagine prin matrice de puncte (placi video, imprimante matriciale, laser);

5. dispozitive vectoriale = deseneaza imaginile prin linii = plottere; Din punctul de vedere al programatorului, interfaţa GDI este formată din câteva sute de rutine si unele tipuri de date, macroinstructiuni si structuri de date. Tipuri de apeluri de functii

• Functii care obtin (sau creaza) si elibereaza (sau distrug) un context de dispozitiv; (in API: BeginPaint ... EndPaint, GetDc, ReleaseDC; in MFC (clase): CDC, CPaintDC, CClientDC, CMetaFileDC, CWindowDC)

• Functii care obtin informatii despre contextul de dispozitiv - structura TEXTMETRICS, GetTextMetrics;

• Functii care deseneaza ceva (TextOut, DrawText, desenarea liniilor, a zonelor colorate si a imaginilor bitmap...);

• Functii care stabiliesc sau obtin atribute ale contextului de dispozitiv - Un atribut al DC specifica modul de lucru al functiilor de desenare; SetTextColor, etc. Toate atributele DC au valori prestabilite care devin active la obtinerea DC. Pentru fiecare functie de tip Set exista si o functie de tip Get, folosita pentru obţinerea valorilor curente ale atributelor DC.

• Functii care lucrează cu obiecte GDI. Primitive GDI

• Linii si curbe: linii drepte, dreptunghiuri, elipse, arce. Liniile sint desenate folosind penita (HPEN, clasa CPen) curenta selectata in DC

• Suprafete pline: Suprafata poate fi umpluta folosind pensula GDI curenta (HBRUSH, clasa CBrush) • Imagini bitmap: matrici dreptunghiulare de biti, care corespund pixelilor unui dispozitiv de afisare - pt.

sisteme grafice de tip rastru. Imagini bitmap - dependente de dispozitiv si imagini bitmap independente de dispozitiv (DIB = Device Independent Bitmap) care pot fi stocate in fisiere.

• Text: afisarea textului este legata de fonturi (HFONT, clasa CFont). Alte aspecte ale GDI:

• Moduri de mapare si transformări: sistem de coordonate în pixeli, inci, mm. • Metafisiere (metafiles): o colectie de comenzi GDI stocate intr-o forma binara; sint folosite pentru

transferarea reprezentarilor unor elemente grafice vectoriale prin intermediul memoriei temporare (clipboard).

Page 64: Programare Windows

64

• Cai (paths): colectie de linii drepte si curbe stocate intern de GDI; pot fi folosite pt. desenare, umplere sau decupare.

• Decupare (clipping): desenarea poate fi limitata la o anumita sectiune a zonei client, numita zona de decupare - definita in general de o cale sau de o regiune.

• Tiparire (printing): Contextul de dispozitiv (DC): Modalitati de obtinere a variabilei handle a DC:

• Varainata 1 (putem desena numai in regiunea invalida a ferestrei): la tratarea mesajului WM_PAINT: PAINTSTRUCT ps; hdc = BeginPaint (hwnd, &ps); ... EndPaint(hwnd, &ps);

structura PAINTSTRUCT contine o structura de tip RECT rcPaint; care defineste dreptunghiul ce cuprinde regiunea invalida a zonei client a ferestrei; se valideaza regiunea invalida.

• Varianta 2 (putem desena in toata zona client a ferestrei; nu se valideaza regiunea invalida ale zonei client):

hdc = GetDC(hwnd); ... ReleaseDC(hwnd, hdc);

• Varianta 3: (DC cuprinde in plus bara de titlu a ferestrei, barele de derulare si chenarul) hdc = GetWindowDC(hwnd); ... ReleaseDC(hwnd, hdc);

Pentru folosirea acestei functii trebuie interceptat mesajul WM_NCPAINT ( non client paint). • Varianta 4: CreateDC

hdc = CreateDC(pszDriver, pszDevice, pszOutput, pData); ... DeleteDC(hdc);

Pentru a obtine o variabila handle a DC pentru spatiul de afisare: hdc = CreateDC(“DISPLAY”, NULL, NULL, NULL); • Varianta 5: Obtinere informatii despre DC - CreateIC care are aceeasi parametri ca si CreateDC

hdc = CreateIC(pszDriver, pszDevice, pszOutput, pData); ... DeleteDC(hdc);

• Varianta 6: context de dispozitiv in memorie - necesar la lucrul cu imagini bitmap: hdcmem = CreateCompatibleDC (hdc) ... DeleteDC(hdcMem);

• Crearea unui metafisier:

Page 65: Programare Windows

65

hdcMeta = CreateMetaFile(pszFileName); ... hmf = CloseMetaFile(hdcMeta);

Atributele Contextului de Dispozitiv Cele mai uzuale atribute ale DC sunt date in urmatorul tabel:

Attribute Default Set with Get with Text color Black CDC::SetTextColor CDC::GetTextColor Background color White CDC::SetBkColor CDC::GetBkColor Background mode OPAQUE CDC::SetBkMode CDC::GetBkMode Mapping mode MM_TEXT CDC::SetMapMode CDC::GetMapMode Drawing mode R2_COPYPEN CDC::SetROP2

CDC::GetROP2 Current position (0,0) CDC::MoveTo CDC::GetCurrentPosition Current pen BLACK_PEN CDC::SelectObject CDC::SelectObject Current brush WHITE_BRUSH CDC::SelectObject CDC::SelectObject Current font SYSTEM_FONT CDC::SelectObject CDC::SelectObject

GDI Moduri de desenare – functia SetROP2 Mod de desenare Operatii executate

R2_NOP dest = dest R2_NOT dest = NOT dest R2_BLACK dest = BLACK R2_WHITE dest = WHITE R2_COPYPEN dest = src R2_NOTCOPYPEN dest = NOT src R2_MERGEPENNOT dest = (NOT dest) OR src R2_MASKPENNOT dest = (NOT dest) AND src R2_MERGENOTPEN dest = (NOT src) OR dest R2_MASKNOTPEN dest = (NOT src) AND dest R2_MERGEPEN dest = dest OR src R2_NOTMERGEPEN dest = NOT (dest OR src) R2_MASKPEN dest = dest AND src R2_NOTMASKPEN dest = NOT (dest AND src) R2_XORPEN dest = src XOR dest R2_NOTXORPEN dest = NOT (src XOR dest)

Moduri de mapare – functia SetMapMode

Mod de mapare Distanta ce corespunde la o unitate logica

Axa x Axa y

MM_TEXT Pixel spre dreapta in jos MM_LOMETRIC 0,1 mm spre dreapta in sus MM_HIMETRIC 0,01 mm spre dreapta in sus MM_LOENGLISH 0.01 inci spre dreapta in sus MM_HIENGLISH 0.001 inci spre dreapta in sus MM_TWIPS 1/1440 inci spre dreapta in sus MM_ISOTROPIC arbitrar (x = y) Selectabil selectabil MM_ANISOTROPIC Arbitrar (x!=y) Selectabil Selectabil Vizorul si fereastra SetWindowExt – setează “extensia ferestrei” – marimea dorita a ferestrei in unitati logice. SetViewportExt – seteaza “extensia vizorului” – marimea in pixeli a ferestrei in care desenam..

Page 66: Programare Windows

66

Marimea fereastrei este masurata in unitati logice. Marimea viewport-ului este masurata in unitati de dispozitiv, sau pixeli. In modul MM_ISOTROPIC ordinea de apel este SetWindowExt si apoi SetViewportExt. Exemple de cod CRect rect; GetClientRect (&rect); dc.SetMapMode (MM_ANISOTROPIC); dc.SetWindowExt (500, 500); dc.SetViewportExt (rect.Width (), rect.Height ()); dc.Ellipse (0, 0, 500, 500); Originea este in coltul din stanga sus. CRect rect; GetClientRect (&rect); dc.SetMapMode (MM_ANISOTROPIC); dc.SetWindowExt (500, -500); dc.SetViewportExt (rect.Width (), rect.Height ()); dc.Ellipse (0, 0, 500, -500); Modul MM_ISOTROPIC CRect rect; GetClientRect (&rect); dc.SetMapMode (MM_ISOTROPIC); dc.SetWindowExt (500, 500); dc.SetViewportExt (rect.Width (), rect.Height ()); dc.Ellipse (0, 0, 500, 500); Pentru vizor se folosesc coordonatele de dispozitiv (pixeli). Pentru toate modurile de mapare, Windows transforma coordonatele ferestrei (coordonate logice) in coordonate ale vizorului (coordonate de dispozitiv) folosind doua formule: xViewport = (xWindow - xWinOrg) * (xViewExt / xWinExt) + xViewOrg yViewport = (yWindow - yWinOrg) * (yViewExt / yWinExt) + yViewOrg unde (xWindow, yWindow) este punct in coordonate logice care trebuie translatat; (xViewport, yViewport) este punct in coordonate de dispozitiv; (xWinOrg, yWinOrg) = originea ferestrei in coordonate logice; (xViewOrg, yViewOrg) = originea vizorului in coordonate dispozitiv. Formulele de mai sus implica faptul ca punctul (xWinOrg, yWinOrg) este intotdeauna mapat la punctul (xViewOrg, yViewOrg). (xWinExt, yWinExt) = extensia ferestrei in coordonate logice; (xViewExt, yViewExt) = extensia vizorului in coordonate de dispozitiv; In majoritatea modurilor de mapare aceste extensii sint prestabilite si nu pot fi modificate. Raportul intre extensia vizorului si extensia ferestrei reprezinta un factor de scalare folosit pentru convertirea unitatilor logice in unitati de dispozitiv. Extensiile pot avea valori negative: aceasta inseamna ca nu este obligatoriu ca valorile pe axa x sa creasca spre dreapta si valorile pe axa y sa creasca in jos.

Page 67: Programare Windows

67

Functiile folosite pentru a realiza conversia intre puncte reprezentate in coordonate de dispozitiv in puncte reprezentate in coordonate logice si invers sunt:

DPtoLP(hdc, pPoints, iNumber); LPtoDP(hdc, pPoints, iNumber);

pPoints = matrice de structuri POINT iNumber = numarul de puncte care urmeaza a fi convertite. Daca dorim sa stim unde este punctul din centru in unitati MM_LOENGLISH, trebuie sa folosim DPtoLP : CRect rect; GetClientRect (&rect); CPoint point (rect.Width () / 2, rect.Height () / 2); CClientDC dc (this); dc.SetMapMode (MM_LOENGLISH); dc.DPtoLP (&point); DPtoLP va returna coordonatele punctului central in coordonate logice. Daca dorim sa stim coordonatele in pixeli al punctului de coordonate logice (100,100) in modul de mapare MM_LOENGLISH vom folosi LPtoDP: CPoint point (100, 100); CClientDC dc (this); dc.SetMapMode (MM_LOENGLISH); dc.LPtoDP (&point); Modul MM_TEXT Functiile SetViewportOrgEx si SetWindowOrgEx modifica originea vizorului si a ferestrei. Aceste functii au ca efect deplasarea axelor astfel incit punctul de coordonate (0,0) nu se mai refera la coltul din stanga sus al ecranului. In general se foloseste doar una din cele doua functii. Explicaţii asupra lucrului cu aceste functii:

• daca schimbam originea vizorului la (xViewOrg, yViewOrg) atunci punctul logic de coordonate (0,0) va fi mapat la punctul de coordonate de dispozitiv (xViewOrg, yViewOrg).

• daca schimbăm originea ferestrei la (xWinOrg, yWinOrg) atunci acest punct logic va fi mapat la punctul de coordonate de dispozitiv (0,0) care este intotdeauna coltul din stinga sus al zonei client.

Exemplu – Mutare origine Sa presupunem ca zona client are latimea cxClient si inaltimea cyClient. Daca dorim ca punctul de coordonate logice (0,0) sa se afle in centrul zonei client, atunci: SetViewportOrgEx(hdc, cxClient/2, cyClient/2, NULL);

Valorile logice ale axei x sint cuprinse in intervalul [-cxClient/2, cxClient/2], iar cele ale axei y in intervalul [-cyClient/2, cyClient/2]. Afisarea de text incepind cu coltul din stanga sus, care are coordonatele de dispozitiv (0,0) inseamna folosirea urmatoarelor coordonate logice:

TextOut(hdc, -cxClient/2, -cyClient/2, “...”,...);

Acelasi rezultat poate fi obtinut si cu functia SetWindowOrgEx in locul functiei SetViewportOrgEx:

SetWindowOrgEx(hdc, -cxClient/2, -cyClient/2, NULL);

Page 68: Programare Windows

68

Setarea extent-ului O aplicatie poate modifica in mod direct extentul ferestrei sau al vizorului numai dacă modul de mapare este MM_ISOTROPIC sau MM_ANISOTROPIC. Modficarea extentului ferestrei se face cu ajutorul functiei SetWindowExt, iar extentul vizorului se modifica cu functia SetViewportExt. Valorile se dau totdeauna in unitati absolute, nu in unitati logice si nu sunt afectate de modul curent de mapare. Setarea unui extent la valoarea zero nu este permisa. Din cauza ca perechea de extent-uri stabileste un factor de scalare ce va fi folosit in conversii, marimea extentului ar trebui sa fie cat mai mica posibila pentru a simplifica calculele, de exemplu folosirea de extent-uri de 400 si 300 este echivalent cu exetent-uri de 4 si 3. Pentru a schimba orientarea unei axe (fata de orientarea implicita data de Windows), factorul de scalare trebuie sa fie negativ. Urmatorul cod are ca efect schimbarea orientarii axei y, y pozitiv va fi “in sus”. SetMapMode(hDC, MM_ANISOTROPIC); SetViewportExt(hDC, 1, -1); SetWindowExt(hDC, 1, 1);

Setarea originilor Functiile folosite sunt: SetWindowOrg, OffsetWindowOrg, SetViewportOrg si OffsetViewportOrg. Originile sunt independente de extent. Originile sunt specificate in unitati absolute ce nu sunt afectate de modul curent de mapare.

Exemple Setam un mod de mapare in care la o unitate logica ii corespund trei unitati de dispozitiv:

SetMapMode(hDC, MM_ANISOTROPIC); SetWindowOrg(hDC, 0, 0); SetWindowExt(hDC, 1, 1); SetViewportOrg(hDC, 0, 0); SetViewportExt(hDC, 3, 3);

Urmatorul cod deseneaza un dreptunghi de 1 pe 2 mm. SetMapMode(hDC, MM_HIMETRIC); SetViewportOrg(hDC, 0, 100); // Ce ...? SetWindowOrg(hDC, 0, 0); Rectangle(hDC, 0, 0, 100, 200); Unitatile de dispozitiv sunt mapate la rezolutia dispozitivului fizic: SetMapMode(hDC, MM_ANISOTROPIC); SetWindowOrg(hDC, 0, 0); SetWindowExt(hDC, 600, 600); // logical window is 600 dpi SetViewportOrg(hDC, 0, 0); // Device viewport is dpi of actual output device. SetViewportExt(hDC, GetDeviceCaps(hDC, LOGPIXELSX), GetDeviceCaps(hDC, LOGPIXELSY));

Page 69: Programare Windows

69

Obtinerea informatiilor despre un periferic Functia CDC::GetDeviceCaps Următorul cod obtine rezolutia ecranului, in pixeli: CClientDC dc (this); int cx = dc.GetDeviceCaps (HORZRES); int cy = dc.GetDeviceCaps (VERTRES); Functia GetDeviceCaps va returna totdeauna valori fizice corecte pentru imprimanta sau orice alt periferic hardcopy (de exemplu LOGPIXELSX si LOGPIXELSY). Pentru o imprimanta laser cu 600 dpi, LOGPIXELSX si LOGPIXELSY vor avea valoarea 600. Pentru lista completă a parametrilor vedeţi MSDN. Useful GetDeviceCaps Parameters

Parameter Returns HORZRES Width of the display surface in pixels VERTRES Height of the display surface in pixels HORZSIZE Width of the display surface in millimeters VERTSIZE Height of the display surface in millimeters LOGPIXELSX Number of pixels per logical inch horizontally LOGPIXELSY Number of pixels per logical inch vertically NUMCOLORS For a display device, the number of static colors; for a printer or plotter, the number

of colors supported BITSPIXEL Number of bits per pixel PLANES Number of bit planes RASTERCAPS Bit flags detailing certain characteristics of the device, such as whether it is

palettized and whether it can display bitmapped images TECHNOLOGY Bit flags identifying the device type—screen, printer, plotter, and so on

Desenarea pe ecran (Exemple de cod) Găsiti explicatiile pentru construirea aplicatiei pe pagina in documentul ce contine cursul complet.

Page 70: Programare Windows

70

Meniuri Windows furnizează suport aplicaţiilor care utilizează meniuri: afişarea barei de meniu, derularea unui

meniu popup când acesta este selectat, notifică aplicaţia când o comandă de meniu a fost selectată. Definire termeni:

Bara de meniu care apare in partea cea mai de sus a ferestrei se numeste top-level menu, iar comenzile se numesc top-level menu items. Meniul care apare cind un articol de meniu este selectat se numeste drop down menu, iar articolele din acest meniu se numesc articole meniu (menu items). Articolele de meniu sunt identificate prin valori intregi, numite ID-ul art. de meniu sau ID-ul comenzii. Windows suporta meniurile popup care sunt asemanatoare cu cele drop down cu deosebirea ca pot fi afisate oriunde pe ecran. Meniurile de context (right click) sunt meniuri popup. Meniul sistem cuprinde comenzi pentru redimensionare, mutare, minimizare, maximizare, inchidere fereastra.

Actiunile legate de meniuri le gasim in clasa CMenu din MFC. CMenu contine o data membru publica HMENU m_hMenu ce pastreaza un handle la meniu, si mai multe functii ce constituie in fapt apeluri ale functiilor din API (CMenu::TrackPopupMenu, CMenu::EnableMenu, etc.). CMenu contine doua functii virtuale DrawItem si MeasureItem care pot fi rescrise daca dorim sa cream articole de meniu stilizate ce contin bitmap-uri si alte elemente grafice. Modalitati de crearea unui meniu in MFC.

1. in mod programabil, CreateMenu, InsertMenu, etc. 2. initializind o serie de structuri de date ce descriu continutul unui meniu si apoi apelam

CMenu::LoadMenuIndirect. 3. cream o resursa de meniu si incarcam meniul rezultat in aplicatie in timpul executiei.

Crearea unui meniu

Metoda cea mai usoara este de a adauga un template de meniu la fisierul de resurse al aplicatiei. Un fisier de resurse este un fisier text care are extensia rc, si care contine instructiuni ce definesc meniul. Acest fisiser de resurse este compilat si legat la aplicatie cu un utilitar numit rc.exe (in MS VC++). O resursa este un obiect binar ca de exemplu un meniu, o icoana, stringuri, bitmap-uri. Fiecare resursa este identificata printr-un ID intreg sau string (“MyMenu” sau IDR_MYMENU). ID-urile sunt definite cu #define. O data ce o resursa este compilata si legata la o aplicatie ea poate fi incarcata printr-un simplu apel de functie. Un fisier de resurse contine: ID resusrsa, numele articolului de meniu, ID-urile articolului de meniu, atribute ale meniului. Exemplu IDR_MAINFRAME MENU PRELOAD DISCARDABLE BEGIN POPUP “&File”

BEGIN MENUITEM “&New\tCtrl+N”, ID_FILE_NEW

... MENUITEM SEPARATOR END POPUP “&View” BEGIN ... END END Indicatii: O elipsa in descrierea meniului inseamna ca sunt necesare informatii suplimentare dupa ce articolul este selectat, ! inseamna ca se executa imediat o comanda (Exit!).

Page 71: Programare Windows

71

Afxres.h defineste valorile ID-urile pentru comenzile uzuale. Valori valide pentru ID-uri sunt in intervalul 1-0xEFFF sau mai exact 0x8000 – 0xF000 confrm Nota Teh. 20 din MFC.

Textul ce urmeaza caracterului TAB \t identifica un accelerator. Un accelerator este o tasta sau o combinatie de taste (Ctrl+C, Ctrl+V, etc.) care cand sunt apasate au acelasi efect ca selectarea articolului din meniu.

Cind definim articolul de meniu, putem sa-i indicam starea initiala, de ex GRAYED, ceea ce-l face disable, CHECKED, etc.

Incărcarea şi afişarea unui meniu

In timpul execuţiei o resursă de meniu trebuie încărcată şi ataşată la fereastra. Cand fereastra este afisata, meniul va fi afisat de asemenea. Metoda 1: apel la functia CFrameWnd::Create Create(NULL, _T(“Nume aplicatie”), WS_OVERLAPPEDWINDOW, rectDefault, NULL, MAKEINTRESOURCE(IDR_MAINFRAME)); Parametrul numarul 6 indica resursa de meniu. MAKEINTRESOURCE transforma un intreg intr-o data de tip LPSTR. Metoda 2: apel la functia CFrameWnd::LoadFrame, care creaza o fereastra cadru si ataseaza un meniu la aceasta LoadFrame(IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL); LoadFrame este asemenatoare cu Create, dar poate incarca si icoane si alte resurse ceea ce nu face Create (in spate este CreateWindow , CreateWindowEx). Metoda 3: construim un obiect de tip meniu, clasa CMenu, si-l incarcam cu CMenu::LoadMenu si apoi il atasam la fereastra CWnd::SetMenu: CMenu menu; menu.LoadMenu(IDR_MAINFRAME); SetMenu(&menu); menu.Detach(); CMenu::Detach() va detasa meniul din obiectul CMenu astfel incat meniul nu va fi distrus prematur cind obiectul menu va fi distrus. Regula generala Un meniu incarcat cu LoadMenu ar trebui sa fie distrus cu DestroyMenu inainte ca aplicatia sa se termine. Metoda 3 este folosita in programe care au mai mult de un meniu. Exemplu de aplicatie cu doua meniuri: Create(NULL, _T(“aplicatie”)); m_menuLong.LoadMenu(IDR_LONGMENU); m_menuShort.LoadMenu(IDR_SHORTMENU); SetMenu(m_bShortMenu ? &m_menuShort, &m_menuLong); Comutarea intre cele doua meniuri se face astfel (ca raspuns la o comanda a utilizatorului) de la meniul lung la cel scurt: m_bShortMenu = TRUE; SetMenu(&m_menuShort); DrawMenuBar();

Page 72: Programare Windows

72

de la meniul scurt la meniul lung m_bShortMenu = FALSE; SetMenu(&m_menuLong); DrawMenuBar(); Functia CWnd::DrawMenuBar() redeseneaza bara meniu pentru a reflecta schimbarile facute in meniu. Daca m_menuLong si m_menuShort sunt variabile membru ale clasei fereastra cadru (derivata din CWnd), destructorii pentru meniuri se vor apela cand fereastra cadru va fi distrusa, deci in acest caz nu mai e nevoie de DestroyMenu.

Raspunsul la comenzile din meniu Cand utilizatorul selecteaza un articol de meniu (bara de meniuri) fereastra primeste o serie de mesaje: WM_INITMENU ce notifica ferestrei ca un articol al meniului superior (top-level menu item) a fost selectat. Inainte ca meniul sa fie afisat, fereastra primeste mesajul WM_INITMENUPOPUP, locul unde ar putea fi actualizate (enable, disable, checked, etc.) articolele meniului, meniul inca nu este afisat. Cand parcurgem articolele unui meniu drop down (popup), fereastra primeste mesajul WM_MENUSELECT, mesaj ce poate fi tratat, in special in SDK, prin afisarea unui text in bara de stare (scurt help). Dar cel mai important mesaj este WM_COMMAND trimis cand utililizatorul selecteaza un articol de meniu. Cuvantul inferior al parametrului wParam pastreaza ID-ul comenzii (articolul din meniul popup) si in SDK se face practic switch pe LOWORD(wParam) pentru a identifica care comanda a fost selectata. In MFC se adauga in harta de mesaje a clasei respective macroul ON_COMMAND care trateaza mesajul WM_COMMAND. ON_COMMAND are doi parametri, primul indica ID_ul comenzii, iar al doilea indica handlerul comenzii (functia ce va fi apelata la selectia acestei comenzi). Exemplu ON_COMMAND(ID_FILE_SAVE, OnFileSave) Functiile ce trateaza comenzile de meniu nu au parametri si nu intorc nimic. Intervale pentru comenzi Sunt folosite pentru a trata un grup de articole meniu cu o singura functie. Functia va avea codul necesar pentru a identifica corect care comanda a fost selectata. Exemplu Tratam culorile penitei si construim un meniu cu comenzile: Rosu, Verde, Albastru. Pentru a manevra mai usor interfata, comenzile acestui meniu vor fi inserate in harta de mesaje a clasei derivate din CView. Presupunem ID-urile sunt: IDM_ROSU, IDM_VERDE, IDM_ALBASTRU iar functiile corespunzatoare OnRosu, OnVerde, OnAlbastru, iar culoarea o pastram intr-un int, m_nCuloare, cu valorile respective 0,1,2. In harta de mesaje vom avea: ON_COMMAND(IDM_ROSU, OnRosu) ON_COMMAND(IDM_VERDE, OnVerde) ON_COMMAND(IDM_ALBASTRU, OnAlbastru) iar functiile vor fi: void CXView::OnRosu() {

Page 73: Programare Windows

73

m_nCuloare = 0; } void CXView::OnVerde() { m_nCuloare = 1; } void CXView::OnAlbastru() { m_nCuloare = 2; } Daca am avea mai multe culori? Cum procedam? Grupam aceste comenzi si vom extrage ID-ul comenzii cu CWnd:;GetCurrentMessage, iar harta de mesaje va fi: ON_COMMAND(IDM_ROSU, OnCuloare) ON_COMMAND(IDM_VERDE, OnCuloare) ON_COMMAND(IDM_ALBASTRU, OnCuloare) si codul din functia OnCuloare() void CXView::OnCuloare() { UNIT nID = (UINT) LOWORD(GetCurrentMessage()->wParam); m_nCuloare = nID – IDM_ROSU; } Indecsii meniului incep cu 0 (zero). Dar harta de mesaje este inca prea mare. Vom folosi macroul ON_COMMAND_RANGE care trateaza o serie de comenzi care au ID-uri contigue (secventiale): ON_COMMAND_RANGE(IDM_ROSU, IDM_ALBASTRU, OnCuloare); In final am obtinut o singura intrare in harta de mesaje si o singura functie ce trateza diverse optiuni de meniu. Actualizarea articolelor intr-un meniu Distingem mai multe posibilitati, pe care le exemplificam in continuare. Metoda 1: actualizare in momentul cand articolul este selectat void CXView::OnCuloare() { CMenu* pMenu = GetMenu(); pMenu->CheckMenuItem(m_nCuloare + IDM_ROSU, MF_UNCHECKED); pMenu->CheckMenuItem(nID, MF_CHECKED); m_nCuloare = nID – IDM_ROSU; } Metoda 2: mutam codul care actualizeaza meniul in tratarea mesajului WM_INITMENUPOPUP iar functia este OnInitMenuPopup. Aici actualizarea se face inainte ca meniul sa fie afisat. Aceasta functie are trei parametri:

• un pointer de tip CMenu ce pointeaza la submeniul care va fi afisat, • un UINT – valoare ce da indexul art din submeniu si • variabila de tip BOOL care este diefrita de 0 daca mesajul este al meniului sistem si 0 altfel.

Page 74: Programare Windows

74

In harta de mesaje avem: ON_WM_INITMENUPOPUP() iar functia: (COLOR_MENU_INDEX este o variabila ce specifica pozitia meniului Color in bara de meniu a aplicatiei) void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { if (!bSysmenu && (nIndex == COLOR_MENU_INDEX)) { pPopMenu->CheckMenuItem(IDM_ROSU, MF_UNCHECKED); pPopMenu->CheckMenuItem(IDM_VERDE, MF_UNCHECKED); pPopMenu->CheckMenuItem(IDM_ALBASTRU, MF_UNCHECKED); pPopMenu->CheckMenuItem(m_nCuloare + IDM_ROSU, MF_CHECKED); } } Un alt mecanism este tot la mesajul WM_INITMENUPOPUP si consta in a completa harta de mesaje cu functii (handleri) pentru comenzi, handleri ce vor fi apelati inainte ca meniul sa fie vizibil si inaintea fiecarei selectii din meniu. Fiecarui handler de actualizare ii este pasat un pointer la un obiect CCmdUI a carei functii membru pot fi folosite pentru a modifica articolul de meniu. Si pentru ca aceasta clasa CCmdUI, nu este specifica unui tip particular al unui element al interfetei utilizatorului, acesti handleri pot fi utilizati si pentru actualizarea butoanelor din toolbar si alte obiecte ale interfetei UI. Exemplu in harta de mesaje: ON_COMMAND_UPDATE_UI(IDM_ROSU, OnUpdateRosu) ON_COMMAND_UPDATE_UI(IDM_VERDE, OnUpdateVerde) ON_COMMAND_UPDATE_UI(IDM_ALBASTRU, OnUpdateAlbastru) void CMainFrame::OnUpdateRosu(CCmdUI* pCmdUI) { pCmdUI->SetCheck(m_nCuloare); } , etc. Metode din CCmdUI: Enable, SetCheck, SetRadio (nu are echivalent in SDK), SetText

Keyboard Acceleratori – Adăugare la aplicatie Acceleratori: combinatii de taste pentru a selecta o comanda. Un accelerator produce mesajul WM_COMMAND. Se creaza o tabela de acceleratori in fisierul de resurse, o resursa speciala care coreleaza ID-ul articolului de meniu cu tastele sau combinatiile de taste, si incarca resursa in program cu un apel de functie. Resursa tabela de acceleratori este definita de un bloc ACCELERATORS in fisierul de resurse. Format general: ResurseID ACCELERATORS BEGIN ... END

Page 75: Programare Windows

75

In MFC tabela de acceleratori are structura: IDR_MAINFRAME ACCELERATORS PRELOAD MOVEABLE BEGIN “N”, ID_FILE_NEW, VIRTKEY, CONTROL ... VK_BACK, ID_EDIT_UNDO, VIRTKEY, ALT END VIRTKEY = spune compilatorului de resurse ca prima intrare este un cod de cheie virtuala, iar urmatorul este CONTROL, ALT sau SHIFT Incarcarea acceleratorilor se face cu functia: LoadAccelTable(MAKEINTRESOURCE(IDR_MAINFRAME)); De asemenea se poate face si cu LoadFrame. Daca doua resurse au acelasi nume, LoadFrame le incarca pe amindoua la un singur apel de functie. Observatie Pentru ca acceleratorii sa lucreze, bucla de mesaje trebuie sa includa un apel la functia API ::TranslateAccelerator Crearea meniului programatic (at run time) Exemplu CMenu menuMain; menuMain.CreateMenu(); Cmenu menuPopup; menuPopup.CreatePopupMenu(); menuPopup.AppendMenu(MF_STRING, IDM_ROSU, “&Rosu”); menuMain.AppendMenu(MF_POPUP, (UINT) menuPopup.Detach(), “&Culori”); menuPopup.CreatePopupMenu(); menuPopup.AppendMenu(MF_STRING, IDM_EXIT, “&Exit”); ... menuMain.AppendMenu(MF_POPUP, (UINT)menuPopup.Detach(), “&Sesiune”); SetMenu(&menuMain); menuMain.Detach();

Modificarea programatica Functiile necesare pentru modificare sunt: AppendMenu, InsertMenu, ModifyMenu, DeleteMenu, RemoveMenu Inainte de a modifica un meniu, trebuie sa obtinem un pointer la acel meniu, CWnd::GetMenu. In top-level menu, articolele sunt referite prin indici ce incep cu 0 (zero) – cel mai din stinga. Exemplu CMenu* pMenu = GetMenu(); pMenu-DeleteMenu(1, MF_BYPOSITION); sau

Page 76: Programare Windows

76

pMenu->DeleteMenu(IDM_CULORI, MF_BYCOMMAND); sau item-uri din meniuri CMenu* pMenu = GetMenu()-GetSubMenu(1); pMenu->DeleteMenu(IDM_VERDE, MF_BYCOMMAND); sau echivalent: CMenu* pMenu = GetMenu(); pMenu->DeleteMenu(IDM_VERDE, MF_BYCOMMAND); // merge bine Cititi clasa CMenu.

Meniul system Obtinerea unui pointer la meniul sistem se face ca in exemplul de mai jos: CMenu* pSysMenu = GetSystemMenu(FALSE); FALSE = inseamna ca dorim un pointer la o copie a meniului sistem pe care vrem sa-l modificam. TRUE = reseteaza meniul sistem la starea sa implicita. Exemplu de adaugare a unei comenzi la meniul sistem: pSysMenu->AppendMenu(MF_SEPARATOR); pSysmenu->AppendMenu(MF_STRING, IDM_COMANDA_NOUA, _T(“&Comanda adaugata”)); in harta de mesaje: ON_WM_SYSCOMMAND() si in implementare avem: void CMainFrame::OnSysCommand(UINT nID, LPARAM lPram) { if ((nID & 0xFFF0 ) == IDM_COMMANDA_NOUA) { // ceva } CFrameWnd::OnSysCommand(nID, lParam); } Meniuri contextuale. Mesajul WM_CONTEXTMENU Se activeaza de obicei la clic dreapta mouse. Un meniu contextual nu este nimic altceva decit un submeniu care nu e atasat la un top-level menu. Functia CMenu::TrackPopupMenu afiseaza un asemenea meniu. Prototipul functiei este: BOOL TrackPopupMenu( UINT nFlags, int x, int y, CWnd* pWnd, LPRECT lpRect = NULL) Descriere parametri

• x, y = locatia pe ecran (coordonate ecran) unde va apare meniul;

Page 77: Programare Windows

77

• nFlags = informatii despre alianiamentul relativ la x (TPM_LEFTALIGN, TPM_CENTERALIGN, TPM_RIGHTALIGN) si care buton este utilizat in continuare pentru a face o selectie (TPM_LEFTBUTTON, TPM_RIGHTBUTTON).

• pWnd = identifica fereastra care va primi mesajul dupa selectia unei comenzi din meniu; • lpRect = dimensiunea unui dreptunghi (coord. ecran) in care utilizatorul poate face clic fara a anula meniul

afisat. Ex. Exemplu CMenu menu; menu.LoadMenu(IDR_CONTEXTMENU); CMenu* pContextMenu = menu.GetSubMenu(0); pContextMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, point.x, point.y, AfxGetMainWnd()); Daca tratam mesajul WM_CONTEXTMENU in harta de mesaje avem macroul: ON_WM_CONTEXTMENU si functia afx_msg OnContextMenu(CWnd* pWnd, CPoint point); unde, pWnd identifica fereastra in care s-a facut clic si point coordonatele punctului unde s-a facut clic.

Page 78: Programare Windows

78

Obiecte nucleu

Ce sunt obiectele nucleu ? Ca dezvoltatori de sofware, noi creăm, deschidem şi manipulăm obiecte nucleu în mod obişnuit.

Sistemul creează şi manipulează câteva tipuri de obiecte nucleu, cum ar fi obiecte token, obiecte eveniment, obiecte fişier, obiecte fişiere mapate în memorie, obiecte de completare I/O, obiecte job, obiecte mailslot, obiecte mutex, obiecte pipe-uri, obiecte proces, obiecte semafor, obiecte fir de execuţie şi obiecte waitable timer.Aceste obiecte sunt create apelând diferite funcţii. De exemplu, funcţia CreateFileMapping face ca sistemul să creeze un obiect fişier mapat în memorie.

Fiecare obiect nucleu este pur şi simplu un bloc de memorie alocat de nucleul sistemului de operare şi accesibil doar acestuia. Acest bloc de memorie este o structură de date ai cărei membri menţin informaţii despre obiect. Unii membri (descriptorii de securitate, contorul de utilizare, şi aşa mai departe) sunt la fel pentru toate obiectele nucleu, dar majoritatea sunt specifice unor obiecte nucleu particulare. De exemplu un obiect nucleu proces are un ID de proces, o prioritate de bază, un cod de ieşire.

Contorul de resurse Obiectele nucleu sunt proprietatea nucleului sistemului de operare, şi nu a unui proces. În alte cuvinte,

dacă procesul nostru apelează o funcţie care creează un obiect nucleu şi apoi procesul se termină, obiectul nucleu nu este neaparat distrus. În majoritatea cazurilor, obiectul va fi distrus; dar dacă un alt proces utilizează acel obiect nucleu creat de proces, nucleul ştie să nu distrugă obiectul până când celălalt proces nu îl mai foloseşte. Un lucru demn de reţinut aici este că un obiect nucleu poate depăşi durata de viaţă a procesului care l-a creat.

Nucleul ştie câte procese folosesc un anumit obiect nucleu deoarece fiecare obiect nucleu are un contor de utilizare. Acesta este una din datele membre comune tuturor obiectelor nucleu. Atunci când un obiect este creat, contorul său de utilizare este setat la 1. Apoi când un alt proces primeşte acces la un obiect nucleu existent, contorul de utilizare este incrementat. Atunci când un proces îşi încheie execuţia, nucleul sistemului decrementează automat contorul de utilizare pentru toate obiectele nucleu pe care le-a deschis procesul. Dacă contorul de utilizare devine 0, nucleul distruge în mod automat obiectul. Acest lucru ne asigură că nici un obiect nucleu nu va rămâne în sistem dacă nu mai există procese care referenţiază acel obiect.

Securitate Obiectele nucleu pot fi protejate cu un descriptor de securitate. Acesta descrie cine creează obiectul,

cine poate primi acces sau poate folosi obiectul şi cine nu are acces la obiect. Descriptorii de securitate sunt de obicei folosiţi atunci când scriem aplicaţii server; putem ignora această facilitate dacă scriem aplicaţii pentru partea de client.

Windows 98 nu este conceput ca un sistem de operare pe partea de server. Din această cauză, Microsoft nu a implementat partea de securitate în Windows 98.

Aproape toate funcţiile care creează obiecte nucleu au un pointer la o structură SECURITY_ATTIBUTES ca un argument, ca în exemplul de mai jos :

HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); Majoritatea aplicaţiilor vor transmite FALSE pentru acest argument astfel încât obiectul este creat cu

setările de securitate implicite. Acestea înseamnă că oricare membru al grupului administrator şi creatorul obiectului vor avea acces nelimitat la obiect; toţi ceilalţi nu au acces. Totuşi, putem aloca o structură SECURITY_ATTRIBUTES, o iniţializăm şi trimitem adresa acesteia pentru acest parametru. O structură SECURITY_ATTRIBUTES arată în modul următor :

typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor;

Page 79: Programare Windows

79

BOOL bInheritHandle; } SECURITY_ATTRIBUTES; Chiar dacă această structură este numită SECURITY_ATTRIBUTES, ea de fapt include doar un

membru care are o legătură cu securitatea: lpSecurityDescriptor. Dacă dorim să restrângem accesul la obiectele nucleu pe care le creăm, trebuie să creăm un descriptor de securitate şi apoi să iniţializăm structura SECURITY_ATTIBUTES în modul următor :

SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); // Folosit pentru redarea versiunii. sa.lpSecurityDescriptor = pSD; // Adresa unei SD neiniţializate. sa.bInheritHandle = FALSE; // Discutat mai tarziu. HANDLE hFileMapping = CreateFileMapping(

INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 0, 1024, "MyFileMapping"); Atunci când dorim să primim acces la un obiect nucleu existent (mai degrabă decât să creăm unul nou),

trebuie să precizăm operaţiile pe care dorim să le efectuăm asupra obiectului. De exemplu, dacă dorim să accesăm un obiect fişier mapat în memorie existent pentru a citi date din el, putem apela OpenFileMapping :

HANDLE hFileMapping = OpenFileMapping( FILE_MAP_READ, FALSE, "MyFileMapping"); Prin trimiterea valorii FILE_MAP_READ ca primul parametru al funcţiei OpenFileMapping, am

indicat că intenţionăm să citim din acest fişier mapat în memorie după ce primim acces la el. Funcţia OpenFileMapping efectuează o verificare a securităţii mai întâi, înainte de a întoarce o valoare de identificator validă. Dacă utilizatorul are voie să acceseze obiectul nucleu fişier mapat în memorie existent, OpenFileMapping returnează un identificator valid. Totuşi, dacă utilizatorul nu are acces, OpenFileMapping returnează NULL, iar un apel al funcţiei GetLastError va returna valoarea 5 (ERROR_ACCES_DENIED). Totuşi, majoritatea aplicaţiilor nu folosesc securitatea.

Deoarece Windows 98 nu are aceşti descriptori de securitate, pot apărea probleme la portarea programelor pe sistemul de operare Windows 2000.

Crearea unui obiect nucleu În continuare vom discuta funcţii care creează obiecte nucleu :

HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, LPTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD dwCreationFlags, PDWORD pdwThreadId); HANDLE CreateFile( PCTSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_ATTRIBUTES psa, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow,

Page 80: Programare Windows

80

PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName);

Toate funcţiile care creează obiecte nucleu returnează identificatori relativi la proces care pot fi folosiţi de toate firele care rulează în acel proces. Ori de câte ori apelăm o funcţie care acceptă identificatorul unui obiect nucleu ca argument, îi trimitem valoarea returnată de una din funcţiile Create*.

Dacă apelăm o funcţie pentru a crea un obiect nucleu şi apelul eşuează, valoarea identificatorului returnată este de obicei 0 (NULL). Sistemul ar trebui să aibă resurse foarte reduse de memorie sau să aibă o problemă de securitate pentru a se întâmpla acest lucru. Din păcate, puţine funcţii returnează o valoare a identificatorului egală cu –1 (INVALID_HANDLE_VALUE) atunci când apelul lor eşuează. De exemplu, dacă funcţia CreateFile eşuează să deschidă fişierul precizat, ea returnează INVALID_HANDLE_VALUE în loc de NULL. Trebuie să fim foarte atenţi atunci când verificăm valoarea returnată de o funcţie care creează un obiect nucleu. Şi anume, putem compara valoarea cu INVALID_HANDLE_VALUE doar atunci când creăm CreateFile. Următorul cod este incorect :

HANDLE hMutex = CreateMutex(…); if (hMutex == INVALID_HANDLE_VALUE) { // Nu vom executa niciodată acest cod deoarece // CreateMutex returnează NULL dacă eşuează. }

De asemenea, şi codul următor este incorect :

HANDLE hFile = CreateFile(…); if (hFile == NULL) { // Nu vom executa niciodată acest cod deoarece // CreateFile returnează INVALID_HANDLE_VALUE (-1) dacă eşuează. }

Închiderea unui obiect nucleu Indiferent de modalitatea de creare a obiectului nucleu, vom indica sistemului că am terminat de

utilizat obiectul apelând CloseHandle : BOOL CloseHandle(HANDLE hobj); Această funcţie verifică mai întâi tabelul de identificatori ai procesului pentru a se asigura că indexul

transmis identifică un obiect la care procesul are de fapt drept de acces. Dacă indexul este valid, sistemul obţine adresa structurii de date a obiectului nucleu şi decrementează contorul de utilizare din structură; dacă contorul este 0, nucleul sistemului de operare distruge obiectul nucleu din memorie.

Dacă contorul de utilizare a oricărui dintre aceste obiecte devine 0, nucleul sistemului de operare distruge obiectul.

Astfel, aplicaţia noastră poate avea scurgeri de memorie în timpul execuţiei sale, dar în momentul terminării execuţiei sistemul garantează că totul este curăţat în mod corespunzător. Acest lucru este efectuat pentru toate obiectele, nu doar pentru obiectele nucleu.

Partajarea obiectelor nucleu dincolo de graniţele procesului În mod frecvent, fire care rulează în procese diferite au nevoie să partajeze obiecte nucleu. Există mai

multe modalităţi : • obiectele fişiere mapate în memorie ne permit să partajăm blocuri de date între două procese care

rulează pe aceeaşi maşină. • mailslot-urile si pipe-urile cu nume permit aplicaţiilor să trimită blocuri de date între procese care

rulează pe maşini diferite conectate la reţea.

Page 81: Programare Windows

81

• mutexurile, semafoarele şi evenimentele permit firelor de execuţie din procese diferite să îşi sincronizeze execuţia, ca în cazul în care o aplicaţie are nevoie să anunţe o altă aplicaţie atunci când şi-a terminat de executat sarcina.

Creatorul obiectului poate preveni accesul unui utilizator neautorizat la acel obiect prin simpla interzicere a accesului la acel obiect.

Moştenirea identificatorilor obiectelor Moştenirea poate fi folosită doar atunci când procesele sunt în relaţia părinte-copil. În acest caz, unul

sau mai mulţi identificatori sunt disponibili procesului părinte, iar acesta decide să creeze un proces copil, dându-i acestuia acces la identificatorii obiectelor procesului părinte. Pentru ca acest fel de moştenire să funcţioneze, procesul părinte trebuie să efectueze câţiva paşi.

În primul rând, atunci când procesul părinte creează un proces copil, el trebuie să indice sistemului că vrea ca identificatorul obiectului să fie moştenibil. Trebuie să avem în vedere că, chiar dacă identificatorii obiectelor nucleu sunt moştenibile, obiectele în sine nu sunt.

Pentru a crea un identificator moştenibil, procesul părinte trebuie să aloce şi să iniţializeze o structură SECURITY_ATTRIBUTES şi să transmită adresa acestei structuri funcţiei Create. Următorul cod creează un mutex şi returnează un identificator moştenibil la acesta :

SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // Make the returned handle inheritable. HANDLE hMutex = CreateMutex(&sa, FALSE, NULL); ...

Această porţiune de cod iniţializează o structură SECURITY_ATTRIBUTES indicând faptul că obiectul trebuie creat folosind descriptorii impliciţi de securitate (ignoraţi în Windows 98) şi că identificatorul returnat trebuie să fie moştenibil.

Chiar dacă Windows 98 nu are suport integral pentru securitate, el totuşi suportă moştenirea; de aceea, Windows 98 foloseşte în mod corect valoarea membrului bInheritHandle.

Următorul pas este ca procesul părinte să creeze procesul copil. Acest lucru este realizat cu ajutorul funcţiei CreateProcess :

BOOL CreateProcess( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES pszThread, BOOL bInheritHandles, DWORD dwCreationFlags, PVOID pvEnvironment, PCTSTR pszCurrentDirectory, LPSTARTUPINFO pStartupInfo, PPROCESS_INFORMATION pProcessInformation);

De obicei, atunci când creăm un nou proces, trimitem valoarea FALSE pentru parametrul bInheritHandle. Această valoare spune sistemului că nu dorim ca procesul copil să moştenească identificatorii moştenibili care sunt în tabelul de identificatori ai procesului părinte.

Dacă totuşi trimitem TRUE pentru acest parametru, copilul va moşteni identificatorii moştenibili ai procesului părinte. Atunci când trimitem valoarea TRUE, sistemul creează noul proces copil dar nu îi permite execuţia imediat.

Trebuie să reţinem că moştenirea identificatorilor obiectelor nucleu are loc doar la momentul creării procesului fiu. Dacă procesul părinte creează noi obiecte nucleu, un proces copil care deja rulează nu va moşteni aceste noi obiecte.

Moştenirea identificatorilor obiectelor are o caracteristică ciudată : atunci când o folosim, procesul copil nu are nici o idee că a moştenit vreun identificator. Moştenirea este utilă doar atunci când procesul copil spune

Page 82: Programare Windows

82

că aşteaptă acces la un obiect nucleu atunci când este creat de alt proces. De obicei, aplicaţiile părinte şi copil sunt scrise de aceeaşi companie; totuşi, o companie diferită poate scrie aplicaţia copil dacă acea companie documentează ce aşteaptă procesul copil.

Obiecte cu nume A doua metodă de a partaja obiecte nucleu dincolo de graniţele proceselor este de a denumi obiectele.

Multe – deşi nu toate – obiecte nucleu pot fi denumite. De exemplu, toate funcţiile următoare creează obiecte nucleu cu nume :

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateEvent( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, BOOL bInitialState, PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName); HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName);

Toate aceste funcţii au un ultim parametru comun, pszName. Atunci când transmitem NULL pentru acest parametru, indicăm sistemului că dorim să creăm un obiect nucleu fără nume. Atunci când creăm un astfel de obiect, îl putem partaja dincolo de graniţele procesului folosind moştenirea sau DuplicateHandle. Pentru a partaja un obiect nucleu prin nume, trebuie să îi atribuim acestuia un nume.

Dacă nu trimitem NULL pentru parametrul pszName, ar trebui să trimitem adresa unui şir terminat cu zero. Acest nume poate fi de lungime maximă egală cu MAX_PATH (definit ca 260).

Acum că ştim cum să numim un obiect nucleu, să vedem cum putem partaja obiectele în acest mod. Să spunem că procesul A îşi începe execuţia şi apelează următoarea funcţie :

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, "myMutex");

Această funcţie creează un nou mutex şi îi atribuie numele „myMutex”. Să observăm că în identificatorul procesului A, hMutexProcessA nu este un identificator moştenibil – şi nu are nevoie să fie atunci când doar denumim obiectele.

După o perioadă de timp, un proces creează procesul B. Acesta nu trebuie să fie copilul procesului A; el poate fi creat din Explorer sau din altă aplicaţie. Faptul că procesul B nu trebuie să fie copilul procesului A este un avantaj al utilizării obiectelor cu nume în locul moştenirii. Atunci când procesul B îşi începe execuţia, el execută următorul cod :

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, "myMutex");

Atunci când apelul CreateMutex al procesului B este efectuat, sistemul verifică mai întâi dacă nu există deja un obiect nucleu cu acelaşi nume. Deoarece există un obiect nucleu cu acelaşi nume, nucleu sistemului de operare verifică tipul obiectului. Deoarece încercăm să creăm un mutex şi obiectul cu numele „myMutex” este de

Page 83: Programare Windows

83

asemenea un mutex, sistemul face o verificare a securităţii pentru a vedea dacă apelantul are acces deplin la obiect, iar în caz afirmativ sistemul găseşte o intrare liberă în tabelul de identificatori ai procesului B şi iniţializează această intrare pentru a pointa la obiectul nucleu existent. Dacă tipurile obiectelor diferă sau apelantului îi este respins accesul, CreateMutex eşuează (returnează NULL).

Atunci când apelul CreateMutex al procesului B este reuşit, nu este creat de fapt un mutex. Procesului B îi este atribuit un identificator relativ la proces care identifică obiectul mutex existent. Bineînţeles, deoarece o nouă intrare din tabelul de identificatori ai procesului B referenţiază acest obiect, contorul de utilizare a mutexului este incrementat; obiectul nu va distrus până când ambele procese A şi B şi-au închis identificatorii la acest obiect. Este foarte posibil ca valorile identificatorilor din cele două procese să difere.

Atunci când obiectul B apelează CreateMutex, el transmite informaţia de atribute de securitate şi un al doilea parametru funcţiei. Aceşti parametri sunt ignoraţi dacă un obiect cu numele specificat există ! O aplicaţie poate să îşi dea seama dacă a creat de fapt un nou obiect nucleu sau dacă a deschis unul deja existent apelând funcţia GetLastError imediat după apelul funcţiei Create* :

HANDLE hMutex = CreateMutex(&sa, FALSE, "JeffObj"); if (GetLastError() == ERROR_ALREADY_EXISTS) { // Am deschis un obiect nucleu existent. // sa.lpSecurityDescriptor şi cel dea- doilea parametru sunt ignoraţi } else { // Am creat un obiect nou. // sa.lpSecurityDescriptor şi cel de-al doilea parametru sunt folosiţi pentru a crea obiectul. }

Există o metodă alternativă pentru partajarea obiectelor nucleu prin nume. În loc să apelăm funcţia Create*, un proces poate apela una din funcţiile Open* următoare :

HANDLE OpenMutex( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenFileMapping( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

Aceste funcţii au acelaşi prototip. Ultimul parametru, pszName, indică numele obiectului nucleu. Nu putem trimite NULL pentru acest parametru; trebuie să trimitem adresa unui şir încheiat cu zero. Aceste funcţii pur şi simplu caută în singurul spaţiu de nume al obiectelor nucleu pentru a găsi o potrivire. Dacă nu există nici un obiect nucleu cu numele precizat, funcţia returnează NULL şi GetLastError returnează 2 (ERROR_FILE_NOT_FOUND). Totuşi, dacă există un obiect nucleu cu acel nume şi dacă este de acelaşi tip, sistemul verifică să vadă dacă accesul cerut (prin intermediul parametrului dwDesiredAcces) este permis; dacă da, tabelul de identificatori ai procesului este actualizat şi contorul de utilizare al obiectului este incrementat. Identificatorul returnat va fi moştenibil dacă trimitem valoarea TRUE pentru parametrul bInheritHandle.

Principala deosebire dintre funcţia Create* şi funcţia Open* este că dacă obiectul nu există deja, funcţia Create* îl va crea, în timp ce apelul funcţiei Open* va eşua.

Page 84: Programare Windows

84

Obiectele cu nume sunt de obicei folosite pentru a preveni rularea instanţelor multiple a unei aplicaţii care rulează. Pentru a realiza acest lucru, putem să apelăm funcţia Create* în funcţia noastră main sau WinMain pentru a crea obiecte cu nume (nu contează ce tip de obiecte creăm). Atunci când funcţia Create* returnează, apelăm GetLastError. Dacă aceasta returnează ERROR_ALREADY_EXISTS, atunci o altă instanţă a aplicaţiei noastre rulează si noua instanţă îşi poate înceta execuţia. int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow) { HANDLE h = CreateMutex(NULL, FALSE, "{FA531CC1-0497-11d3-A180-00105A276C3E}"); if (GetLastError() == ERROR_ALREADY_EXISTS) { // Mai există o instanţă a aplicaţiei care rulează. return(0); } // Aceasta este prima instanţă a aplicaţiei. ..... // Înainte de ieşi, se inchide obiectul. CloseHandle(h); return(0); }

Spaţiile de nume Terminal Server Trebuie notat că Terminal Server schimbă puţin scenariul anterior. O maşină Terminal Server va avea

spaţii de nume multiple pentru obiectele nucleu. Există doar un spaţiu de nume global, care este folosit de obiectele nucleu care sunt proiectate să fie accesibile din toate sesiunile client. Acest spaţiu de nume este în general folosit de către servicii. În plus, fiecare sesiune client are propriul spaţiu de nume. Acest lucru face ca două sau mai multe sesiuni care rulează aceeaşi aplicaţie să nu interfereze una cu alta – o sesiune nu poate accesa obiectele celeilalte sesiuni chiar dacă obiectele au acelaşi nume. Pe o maşină fără Terminal Server, serviciile şi aplicaţiile partajează acelaşi spaţiu de nume.

Obiectele nucleu cu nume ale unui serviciu întotdeauna se găsesc în spaţiul global de nume. Implicit, în Terminal Server, un obiect nucleu cu nume al unei aplicaţii este în spaţiul de nume al sesiunii. Totuşi, este posibil să forţăm ca un obiect nucleu cu nume să meargă în spaţiul de nume global prefixând numele cu „Global”, ca în exemplul următor :

HANDLE h = CreateEvent(NULL, FALSE, FALSE,

"Global\\MyName"); Putem de asemenea să precizăm că vrem ca obiectul nucleu să meargă în spaţiul de nume al sesiunii

prefixând numele cu „Local\”, ca mai jos : HANDLE h = CreateEvent(NULL, FALSE, FALSE, "Local\\MyName");

Microsoft consideră cuvintele Global şi Local drept cuvinte rezervate pe care nu trebuie să le folosim în numele obiectelor exceptând cazul în care dorim să forţăm un anumit spaţiu de nume. Microsoft consideră de asemenea cuvântul Session drept un cuvânt rezervat, deşi deocamdată nu are nici înţeles deosebit. Toate aceste trei cuvinte rezervate sunt case-sensitive. Ele sunt ignorate dacă maşina gazdă nu rulează Terminal Server.

Page 85: Programare Windows

85

Procese

Un proces este în mod normal definit ca o instanţă a unui program în rulare. În Win32, un proces este posesorul unui spaţiu de adrese de 4 GB. Spre deosebire de corespondentele lor din MS-DOS şi Windows pe 16 biţi, procesele Win32 sunt inerte : adică, un proces Win32 nu face nimic, el doar are un spaţiu de 4 GB de spaţiu de adrese care conţine codul şi data pentru fişierul unei aplicaţii .EXE. Orice DLL necesar fişierului .EXE va avea codul şi datele sale încărcate în spaţiul de adrese al procesului. În completarea spaţiului de adrese, un proces este posesorul unor diferite resurse, cum ar fişiere, alocări dinamice ale memoriei şi fire de execuţie. Diferitele resurse create în timpul vieţii unui proces sunt distruse atunci când procesul se termină – garantat.

După cum am spus, procesele sunt inerte. Pentru ca un proces să realizeze ceva, el trebuie să fie posesorul unui fir de execuţie; acest fir de execuţie este responsabil cu executarea codului conţinut în spaţiul de adrese al procesului. De fapt, un proces poate conţine mai multe fire de execuţie, toate rulând un anumit cod în mod simultan. Pentru a face acest lucru, fiecare fir are propriul său set de regiştri ai procesorului şi stiva sa proprie. Fiecare proces are cel puţin un fir care execută cod conţinut în spaţiul de adrese al procesului. Dacă nu ar fi fost nici un fir de execuţie, atunci nu ar fi fost nici un motiv ca procesul să mai existe, iar sistemul ar distruge în mod automat procesul şi spaţiul său de adrese.

Pentru ca toate procesele să ruleze, sistemul de operare programează o parte din timpul procesorului pentru fiecare fir de execuţie. Sistemul de operare dă iluzia că toate firele rulează concurent, oferind bucăţi de timp (numite cuante) firelor.

Atunci când este creat un proces Win32, primul său fir de execuţie, numit firul principal de execuţie, este creat în mod automat de către sistem. Firul principal poate apoi crea alte fire iar acestea pot crea la rândul lor alte fire.

Windows NT este capabil să utilizeze maşini care conţin mai multe procesoare. Astfel, pe un calculator cu două procesoare pot rula în acelaşi timp două fire de execuţie diferite. Nucleul Windows NT face tot managementul şi programarea firelor în acest tip de sistem. Noi ca programatori nu trebuie să facem nimic special pentru a ne putea bucura de avantajele oferite de un calculator multiprocesor.

Windows 95 poate folosi doar un singur procesor. Chiar dacă maşina pe care rulează Windows 95 are mai mult de un procesor, Windows 95 poate programa doar un singur fir la un moment dat; celelalte procese stau şi aşteaptă.

Atributele unui proces

Identificatorul de instanţă al procesului Fiecarui fişier EXE sau DLL încărcat în spaţiul de adrese al unui proces îi este atribuit un identificator

de instanţă unic. Instanţa fisierului EXE este transmisă ca fiind ca primul parametru al funcţiei WinMain, hinstExe. Valoarea identificatorului este în mod normal necesară pentru apeluri care încarcă resurse. De exemplu, pentru a încărca o resursă de tip icoană dintr-un fişier EXE, vom apela :

HICON LoadIcon (HINSTANCE hinst, LPCTSTR lpszIcon);

Primul parametru al funcţiei LoadIcon indică ce fişier (EXE sau DLL) conţine resursa pe care dorim să o încărcăm. Multe aplicaţii salvează parametrul hinstExe al funcţiei WinMain într- variabilă globală pentru a fi uşor accesibilă întregului cod din fişierul EXE.

Valoarea reală a parametrului hinstExe a funcţiei WinMain este adresa de bază a memoriei care indică unde a încărcat sistemul imaginea fişierului EXE în spaţiul de adrese al procesului. De exemplu, dacă sistemul deschide fişierul EXE şi îi încarcă conţinutul la adresa 0x00400000, parametrul hinstExe a funcţiei WinMain va avea valoarea 0x00400000.

Adresa de bază la care o aplicaţie este încărcată este determinată de către linker. Linker-e diferite pot folosi adrese de bază diferite. Linker-ul Visual C++ foloseşte o adresă de bază implicită egală cu 0x00400000 deoarece aceasta este cea mai mică adresă la care se poate încărca imaginea unui fişier executabil în Windows 95. Unele linker-e mai vechi folosesc o adresă de bază implicită egală cu 0x00010000 deoarece aceasta este cea mai mică adresă la care imaginea unui fişier executabil poate fi încărcată în Windows NT. Putem schimba adresa de bază pe care o încarcă aplicaţia noastră folosind switch-ul de adresă al linker-ului pentru linker-ul Microsoft. Funcţia GetModuleHandle

HMODULE GetModuleHandle (LPCTSTR lpszModule);

Page 86: Programare Windows

86

returnează adresa de bază care indică unde este încărcat un fişier EXE sau DLL în spaţiul de adrese al procesului. Atunci când apelăm această funcţie, îi transmitem un şir terminat cu zero care precizează numele fişierului EXE sau DLL încărcat în spaţiul de adrese al procesului apelant. Dacă sistemul găseşte numele acelui fişier, GetModuleHandle returnează adresa de bază la care este încărcat fişierul. Sistemul returnează NULL dacă nu poate găsi fişierul specificat. Putem de asemenea apela GetModuleHandle, transmiţându-i valoarea NULL parametrului lpszModule. Atunci când procedăm astfel, GetModuleHandle returnează adresa de bază a fişierului EXE. Acest lucru este făcut de codul C run-time de pornire atunci când apelează funcţia noastră WinMain.

Trebuie să ţinem minte două caracteristici importante ale funcţiei GetModuleHandle. În primul rând, GetModuleHandle examinează doar spaţiul de adrese al procesului apelant. Dacă acest proces nu foloseşte nici o funcţie GDI, apelarea funcţiei GetModuleHandle şi transmiterea către acesta valoarea „GDI32” va cauza returnarea valorii NULL, deşi este posibil că fişierul GDI32.DLL este încărcat în spaţiul de adrese al procesului. În al doilea rând, apelarea GetModuleHandle transmiţându-i valoarea NULL returnează adresa de bază a fişierului EXE în spaţiul de adrese al procesului. Aşa că chiar dacă apelăm GetModuleHandle(NULL) din codul conţinut în interiorul unui fişier DLL, valoarea returnată este adresa de bază a fişierului EXE şi nu cea a fişierului DLL. Funcţia GetModuleHandle funcţionează diferit în Windows pe 16 biţi.

În Win32, fiecare proces are propriul spaţiu de adrese, asta însemnând că fiecare proces crede că este

singurul proces care rulează în sistem. Un proces nu poate vedea uşor un alt proces. Din acest motiv, nu este făcută nici o deosebire între valorile hinstExe şi hmodExe ale unui proces, ele sunt şi aceeaşi valoare. Pentru motive de compatibilitate, cei doi termeni continuă să existe în documentaţia Win32.

Linia de comandă a unui proces Atunci când este creat un nou proces, îi este transmisă o linie de comandă. Aceasta nu este aproape

niciodată goală; cel mai puţin, numele fişierului executabil folosit pentru a crea noul proces este primul şir din linia de comandă.

Este important de reţinut că parametrul lpszCmdLine pointează întotdeauna către un şir de caractere ANSI. Microsoft a ales ANSI pentru a ajuta la portarea aplicaţiilor din Windows pe 16 biţi pe Win32, deoarece aplicaţiile Windows pe 16 biţi aşteaptă un şir ANSI.

Putem de asemenea obţine un pointer la linia completă de comandă a procesului nostru apelând funcţia GetCommandLine :

LPTSTR GetCommandLine (VOID);

Această funcţie returnează un pointer la un buffer care conţine linia de comandă completă, incluzând calea completă la fişierul executabil. Probabil cel mai silit (compelling) motiv de a folosi GetCommandLine în loc de parametrul lpszCmdLine este că atât versiunea ANSI, cât şi cea UNICODE a funcţiei GetCommandLine există în Win32 pe când parametrul lpszCmdLine întotdeauna indică către un buffer care conţine un şir de caractere ANSI.

Variabilele de mediu ale unui proces Fiecare proces are un bloc de mediu asociat lui. Un astfel de bloc este un bloc de memorie alocat în

interiorul spaţiului de adrese al unui proces. Fiecare bloc conţine un set de şiruri cu următorul aspect : VarName1 = VarValue1\0 VarName2 = VarValue2\0 VarName3 = VarValue3\0 ..... VarNameX = VarValueX\0 \0

Prima parte a fiecărui şir este numele unei variabile de mediu. Acest nume este urmat de semnul egal, care este urmat de valoarea pe care vrem să i-o atribuim variabilei. Toate şirurile din blocul de mediu trebuie să fie sortate alfabetic după numele variabilelor de mediu.

Deoarece semnul egal este folosit pentru a separa numele de valoare, simbolul egal nu poate să apară în nume. De asemenea, spaţiile sunt semnificative. De exemplu, dacă declarăm aceste două variabile : XZY= Win32 (După egal este un spaţiu gol.) ABC=Win32

Page 87: Programare Windows

87

şi apoi le comparăm valorile, sistemul va declara că cele două variabile sunt diferite. Acest lucru deoarece orice spaţiu gol care apare imediat înainte sau după egal este luat în considerare. De exemplu, dacă ar trebui să adăugăm două şiruri la blocul de mediu, XYZ =Home XYZ=Work atunci varibila de mediu “XYZ ” va conţine “Home” iar cealaltă variabilă va conţine “Work”. În final, un caracter 0 trebuie plasat la sfărşitul tuturor variabilelor de mediu pentru a marca sfârşitul blocului de mediu.

Pentru a crea un set iniţial de varibile de mediu pentru Windows 95, trebuie să modificăm fişierul sistem AUTOEXEC.BAT prin plasarea unor linii SET. Fiecare linie treebuie să fie de forma :

SET VarName=VarValue

Atunci când repornim sistemul, conţinutul fişierului este parsat şi variabilele de mediu pe care le-am setat vor fi disponibile fiecărui proces pe care îl pornim în sesiunea Windows.

Atunci când un utilizator se loghează în Windows NT, sistemul creează nucleul procesolui şi îi asociază un set de şiruri de mediu. Sistemul obţine setul iniţial de şiruri de mediu examinând două intrări din registrul Windows. Prima cheie,

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\Environment

conţine lista cu toate variabilele de mediu din sistem. Cea de-a doua, HKEY_CURRENT_USER\Environment, conţine lista tuturor variabilelor de mediu care

se aplică doar pentru utilizatorul logat în acel moment. Un utilizator poate adăuga, şterge sau schimba oricare din caeste intrări făcând dublu clic pe opţiunea

System din Control Panel şi apoi selectând tabul Environment. Doar un utilizator cu drepturi de administrator pe acel sistem poate modifica variabilele din lista System

Variables. Aplicaţia noastră poate folosi diferite funcţii din registrul Windows pentru a modifica aceste intrări.

Totusi, pentru ca schimbările să aibă loc pentru toate aplicaţiile, utilizatorul trebuie să se delogheze şi apoi să se logheze din nou. Unele aplicaţii, cum ar fi Explorer, Task Manager şi Control Panel pot să îşi actualizeze blocul de mediu cu intrări noi de registru atunci când fereastra lor principală primeşte un mesaj WM_WININICHANGE. De exemplu, dacă actualizăm intrările din registrul Windows şi vrem ca anumite să-şi actualizeze blocurile lor de mediu, putem apela :

SendMessage (HWND_BROADCAST, WM_WININICHANGE,

0L, (LPARAM)"Environment");

În mod normal, un proces copil moşteneşte un set de variabile de mediu care identice cu cele ale părintelui. Totuşi, procesul părinte poate controla ce varibile de mediu moşteneşte copilul. Prin moştenire înţelegem că procesul copil primeşte copia proprie a bloculuim de meiu al părintelui, nu că procesul copil şi procesul părinte împart acelaşi bloc de mediu. Acest lucru înseamnă că un proces copil poate adăuga, şterge sau modifica o variabilă în blocul propriu de mediu iar schimbarea să nu fie reflectată în blocul de mediu al părintelui.

Dacă dorim să folosim variabilele de mediu, Win32 oferă câteva funcţii pe care le poate folosi aplicaţia noastră. Funcţia GetEnvironmentVariable ne permite să detectăm prezenţa şi valoarea unei variabile de mediu:

DWORD GetEnvironmentVariable (LPCTSTR lpszName,

LPTSTR lpszValue, DWORD cchValue);

Atunci când apelăm GetEnvironmentVariable, lpszName pointează la numele de variabilă dorit, lpszValue pointează la bufferul care reţine valoarea variabilei şi cchValue indică mărimea acestui buffer în caractere. Funcţia returnează fie numărul de caractere copiate în buffer, fie 0 dacă numele variabilei nu a fost găsit în mediu.

Funcţia SetEnvironmentVariable ne dă posibilitatea să adăugăm o variabilă, să ştergem o variabilă sau să modificăm valoarea unei variabile :

BOOL SetEnvironmentVariable (LPCTSTR lpszName, LPCTSTR lpszValue);

Page 88: Programare Windows

88

Această funcţie setează variabila identificată de parametrul lpszName la valoarea identificată de parametrul lpszValue. Dacă variabila cu numele specificat nu există, variabila este adăugată şi dacă valoarea variabilei lpszValue este NULL, atunci valoarea este ştearsă din blocul de mediu.

Modulul de eroare al unui proces Cu fiecare proces este asociat un set de indicatori care precizează sistemului cum trebuie să răspundă

procesul la erorile serioase. Erorile serioase reprezintă erori de disc, excepţii netratate, eşecuri de găsire de fişiere şi aranjare defectuoasă a datelor. Un proces poate spune sistemului cum să trateze fiecare din aceste erori apelând funcţia SetErrorMode :

UINT SetErrorMode (UINT fuErrorMode);

Parametrul fuErrorMode este o combinaţie între oricare dintre indicatorii din tabelul următor (bitwise ORed together):

Indicator Descriere SEM_FAILCRITICALERRORS Sistemul nu afişează căsuţa de mesaj a

identificatorului de eroare critică şi returnează eroarea la procesul apelant.

SEM_NOGPFAULTERRORBOX Sistemul nu afişează căsuţa de mesaj cu general-protection-fault. Acest indicator trebuie setat doar pentru depanarea aplicaţiilor care tratează ele însele erori de tipul general-protection-fault GP cu un identificator de excepţii.

SEM_NOOPENFILEERRORBOX Sistemul nu afişează o căsuţă de mesaj atunci când nu găseşte un fişier.

SEM_NOALIGNMENTFAULTEXCEPT Sistemul repară automat erorile de aliniere a memoriei şi le face invizibile pentru aplicaţie. Acest indicator nu are nici un efect pe procesoarele x86 sau Alpha.

În mod implicit, un proces copil moşteneşte indicatorii de moduri de eroare ai părintelui său. În alte cuvinte, dacă un proces are în mod curent indicatorul SEM_NOGPFAULTERRORBOX setat şi apoi creează un proces copil, acesta din urmă va avea la rândul său acest indicator setat. Totuşi, procesul copil nu este anunţat de acest lucru şi este posibil să se nu se fi scris în indicatorul de erori GP. Dacă apare o eroare GP în unul din firele procesului copil, aplicaţia corespunzătoare acestui proces s-ar putea să se termine fără a anunţa utilizatorul. Un proces părinte poate preveni ca un proces copil să moştenească modul de erori al părintelui precizând indicatorul CREATE_DEFAULT_EROR_MODE atunci când apelează CreateProcess.

Unitatea de disc curentă şi directorul curent ale unui proces Directorul curent al unităţii de disc curente reprezintă locaţia unde diferite funcţii Win32 caută fişiere şi

directoare atunci când nu sunt căile complete nu sunt furnizate. Sistemul păstrează intern evidenţa unităţii de disc şi a directorului curent al unui proces. Deoarece această informaţie este menţinută pe un întreg proces, un fir de execuţie care schimbă aceste valori le schimbă pentru toate firele din proces.

Un fir poate obţine şi poate seta unitatea de disc curentă proprie şi directorul curent propriu apelând următoarele două funcţii :

DWORD GetCurrentDirectory (DWORD cchCurDir, LPTSTR lpszCurDir); BOOL SetCurrentDirectory (LPCTSTR lpszCurDir);

Procesul părinte poate obţine directoarele curente proprii prin apelul funcţiei GetFullPathName :

DWORD GetFullPathName ( LPCTSTR lpszFile,

DWORD cchPath, LPTSTR lpszPath, LPTSTR *ppszFilePart);

Page 89: Programare Windows

89

De exemplu, pentru a afla directorul curent pentru unitatea de disc C, am putea face următorul apel:

TCHAR szCurDir [MAX_PATH]; DWORD GetFullPathName (__TEXT(“C:”), MAX_PATH, szCurDir, NULL);

Trebuie să ţinem minte că variabilele de mediu ale unui proces trebuie întotdeauna să fie păstrate în

ordine alfabetică. Din cauza acestei necesităţi, variabilele de mediu de litera de unitate de disc vor trebui să fie plasate la începutul blocului de mediu

Funcţia CreateProcess

Un proces este creat atunci când aplicaţia noastră apelează funcţia CreateProcess:

BOOL CreateProcess( LPCTSTR lpszApplicationName, LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInherithandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPTSTR lpszCurDir, LPSTARTUPINFO lpsiStartupInfo, LPPROCESS_INFORMATION lppiProcInfo);

Pentru descrierea parametrilor consultai MSDN.

Oprirea execuţiei unui proces Un proces se poate termina în trei feluri : 1. Un fir de execuţie din proces apelează funcţia ExitProcess. (Cel mai frecvent) 2. Un fir din alt proces apelează funcţia TerminateProcess (Ar trebui evitat) 3. Toate firele din proces se opresc singure. (Foarte rar)

Funcţia ExitProcess Un proces se termină când unul din firele din proces apelează ExitProcess :

VOID ExitProcess (UINT fuExitCode);

Această funcţie opreşte execuţia procesului şi setează codul de ieşire al procesului la fuExitCode. ExitProcess nu returnează o valoare deoarece procesul s-a terminat. Dacă includem orice cod după apelul funcţiei ExitProcess, acest cod nu se va executa niciodată.

Aceasta este metoda cea mai folosită pentru terminarea unui proces deoarece ExitProcess este apelată atunci când funcţia WinMain returnează în codul C run-time de pornire. Acest cod apelează ExitProcess, transmiţând valoarea returnată din WinMain. Orice alt fir de execuţie care rulează în cadrul procesului îşi va termina la rândul lui execuţia.

Documentaţia Win32 stipulează faptul că un proces nu se termină până când toate firele îşi termină execuţia. Din punctul de vedere al sistemului de operare, acest lucru este adevărat. Totuşi, codul C run-time impune o abordare diferită a aplicaţiei; codul C run-time se asigură că procesul se termină atunci când firul principal de execuţie returnează din funcţia WinMain, chiar şi în cazul în care celelalte fire sunt în execuţie, apelând ExitProcess. Totuşi, dacă apelezi ExitThread în funcţia WinMain în loc să apeleze ExitProcess sau pur şi simplu returnăm, firul principal de execuţie al aplicaţiei îşi va înceta execuţia, dar procesul nu se va termina dacă măcar unul din celelalte fire din proces încă mai rulează.

Funcţia TerminateProcess Un apel al funcţiei TerminateProcess poate şi el opri execuţia unui proces : BOOL TerminateProcess (HANDLE hProcess,UINT fuExitCode);

Page 90: Programare Windows

90

Această funcţie este diferită de ExitProcess în mod foarte important : orice fir poate apela TerminateProcess pentru a termina execuţia unui alt proces sau a propriului proces. Parametrul hProcess reprezintă identificatorul procesului care trebuie să fie terminat. Atunci când procesul se termină, propriul său cod de ieşire devine valoarea pe care am transmis-o ca fiind parametrul fuExitCode.

Folosirea funcţiei TerminateProcess este descurajată; este de preferat să o folosim doar dacă nu putem să oprim execuţia unui proces folosind alte metode. În mod normal, la terminarea execuţiei unui proces, sistemul anunţă orice DLL ataşat procesului faptul că procesul îşi încheie execuţia. Dacă apelăm TerminateProcess, totuşi, sistemul nu anunţă nici un DLL ataşat procesului, lucru care face ca procesul să nu se termine corect. De exemplu, putem scrie un DLL pentru a goli datele într-un fişier pe disc în momentul în care procesul se detaşează de DLL. Această detaşare are loc atunci când o aplicaţie descarcă DLL-ul apelând FreeLibrary. Deoarece DLL-ul nu este notificat de această detaşare, el nu poate să îşi desfăşoare activitatea. Sistemul anunţă DLL-ul atunci când un proces se termină normal sau când apelăm ExitProcess.

Deşi este posibil ca DLL-ul să nu poată să îşi termine acţiunea, sistemul garantează faptul că toată memoria alocată este eliberată, toate fişierele deschise vor fi închise, toate obiectele nucleu vor avea contorul de utilizare decrementat şi toate celelalte obiecte utilizator sau GDI vor fi eliberate indiferent de modul de terminare al procesului.

Ce se întâmplă atunci când firele din proces îşi încheie execuţia Dacă toate firele din proces îşi încheie execuţia (în urma fie a unor apeluri ExitThread, fie

TerminateThread), sistemul de operare presupune că nu mai este nici un motiv pentru a mai păstra spaţiul de adrese al procesului. Aceasta este o presupunere corectă deoarece nu mai există nici un fir în execuţie în spaţiul de adrese al procesului. Atunci când sistemul detectează că firele nu mai rulează, el opreşte procesul. Atunci când se întâmplă acest lucru, codul de ieşire al procesului este setat la codul de ieşire al ultimului fir de execuţie care s-a terminat.

Ce se întâmplă la terminarea unui proces La terminarea unui proces au loc următoarele acţiuni : 1. Orice alte fire din proces sunt oprite. 2. Toate obiectele utilizator sau GDI alocate de către proces sunt eliberate iar obiectele nucleu sunt

închise. 3. Starea procesului obiect nucleu devine semnalată. Alte fire de execuţie din sistem pot să se

suspende până la terminarea procesului. 4. Codul de ieşire al procesului se schimbă de la STIL_ACTIVE la codul transmis de către

ExitProcess sau TerminateProcess. 5. Contorul de utilizare al procesului obiect de nucleu este decrementat cu 1. La terminarea unui proces, procesul obiect nucleu asociat lui nu este eliberat până când toate referinţele

existente la acel obiect nu sunt închise. De asemenea, oprirea unui proces nu face ca procesele sale copil să se oprească şi ele.

Atunci când procesul se termină, codul procesului şi orice alte resurse alocate de către proces sunt scoase din memorie. Totuşi, memoria privată alocată de sistem procesului obiect nucleu nu este eliberată până când contorul de utilizare nu atinge valoarea 0.Acest lucru se poate întâmpla numai dacă toate celelalte procese care au creat sau au deschis identificatori către procesul care este acum terminat anunţă sistemul că nu mai au nevoie să referenţieze procesul. Aceste procese anunţă sistemul apelând CloseHandle.

După ce un proces nu mai rulează, procesul părinte nu poate face mare lucru cu identificatorul procesului. Totuşi, el poate apela GetExitCodeProcess pentru a verifica dacă procesul identificat prin hProcess s-a terminat şi dacă da, să îi verifice codul de ieşire.

BOOL GetExitCodeProcess (HANDLE hProcess, LPWORD lpwdExitCode);

Codul de ieşire este returnat în DWORD-ul lpwdExitCode. Dacă procesul nu s-a terminat atunci când apelăm GetExitCode, funcţia setează acest parametru la valoarea STILL_ACTIVE (definit ca 0x103). Dacă funcţia a fost apelată cu succes, este returnat TRUE.

Page 91: Programare Windows

91

Fire de execuţie În acest capitol vom discuta conceptul de fir de execuţie şi vom descrie modul în care sistemul foloseşte fire de execuţie pentru a executa codul aplicaţiei noastre. La fel ca şi procesele, firele au proprităţi asociate lor şi vom discuta despre unele dintre funcţiile disponibile pentru interogarea şi schimbarea acestor proprietăţi. De asemenea vom examina funcţiile care ne permit să creăm noi fire in sistem. În final vom vorbi despre terminarea frelor de execuţie.

Când creăm un fir de execuţie Un fir de execuţie descrie o cale de execuţie în interiorul unui proces. De fiecare dată când un proces

este iniţializat, sistemul creează un fir de execuţie principal. Acest fir porneşte codul C run-time de pornire, care la rândul lui apelează funcţia noastră WinMain, apoi continuă să se execute până când funcţia WinMain returnează şi codul C run-time de pornire apelează ExitProcess. Pentru multe aplicaţii, firul principal de execuţie este singurul fir de execuţie de care are nevoie aplicaţia. Totuşi, procesele pot crea fire suplimentare pentru a le ajuta să îşi îndeplinească sarcinile. Ideea din spatele creării firelor de execuţie este de a utiliza cât mai mult timp de procesor.

De exemplu, un program care lucrează cu foi de calcul are nevoie să efectueze recalculări pe măsură ce utilizatorul introduce date în celule. Deoarece recalculările unei foi de calcul complexe pot avea de nevoie de câteva secunde pentru a fi efectuate, o aplicaţie bine gândită nu ar trebui să recalculeze foaia de calcul după fiecare schimbare pe care o efectuează utilizatorul. În schimb, recalcularea ar trebui făcută într-un fir de execuţie cu prioritate mai redusă decât firul principal de execuţie. În acest mod, dacă utilizatorul introduce date, firul principal rulează, lucru care înseamnă că sistemul nu va programa nici un timp de calcul firului care efectuează recalcularea. Atunci când utilizatorul se opreşte din scris, firul principal este suspendat, aşteptând o intrare iar firului care face recalcularea îi este dat timp de lucru. De îndată ce utilizatorul începe să scrie din nou, firul principal de execuţie, având prioritate mai mare, trece in faţa firului cu recalcularea. Crearea firelor de execuţie adiţionale face ca aplicaţia să devină „înţelegătoare” cu utilizatorul. De asemenea este destul de uşor de implementat.

Când nu trebuie să creeăm un fir de execuţie Firele sunt incredibil de folositoare şi îşi au locul lor, dar atunci când folosim fire de execuţie putem crea

noi potenţiale probleme în timp ce încercăm să rezolvăm pe cele vechi. De exemplu, să spunem că dezvoltăm a aplicaţie care procesează cuvinte si vrem să permitem ca funcţia de tipărire să ruleze ca propriul fir de execuţie. Acest lucru pare o idee bună deoarece utilizatorul poate imediat să se întoarcă la editarea documentului în timp ce acesta este tipărit. Dar totuşi acest lucru înseamnă că datele din document pot fi schimbate în timp ce documentul este tipărit. Acest lucru creează un nou tip de problemă pe care trebuie să o rezolvăm. Totuşi poate ar fi mai bine să nu lăsăm tipărirea în propriul fir de execuţie; dar această „soluţie” este puţin mai drastică. Ce ar fi dacă am permite utilizatorului să editeze un alt document dar să blocăm documentul care trebuie tipărit astfel încât să nu poată fi modificat până când procesul de tipărire nu s-a terminat ? Cea de-a treia soluţie este : copiem fişierul de tipărit într-un fişier temporar şi lăsăm utilizatorul să îl modifice pe cel original. Atunci când fişierul temporar care conţine documentul s-a terminat de tipărit, putem şterge fişierul temporar.

După cum se observă, firele de execuţie rezolvă unele probleme cu riscul creării altora noi. O altă utilizare greşită a firelor de execuţie poate apărea în dezvoltarea interfeţei utilizator a unei aplicaţii. În majoritatea aplicaţiilor, toate componentele interfeţei utilizator ar trebui să împartă acelaşi fir de execuţie. Dacă creăm o casetă de dialog, de exemplu, nu ar avea nici un sens ca un listbox să fie creat de un fir de execuţie şi un buton de alt fir.

Firele de execuţie în MFC MFC încapsulează fire de execuţie în clasa CWinThread. De asemenea încapsulează evenimente,

mutex-uri si alte obiecte de sincronizare Win32 in clase C++ uşor de folosit. Din punctul de vedere al Windows-ului toate firele de execuţie sunt la fel. Totuşi, MFC-ul distinge

două tipuri de fire de execuţie : fire de execuţie cu interfaţă cu utilizatorul şi fire de execuţie de lucru. Diferenţa dintre cele două este ca primele pot crea ferestre si pot procesa mesaje trimise către aceste ferestre. Cel de-al doilea tip de fire de execuţie efectuează operaţii în fundal care nu primesc o intrare directă de la utilizator şi de aceea nu au nevoie de ferestre si de cozi de mesaje.

Firele de execuţie de lucru sunt ideale pentru efectuarea unor operaţii izolate care pot fi despărţite de restul aplicaţiei si pot fi efectuate în fundal. Un exemplu clasic de fir de execuţie de lucru este cel folosit de un control de animaţie pentru a rula clipuri cu extensia .AVI. Acel fir de execuţie face mai mult decât să deseneze un

Page 92: Programare Windows

92

cadru, se pune in starea de sleep pentru o fracţiune de secundă iar apoi se „trezeşte” şi repetă procesul. În acest mod el adaugă doar puţin la încărcătura de lucru a procesorului deoarece îşi petrece majoritatea vieţii suspendat între cadre şi totuşi asigură un serviciu de valoare. Acesta este un exemplu foarte bun de proiectare cu multiple fire de execuţie deoarece firul de execuţie din fundal primeşte de făcut o anumită operaţie şi apoi îi este permis să repete acea operaţie până când firul principal de execuţie anunţă că operaţia se poate termina.

Funcţia de fir de execuţie în SDK Toate firele de execuţie îşi încep execuţia la o funcţie care trebuie să o precizăm în prealabil. Funcţia

trebuie să aibă următorul prototip :

DWORD WINAPI YourThreadFunc (LPVOID lpvThreadParm);

La fel ca şi WinMain, funcţia nu este apelată chiar de sistemul de operare. În schimb, acesta apelează o funcţie internă, care nu face parte din funcţiile C run-time, conţinută în KERNEl32. Putem să o numim StartOfThread; numele real nu este important. Această funcţie arată astfel : void StartOfThread ( LPTHREAD_START_ROUTINE lpStartAddr,

LPVOID lpvThreadParm) { try

{ DWORD dwThreadExitCode = lpStartAddr (lpvThreadParm); ExitThread (dwThreadExitCode); }

__except (UnhandledExceptionFilter (GetExceptionInformation())) { ExitProcess (GetExceptionCode()); } }

Funcţia StartOfThread pune în mişcare următoarele acţiuni : 1. Este setat un frame structurat de tratare a erorilor în jurul funcţiei noastre a firului de execuţie

astfel încât orice excepţie care apare în timpul execuţiei firului va avea o tratare implicită din partea sistemului. 2. sistemul apelează funcţia noastră a firului de execuţie, transmiţându-i parametrul pe 32 biţi

lpvThreadParm pe care l-am transmis funcţiei CreateThread. 3. Atunci când funcţia firului returnează, funcţia StartOfThread apelează ExitThread, transmiţându-i

valoarea de retur a funcţiei firului. Contorul de utilizare al obiectului nucleu este decrementat iar firul îşi întrerupe execuţia.

4. Dacă firul determină apariţia unei excepţii care nu este tratată, atunci frame-ul SEH setat de funcţia StartOfThread va trata excepţia. În mod normal, acest lucru înseamnă că o căsuţă de dialog este prezentată utilizatorului iar atunci când acesta închide această căsuţă, funcţia StartOfThread apelează ExitProcess pentru a termina execuţia întregului proces, nu doar firul în cauză.

Firul principal de execuţie al unui proces începe executând funcţia StartOfThread a sistemului. Aceasta apelează la rândul ei codul C run-time de startup, care apelează funcţia noastră WinMain. Codul C run-time de startup, totuşi, nu se întoarce în funcţia StartOfThread deoarece codul de startup apelează în mod explicit ExitProcess.

Funcţia firului de execuţie în MFC O funcţie de fir de execuţie este o funcţie cu apel invers, aşa că ea trebuie să fie o funcţie statică

membră a unei clase sau o funcţie globală declarată în afara unei clase. Prototipul ei este :

UINT ThreadFunc (LPVOID pParam);

pParam este o valoare pe 32 de biţi a cărei valoare este egală cu parametrul pParam transmis funcţiei AfxBeginThread. Foarte des, pParam este adresa unei structuri de date definită de aplicaţie care conţine informaţia transmisă firului de execuţie de lucru de către firul care l-a creat. De asemenea poate fi o valoare scalară, un handle sau chiar un pointer către un obiect. Este posibil să folosim aceeaşi funcţie de fir de execuţie pentru două sau mai multe fire de execuţie dar trebuie să fim atenţi la problemele de reintrare cauzate de variabilele globale şi statice.

Page 93: Programare Windows

93

Atâta timp cât variabilele (şi obiectele) pe care le foloseşte un fir de execuţie sunt create în stivă, problemele de reintrare nu mai apar deoarece fiecare fir de execuţie are stiva sa proprie.

Crearea unul fir de execuţie de lucru în MFC Cea mai bună modalitate de a lansa un fir de execuţie într-o aplicaţie MFC este apelul

AfxBeginThread. MFC defineşte 2 versiuni diferite de AfxBeginThread : una care porneşte un fir de execuţie cu interfaţă cu utilizatorul şi alta care porneşte un fir de execuţie de lucru. Codul sursă pentru ambele este găsit în Thrdcore.cpp. Nu trebuie să folosim funcţia Win32::CreateThread pentru a crea un fir de execuţie într-un program MFC decât doar dacă firul nu foloseşte MFC. AfxBeginThread nu este nici pe departe un wrapper pentru funcţia CreateThread ; în plus faţă de lansarea unui nou fir de execuţie, ea iniţializează informaţia internă de stare folosită de cadrul de lucru, efectuează verificări la diferite momente din timpul procesului de creare şi se asigură că funcţiile din biblioteca run-time C sunt accesate într-o modalitate sigura din punctul de vedere al firelor de execuţie.

AfxBeginThread facilitează crearea unui fir de execuţie de lucru. Atunci când este apelată, ea creează un nou obiect de tip CWinThread, lansează un fir de execuţie, îl ataşează obiectului CWinThread şi returnează un pointer CWinThread. Declaraţia

CWinThread* pThread = AfxBeginThread (ThreadFunc, &threadInfo);

porneşte un fir de execuţie de lucru şi îi transmite structura de date predefinită de aplicaţie (&ThreadInfo) care conţine intrarea către firul de execuţie. ThreadFunc este funcţia de fir de execuţie – funcţia care este executată atunci când firul de execuţie începe să se execute. O funcţie de fir de execuţie foarte simplă care se învârte într-o buclă care „mănâncă” din procesor şi apoi se termină arată în felul următor : UINT ThreadFunc (LPVOID pParam) {

UINT nlterations = (UINT) pParam; for(UINT i=0; i<nlterations; i++);

return 0; }

În acest exemplu, valoarea transmisă în pParam nu este un pointer, ci un UINT normal. Funcţiile de fir de execuţie sunt descrise mai în detaliu în următoarea secţiune.

Funcţia AfxBeginThread acceptă patru parametri opţionali care specifică prioritatea firului, mărimea stivei, indicatorii de creere şi atributele de securitate. Prototipul complet al funcţiei este:

CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc,

LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES 1pSecurityAttrs = NULL)

nPriority defineşte prioritatea firului de execuţie. Fire de execuţie cu prioritate mare sunt programate

pentru procesor în faţa celor cu prioritate mai scăzută, dar în practică, chiar şi fire de execuţie cu prioritate foarte scăzută de obicei au tot timpul de procesor de care au nevoie. nPriority nu reprezintă un nivel absolut de prioritate. El precizează un nivel de prioritate relativ faţă de nivelul de prioritate al procesului caruia îi aparţine firul. Implicit prioritatea este THREAD_PRIORITY_NORMAL, care atribuie firului de execuţie aceeaşi prioritate ca şi a procesului care îl deţine. Nivelul de prioritate se poate schimba în orice moment cu comanda CWinThread::SetThreadPriority.

Parametrul nStackSize transmis către AfxBeginThread precizează mărimea maximă a stivei a firului. In mediul Win32, fiecare fir de execuţie primeşte stiva sa proprie. Valoarea 0 implicită a variabilei nStackSize permite stivei să atingă mărimea maximă de 1MB. Aceasta nu înseamnă ca fiecare fir de execuţie are nevoie de minim 1 MB de memorie; înseamnă ca fiecărui fir de execuţie i se alocă 1 MB din spaţiul de adrese din spaţiul de 4GB în care se execută aplicaţiile Windows pe 32 de biţi. Memoria nu este alocată spaţiului de adrese al stivei până când nu este necesar, aşa că majoritatea stivelor nu folosesc niciodată mai mult de câţiva KB din memoria fizică. Precizând o limită pentru mărimea stivei permite sistemului de operare să „prindă” funcţiile care se apelează

Page 94: Programare Windows

94

recursiv la infinit şi care eventual consumă stiva. Limita standard de 1 MB este suficientă pentru aproape toate aplicaţiile.

dwCreateFlags poate fi doar una din două valori. Valoarea implicită 0 precizează sistemului să execute firul imediat. Dacă este specificat în schimb CREATE_SUSPENDED, firul porneşte în starea suspendată şi nu îşi începe execuţia până când alt fir de execuţie (de obicei firul care l-a creat ) apelează CWinThread::ResumeThread asupra firului suspendat, ca mai jos :

CWinThread* pThread = AfxBeginThread (ThreadFunc, SthreadInfo,

THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pThread->ResumeThread(); //Porneşte firul de execuţie.

Uneori este util să creăm un fir şi să îi amânăm execuţia până mai târziu. Indicatorul

CREATE_SUSPENDED este mecanismul prin care putem declara o execuţie cu întârziere. Ultimul parametru din lista de argumente a funcţiei AfxBeginThread, IpSecurityAttrs, este un pointer

către o structură SECURITY_ATTRIBUTES care conţine atributele de securitate ale firului de execuţie nou creat şi care de asemenea precizează sistemului dacă procesele copil ar trebui să moştenească handler-ul firului de execuţie. Valoarea implicită NULL atribuie noului fir de execuţie aceleaşi proprietăţi ca şi a firului care l-a creat.

Crearea unui fir de execuţie cu interfaţă cu utilizatorul în MFC Crearea unui astfel de fir de execuţie este diferită de procesul de creare a unui fir de execuţie de lucru.

Un fir de lucru este definit de funcţia sa iar comportarea unui fir de execuţie cu interfaţă este dată de o clasă care se poate crea dinamic şi care este derivată din CWinThread, asemănător unei clase de aplicaţie derivată din CWinApp. Clasa de mai jos creează o fereastră de cadru de tip top-level care se închide singură când este apăsat butonul stânga al mouse-ului. Închiderea ferestrei opreşte şi firul de execuţie deoarece funcţia CWnd::OnNcDestroy trimite un mesaj WM_QUIT către coada de mesaje a firului de execuţie. Trimiterea acestui mesaj WM_QUIT către un fir de execuţie principal opreşte firul de execuţie şi termină aplicaţia.

// Clasa CUIThread class CUIThread : public CWinThread DECLARE_DYNCREATE (CUIThread) public: virtual BOOL Initlnstance (); IMPLEMENT_DYNCREATE (CUIThread, CWinThread) BOOL CUIThread::Initlnstance () {

m_pMainWnd = new CMainWindow m_pMainWnd->ShowWindow (SW_SHOW); m_pMainWnd->UpdateWindow (); return TRUE;

} // Clasa CMainWindow class CMainWindow : public CFrameWnd { public: CMainWindow (); protected: afx_msg void OnLButtonDown (UINT, CPoint); // Se declară harta de mesaje DECLARE_MESSAGE_MAP () }; BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)

ON_WM_LBUTTONDOWN ()

Page 95: Programare Windows

95

END_MESSAGE_MAP () CMainWindow::CMainWindow () {

Create (NULL, _T ("UI Thread Window")); } void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) {

PostMessage (WM_CLOSE, 0, 0); }

Trebuie remarcat parametrul SW_SHOW transmis către ShowWindow în locul parametrului normal m_nCmdShow. Acesta este un membru al CWinApp, aşa că atunci când creăm o fereastră top-level de la un fir de execuţie cu interfaţă este de datoria noastră să precizăm starea iniţială a ferestrei.

Putem lansa un CUIThread apelând o formă a funcţiei AfxBeginThread care acceptă un pointer CRuntimeClass către clasa firului de execuţie :

CWinThread* pThread = AfxBeginThread(RUNTIME_CLASS (CUIThread));

Versiunea funcţiei AfxBeginThread pentru fire de execuţie cu interfaţă acceptă aceiaşi patru parametri opţionali ca şi versiunea pentru firele de lucru, dar nu acceptă o valoare pParam. Odată pornit, un fir de execuţie cu interfaţă rulează asincron respectând firul care l-a creat.

Suspendarea şi repornirea firelor de execuţie Un fir de execuţie în MFC care rulează poate fi suspendat prin apelul CWinThread::SuspendThread şi

poate si repornit cu CWinThread::ResumeThread. Un fir de execuţie poate apela funcţia SuspendThread pe el însuşi sau un alt fir poate face acest apel pentru el. Totuşi, un fir suspendat nu poate apela ResumeThread pentru a se reporni; altcineva trebuie să facă acest apel. Un fir de execuţie suspendat nu consumă aproape deloc din timpul procesorului şi impune în sistem o suprasarcină practic nulă.

Pentru fiecare fir de execuţie Windows-ul păstrează un contor de suspendare care este incrementat de SuspendThread şi decrementat de ResumeThread. Un fir de execuţie este programat pentru timpul procesorului doar atunci când contorul său de suspendare este 0. Dacă funcţia SuspendThread este apelată de două ori succesiv, ResumeThread trebuie apelată de asemenea de două ori. Un fir de execuţie creat fără atributul CREATE_SUSPENDED are iniţial contorul de suspendare egal cu 0. Un fir creat cu atributul CREATE_SUSPENDED se porneşte având iniţial contorul de suspendare iniţializat cu valoarea 1. Atât SuspendThread cât şi ResumeThread returnează contorul de suspendare anterior, aşa că pentru a fi siguri că un fir de execuţie va fi repornit indiferent de cat este de mare valoarea contorului de suspendare trebuie să apelăm ResumeThread în mod repetat până când returnează valoarea 1. ResumeThread returnează 0 dacă firul nu este apelat sau dacă nu este suspendat.

În viaţa reală, o aplicaţie trebuie să fie atentă atunci când apelează SuspendThread deoarece nu avem nici o idee despre ce poate acesta să facă atunci când încercăm să îi reluam execuţia. Dacă firul încearcă să aloce memorie dintr-un heap, de exemplu, firul va trebui să se blocheze pe heap. Pe măsură ce alte fire încearcă să acceseze heap-ul, execuţia lor va fi oprită până când primul fir îşi reia execuţia. SuspendThread este neprimejdios doar atunci când ştim ce este exact firul ţintă (sau ce face) şi luăm masuri extreme pentru a evita problemele sau blocajele cauzate de suspendarea firului.

În SDK, un fir de execuţie poate fi suspendat prin apelul funcţiei SuspendThread şi apoi îi putem relua execuţia cu ajutorul funcţiei ResumeThread. Ambele funcţii sunt asemănătoare cu funcţiile similare din MFC.

Punerea unui fir de execuţie în starea de „sleep” Un fir de execuţie poate să fie suspendat pentru o anumită perioadă de timp prin apelul funcţiei API

::Sleep. Un fir suspendat nu consumă din timpul procesorului. Declaraţia ::Sleep (10000) suspendă firul de execuţie curent pentru 10 secunde.

O utilizare pentru ::Sleep este pentru a implementa fire de execuţie ale căror acţiuni sunt înnăscut bazate pe timp, ca de exemplu firul de execuţie din fundalul unui control de animaţie sau un fir care muta acele unui ceas. ::Sleep poate fi folosită şi pentru a elibera resturile unor alte perioade de timp ale firului de execuţie.

Page 96: Programare Windows

96

Declaraţia ::Sleep(0) suspendă firul curent şi permite planificatorului să ruleze alte fire de aceeaşi prioritate sau de prioritate mai ridicată. Dacă nici unul din astfel de fire nu aşteaptă execuţia, apelul funcţiei returnează imediat şi firul curent îşi continua execuţia. În Windows NT 4.0 sau mai sus se poate comuta pe alt fir de execuţie prin apelul funcţiei SwitchToThread. Vom folosi ::Sleep(0) dacă codul pe care îl scriem trebuie să meargă pe toate platformele Win32.

Dacă scriem o aplicaţie care foloseşte mai multe fire de execuţie pentru a desena pe ecran, câteva declaraţii ::Sleep(0) plasate strategic pot face adevărate minuni pentru calitatea ieşirii. Să presupunem că realizăm o animaţie cu mişcarea a patru obiecte şi atribuim fiecărui obiect firul său propriu de execuţie. Fiecare fir este responsabil cu mişcarea unui obiect pe ecran. Dacă doar rulăm fiecare fir într-o buclă şi îi dăm posibilitatea să acapareze tot timpul de procesor pe care îl poate lua, este foarte posibil ca mişcarea obiectelor să fie neregulată. Dar dacă lăsăm fiecare fir să îşi mute obiectul câţiva pixeli şi apoi apelăm ::Sleep(0), animaţia va fi mult mai lină.

Valoarea pe care o transmitem funcţiei ::Sleep nu garantează că firul va fi repornit exact la momentul terminării intervalului de timp. Transmiterea către ::Sleep a valorii 10000 ne garantează că firul va fi repornit după cel puţin 10 secunde. Firul va putea să rămână suspendat 10 secunde, sau chiar 20, acest lucru fiind în funcţie de sistemul de operare. În practică, firul de obicei va fi repornit cu o fracţiune de secundă după terminarea intervalului de timp, dar nu există nici o garanţie. La momentul actual nu există în nici o versiune de Windows o metodă de a suspenda un fir de execuţie pentru o perioadă precisă de timp.

Schimbarea către un alt fir de execuţie Sistemul oferă o funcţie numită SwitchToThread care permite rularea unui alt fir de execuţie dacă

există :

BOOL SwitchToThread ();

Atunci când apelăm această funcţie, sistemul verifică dacă există un fir care este privat de timp de procesor. Dacă nu este găsit nici un astfel de fir, funcţia SwitchToThread returnează imediat. Dacă există un astfel de fir, funcţia planifică firul (care este posibil să aibă o prioritate mai scăzută decât firul care a apelat funcţia SwitchToThread). Acestui fir îi este permis să ruleze pentru o cuantă de timp iar după aceea planificatorul funcţionează normal.

Această funcţie permite unui fir de execuţie care doreşte accesul la o resursă să forţeze un fir cu o prioritate mai mică care deţine acea resursă să elibereze resursa. Dacă nici un fir nu poate fi executat după apelul SwitchToThread, funcţia returnează FALSE; în caz contrar, ea returnează o valoare nenulă.

Apelul SwitchToThread este similar cu apelul funcţiei Sleep cu parametrul având valoarea 0. Diferenţa este că SwitchToThread permite execuţia firelor de execuţie cu prioritate mai mică. Sleep replanifică firul apelant chiar dacă există fire cu prioritate mai redusă care sunt private de timp de procesor.

Windows 98 nu are o implementare folositoare pentru această funcţie.

Terminarea unui fir de execuţie în SDK Asemenea unui proces, un fir poate fi oprit în trei feluri : 1. Firul se opreşte singur apelând funcţia ExitThread. (cea mai întâlnită metodă) 2. Un fir din acelaşi proces sau din alt proces apelează TerminateThread. (de evitat) 3. Procesul care conţine firul se opreşte.

Funcţia ExitThread Un fir se opreşte atunci când apelează ExitThread :

VOID ExitThread (UINT fuExitCode);

Această funcţie opreşte firul şi setează codul de ieşire al firului la fuExitCode. Funcţia ExitThread nu returnează o valoare deoarece firul nu s-a oprit.

Această metodă este cea mai întâlnită deoarece ExitThread este apelat atunci când funcţia de fir returnează către funcţia internă a sistemului StartOfThread. Aceasta apelează la rândul ei ExitThread, transmiţându-i valoarea returnată din funcţia noastră de fir.

Funcţia TerminateThread Un apel TerminateThread termină de asemenea firul :

Page 97: Programare Windows

97

BOOL TerminateThread (HANDLE hThread, DWORD dwExitCode);

Funcţia setează firul identificat de parametrul hThread şi setează codul de ieşire la dwExitCode. Funcţia TerminateThread există astfel încât să putem să oprim un fir atunci când nu mai răspunde. Este recomandat să o folosim doar ca ultim mijloc.

Atunci când un fir „moare” prin apelul ExitThread, stiva firului este distrusă. Totuşi, dacă firul este oprit prin TerminateThread, sistemul nu distruge stiva până când procesul care este proprietarul firului se termină, deoarece alte fire de execuţie s-ar putea să folosească pointeri care referenţiază date conţinute pe stiva firului care s-a oprit. Dacă aceste alte fire încearcă să acceseze stiva, apare o violare de acces.

Atunci când firul se termină, sistemul anunţă orice DLL-uri ataşate procesului proprietar al firului că firul se opreşte. Dacă apelăm însă TerminateThread, sistemul nu anunţă nici un DLL ataşat procesului, adică procesul nu va fi închis corect. De exemplu, un DLL poate fi scris pentru a goli un buffer într-un fişier pe disc atunci când firul se detaşează de DLL. Deoarece DLL-ul nu este anunţat de sistem de detaşare atunci când folosim TerminateThread, DLL-ul nu poate efectua operaţia sa normală de curăţire.

Oprirea unui fir de execuţie în MFC Odată ce un fir de execuţie este pornit, el poate fi oprit în două moduri. Un fir de execuţie de lucru se

opreşte atunci când funcţia de fir execută o declaraţie return sau când orice altă funcţie oriunde în fir apelează AfxEndThread. Un fir de execuţie cu interfaţă este oprit atunci când este trimis un mesaj WM_QUIT către coada de mesaje sau când firul însuşi apelează AfxEndThread. Un fir de execuţie poate adăuga un mesaj WM_QUIT la el însuşi cu funcţia API ::PostQuitMessage. AfxEndThread, ::PostQuitMessage şi return acceptă toate un cod de ieşire pe 32 de biţi care poate fi extras cu ::GetExitCodeThread după oprirea firului de execuţie. Următoarea declaraţie copie codul de ieşire al firului indicat de pThread în dwExitCode:

DWORD dwExitCode; ::GetExitCodeThread (pThread->m_hThread, &dwExitCode);

Dacă funcţia ::GetExitCodeThread este apelată pentru un fir care este în execuţie , atunci atribuie

variabilei dwExitCodeThread valoarea STILL_ACTIVE(0x103). În acest exemplu, identificatorul firului transmis către funcţia ::GetExitCodeThread este extras din data membru m_hThread a obiectului CWinThread care încapsulează firul de execuţie. Ori de câte ori avem un CWinThread şi vrem să apelăm o funcţie API care are nevoie de un handle de fir, putem obţine acest handle din m_hThread.

Ce se întâmplă atunci când procesul se opreşte Funcţiile ExitProcess sau TerminateProcess discutate anterior termină de asemenea execuţia firelor.

Diferenţa este că aceste funcţii opresc toate firele conţinute în procesul care se termină.

Ce se întâmplă la oprirea unui fir de execuţie Următoarele acţiuni au loc la oprirea unui fir de execuţie : 1. Toţi identificatorii obiectelor utilizator aparţinând firului sunt eliberaţi. În Win32, majoritatea

obiectelor sunt proprietatea procesului care conţine firul care a creat obiectele. Totuşi, două obiecte utilizator pot fi proprietatea unui fir de execuţie : ferestre şi legături. Atunci când firul care a creat aceste obiecte îşi termină execuţia, sistemul distruge în mod automat obiectele. Celelalte obiecte sunt distruse doar atunci când procesul proprietar se termină.

2. Starea obiectului nucleu fir de execuţie devine semnalată. 3. Codul de ieşire al firului se schimbă de la STILL_ACTIVE la codul de terminare al firului. 4. Dacă firul este ultimul fir activ din proces, procesul se opreşte. 5. Contorul de utilizare al obiectului nucleu fir este decrementat cu 1. Atunci când firul se termină, obiectul nucleu fir asociat lui nu devine automat liber până când toate

referinţele rămase la obiect nu sunt închise. De îndată de un fir nu mai rulează, orice alt fir din sistem nu mai are ce face cu identificatorul primului

gir. Totuşi, aceste alte fire pot apela GetExitCodeThread pentru a verifica dacă firul identificat prin hThread s-a terminat şi, în caz afirmativ, să îi determine codul său de ieşire.

BOOL GetExitCodeThread (HANDLE hThread, LPDWORD lpdwExitCode);

Page 98: Programare Windows

98

Valoarea de ieşire este returnată în DWORD-ul la care pointează lpdwExitCode. Dacă firul nu este terminat atunci când este apelat GetExitCodeThread, funcţia scrie în DWORD identificatorul STILL_ACTIVE ( definit ca 0x103). Dacă funcţia reuşeşte este returnat TRUE.

Fire de execuţie şi sincronizarea firelor de execuţie

În mediul Microsoft Win32, fiecare aplicaţie rulată reprezintă un proces şi fiecare proces conţine unul sau mai multe fire de execuţie. Un fir de execuţie este o cale de execuţie prin codul unui program şi în plus un set de resurse (stive, registre de stare şi aşa mai departe) atribuite de către sistemul de operare.

Multiplicarea firelor de execuţie nu este la îndemâna oricui. Aplicaţiile cu mai multe fire de execuţie sunt dificil de scris şi de depanat din cauză că paralelismul firelor de execuţie concurente adaugă un nivel suplimentar de complexitate codului programului. Folosite corect insă, firele de execuţie multiple pot să îmbunătăţească dramatic timpul de răspuns al unei aplicaţii. Un procesor de cuvinte care face verificarea de sintaxă într-un fir de execuţie dedicat, de exemplu, poate continua să proceseze mesajele în firul principal de execuţie şi îi dă posibilitatea utilizatorului să continue să lucreze în timp ce verificarea de sintaxă îşi continua rularea. Ce face mai dificil să scriem un procesor de text cu mai multe fire de execuţie este faptul că firul de execuţie care efectuează verificarea de sintaxă va trebui in mod inevitabil să îşi sincronizeze acţiunile cu alte fire de execuţie din cadrul aplicaţiei. Majoritatea programatorilor au fost condiţionaţi să gândească în termeni sincroni la codul lor : funcţia A apelează funcţia B, funcţia B efectuează o operaţie şi returnează în A şi aşa mai departe. Dar firele de execuţie sunt asincrone prin natura lor. Într-o aplicaţie cu mai multe fire de execuţie, trebuie să ne gândim la ce se întâmplă dacă, de exemplu, două fire de execuţie apelează funcţia B în acelaşi moment sau un fir de execuţie citeşte o variabilă în timp ce celălalt o scrie. Dacă funcţia A lansează funcţia B într-un fir de execuţie separat, trebuie să anticipăm de asemenea problemele care ar putea să apară dacă funcţia A continuă să ruleze in timpul execuţiei funcţiei B. De exemplu, este uzual să transmitem adresa unei variabile creată în stivă în funcţia A către funcţia B pentru procesare. Dar funcţia B este într-un alt fir de execuţie, variabila s-ar putea să nu mai existe atunci când funcţia B ajunge să o proceseze. Chiar şi cel mai inofensiv cod la prima vedere poate fi corupt în mod fatal atunci când implică două fire de execuţie diferite.

Fire de execuţie, procese şi priorităţi Planificatorul este acea componentă a sistemului de operare care decide care fire de execuţie rulează,

când şi pentru cât timp. Planificarea firelor de execuţie este o sarcină complexă al cărei obiectiv este de a împărţi timpul procesorului între firele de execuţie cât mai eficient cu putinţă pentru a crea iluzia că toate rulează în acelaşi timp. Pe maşinile cu mai multe procesoare, Windows NT şi Windows 2000 rulează în realitate două sau mai multe fire de execuţie în acelaşi timp distribuind firele între procesoare folosind o schemă numită procesare multiplă simetrică, sau SMP. Windows 95 şi Windows 98 nu sunt sisteme de operare SMP, astfel încât ele distribuie toate firele aceluiaşi procesor chiar şi pe calculatoarele cu mai multe procesoare.

Planificatorul foloseşte o varietate de tehnici pentru a îmbunătăţi performanţa procesării multiple şi pentru a încerca să asigure faptul că fiecare fir de execuţie primeşte suficient timp de procesor. În cele din urmă, totuşi, decizia care fir să se execute mai întâi este dată de prioritatea firelor. La orice moment dat, fiecare fir are atribuită un nivel de prioritate de la 0 la 31, cu un număr mai mare indicând o prioritate mai mare. Dacă un fir cu prioritatea 11 aşteaptă execuţia şi toate celelalte fire care aşteaptă execuţia au priorităţi mai mici decât 11, firul cu prioritatea 11 rulează primul. Dacă două fire au aceeaşi prioritate, planificatorul îl execută pe cel care a fost executat mai de mult timp. Atunci când perioada de timp, sau cuantum, acordată firului expiră, celălalt fir cu prioritate egală cu primul este executat dacă toate celelalte fire active au priorităţi mai mici. Ca o regulă generală, planificatorul întotdeauna acordă următoarea perioadă de timp firului în aşteptare cu cea mai mare perioadă.

Acest lucru înseamnă că firele cu prioritate scăzută nu sunt executate niciodată ? Nici gând. În primul rând, să ne aducem aminte că Windows-ul este un sistem de operare bazat pe masaje. Daca un fir de execuţie apelează ::GetMessage şi coada sa de mesaje este goală, firul se blochează până când devine disponibil un mesaj. Acest lucru dă şansa firelor cu prioritate redusă de a fi executate. Majoritatea firelor cu interfaţă îşi petrec mare parte din timpul lor blocate pe coada de mesaje, aşa că atât timp cât un fir de lucru cu prioritate ridicată nu monopolizează procesorul, chiar şi firele cu prioritate redusă practic beneficiază de tot timpul de procesor de care au nevoie. (Un fir de execuţie de lucru nu se blochează pe coada de mesaje deoarece nu procesează mesaje.)

Planificatorul face mai multe trucuri cu nivelele de prioritate pentru a îmbunătăţi disponibilitatea sistemului de răspuns şi pentru a reduce pericolul ca un fir oarecare să nu primească timp de procesor. Dacă un fir cu prioritatea 7 stă mai mult timp fără să primească timp de procesor, planificatorul poate mari temporar prioritatea firului la 8, 9 sau chiar mai mare pentru a-i da şansa de a se executa. Windows NT 3.x creşte priorităţile firelor care

Page 99: Programare Windows

99

se execută în prim-plan pentru a îmbunătăţi timpul de răspuns al aplicaţiei în care lucrează utilizatorul, iar Windows NT 4.0 Workstation creşte perioada de timp acordată acestor fire. Windows-ul foloseşte o tehnică numită moştenirea priorităţii pentru a preveni blocarea pentru prea mult timp a firelor cu prioritate ridicată pe obiecte de sincronizare deţinute de fire cu prioritate scăzută. De exemplu, dacă un fir de execuţie cu prioritatea 11 încearcă să revendice un mutex deţinut de un fir cu prioritatea 4, planificatorul poate să mărească prioritatea celui de-al doilea fir pentru ca mutex-ul să se elibereze mai devreme.

De fapt cum sunt atribuite priorităţile prima dată ? Atunci când apelăm AfxBeginThread sau CWinThread::SetThreadPriority noi specificăm prioritatea relativă a firului. Sistemul de operare combină nivelul de prioritate relativ cu clasa de prioritate a procesului tată al firului pentru a calcula un nivel de prioritate de bază pentru fir. Nivelul real de prioritate al firului, un număr între 0 şi 31, poate varia continuu deoarece prioritatea poate creşte şi scădea. Nu putem controla creşterea (şi nici nu am vrea să facem acest lucru chiar dacă am putea să îl facem), dar putem să controlăm nivelul de prioritate de bază setând clasa de prioritate a procesului şi nivelul de prioritate relativ al firului.

Prioritatea proceselor şi a firelor de execuţie

Clasele de prioritate ale proceselor Majoritatea proceselor îşi încep existenţa cu clasa de prioritate NORMAL_PRIORITY_CLASS. O

dată pornite însă, un proces să îşi schimbe prioritatea apelând ::SetPriorityClass, care acceptă ca argumente un identificator de proces (care poate fi obţinut cu apelul ::GetCurrentProcess) şi unul din specificatorii din tabelul următor :

Clasele de prioritate ale proceselor

Clasa de prioritate Descriere IDLE_PRIORITY_CLASS Procesul rulează doar când sistemul este liber, de exemplu

când nici un alt fie de execuţie nu aşteaptă să fie executat. NORMAL_PRIORITY_CLASS Clasa de prioritate implicită. Procesul nu are nevoi

speciale de planificare. HIGH_PRIORITY_CLASS Procesul primeşte o prioritate mai mare ca un proces

IDLE_PRIORITY_CLASS sau NORMAL_PRIORITY_CLASS.

REALTIME_PRIORITY_CLASS Procesul are cea mai mare prioritate posibilă, mai ridicată chiar decât HIGH_PRIORITY_CLASS.

Majoritatea aplicaţiilor nu au nevoie să îşi schimbe clasa de prioritate. Procesele cu priorităţile HIGH_PRIORITY_CLASS sau REALTIME_PRIORITY_CLASS pot afecta timpul de răspuns al sistemului şi pot chiar întârzia activităţi sistem vitale, cum ar golirea zonei de cache a harddiskului. O folosire corectă a clasei HIGH_PRIORITY_CLASS este pentru aplicaţiile sistem care rămân ascunse majoritatea timpului dar produc o fereastră atunci când are loc un eveniment de intrare. Aceste aplicaţii impun o sarcină foarte mică asupra sistemului atât timp cât sunt blocate aşteptând o intrare, dar o dată ce apare o intrare ele primesc o prioritate mai mare decât restul aplicaţiilor. Clasa REALTIME_PRIORITY_CLASS este furnizată în primul rând pentru programele care primesc date în mod real şi care trebuie să aibă parte de partea leului din timpul procesorului pentru a funcţiona corect. Clasa IDLE_PRIORITY_CLASS este ideală pentru protecţia ecranului, monitoare de sistem sau alte aplicaţii cu prioritate redusă care sunt proiectate să opereze neobstructiv în fundal.

Algoritmul de planificare are un efect important asupra tipurilor de aplicaţii pe care le pot rula utilizatorii. Încă de la început, dezvoltatorii de la Microsoft şi-au dat seama că vor trebui să modifice algoritmul de planificare în timp pe măsură ce scopul calculatoarelor se va schimba. Dar dezvoltatorii de software au nevoie să scrie programe acum şi Microsoft garantează că programele vor rula şi în versiuni ulterioare ale sistemului. Cum poate Microsoft să schimbe modulde funcţionare al sistemului şi totuşi să păstreze softul în stare de funcţionare ? Iată câteva răpunsuri :

• Microsoft nu oferă documentaţia completă a planificatorului; • Microsoft nu permite aplicaţiilor să beneficieze de toate avantajele planificatorului; • Microsoft ne spune că algoritmul de planificare poate fi schimbat astfel incât noi programăm

defensiv.

Page 100: Programare Windows

100

API-ul Windows oferă un strat abstract pentru planificatorul sistemului, astfel încât nu avem acces direct la planificator. În schimb, noi apelăm funcţii Windows care interpretează parametrii noştri în funcţie de versiunea sistemului de operare pe care rulăm.

Atunci când proiectăm o aplicaţie, trebuie să ne gândim la alte aplicaţii pe care utilizatorul le va rula împreună cu aplicaţia noastră. Apoi va trebui să alegem o clasă de prioritate bazată pe viteza de răspuns pe care dorim să o aibă firele de execuţie din această aplicaţie.

O dată ce alegem o clasă de prioritate, nu trebuie să ne mai gândim cum interacţionează aplicaţia noastră cu alte aplicaţii si trebuie să ne concentrăm asupra firelor din aplicaţie. Windows suportă şapte priorităţi relative pentru fire de execuţie : idle, lowest, below normal, normal, above normal, highest şi time-critical. Aceste priorităţi sunt relative la clasa de prioritate a procesului. Din nou, majoritatea firelor au prioritatea normal.

Prioritatea relativă a firelor de execuţie Descriere

Time-critical Firul rulează la 31 pentru clasa de prioritate real-time şi la 15 pentru toate celelalte clase de prioritate.

Highest Firul rulează cu două nivele peste normal. Above normal Firul rulează cu un nivel peste normal. Normal Firul rulează normal pentru clasa de prioritate a

procesului. Below normal Firul rulează cu un nivel sub normal. Lowest Firul rulează cu două nivele sub normal. Idle Firul rulează cu prioritatea 16 pentru clasa de

prioritate real-time şi cu 1 pentru celelalte clase de prioritate.

În concluzie, procesul face parte dintr-o clasă de prioritate şi în cadrul acestuia atribuim priorităţi relative firelor de execuţie. Prioritatea absolută diferă de la un sistem de operare la altul. La Windows 2000 ea se calculează în modul următor:

Clasa de prioritate a procesului Prioritatea reltivă a firului Idle Below

Normal Normal Above

Normal High Real-

Time Time-critical 15 15 15 15 15 31 Highest 6 8 10 12 15 26 Above normal 5 7 9 11 14 25 Normal 4 6 8 10 13 24 Below normal 3 5 7 9 12 23 Lowest 2 4 6 8 11 22 Idle 1 1 1 1 1 16

După cum se observă nu există nivelul de prioritate 0, care este rezervat. De asemenea, nivelurile 17,

18, 19, 20, 21, 27, 28, 29 sau 30 pot fi obţinute doar dacă scriem un driver care rulează în mod nucleu. O aplicaţie utilizator nu poate obţine aceste priorităţi. De asemenea trebuie observat că un fir dintr-o clasa de prioritate real-time nu poate avea o prioritate mai mică de 16. De asemenea, un fir dintr-o clasă non-real-time nu poate avea o prioritate mai mare de 15.

Procesele nu sunt niciodată planificate, doar firele pot fi planificate. Clasa de prioritate a proceselor este o abstracţiune introdusă de Microsoft pentru a ne îndepărta de funcţionarea internă a planificatorului.

Un fir de execuţie cu prioritate ridicată ar trebui să execute mai puţine instrucţiuni, având acces la procesor aproape imediat, iar cele cu prioritate rămân planificabile pentru o perioada mai mare de timpi execută mai multe instrucţiuni. Dacă se respectă aceste reguli, întregul sistem de operare va răspunde mult mai repede la acţiunile utilizatorilor.

Page 101: Programare Windows

101

Programarea priorităţilor Cum atribuim unui proces o clasă de prioritate în SDK? Atunci când apelăm CreateProcess, putem

transmite clasa de prioritate dorită în parametrul fdwCreate. O dată ce procesul copil rulează, el poate să îşi schimbe propria prioritate apelând SetPriorityClass :

Bool SetPriorityClass (HANDLE hProcess, DWORD fdwPriority);

Această funcţie schimbă clasa de prioritate identificată de hProcess la valoarea specificată de parametrul fdwPriority. Acest parametru poate fi unul din identificatorii din tabelul de mai sus. Deoarece această funcţie ia identificatorul unui proces, putem schimba clasa de prioritate a oricărui proces care rulează in sistem atâta timp cât avem un identificator al lui şi avem accesul corespunzător.

În mod normal, un proces va încerca să îşi modifice propria sa clasă de prioritate. Bool SetPriorityClass (GetCurrentProcess(), IDLE_PRIORITY_CLASS); O funcţie complementară folosită pentru a extrage clasa de prioritate a unui proces este :

DWORD GetPriorityClass (HANDLE hProcess);

Funcţia returnează unul din identificatorii din tabelul de mai sus. Task Managerul din Windows 2000 permite utilizatorilor să schimbe prioritatea unui proces. La crearea unui fir de execuţie, prioritatea sa relativă este întotdeauna setată implicit la normal. Pentru

a seta prioritatea unui fir, trebuie să apelăm funcţia SetThreadPriority :

BOOL SetThreadPriority (HANDLE Thread, int nPriority);

Bineînţeles, parametrul hThread identifică firul singular a cărui prioritate vrem să o schimbăm, iar parametrul nPriority este unul din cei 7 identificatori din tabelul de mai jos.

Prioritatea relativă a firului de execuţie Constanta simbolică Time-critical THREAD_PRIORIY_TIME_CRITICAL Highest THREAD_PRIORIY_TIME_HIGHEST Above normal THREAD_PRIORIY_TIME_ABOVE_NORMAL Normal THREAD_PRIORIY_TIME_NORMAL Below normal THREAD_PRIORIY_TIME_BELOW_NORMAL Lowest THREAD_PRIORIY_TIME_LOWEST Idle THREAD_PRIORIY_TIME_IDLE

Funcţia complementară care extrage prioritatea relativă a firului este:

int GetThreadPriority (HANDLE hThread);

Această funcţie returnează unul din identificatorii din tabelul de mai sus. În MFC putem trimite una din valorile de mai sus funcţiilor AfxBeginThread şi

CWinThread::SetThreadPriority. În SDK, CreateThread creează întotdeauna un nou fir cu prioritatea relativă normal. Pentru a face ca

firul să aibă prioritatea „idle”, putem transmite indicatorul CREATE_SUSPENDED funcţiei CreateThread. Acest lucru face ca firul să nu execute cod deloc. Putem apela apoi SetThreadPriority pentru a schimba prioritatea firului la prioritatea „idle”. Apoi apelăm ResumeThread astfel încât firul poate fi planificabil. Nu ştim când va primi mai mult timp de procesor, dar planificatorul ia in considerare faptul că acest fir are prioritatea „idle”. În cele din urmă, închidem identificatorul către noul fir astfel încât obiectul nucleu poate fi distrus de îndată ce firul îşi termină execuţia.

Windows nu oferă o funcţie care returnează nivelul de prioritate a unui fir de execuţie. Această omisiune este deliberată. Microsoft îşi rezervă dreptul de a schimba algoritmul de planificare în orice moment. Este recomandat să nu dezvoltăm o aplicaţie care are nevoie de cunoştinţe specifice ale alogoritmului planificatorului. Dacă rămânem cu clasele de prioritate ale proceselor şi cu nivelele relative de prioritate ale firelor de execuţie, aplicaţia noastră va rula bine atât în sistemul actual, cât şi în versiunile următoare.

Page 102: Programare Windows

102

Sincronizarea firelor de execuţie

Accesul atomic : familia de funcţii de sincronizare API-ul Win32 include o familie de funcţii numite ::InterlockedIncrement, ::InterlockedDecrement,

::InterlockExchange, ::InterlockCompareExchange şi ::InterlockExchangeAdd pe care le putem folosi pentru a opera în siguranţă asupra unor valori pe 32 de biţi fără să folosim în mod explicit obiecte de sincronizare. De exemplu, dacă nVar este un UINT, DWORD sau alte tipuri de date pe 32 de biţi, putem să o incrementăm cu declaraţia

::InterlockedIncrement (& nVar);

şi sistemul se va asigura că nici un alt acces la nVar folosind funcţii Interlocked nu se va suprapune cu acesta. nVar va trebui să fie aliniată pe o limită de 32 de biţi, deoarece altfel funcţiile Interlocked s-ar putea să dea greş pe sistemele Windows NT cu mai multe procesoare. De asemenea, ::InterlockCompareExchange şi ::InterlockExchangeAdd sunt suportate doar în Windows NT 4.0 sau mai recent şi în Windows 98.

Secţiuni critice în SDK O secţiune critică este o mică porţiune de cod care cere acces exclusiv la o resursă comună înainte de

execuţia codului. Aceasta este modalitatea de a manipula „atomic” o resursă. Prin atomic înţelegem faptul că codul ştie că nici un alt fir de execuţie nu va accesa resursa. Bineînţeles, sistemul poate porni alt fir de execuţie şi poate planifica altele. Totuşi, el nu va planifica fire care doresc să acceseze resursa până când firul nostru nu părăseşte secţiunea critică.

Care sunt punctele de reper în folosirea secţiunilor critice?

• Atunci când avem o resursă care este accesată de multiple fire de execuţie, trebuie să creăm o structură

CRITICAL_SECTION. • Dacă avem resurse care sunt întotdeauna folosite împreună, putem crea o singură structură

CRITICAL_SECTION care să le păzească pe toate. • Dacă avem resurse multiple care nu sunt utilizate tot timpul împreună, trebuie să creăm o structură

CRITICAL_SECTION pentru fiecare resursă. În cazul în care avem o secţiune critică, trebuie să apelăm EnterCriticalSection căreia îi transmitem

adresa structurii CRITICAL_SECTION care identifică resursa. Structura CRITICAL_SECTION identifică ce resursă vrea firul să acceseze iar EnterCriticalSection verifică dacă acea resursă este disponibilă.

Dacă EnterCriticalSection vede că nici un alt fir nu accesează resursa, atunci permite accesul firului apelant. Dacă funcţia vede că există un fir de execuţie care accesează resursa, firul apelant trebuie să aştepte până la eliberarea resursei.

Atunci când un fir execută cod care nu accesează resursa, trebuie să apeleze LeaveCriticalSection. Aceasta este modalitatea prin care firul spune sistemului că nu mai vrea să acceseze resursa. Dacă uităm să apelăm LeaveCriticalSection, sistemul va crede că resursa este ocupată şi nu va permite accesul altor fire de execuţie.

Este foarte important să împachetăm codul care vrea să acceseze resursa în interiorul funcţiilor EnterCriticalSection şi LeaveCriticalSection. Dacă uităm să facem acest lucru chiar şi doar într-un singur loc resursa va fi predispusă la corupere.

Atunci când nu putem rezolva problema de sincronizare cu ajutorul funcţiilor de sincronizare, trebuie să încercăm să folosim secţiunile critice. Avantajul secţiunilor critice este că sunt uşor de folosit şi folosesc funcţiile de sincronizare intern, astfel încât se execută foarte rapid. Dezavantajul major este că nu le putem folosi pentru sincronizarea firelor în procese diferite.

În mod normal, structura CRITICAL_SECTION este alocată ca o variabilă globală pentru a oferi tuturor firelor din proces o modalitate simplă de a referi structura; prin numele variabilei. Totuşi, structura CRITICAL_SECTION poate fi alocată ca o variabilă locală sau poate fi alocată dinamic în heap . Sunt doar două cerinţe. 1. Prima este că toate firele care vor să acceseze resursa trebuie să ştie adresa structurii CRITICAL_SECTION

care protejează resursa. Putem transmite această adresă firelor prin orice mecanism dorim.

Page 103: Programare Windows

103

2. A doua cerinţă este că membrii din structura CRITICAL_SECTION trebuie să fie iniţializaţi înainte ca alte fire să încerce să acceseze resursa. Structura este iniţializată în modul următor :

VOID InitializeCriticalSection (PCRITICAL_SECTION pcs);

Această funcţie iniţializează membrii structurii CRITICAL_SECTION. Deoarece această funcţie doar setează nişte variabile membru, nu poate eşua şi de aceea valoarea de retur este VOID. Această funcţie trebuie apelată înainte ca orice fir de execuţie să apeleze EnterCriticalSection. Platforma SDK menţionează clar că rezultatele nu sunt definite dacă un fir încearcă să intre într-o structură CRITICAL_SECTION neiniţializată.

Atunci când suntem siguri ca firele de execuţie nu vor mai încerca să acceseze resursa comună, trebuie să stergem (curăţăm) structura CRITICAL_SECTION prin următorul apel :

VOID DeleteCriticalSection (PCRITICAL_SECTION pcs);

DeleteCriticalSection examinează variabilele membru din interiorul structurii. Aceste variabile indică ce fir de execuţie, dacă există unul, accesează în momentul respectiv resursa comună.

EnterCriticalSection efectuează următoarele teste : • dacă nici un fir nu accesează resursa, EnterCriticalSection actualizează variabilele membru pentru

a indica faptul că firul apelant a primit dreptul de a accesa resursa şi apoi returnează imediat, permiţând firului să îşi continue execuţia.

• dacă variabilele membru indică faptul că firul apelant a primit deja dreptul de a accesa resursa, EnterCriticalSection actualizează variabilele pentru a indica de câte ori a primit accesul la resursă firul apelant şi apoi funcţia returnează imediat, permiţând firului să îşi continue execuţia. Această situaţie este rar întâlnită şi apare doar atunci când firul apelează EnterCriticalSection de două ori la rând fără a apela LeaveCriticalSection.

• dacă variabilele membru indică faptul că un fir (altul decât cel apelant) a primit dreptul de a accesa resursa, EnterCriticalSection pune firul apelant în starea de aşteptare. Acest lucru este benefic deoarece astfel firul nu mai consumă timp de procesor. Sistemul reţine ce fire de execuţie doresc să acceseze resursa, actualizează automat variabilele membru ale structurii CRITICAL_SECTION şi permite firului să devină planificabil de îndată ce firul care accesează resursa apelează LeaveCriticalSection.

EnterCriticalSection nu are o structură internă prea complexă; ea efectuează doar câteva teste simple. Ce face funcţia aceasta atât de valoroasă este că poate efectua aceste teste atomic. Dacă două fire apelează EnterCriticalSection în acelaşi timp pe un sistem multiprocesor, funcţia se comportă corect : un fir primeşte permisiunea să acceseze resursa iar celălalt este plasat în starea de aşteptare.

Dacă EnterCriticalSection pune un fir în starea de aşteptare, firul s-ar putea să nu mai fie planificat pentru o perioadă mai mare de timp. De fapt, într-o aplicaţie scrisă prost, firul ar putea să nu mai fie planificat nici o dată pentru timp de procesor. Dacă acest lucru are loc, spunem că firul este înfometat de timp de procesor.

În realitate, firele care aşteaptă o secţiune critică nu înfometează niciodată. Apelurile EnterCriticalSection în cele din urmă expiră, cauzând apariţia unei excepţii. Putem ataşa un debugger aplicaţiei noastre pentru a determina ce nu a funcţionat corect. Durata de timp care trebuie să expire este dată de valoarea conţinută în regiştri la adresa :

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager

Putem folosi următoarea funcţie în locul funcţiei EnterCriticalSection :

BOOL TryEnterCriticalSection (PCRITICAL_SECTION pcs);

Această funcţie nu permite firului apelant să intre în starea de aşteptare. În schimb, valoarea returnată ne spune dacă firul apelant a fost în stare să primească acces la resursă. Dacă TryEnterCriticalSection vede că resursa este accesată de un alt fir, ea returnează FALSE. În toate celelalte cazuri ea returnează TRUE.

Cu această funcţie, un fir poate verifica rapid dacă poate accesa o resursă comună şi în caz contrar continuă să facă altceva. Dacă funcţia returnează TRUE, variabilele membru ale structurii CRITICAL_SECTION au fost actualizate pentru a reflecta faptul că firul accesează o resursă. De aceea, fiecare apel al funcţiei TryEnterCriticalSection care returnează TRUE trebuie să fie potrivit cu un apel al funcţiei LeaveCriticalSection.

Windows 98 nu are o implementare utilă pentru funcţia TryEnterCriticalSection. Ea returnează întotdeauna FALSE.

La sfârşitul codului care accesează resursa, trebuie să apelăm această funcţie :

Page 104: Programare Windows

104

VOID LeaveCriticalSection (PCRITICAL_SECTION pcs);

LeaveCriticalSection examinează variabilele membre din interiorul structurii. Funcţia decrementează cu 1 un contor care indică de câte ori a primit accesul la resursă firul apelant. Dacă acest contor este mai mare decât 0, LeaveCriticalSection nu face nimic altceva şi returnează.

Dacă contorul devine 0, ea verifică dacă mai sunt alte fire în aşteptare apelând EnterCriticalSection. Dacă măcar un fir este in aşteptare, actualizează variabilele membre şi face unul din firele în aşteptare planificabil. Dacă nici un fir nu este în aşteptare, LeaveCriticalSection actualizeată variabilele membre pentru a indica faptul că firul nu mai accesează resursa.

Ca şi funcţia EnterCriticalSection, funcţia LeaveCriticalSection efectuează toate aceste teste şi actualizează automat. Totuşi, LeaveCriticalSection nu plasează niciodată un fir în starea de aşteptare; ea returnează imediat.

Atunci când un fir încearcă să intre într-o secţiune critică care este deţinută de un alt fir, firul apelant este plasat imediat într-o stare de aşteptare. Acest lucru înseamnă că firul trebuie să tranziteze din modul utilizator în modul nucleu (aproape 1000 de cicluri de procesor). Această tranziţie este foarte costisitoare. Pe un sistem multiprocesor, firul care este proprietarul curent al resursei se poate executa pe un procesor diferit şi poate reda controlul asupra resursei rapid. De fapt, firul care este proprietarul resursei o poate elibera înainte ca celălalt fir să îşi termine tranziţia în modul nucleu. Dacă acest lucru are loc, se pierde mult timp de procesor.

Pentru a îmbunătăţi performanţa secţiunilor critice, Microsoft a incorporat în ele spinlocks. Astfel, atunci când apelăm EnterCriticalSection, ea intră într-o buclă folosind un spinlock pentru a încerca să preia controlul asupra resursei pentru un anumit numar de ori. Doar dacă o incercare eşuează firul face tranziţia în modul nucleu pentru a intra in starea de aşteptare.

Pentru a utiliza un spinlock cu o secţiune critică, trebuie să iniţializăm secţiunea critică prin apelul :

BOOL InitializeCriticalSectionAndSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount);

La fel ca şi funcţia InitializeCriticalSection, primul parametru al funcţiei InitializeCriticalSectionAndSpinCount este adresa structurii secţiune critică. Dar în cel de-al doilea parametru, dwSpinCount, transmitem numarul de cate ori dorim să se execute spinlock, cât timp încearcă să obţină resursa înainte de a trimite firul în starea de aşteptare. Această valoare poate fi orice număr de la 0 la 0x00FFFFFF. Dacă apelăm această funcţie când rulăm pe un sistem cu un singur procesor, parametrul dwSpinCount este ignorat şi counterul este setat întotdeauna la 0. Acest lucru este bun deoarece setarea unui contor de revenire pe un sistem cu un singur procesor este inutil : firul care este proprietarul resursei nu o poate elibera dacă celălalt fir este în buclă.

Putem modifica acest contor folosind următoarea funcţie :

DWORD SetCriticalSectionSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount);

Folosirea acestor spinlocks cu secţiuni critice este utilă deoarece nu avem nimic de pierdut. Partea cea mai grea este determinarea valorii pentru parametrul dwSpinCount. Pentru cea mai bună performanţă, trebuie să ne jucăm cu numerele până când suntem satisfăcuţi de performaţele rezultatelor. Ca exemplu, secţiunea critică care păzeşte accesul la heap-ul procesului nostru are contorul egal cu 4000.

Probabilitatea ca funcţia InitializeCriticalSection să eşueze este foarte redusă. Microsoft nu a prevăzut acest lucru atunci când a proiectat-o. Funcţia poate eşua deoarece ea alocă un bloc de memorie pentru ca sistemul să aibă nişte informaţie internă pentru depanare. Dacă această alocare eşuează, este generată o eroare STATUS_NO_MEMORY.

Putem capta mai uşor această problemă folosind InitializeCriticalSectionAndSpinCount. Această funcţie alocă la rândul ei memorie dar returnează FALSE dacă nu reuşeşte.

O altă problemă poate să apară. Intern, secţiunile critice folosesc un obiect nucleu eveniment dacă două sau mai multe fire se luptă

pentru secţiunea critică în acelaşi timp. Deoarece acest caz este rar întâlnit, sistemul nu creează obiectul nucleu până

Page 105: Programare Windows

105

când nu este necesar prima dată. Acest lucru salvează multe resurse de sistem deoarece majoritatea secţiunilor critice nu au niciodată controverse.

Într-o situaţie când avem puţină memorie, o secţiune critică ar putea avea un conflict şi sistemul poate să nu fie capabil să creeze obiectul nucleu eveniment cerut. Funcţia EnterCriticalSection va cauză apariţia excepţiei EXCEPTION_INVALID_HANDLE. Majoritatea programatorilor ignoră această eroare posibilă şi nu au o tratare specială în codul lor deoarece această eroare este foarte rar întâlnită. Totuşi, dacă dorim să fim pregătiţi pentr aşa ceva, avem două opţiuni.

Putem folosi tratarea structurată a erorilor şi putem prinde eroarea. Atunci când apare o eroare, putem fie să nu accesăm resursa protejată de sercţiunea critică, fie să aşteptăm să se elibereze memoria şi apoi să apelăm EnterCriticalSection din nou.

Cealaltă opţiune este să creăm secţiunea critică cu InitializeCriticalSectionAndSpinCount şi să ne asigurăm că am setat bitul superior din parametrul dwSpinCount. Atunci când această funcţie vede că acest parametru este setat, creează obiectul nucleu eveniment şi îl asociează cu secţiunea critică la momentul iniţializarii. Dacă evenimentul nu poate fi creat, funcţia returnează FALSE şi putem trata acest lucru mult mai elegant în cod.

Există mai multe tehnici şi informaţii folositoare atunci când lucrăm cu secţiuni critice. Prima ar fi folosirea unei singure secţiui critice pentru fiecare resursă. Dacă avem câteva structuri de date care nu legătură între ele, ar trebui să creăm o variabilă

CRTICAL_SECTION pentru fiecare structură de date. Acest lucru este mai bun decât să avem o singură structură CRITICAL_SECTION care păzeşte accesul la toate resursele.

int g_nNums[100]; // O resursă comună. TCHAR g_cChars[100]; // O altă resursă comună. CRITICAL_SECTION g_cs; // Păzeşte ambele resurse. DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs); for (int x = 0; x < 100; x++) { g_nNums[x] = 0; g_cChars[x] = TEXT('X'); } LeaveCriticalSection(&g_cs); return(0); }

Acest cod foloseşte o singură secţiune critică pentru a proteja tablourile g_nNums şi g_cChars atunci când acestea sunt iniţializate. Dar aceste tablouri nu au nici o legătură unul cu altul. Dacă funcţia ThreadFunc este implementată ca în continuare, cele două tablouri sunt iniţializate separat :

DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs); for (int x = 0; x < 100; x++) g_nNums[x] = 0; for (x = 0; x < 100; x++) g_cChars[x] = TEXT('X'); LeaveCriticalSection(&g_cs); return(0); }

Teoretic, după ce tabloul g_nNums a fost iniţializat, un fir diferit de cel ce a initializat structura g_nNums, care doreşte să acceseze doar tabloul g_nNums şi nu la g_cChars poate începe execuţia în timp ce ThreadFunc continuă să iniţializeze tabloul g_cChars. Dar din păcate, acest lucru nu este posibil deoarece o singură secţiune critică protejează ambele structuri de date. Pentru a repara acest lucru, putem crea două secţiuni critice:

Page 106: Programare Windows

106

int g_nNum[100]; // O resursă comună CRITICAL_SECTION g_csNums; // Păzeşte g_nNums TCHAR g_cChars[100]; // O altă resursă comună. CRITICAL_SECTION g_csChars; // Păzeşte g_cChars DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csNums); for (int x = 0; x < 100; x++) g_nNums[x] = 0; LeaveCriticalSection(&g_csNums); EnterCriticalSection(&g_csChars); for (x = 0; x < 100; x++) g_cChars[x] = TEXT('X'); LeaveCriticalSection(&g_ csChars); return(0); }

Cu această implementare, un alt fir poate începe să folosească tabloul g_nNums de îndată ce funcţia ThreadFunc a terminat iniţializarea lui. Putem de asemenea să avem un fir care iniţializează tabloul g_nNums şi un fir separat iniţializează tabloul g_cChars.

O altă posibilitate este accesarea resurselor multiple simultan. Uneori avem nevoie să accesăm două resurse simultan. Dacă aceasta a fost o cerinţă a funcţiei

ThreadFunc, ea va trebui să fie implementată ca în exemplul următor :

DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csNums); EnterCriticalSection(&g_csChars); // Această buclă are nevoie de acces simultan la ambele resurse. for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x]; LeaveCriticalSection(&g_csChars); LeaveCriticalSection(&g_csNums); return(0); }

Să presupunem că alt fir din proces are nevoie la rândul lui să acceseze tablourile :

DWORD WINAPI OtherThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csChars); EnterCriticalSection(&g_csNums); for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x]; LeaveCriticalSection(&g_csNums); LeaveCriticalSection(&g_csChars); return(0); }

Tot ce am făcut în aceste funcţii a fost să schimbăm ordinea apelurilor funcţiilor EnterCriticalSection şi LeaveCriticalSection. Dar deoarece aceste două funcţii sunt scrise ca în exemplul de mai sus, poate apărea un blocaj. Să presupunem că funcţia ThreadFunc îşi începe execuţia şi devine proprietatea secţiunii critice a tabloului g_csChars. Apoi firul care execută OtherThreadFunc primeşte timp de procesor şi devine proprietar al secţiunii

Page 107: Programare Windows

107

critice a tabloului g_csChars. În acest fel, avem o situaţie de blocaj. Atunci când una din funcţiile ThreadFunc şi OtherThreadFunc încearcă să îşi continue execuţia, nici o funcţie nu poate deveni proprietara celeilalte secţiuni critice de care are nevoie.

Pentru a rezolva această problemă, trebuie să cerem întotdeauna accesul la resursă în exact aceeaşi ordine. Trebuie să notăm că ordinea nu are importanţă atunci când apelăm LeaveCriticalSection deoarece această funcţie nu are niciodată ca efect intrarea unui fir în starea de aşteptare.

Altă sugestie este să nu ţinem ocupate secţiunile critice pentru o perioadă prea mare de timp. Atunci când o secţiune critică este ocupată pentru o perioadă mai mare de timp, alte fire s-ar putea să

intre în starea de aşteptare, care afectează performanţa aplicaţiei. În continuare avem o tehnică cu care putem să reducem timpul pierdut în timpul secţiunii critice. Următorul cod previne alte fire să schimbe valoarea din g_s înainte ca mesajul WM_SOMEMSG să fie trimis ferestrei si procesat.

SOMESTRUCT g_s; CRITICAL_SECTION g_cs; DWORD WINAPI SomeThread(PVOID pvParam) { EnterCriticalSection(&g_cs); // Trimite un mesaj către fereastră. SendMessage(hwndSomeWnd, WM_SOMEMSG, &g_s, 0); LeaveCriticalSection(&g_cs); return(0); }

Este imposibil să spunem de cât timp are nevoie funcţia de fereastră pentru procesarea mesajului WM_SOMEMSG – ar putea fi câteva milisecunde sau câţiva ani. În acest timp, nici un alt fir nu poate accesa structura g_s. Este mai bine să scriem cod ca mai jos :

SOMESTRUCT g_s; CRITICAL_SECTION g_cs; DWORD WINAPI SomeThread(PVOID pvParam) { EnterCriticalSection(&g_cs); SOMESTRUCT sTemp = g_s; LeaveCriticalSection(&g_cs); // Trimite un mesaj către fereastră. SendMessage(hwndSomeWnd, WM_SOMEMSG, &sTemp, 0); return(0); }

Acest cod salvează valoarea în sTemp, o variabilă temporară. Probabil putem ghici cât timp are nevoie procesorul pentru a executa această linie – doar câteva cicluri de procesor. Imediat după ce variabila temporară este salvată, LeaveCriticalSection este apelată deoarece structura globală nu mai are nevoie să fie protejată. A doua implementare este mult mai buna decât prima deoarece alte fire sunt oprite să folosească structura g_s pentru doar câteva cicluri de procesor în loc de o perioadă nedefinită de timp. Bineînţeles, această tehnică presupune că instantaneul structurii este destul de bun pentru ca funcţia ferestrei să o citească. De asemenea presupune că funcţia de fereastră nu are nevoie să schimbe membrii structurii.

Secţiuni critice în MFC MFC-ul implementează secţiunile critice cu ajutorul clasei CCriticalSection. CCriticalSection::Lock blochează o secţiune critică şi CCriticalSection::Unlock o deblochează. Să

presupunem că clasa unui document include o dată membru de tip listă înlănţuită creată din clasa MFC CList şi că două fire separate folosesc această listă. Una scrie în listă şi alta citeşte din ea. Pentru a preveni cele două fire de a accesa lista în acelaşi moment, putem să o protejăm cu o secţiune critică. Următorul exemplu foloseşte un obiect global CCriticalSection pentru a demonstra cele spuse anterior. (S-au folosit obiecte de sincronizare globale în exemple pentru a ne asigura că obiectele sunt vizibile în mod egal fiecărui fir in proces, dar obiectele de sincronizare nu trebuie să aibă domeniul global.)

Page 108: Programare Windows

108

// Date globale CCriticalSection g_cs; // Firul A g_cs.Lock(); //Scrie în lista înlănţuită g_cs.Unlock(); //Firul B g_cs.Lock(); //Citeşte din lista înlănţuită g_cs.Unlock();

Acum este imposibil ca firele A şi B să acceseze lista înlănţuită în acelaşi timp deoarece lista este „păzită” de aceeaşi secţiune critică.

O formă alternativă a funcţiei CCriticalSection::Lock acceptă o valoare de expirare şi unele documentaţii MFC precizează că dacă îi transmitem o valoare, ea va returna dacă perioada expiră înainte ca secţiunea critică să devină liberă. Documentaţia este greşită. Putem specifica un timp de expirare dacă vrem, dar Lock nu va returna până când nu se va elibera secţiunea critică.

Este evident de ce o listă înlănţuită ar trebui protejată de apelurile unor fire concurente , dar cum rămâne cu variabilele simple ? De exemplu, să presupunem că firul A incrementează o variabilă cu declaraţia :

nVar++;

şi firul B face altceva cu acea variabilă. Ar trebui ca nVar să fie protejată cu o secţiune critică ? În general, da. Ceea ce pare o operaţie indivizibilă într-un program C++ poate fi compilată într-o secvenţă de câteva instrucţiuni maşină. Şi un fir poate primi accesul înaintea altuia între orice două secvenţă maşină. Ca o regulă, este o idee bună de a proteja orice dată de accesul simultan pentru scriere sau pentru accesul simultan pentru scriere şi citire. O secţiune critică este instrumentul perfect pentru acest lucru.

Sincronizarea firelor de execuţie folosind obiecte nucleu

Evenimente în SDK Dintre toate obiectele nucleu, evenimentele sunt cele mai primitive. Ele conţin un contor de

utilizare, o valoare de tip Boolean care indică dacă evenimentul este cu resetare manuală sau automată, şi o altă valoare de tip Boolean care ne spune că obiectul este în starea semnalat sau nesemnalat.

Evenimentul in starea semnalat are semnificatia ca evenimentul poate fi folosit in continuare de alte fire, sau mai corect, firele ce asteapta pe acel eveniment devin planificabile.

Există două tipuri de obiecte eveniment : cu resetare manuală sau cu resetare automată. • Atunci când un eveniment cu resetare manuală este semnalat, toate firele care aşteaptă obiectul devin

planificabile. • Atunci când un eveniment cu resetare automată este semnalat, doar unul din firele care aşteaptă

evenimentul devine planificabil. Evenimentele reprezintă cea mai folosită metodă atunci când un fir face o operaţie de iniţializare şi

apoi semnalează unui alt fir să efectueze restul sarcinii. Evenimentul este iniţializat ca nesemnalat, iar după ce firul îşi termină sarcina iniţială, setează evenimentul la semnalat. În acest moment al execuţiei, un alt fir, care aştepta evenimentul, observă că evenimentul este semnalat şi devine planificabil. Al doilea fir ştie că primul fir şi-a efectuat sarcina.

Funcţia CreateEvent creează un obiect nucleu eveniment :

HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa, BOOL fManualReset, BOOL fInitialState, PCTSTR pszName);

Parametrul fManualReset este o valoare de tip Boolean care spune sistemului dacă să creeze un eveniment cu resetare manuală (TRUE)sau un eveniment cu resetare automată (FALSE).

Parametrul fInitialState indică dacă evenimentul se creaza in starea semnalat (TRUE) sau nesemnalat (FALSE).

Page 109: Programare Windows

109

După ce sistemul creează obiectul, CreateEvent returnează identificatorul obiectului relativ la proces. Firele din alte procese pot avea acces la obiect apelând CreateEvent folosind aceeaşi valoare trimisă în parametrul pszName; folosind moştenirea; folsind funcţia DuplicateHandle; apelând OpenEvent, precizând un nume în parametrul pszName care se potriveşte cu numele precizat în apelul CreateEvent:

HANDLE OpenEvent(

DWORD fdwAccess, BOOL fInherit, PCTSTR pszName);

Ca întotdeauna, trebuie să apelăm funcţia CloseHandle atunci când nu mai avem nevoie de obiectul

nucleu. O dată ce un obiect nucleu este creat, putem controla direct starea sa. Atunci când apelăm SetEvent,

schimbăm starea evenimentului la semnalat :

BOOL SetEvent(HANDLE hEvent);

Atunci când apelăm ResetEvent, setăm starea evenimentului la nesemnalat:

BOOL ResetEvent(HANDLE hEvent);

Microsoft a definit un efect secundar pentru aşteptarea reuşită pentru un obiect cu resetare automată : el este automat resetat la starea nesemnalată atunci când un fir efectuează o aşteptare reuşită a obiectului. De obicei nu este necesar să apelăm ResetEvent pentru un eveniment cu resetare automată deoarece sistemul resetează automat evenimentul. Microsoft nu a definit un efect secundar pentru o aşteptare reuşită pentru evenimentele cu resetare manuală.

Avem în continuare un exemplu pentru sincronizarea firelor de execuţie folosind obiectele nucleu eveniment:

// Creăm un identificator global la un eveniment // nesemnalat şi cu resetare manuală HANDLE g_hEvent; int WINAPI WinMain(...) { // Creăm evenimentul nesemnalat cu resetare manuală g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // Creăm 3 noi fire. HANDLE hThread[3]; DWORD dwThreadID; hThread[0] = _beginthreadex(NULL, 0, WordCount,

NULL, 0, &dwThreadID); hThread[1] = _beginthreadex(NULL, 0, SpellCheck,

NULL, 0, &dwThreadID); hThread[2] = _beginthreadex(NULL, 0, GrammarCheck,

NULL, 0, &dwThreadID); OpenFileAndReadContentsIntoMemory(...); // Permitem tuturor firelor să acceseze resursa SetEvent(g_hEvent); ... } DWORD WINAPI WordCount(PVOID pvParam) { // Aşteptăm până când datele din fişier ajung în memorie. WaitForSingleObject(g_hEvent, INFINITE); // Accesăm blocul de memorie. ... return(0);

Page 110: Programare Windows

110

} DWORD WINAPI SpellCheck (PVOID pvParam) { // Aşteptăm până când datele din fişier ajung în memorie. WaitForSingleObject(g_hEvent, INFINITE); // Accesăm blocul de memorie. ... return(0); } DWORD WINAPI GrammarCheck (PVOID pvParam) { // Aşteptăm până când datele din fişier ajung în memorie. WaitForSingleObject(g_hEvent, INFINITE); // Accesăm blocul de memorie. ... return(0); }

La pornirea procesului, el creează un obiect cu resetare manuală în starea nesemnalată şi salvează

identificatorul într-o variabilă globală. Acest lucru uşurează accesarea aceluiaşi obiect eveniment de către alte fire din acest proces. Creăm trei fire de execuţie. Aceste fire aşteaptă până când conţinutul unui fişier este citit în memorie, iar apoi fiecare fir accesează datele : un fir efectuează o numărare a cuvintelor, un altul rulează verificatorul de sintaxă, iar ultimul lansează verificatorul gramatical. Codul pentru aceste fire începe în mod identic : fiecare fir apelează WaitForSingleObject, care suspendă firul până când conţinutul fişierului este citit în memorie de către firul principal.

De îndată ce firul primar are datele, el apelează SetEvent, care semnalizează evenimentul. În acest moment al execuţiei, sistemul face toate cele trei fire planificabile – toate primesc timp de procesor şi accesează blocul de memorie. Trebuie să remarcăm că toate cele trei fire vor accesa memoria în mod read-only. Acesta este singurul motiv pentru care toate cele trei fire pot rula simultan. De asemenea, trebuie să observăm că dacă sistemul are mai multe procesoare, toate aceste fire se pot executa simultan, executând un volum are de lucru într-o perioadă scurtă de timp.

Dacă folosim un eveniment cu resetare automată în loc de unul cu resetare manuală, aplicaţia se comportă diferit. Sistemul permite doar unui fir secundar să devină planificabil după ce firul principal apelează SetEvent. Din nou, nu avem nici garanţie care fir va fi făcut planificabil de către sistem. Celelalte două fire vor rămâne în aşteptare.

Firul care devine planificabil are acces exclusiv la blocul de memorie. Putem rescrie codul de mai sus astfel încât fiecare fir să apeleze SetEvent înainte de a returna:

DWORD WINAPI WordCount(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. ... SetEvent(g_hEvent); return(0); } DWORD WINAPI SpellCheck (PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. ... SetEvent(g_hEvent); return(0);

Page 111: Programare Windows

111

} DWORD WINAPI GrammarCheck (PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. ... SetEvent(g_hEvent); return(0); }

Atunci când un fir îşi încheie accesul exclusiv asupra datelor, el apelează SetEvent, care permite sistemului să facă unul din cele două fire care aşteaptă planificabil. Din nou, nu ştim care din ele va fi făcut planificabil, dar acest fir va avea propriul acces exclusiv la blocul de memorie. Atunci când acest fir îşi termină sarcina, apelează la rândul lui SetEvent, făcând ca al treilea fir să primească şi el la rândul lui acces exclusiv asupra memoriei.

Putem folosi încă o funcţie pentru evenimente :

BOOL PulseEvent(HANDLE hEvent);

PulseEvent face un eveniment semnalat şi apoi imediat nesemnalat. Dacă apelăm PulseEvent pentru un eveniment cu resetare manuală, toate firele care aşteptau pe acel eveniment devin planificabile. Dacă apelăm PulseEvent pe un eveniment cu resetare automată, doar unul din fire care aşteaptă devine planificabil. Dacă nu există nici un fir în aşteptare, funcţia nu are nici un efect.

PulseEvent nu este foarte folositoare. Dacă o aplicăm in practică, nu putem şti care fire, dacă există, vor vedea acest efect şi vor deveni planificabile.

Evenimente în MFC Clasa MFC CEvent încapsulează obiectele eveniment Win32. Un eveniment este puţin mai mult decât

un indicator în nucleul sistemului de operare. La orice moment dat, el poate fi în una din următoarele două stări : setat sau nesetat. Un eveniment setat spunem că este în starea semnalată, iar un eveniment nesetat spunem ca este nesemnalat. CEvent::SetEvent setează un eveniment, iar CEvent::ResetEvent îl resetează. O funcţie înrudită, CEvent::PulseEvent setează un eveniment şi îl eliberează într-o singură operaţie.

Evenimentele sunt uneori descrise ca “declanşatoare de fire”. Un fir de execuţie apelează CEvent::Lock() pentru a se bloca pe un eveniment şi a aştepta ca acesta să devină disponibil. Un alt fir setează evenimentul şi prin urmare eliberează firul în aşteptare. Setarea evenimentului este asemănătoare acţionării unui declanşator : deblochează firul în aşteptare şi îi dă posibilitatea să îşi rezume execuţia. Un eveniment poate avea blocate pe el unul sau mai multe fire de execuţie, iar dacă codul nostru este scris corect, toate firele în aşteptare vor fi eliberate atunci când evenimentul devine setat.

Windows suportă două tipuri diferite de evenimente : evenimente care se resetează automat şi evenimente care se resetează manual. Diferenţa dintre ele este destul de simplă, dar implicaţiile sunt importante. Un eveniment cu resetare automată este resetat automat în starea nesemnalată atunci când un fir blocat pe el este eliberat. Un eveniment care se resetează manual nu se resetează automat, trebuie resetat prin cod. Regulile pentru alegerea unuia dintre aceste două tipuri de evenimente şi pentru folosirea lor după ce am făcut alegerea sunt următoarele :

• Dacă doar un fir este declanşat de eveniment, folosim un eveniment care se autoresetează şi eliberăm firul în aşteptare cu SetEvent. Nu este nevoie să apelăm ResetEvent deoarece evenimentul este resetat automat în momentul când firul este eliberat.

• Dacă două sau mau multe fire vor fi declanşate de eveniment, folosim un eveniment cu setare manuală şi eliberăm toate firele în aşteptare cu PulseEvent. Din nou, nu este nevoie să apelăm ResetEvent deoarece PulseEvent resetează evenimentul după eliberarea firelor.

Este vital să folosim un eveniment cu resetare manuală pentru a declanşa fire multiple de execuţie. De ce? Deoarece un eveniment cu resetare automată va fi resetat în momentul în care unul din fire este eliberat şi din această cauză va declanşa doar un fir. Este în aceeaşi măsură important să folosim PulseEvent pentru a activa declanşatorul unui eveniment cu resetare manuală. Dacă folosim SetEvent şi ReleaseEvent, nu avem nici o garanţie

Page 112: Programare Windows

112

că toate firele în aşteptare vor fi eliberate. PulseEvent nu doar setează şi resetează evenimentul, ci de asemenea se asigură că toate firele care aşteaptă acel eveniment sunt eliberate înainte de resetarea evenimentului.

Un eveniment este creat prin construirea unui obiect CEvent. CEvent::CEvent acceptă patru parametri, toţi opţionali. Prototipul funcţiei este :

CEvent ( BOOL bInitiallyOwn = FALSE,

BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)

Primul parametru, bInitiallyOwn, precizează că obiectul este iniţial semnalat (TRUE) sau nesemnalat

(FALSE). Opţiunea implicită este bună în majoritatea cazurilor. bManualReset precizează dacă obiectul este un eveniment cu resetare manuală (TRUE) sau automată (FALSE). Cel de-al treilea parametru, lpszName, atribuie un nume obiectului de tip eveniment. Ca şi mutexurile, evenimentele pot fi folosite pentru a coordona fire de execuţie rulând în procese diferite, iar pentru ca un eveniment să treacă de graniţele proceselor, trebuie să îi atribuim un alt nume. Dacă firul care utilizează evenimentul aparţine aceluiaşi proces, lpszName trebuie să fie NULL. Ultimul parametru, lpsaAttribute, este un pointer către o structură SECURITY_ATTRIBUTES care descrie atributele de securitate ale obiectului. NULL acceptă atributele implicite de securitate, care sunt suficiente pentru majoritatea aplicaţiilor.

Cum folosim evenimentele pentru a sincroniza firele? În continuare este un exemplu care implică un fir (firul A) care umple o zonă de date tampon cu date şi un alt fir (firul B) care face nişte operaţii cu acele date. Să presupunem că firul B trebuie să aştepte un semnal de la firul A care să spună că zona de date tampon este iniţializată şi pregătită. Un eveniment cu resetare automată este instrumentul perfect pentru această operaţie:

// Global data // Eveniment cu resetare automată, iniţial nesemanalat CEvent g_event; // Firul A InitBuffer (&buffer); // Iniţializăm zona de date tampon // Eliberăm firul B g_event.SetEvent(); // Firul B g_event.Lock(); // Aşteptăm semnalul

Firul B apelează pentru a se bloca pe obiectul eveniment. Firul A apelează SetEvent atunci când este

pregătit să elibereze firul B. Unicul parametru transmis funcţiei Lock specifică cât timp este dispus să aştepte apelantul, în

milisecunde. Valoarea implicită este INFINITE, care înseamnă să însemne atât cât este necesar. O valoare diferită de zero înseamnă că funcţia Lock a returnat deoarece un obiect a devenit semnalat; o înseamnă că perioada de aşteptare a expirat sau a intervenit o eroare. MFC-ul nu face nimic deosebit aici. El pur şi simplu reconverteşte obiectele nucleu de sincronizare a obiectelor şi funcţiile API care operează asupra lor într-o formă mai mult orientată obiect.

Evenimentele care se resetează automat sunt eficiente pentru declanşarea firelor singulare, dar ce se întâmplă dacă un fir C rulând în paralel cu firul B face ceva total diferit cu data din buffer? Atunci avem nevoie de un eveniment care se resetează automat pentru a elibera firele B şi C deoarece un eveniment cu resetare automată ar elibera fie pe unul, fie pe altul, dar nu pe amândouă. Codul pentru a declanşa două sau mai multe fire cu ajutorul unui eveniment cu resetare manuală este următorul:

// Date globale //Nesemnalat, cu resetare manuală CEvent g_event (FALSE, TRUE);

Page 113: Programare Windows

113

// Firul A // Iniţializarea bufferului InitBuffer (& buffer); // Eliberăm firele B şi C g_event.PulseEvent (); // Firul B g_event.Lock (); // Aşteptăm semnalul //Firul C g_event.Lock (); // Aşteptăm semnalul

Trebuie să observăm că firul A utilizează PulseEvent pentru a acţiona declanşatorul, conform cu cea

de-a doua regulă descrisă mai sus. În concluzie, folosim evenimente care se resetează automat şi CEvent::SetEvent pentru a elibera firele

singulare blocate pe un eveniment şi folosim evenimente cu resetare manuală şi CEvent::PulseEvent pentru a elibera fire multiple. Dacă respectăm aceste reguli, atunci evenimentele ne vor servi într-un mod capabil şi de încredere.

Uneori evenimentele nu sunt folosite ca declanşatoare, ci ca mecanisme primitive de semnalizare. De exemplu, poate firul B vrea să ştie dacă firul A a terminat o operaţie, dar nu vrea să se blocheze dacă răspunsul este negativ. Firul B poate verifica starea unui eveniment fără a se bloca trimiţând către ::WaitForSingleObject identificatorul de fir şi o valoare de expirare de timp egală cu 0. Identificatorul firului poate fi extras din membrul de date m_hObject al clasei CEvent :

if (::WaitForSingleObject (

g_event.m_hObject, 0) == WAIT_OBJECT_0) { // Evenimentul este semnalat } else { // Evenimentul nu este semnalat }

Atunci când folosim un eveniment în acest mod trebuie să fim atenţi deoarece dacă firul B verifică evenimentul în mod repetat până când devine setat, trebuie să ne asigurăm ca evenimentul este cu resetare manuală şi nu unul cu resetare automată. În caz contrar, chiar actul verificării evenimentului îl va reseta.

Mutexuri în SDK Obiectele nucleu mutex asigură accesul mutual exclusiv asupra al unui fir asupra unei resurse. De fapt,

astfel si-a primit mutexul numele. Un mutex conţine un contor de utilizare, un ID de fir şi un contor de revenire. Mutexurile se comportă identic cu sectiunile critice, dar mutexurile sunt obiecte nucleu, pe când secţiunile critice sunt obiecte utilizator. Acest lucru înseamnă că mutexurile sunt mai lente decât secţiunile critice. Dar de asemenea înseamnă ca fire din diferite procese pot accesa un singur mutex şi că un fir poate preciza o valoare de timeout când aşteaptă o resursă.

ID-ul de fir identifică care fir din sistem este proprietarul mutexului în acel moment, iar contorul de reîntoarcere indică numărul de ori de care firul a fost proprietarul mutexului. Mutexurile au mai multe utilizări şi sunt printre cele mai folosite obiecte nucleu. În mod tipic, ele sunt folosite pentru a păzi un block de memorie care este accesat de mai multe fire de execuţie. Dacă acele fire ar accesa acel bloc de memorie simultan, data din acel bloc ar fi coruptă. Mutexurile asigură că fiecare fir care accesează blocul de memorie are acces exclusiv aasupra blocului astfel incât integritatea datelor este menţinută.

Regulile pentru mutexuri sunt următoarele : • dacă ID-ul de fir este 0 (un ID invalid de fir), mutexul nu este deţinut de nici un fir şi este

semnalat; • dacă ID-ul de fir este diferit de 0, un fir este propritarul mutexului şi mutexul este semnalat;

Page 114: Programare Windows

114

• spre deosebire de toate celelalte obiecte nucleu, mutexurile au cod special în sistemul de operare care le permite să încalce regulile normale.

Pentru a folosi un mutex, un proces trebuie mai întâi să creeze mutexul apelând CreateMutex :

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL fInitialOwner, PCTSTR pszName);

Un alt proces poate obţine un identificator relativ la el însusi, la un mutex existent apelând OpenMutex :

HANDLE OpenMutex(DWORD fdwAccess,

BOOL bInheritHandle, PCTSTR pszName);

Parametrul fInitialOwner controlează starea iniţială a mutexului. Dacă trimitem FALSE (in cele mai

multe cazuri), atât ID-ul de fir cât şi contorul de revenire sunt setate la 0. Acest lucru înseamnă că mutexul nu este în posesia nimănui şi de aceea este semnalat.

Dacă trimitem TRUE pentru fInitialOwner, ID-ul de fir al obiectului este setat la ID-ul firului şi contorul de revenire este setat la 1. Deoarece ID-ul firului este diferit de 0, mutexul este iniţial nesemnalat.

Un fir poate primi acces la o resursă comună apelând o funcţie wait, căreia îi transmite identificatorul unui mutex care păzeşte resursa. Intern, funcţia wait verifică ID-ul firului pentru a vedea dacă este egal cu 0 (mutexul este semnalat). Dacă ID-ul firului este 0, parametrul de ID de fir este setat la ID-ul firului apelant, contorul de revenire este iniţializat cu 1, iar firul apelant rămâne planificabil.

Dacă funcţia wait detectează că ID-ul de fir nu este 0 (mutexul este nesemnalat), firul apelant intră în starea de aşteptare. Sistemul ţine minte acest lucru şi atunci când ID-ul de fir al mutexului redevine 0, sistemul setează ID-ul de fir la ID-ul firului în aşteptare, setează contorul de revenire la 1 şi permite firului în aşteptare să fie planificabil din nou. Ca întotdeauna, aceste verificări şi schimbări asupra obiectului nucleu mutex sunt efectuate atomic.

Pentru mutexuri, există o excepţie specială de la regulile normale ale unui obiect semnalat sau nesemnalat. Să presupunem că un fir încearcă să aştepte un obiect mutex nesemnalat. În acest caz, firul este de obicei plasat în starea de aşteptare. Totuşi, sistemul verifică să vadă dacă firul care încearcă să preia controlul mutexului are acelaşi ID de fir ca în interiorul obiectului nucleu. Dacă cele 2 ID-uri coincid, sistemul permite firului să rămână planificabil – chiar dacă mutexul era nesemnalat. Acest comportament nu îl vom întâlni la nici un alt tip de obiecte nucleu. De fiecare dată când un fir aşteaptă cu succes un mutex, contorul de revenire al firului este incrementat. Singura modalitate ca să avem contorul de revenire mai mare de 1 este ca firul să aştepte acelaşi mutex de mai multe ori, profitând de această excepţie de la regulă.

O dată ce un fir a aşteptat cu succes un mutex, firul ştie că are acces exclusiv la resursa protejată. Orice alt fir care încearcă să preia accesul la resursă (aşteptând la un mutex oarecare) este plasat în starea de aşteptare. Atunci când firul care este proprietarul în acel moment al resursei nu mai are nevoie de aceasta, trebuie să elibereze mutexul apelând ReleaseMutex :

BOOL ReleaseMutex(HANDLE hMutex);

Această funcţie decrementează contorul de revenire al obiectului cu 1. Dacă firul îşi termină cu succes aşteptarea unui mutex de mai multe ori, acel fir trebuie să apeleze ReleaseMutex de acelaşi număr de ori înainte ca, contorul de recursie să devină 0. Atunci când contorul de recursie devine 0, ID-ul firului este la rândul lui setat la 0 şi obiectul devine semnalat.

Atunci când obiectul devine semnalat, sistemul verifică dacă orice alt fir de execuţie aşteaptă mutexul. Dacă da, sistemul alege în mod „corect” unul din firele care aşteaptă şi îl lasă să preia controlul mutexului. Acest lucru înseamnă, bineînţeles, că ID-ul firului este setat la ID-ul firului selectat şi contorul de recursie este setat la 1. Dacă nici un alt fir nu aşteaptă acel mutex, mutexul stă în starea semnalată astfel încât următorul fir care aşteaptă mutexul îl primeşte imediat.

Page 115: Programare Windows

115

Probleme legate de abandonare Obiectele mutex sunt diferite de celelalte obiecte nucleu deoarece ele au o noţiune de „proprietate a

firului de execuţie”. Nici unul din celelalte obiecte nucleu pe care le-am discutat nu reţine ce fir de execuţie l-a aşteptat cu succes; doar mutexurile reţin acest lucru. Acest concept pentru mutexuri reprezintă motivul pentru care mutexurile au o excepţie specială de la regulă care permite unui fir de execuţie să preia controlul unui mutex chiar şi atunci când acesta nu este semnalat.

Această excepţie nu se aplică doar unui fir de execuţie care încearcă să preia controlul unui mutex, ci de asemenea şi firelor care încearcă să elibereze un mutex. Atunci când un fir apelează ReleaseMutex, funcţia verifică să vadă dacă ID-ul firului apelant coincide cu ID-ul de fir în obiectul mutex. Dacă cele două ID-uri coincid, contorul de utilizare este decrementat ca mai jos. Dacă ele nu coincid, ReleaseMutex nu face nimic şi returnează FALSE (indicând eşecul) apelantului. Efectuarea unui apel al funcţiei GetLastError în acest moment va returna ERROR_NOT_OWNER (încercarea de a elibera un mutex care nu este deţinut de apelant).

În concluzie, dacă un fir care este proprietarul unui mutex îşi încheie execuţia (folosind ExitThread, TerminateThread, ExitProcess sau TerminateProcess) înainte de a elibera mutexul, ce se întâmplă cu mutexul şi cu celelalte fire care aşteaptă acel mutex ? Răspunsul este că sistemul consideră că mutexul este abandonat – firul care îl controla nu îl mai poate elibera deoarece el şi-a încetat execuţia.

Deoarece sistemul păstrează evidenţa tuturor mutexurilor şi a obiectelor nucleu, el ştie exact când mutexurile devin abandonate. Într-un astfel de caz, sistemul resetează automat ID-ul de fir al mutexului la 0 şi contorul de recursie la 0. Apoi, sistemul verifică să vadă dacă există fire ce aşteaptă mutexul. În caz afirmativ, sistemul alege în mod „corect” un fir în aşteptare, setează ID-ul de fir la ID-ul firului apelant şi setează contorul de recursie la 1; firul selectat devine planificabil.

Aceeaşi situaţie aveam şi mai devreme cu excepţia faptului că funcţia wait nu returnează valoarea normală WAIT_OBJECT_0 firului. În schimb, ea returnează valoarea WAIT_ABANDONED. Această valoare specială de retur (care se aplică doar mutexurilor) indică faptul că mutexul pe care îl aştepta firul era în posesia unui alt fir care şi-a încetat execuţia înainte de a termina de folosit resursa comună. Această situaţie este delicată deoarece firul planificat nu are nici o idee despre integritatea resursei comune. În acest caz este de datoria noastră ca dezvoltatori sa decidem ce trebuie făcut.

În viaţa reală, majoritatea aplicaţiilor nu verifică în mod explicit pentru valoarea de retur WAIT_ABANDONED deoarece foarte rar un fir îşi încheie brusc execuţia.

Mutexuri în MFC Cuvântul mutex vine de la cuvintele mutual şi exclusiv. La fel ca secţiunile critice, mutexurile sunt

folosite pentru a căpăta acces exclusiv la o resursă împărţită între două sau mai multe fire de execuţie. Spre deosebire de secţiunile critice, mutexurile pot fi folosite pentru a sincroniza firele rulând în cadrul aceluiaşi proces sau în cadrul unor procese diferite. Secţiunile critice sunt în general preferate în locul mutexurilor pentru sincronizarea firelor în cadrul unui proces deoarece secţiunile critice sunt mai rapide, dar dacă vrem să sincronizăm fire rulând în două sau mai multe procese, mutexurile reprezintă soluţia cea mai bună.

Să presupunem că două aplicaţii folosesc un bloc de memorie comună pentru a interschimba date. În cadrul acelei memorii comune există o listă înlănţuită care trebuie protejată împotriva acceselor unor fire concurente. Secţiunea critică A nu va funcţiona deoarece nu poate ajunge în afara graniţelor procesului, dar un mutex va reuşi să facă acest lucru. Iată ce trebuie să facem înainte de a scrie sau de a citi în lista înlănţuită :

//Date globale CMutex g_mutex (FALSE, _T (“MyMutex”)); ... g_mutex.Lock(); // Citire sau scriere în listă g_mutex.Unlock();

Primul parametru transmis către constructorul CMutex specifică dacă mutexul este iniţial blocat (TRUE) sau deblocat (FALSE). Al doilea parametru desemnează numele mutexului, care este obligatoriu dacă mutexul este folosit pentru a sincroniza fire în două procese diferite. Noi alegem numele, dar ambele procese trebuie să precizeze acelaşi nume astfel încât cele două obiecte CMutex să referenţieze acelaşi obiect mutex în nucleul

Page 116: Programare Windows

116

Windows. În mod normal, Lock un fir se blochează pe un mutex blocat de alt fir şi Unlock eliberează mutexul pentru ca alte fire să poată să îl blocheze.

Implicit, Lock va aştepta pentru o perioadă nedefinită de timp pentru ca un mutex să devină liber. Putem să construim un mecanism cu şanse mici de eşec specificând un timp maxim de aşteptare în secunde. În următorul exemplu, firul aşteaptă până la 1 minut înainte de accesa resursa protejată de mutex.

g_mutex.Lock (60000); // Citire sau scriere în lista înlănţuită g_mutex.Unlock();

Valoarea returnată de Lock ne spune de ce apelul funcţiei a returnat. O valoare diferită de zero înseamnă că mutexul s-a eliberat, iar zero indică faptul că timpul de aşteptare a expirat primul. Dacă funcţia Lock returnează 0, este în mod normal prudent să nu accesăm resursa comună deoarece ar putea să conducă la un acces suprapus. Astfel, codul care foloseşte facilitatea de timp maxim de aşteptare este în mod uzual structurat astfel : if (g_mutex.Lock (60000)) { // Citire sau scriere în lista înlănţuită g_mutex.Unlock(); }

Există o deosebire între mutexuri şi secţiuni critice. Dacă un fir blochează o secţiune critică şi se termină fără să o elibereze, celelalte fire de execuţie care aşteaptă ca secţiunea critică să se elibereze se vor bloca pentru o perioadă nedefinită de timp. Totuşi, dacă un fir de execuţie care blochează un mutex nu reuşeşte să îl deblocheze înainte de a se termina, sistemul consideră că mutexul este „abandonat” şi îl eliberează automat astfel încât celelalte fire în aşteptare să îşi poată continua execuţia.

Semafoare în SDK Semafoarele sunt folosite pentru numărarea resurselor. Ele conţin un contor de utilizare, aşa cum fac

toate obiectele nucleu, dar ele conţin de asemenea două valori suplimentare pe 32 de biţi; un contor maxim de resurse şi contorul de resurse curent. Contorul maxim de resurse identifică numărul maxim de resurse pe care îl poate controla semaforul.; contorul curent de resurse indică numărul de resurse care sunt disponibile.

Pentru a înţelege mai bine acest mecanism, să analizăm modul în care o aplicaţie poate folosi semafoarele. Să spunem că dezvoltăm un proces server în care am alocat un buffer care poate păstra cererile clienţilor. Am codat mărimea bufferului astfel încât poate păstra un număr de maxim de cinci clienţi la un moment dat. Dacă un nou client încearcă să contacteze serverul în timp ce cinci cereri sunt nerezolvate, clientul este respins, este generată o eroare care indică faptul că serverul este ocupat si clientul trebuie să încerce mai târziu. Atunci când procesul server se iniţializează, el creează un stoc de fire ( thread pool ) format din cinci fire, fiecare fir fiind pregătit să proceseze cereri de la clienţi individuali pe măsură ce apar.

Iniţial, nici un client nu a făcut nici o cerere, astfel încât serverul nu permite nici unui fir să fie planificabil. Totuşi, dacă trei clienţi fac cereri simultane, trei fire din acest stoc de fire trebuie să fie planificabile. Putem trata această monitorizare a resurselor şi planificarea firelor foarte simplu folosind un semafor : contorul maxim de resurse este setat la 5 deoarece aceasta este mărimea bufferului nostru în acest exemplu. Contorul iniţial de resurse este 0 deoarece nici un client nu a făcut nici o cerere. Pe măsură ce cererile clienţilor sunt acceptate, contorul curent de resurse este incrementat, iar după ce cererile sunt satisfăcute, contorul este decrementat.

Regulile pentru un semafor sunt următoarele : • dacă contorul curent de resurse este mai mare de 0, semaforul este semnalat; • dacă contorul curent de resurse este 0, semaforul este nesemnalat; • sistemul nu permite niciodată contorului curent de resurse să fie negativ; • contorul curent de resurse nu poate fi niciodată mai mare decât contorul maxim de resurse. Atunci când folosim un semafor, nu trebuie să confundăm contorul de utilizare al obiectului cu

contorul curent de resurse. Această funcţie creează obiectul nucleu semafor :

Page 117: Programare Windows

117

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName);

Un alt proces poate obţine identificatorul relativ la sine al unui semafor existent apelând OpenSemaphore :

HANDLE OpenSemaphore(DWORD fdwAccess,

BOOL bInheritHandle, PCTSTR pszName);

Parametrul lMaximumCount precizează sistemului numărul maxim de resurse pe care le poate trata

aplicaţia noastră. Deoarece acesta este o valoare cu semn pe 32 de biţi, putem avea 2147483647 resurse. Parametrul lInitialCount precizează câte dintre aceste resurse sunt iniţial disponibile. La iniţializarea procesului server pe care l-am creat, nu există cereri sin partea clienţilor, deci apelăm CreateSemaphore astfel:

HANDLE hsem = CreateSemaphore(NULL, 0, 5, NULL);

Această funcţie creează un semafor cu un contor maxim de resurse egal cu 5, dar iniţial sunt

disponibile 0 resurse. Întâmplător, contorul obiectului nucleu este 1 deoarece de abia am creat acest obiect nucleu; nu trebuie să facem o confuzie între contoare. Deoarece contorul curent de resurse este iniţializat cu 0, semaforul este nesemnalat. Orice fir care aşteaptă acel semafor sunt astfel puse în starea de aşteptare.

Un fir primeşte acces la resursă apelând o funcţie wait, căreia îi transmite identificatorul semaforului care păzeşte resursa. Intern, funcţia wait verifică contorul curent de resurse al semaforului şi dacă acesta este mai mare decât 0 (semaforul este semnalat), contorul este decrementat cu 1 şi firul apelant rămâne planificabil. Cel mai bun la lucru la semafoare este că ele efectuează această operaţie de test şi setare atomic; adică, atunci când cerem o resursă de la un semafor, sistemul de operare verifică dacă resursa este disponibilă şi decrementează contorul de resurse disponibile fără a lăsa alt fir să interfereze cu acest lucru. Doar după ce contorul de resurse a fost decrementat, sistemul permite unui alt fir să ceară acea resursă.

Dacă funcţia wait determină că contorul curent de resurse al semaforului este 0 (semaforul este nesemnalat), sistemul pune firul apelant în starea de aşteptare. Atunci când un alt fir incrementează contorul curent de resurse al semaforului, sistemul îşi aduce aminte de firul din starea de aşteptare şi îi permite acestuia să devină planificabil (decrementând corespunzător contorul curent de resurse).

Un fir incrementează contorul curent de resurse al unui semafor apelând funcţia ReleaseSemaphore :

BOOL ReleaseSemaphore(HANDLE hsem, LONG lReleaseCount, PLONG plPreviousCount);

Această funcţie pur şi simplu adaugă valoarea din lReleaseCount la contorul curent de resurse al semaforului. În mod normal, transmitem valoarea 1 pentru parametrul lReleaseCount, dar acest lucru nu este absolut necesar. Funcţia returnează de asemenea valoarea iniţială a contorului curent de resurse în *plPreviosCount. Puţine aplicaţii ţin cont de această valoare, astfel încât putem transmite NULL pentru a o ignora.

Uneori este folositor să ştim că contorul curent de resurse al unui semafor fără să modificăm contorul, dar nu există nici o funcţie care face acest lucru.

Semafoare în MFC Un alt tip de obiecte de sincronizare îl reprezintă semafoarele. Evenimentele, secţiunile critice şi

mutexurile sunt obiect de tipul „totul sau nimic” în sensul că Lock se blochează pe ele dacă orice alt fir le are deja blocate. Semafoarele sunt diferite. Semafoarele menţin contoarele de resurse reprezentând numărul resurselor disponibile. Blocarea unui semafor decrementează contorul acestuia de resurse, iar blocarea lui incrementează

Page 118: Programare Windows

118

acelaşi contor. Un fir se blochează doar dacă încearcă să blocheze un semafor având contorul de resurse egal cu 0. În acest caz, firul se blochează până când alt fir deblochează semaforul, incrementând contorul de resurse, sau până la expirarea unei perioade specificate. Semafoarele pot fi folosite pentru a sincroniza fire din cadrul unui proces sau din cadrul unor procese diferite.

MFC-ul reprezintă semafoarele cu instanţele clasei CSemaphore. Prototipul constructorului acestei clase este :

CSemaphore( LONG lInitialCount = 1,

LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );

Declaraţia urmatoare CSemaphore g_semaphore (3, 3);

construieşte un obiect de tip semafor care are contorul de resurse iniţial egal cu 3 (parametrul 1) şi contorul maxim egal cu 3 (parametrul 2). Dacă semaforul va fi folosit pentru a sincroniza fire din procese diferite, va trebui să adăugăm un al treilea parametru care atribuie un nume semaforului. Un al patrulea parametru opţional pointează către o structură de tip SECURITY_ATTRIBUTES (valoarea implicită este NULL). Fiecare fir care accesează o resursă controlată de un semafor poate proceda în modul următor : g_semaphore.Lock (); // Accesarea resursei comune g_semaphore.Unlock ();

Atât timp cât nu mai mult de trei fire încearcă să acceseze resursa în acelaşi timp, Lock nu va suspenda firul. Dar dacă semaforul este blocat de trei fire şi un al patrulea apelează Lock, firul se va bloca până când unul din celelalte trei fire apelează Unlock. Pentru a limita timpul în care funcţia Lock aşteaptă ca valoarea contorului de resurse să devină pozitivă, putem să transmitem o valoare maxima de aşteptare (în milisecunde, ca întotdeauna) funcţiei Lock.

CSemaphore::Unlock poate fi folosită pentru a incrementa contorul de resurse cu mai mult de 1 şi de asemenea pentru a afla care a fost contorul de resurse înainte de apelul Unlock. De exemplu, să presupunem că acelaşi fir apelează Lock de două ori la rând pentru a revendica două resurse protejate de un semafor. În loc să apelăm Unlock de două ori, firul poate face acest lucru astfel: LONG lPrevCount; g_semaphore.Unlock (2, & lPrevCount);

Nu există funcţii nici în MFC nici în API care să returneze contorul de resurse al unui semafor, altele

decât CSemaphore::Unlock şi echivalentul din API ::ReleaseSemaphore. O folosire uzuală pentru semafoare este de a permite unui grup de m fire accesul la n resurse, unde m

este mai mare decât n. De exemplu, să presupunem că lansăm 10 fire de lucru şi le dăm la fiecare sarcina de a aduna date. Ori de câte ori un fir umple un buffer cu date, el transmite datele printr-un socket, eliberează bufferul şi reîncepe operaţia de strângere de date. Acum să presupunem că doar trei socketuri sunt disponibile la un moment dat. Dacă noi protejăm aceste socketuri cu un semafor al cărui contor de resurse este 3 şi programăm fiecare fir astfel încât să blocheze semaforul înainte de a pretinde accesul la socket, atunci firele nu vor consuma deloc timp din procesor atât timp cât aşteaptă ca un socket să devină liber.

Clasele CSingleLock şi CMultiLock MFC-ul include o pereche de clase numite CSingleLock şi CMultiLock care au funcţiile Lock şi Unlock

proprii. Putem împacheta o secţiune critică, un mutex, un eveniment sau un semafor într-un obiect CSingleLock şi putem folosi CSingleLock::Lock pentru a aplica un blocaj, ca mai jos :

CCriticalSection g_cs; // Împachetarea într-un CSingleLock

Page 119: Programare Windows

119

CSingleLock lock ( & g_cs); // Blocarea secţiunii critice lock.Lock();

Există vreun avantaj pentru blocarea secţiunii critice în acest mod în locul apelării directe a funcţiei

Lock a obiectului CCriticalSection ? Uneori, da. Să considerăm ceea ce se întâmplă dacă următorul cod aruncă o excepţie între apelurile Lock şi Unlock :

g_cs.Lock (); ... g_cs.Unlock ();

În cazul în care apare o excepţie, secţiunea critică va rămâne blocată definitiv deoarece apelul către Unlock nu va mai avea loc. Putem face însă în felul următor :

CSingleLock (&g_cs); lock.Lock (); ... lock.Unlock ();

Secţiunea critică nu rămâne blocată definitiv. De ce? Deoarece obiectul CSingleLock este creat în stivă, destructorul său este apelat dacă apare o excepţie. Acest destructor apelează Unlock asupra obiectului de sincronizare blocat. Cu alte cuvinte, CSingleLock este un instrument foarte util pentru a ne asigura că un obiect de sincronizare este deblocat chiar şi în cazul apariţiei unor excepţii.

CMultiLock este cu totul diferit. Folosind un CMultiLock, un fir poate bloca pe mai multe obiecte de

sincronizare în acelaşi timp (până la 64). În funcţie de cum apelează CMultiLock::Lock, un fir se poate bloca până când unul din obiectele de sincronizare devine liber sau până când toate devin libere.

CMultiLock::Lock acceptă trei parametri, toţi opţionali. 1. Primul specifică o perioadă maximă de aşteptare (implicit INFINITE). 2. Al doilea parametru specifică dacă firul trebuie să fie trezit când unul din obiectele de sincronizare este

deblocat (FALSE) sau când toate devin deblocate (TRUE, implicit). 3. Al treilea este o mască de „trezire” care specifică alte condiţii care vor trezi firul, de exemplu mesaje

WM_PAINT sau mesaje generate de butoanele mouse-ului. Valoarea implicită pentru acest parametru egală cu 0 previne trezirea firului din alte motive decât din cauză eliberării unuia sau mai multe obiecte de sincronizare sau a expirării perioadei de aşteptare.

Următorul exemplu demonstrează cum un fir se poate bloca pe două evenimente şi un mutex simultan. Trebuie să fim conştienţi de faptul că evenimentele, mutexurile şi semafoarele pot fi împachetate în obiecte CMultiLock, dar secţiunile critice nu pot fi.

CMutex g_mutex ; CEvent g_event [2]; CSyncObject* g_pObjects [3] = {

&g_mutex, &g_event [0], &g_event [1]};

// Blocare până când toate cele trei obiecte // devin semnalate CMultiLock multiLock (g_pObjects, 3); multiLock.Lock (); // Blocare până când unul din obiecte devine semnalat CMultiLock multiLock (g_pObjects, 3); multiLock.Lock (INFINITE, FALSE);

Page 120: Programare Windows

120

Dacă un fir este deblocat după apelul CMultiLock::Lock pentru a se bloca până când doar un obiect de sincronizare devine semnalat, este foarte frecvent cazul în care firul va trebui să ştie ce obiect de sincronizare devine semnalat. Răspunsul poate fi obţinut din valoarea returnată de Lock :

CMutex g_mutex ; CEvent g_event [2] ; CSyncObject* g_pObjects [3] = {&g_mutex, &g_event[0], & g_event[1]}; CMultiLock multiLock (g_pObjects, 3) ; DWORD dwResult = multiLock.Lock (INFINITE, FALSE); DWORD nIndex = dwResult – WAIT_OBJECT_0 ; if ( nIndex == 0) { // Mutexul a devenit semnalat. } else { if ( nIndex == 1) { // Primul eveniment a devenit semnalat. } else if ( nIndex == 2) { // Al doilea eveniment a devenit semnalat. } }

Trebuie să fim conştienţi că dacă trimitem funcţiei Lock o valoare de expirare, alta decât INFINITE, trebuie să comparăm valoarea returnată cu WAIT_TIMEOUT înainte de a scădea WAIT_OBJECT_0 în cazul în care Lock a returnat din cauza expirării perioadei de aşteptare. De asemenea, dacă Lock returnează deoarece un mutex abandonat a devenit semnalat, trebuie să scădem WAIT_ABANDONED_0 din valoarea returnată în loc de WAIT_OBJECT_0.

În continuare vom prezenta un exemplu de o situaţie în care CMultiLock poate fi folositoare. Să presupunem că trei fire separate, firele A, B, C, lucrează împreună pentru a pregăti date într-un buffer. Îndată ce datele sunt pregătite, firul D trimite datele prin intermediul unui socket sau le scrie într-un fişier. Totuşi, firul D nu poate fi apelat până când firele A, B şi C nu şi-au terminat operaţiile curente. Soluţia ? Să creăm obiecte eveniment separate pentru a reprezenta firele A, B şi C şi să lăsăm firul D să folosească un obiect CMultiLock pentru a se bloca până când toate cele trei evenimente devin semnalate. Pe măsură ce fiecare fir îşi termină munca, el setează obiectul eveniment corespunzător la o stare semnalată. Firul D se blochează din această cauză până când ultimul din cele trei semnale de fir este terminat. Funcţiile Wait

Funcţiile wait permit unui fir să intre voluntar în starea de aşteptare până când obiectul nucleu specificat devine semnalat. Pe departe cea mai întâlnită funcţie din această familie este

DWORD WaitForSingleObject (

HANDLE hObject, DWORD dwMilliseconds);

Atunci când un fir apelează această funcţie, primul parametru, hObject, identifică un obiect nucleu care

suportă să fie semnalat/nesemnalat. Un obiect menţionat în listă în secţiunea precedentă funcţionează excelent. Al doilea parametru, dwMilliseconds, permite firului să precizeze cât timp este dispus să aştepte pentru ca obiectul să devină semnalat.

Următoarea funcţie spune sistemului că firul apelant vrea să aştepte până când procesul identificat de hProcess îşi termină execuţia :

WaitForSingleObject (hProcess, INFINITE);

Page 121: Programare Windows

121

Al doilea parametru spune sistemului că firul apelant este dispus să aştepte la infinit până când procesul îşi termină execuţia.

De obicei transmitem INFINITE pentru al doilea parametru, dar putem trimite orice valoare (în milisecunde). INFINITE este definit ca 0xFFFFFFFF (sau -1). Bineînţeles, poate fi periculos să trimitem INFINITE. Dacă obiectul nu devine niciodată semnalat, atunci firul apelant nu se trezeşte – este blocat, din fericire fără a consuma resurse de procesor.

În continuare avem un exemplu pentru funcţia WaitForSingleObject cu al doilea parametru diferit de INFINITE :

DWORD dw = WaitForSingleObject(hProcess, 5000); switch (dw) { case WAIT_OBJECT_0: // Procesul îşi termină execuţia. break; case WAIT_TIMEOUT: // Procesul nu s-a terminat în 5000 de milisecunde. break; case WAIT_FAILED: // Apel greşit al funcţiei (identificator invalid). break; }

Codul de mai sus spune sistemului că firul apelant nu trebuie să fie planificabil până când fie procesul specificat s-a terminat sau au trecut 5000 de milisecunde (care are loc prima). Astfel acest apel returnează în mai puţin de 5000 de milisecunde dacă procesul se termină sau returnează în 5000 de milisecunde dacă procesul nu s-a terminat. Trebuie să reţinem că putem transmitem 0 pentru parametrul dwMilliseconds. În acest caz, WaitForSingleObject returnează imediat.

Valoarea returnată de această funcţie indică de ce firul apelant a devenit planificabil din nou. Dacă obiectul pe care firul îl aşteaptă devine semnalat, valoarea returnată este WAIT_OBJECT_0; dacă perioada de timp expiră, valoarea returnată este WAIT_TIMEOUT. Dacă îi transmitem un parametru invalid, valoarea returnată este WAIT_FAILED. Pentru mai multe informaţii putem apela GetLastError.

Funcţia de mai jos WaitForMultipleObjects este similară cu WaitForSingleObject în afara cazului în care firul apelant să verifice dacă starea semnalată a mai multor obiecte nucleu simultan :

DWORD WaitForMultipleObjects(

DWORD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);

Parametrul dwCount indică numărul de obiecte nucleu pe care dorim să îl verifice funcţia noastră.

Această valoare trebuie să fie între 1 şi MAXIMUM_WAIT_OBJECT (definită ca 64 în fişierele de definiţii ale Windows-ului). Parametrul phObjects este un pointer la un tablou de identificatori de obiecte nucleu.

Putem folosi WaitForMultipleObjects în două moduri diferite – să permitem firului să intre în starea de aşteptare până când unul din obiectele nucleu precizate devine semnalat, sau să permitem unui fir să aştepte până când toate obiectele nucleu precizate devin semnalate. Parametrul fWaitAll spune funcţiei în ce mod dă funcţioneze. Dacă trimitem valoarea TRUE pentru acest parametru, funcţia nu va permite firului apelant să se execute până când toate obiectele au devenit semnalate.

Parametrul dwMilliseconds funcţionează la fel ca şi pentru WaitForSingleObject. Dacă în timpul aşteptării perioada de timp expiră, funcţia returnează oricum. Din nou, INFINITE este trimis în mod uzual pentru acest parametru, dar trebuie să scriem codul foarte atenţi pentru a evita blocajele.

Valoarea returnată de această funcţie spune apelantului de ce a fost replanificat. Valorile posibile de retur sunt WAIT_FAILED şi WAIT_TIMEOUT, a căror semnificaţie este evidentă. Dacă trimitem TRUE pentru fWaitAll şi toate obiectele devin semnalate, valoarea returnată este WAIT_OBJECT_0. Dacă trimitem FALSE

Page 122: Programare Windows

122

pentru fWaitAll, funcţia returnează de îndată ce oricare dintre obiecte devine semnalat. În acest caz, probabil că vrem să ştim care obiect a devenit semnalat. Valoare de retur este o valoare între WAIT_OBJECT_0 şi WAIT_OBJECT_0 + dwCount – 1. În alte cuvinte, dacă valoarea nu este WAIT_TIMEOUT sau WAIT_FAILE, trebuie să scădem WAIT_OBJECT_0 din valoarea de retur. Membru rezultat este un index în tabloul de identificatori pe care l-am trimis ca al doilea parametru funcţiei WaitForMultipleObjects . Indexul ne spune care obiect a devenit semnalat.

HANDLE h[3]; h[0] = hProcess1; h[1] = hProcess2; h[2] = hProcess3; DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); switch (dw) { case WAIT_FAILED: // Apel greşit al funcţiei (identificator greşit ?). break; case WAIT_TIMEOUT:

// Nici unul dintre obiecte nu a devenit semnalat // în 5000 de milisecunde.

break; case WAIT_OBJECT_0 + 0: // Procesul identificat de h[0] (hProcess1) s-a terminat. break; case WAIT_OBJECT_0 + 1: // Procesul identificat de h[1] (hProcess1) s-a terminat. break; case WAIT_OBJECT_0 + 2: // Procesul identificat de h[2] (hProcess1) s-a terminat. break; }

Dacă trimitem valoarea FALSE pentru parametrul fWaitAll, WaitForMultipleObjects scanează identificatorul de la indexul 0 în sus, iar primul obiect care este semnalat termină aşteptarea. Acest lucru poate avea nişte ramificaţii nedorite. De exemplu, firul nostru ar putea să aştepte trei procese copil să se termine şi trimite trei identificatori acestei funcţii. Dacă procesul de la indexul 0 se termină, funcţia WaitForMultipleObjects returnează. Acum firul poate face ce are doreşte şi apoi revine în buclă, aşteptând terminarea unui alt proces. Dacă firul trimite aceleaşi trei argumente, funcţia returnează imediat cu WAIT_OBJECT_0 din nou. Dacă nu înlăturăm identificatorii de la care am primit deja notificări, codul nu va funcţiona corect.

Efectele secundare ale stării de aşteptare Pentru unele obiecte nucleu, un apel reuşit al funcţiei WaitForSingleObject sau

WaitForMultipleObjects schimă de fapt starea obiectului. Un apel reuşit este unul în care funcţia vede că obiectul era semnalat şi returnează o valoare relativă la WAIT_OBJECT_0. Un apel este nereuşit dacă funcţia returnează WAIT_TIMEOUT sau WAIT_FAILED. Obiectele nu îşi schimbă starea niciodată dacă apelul este nereuşit.

Atunci când starea unui obiect este schimbată, numim acesta un efect secundar de aşteptare. De exemplu, să spunem că un fir aşteaptă un obiect eveniment autoresetabil. Atunci când acest obiect devine semnalat, funcţia detectează acest lucru şi poate returna WAIT_OBJECT_0 firului apelant. Totuşi, chiar înainte ca funcţia să returneze, evenimentul este setat la starea nesemnalată – efectul secundar al aşteptării reuşite.

Acest efect secundar este aplicat obiectului eveniment autoresetabil deoarece este una dintre regulile pe care Microsoft le-a definit pentru acest tip de obiect. Alte obiecte au efecte secundare diferite, iar unele obiecte nu au efecte secundare de loc. Obiectele nucleu proces şi fir de execduţie nu au nici un efect secundar – adică aşteptarea unui astfel de obiect nu schimbă niciodată starea obiectului.

Ceea ce face ca funcţia WaitForMultipleObjects să fie atât de utilă este că ea efectuează toate operaţiile sale intern. Atunci când un fir apelează WaitForMultipleObjects, funcţia poate testa starea semnalată a tuturor obiectelor şi poate efectua efectele secundare necesare toate ca o singură operaţie.

Page 123: Programare Windows

123

HANDLE h[2]; h[0] = hAutoResetEvent1; // Initially nonsignaled h[1] = hAutoResetEvent2; // Initially nonsignaled WaitForMultipleObjects(2, h, TRUE, INFINITE);

Atunci când apelăm WaitForMultipleObjects, ambele obiecte eveniment sunt nesemnalate; acest lucru

fortează ambele fire să intre în starea de aşteptare. Apoi obiectul hAutoResetEvent1 devine semnalat. Ambele fire observă că evenimentul a devenit semnalat, dar nici unul dintre ele nu se poate trezi deoarece obiectul hAutoResetEvent2 este în continuare nesemnalat. Deoarece nici unul din fire nu şi-a incheiat cu succes aşteptarea, nu are loc nici un efect secundar asupra obiectului hAutoResetEvent1.

În continuare, obiectul hAutoResetEvent2 devine semnalat. În acest moment, unul din cele două fire detectează că ambele obiecte pe care le aştepta au devenit semnalate. Aşteptarea este reuşită, ambele obiecte sunt setate la starea nesemnalată, iar firul devine planificabil. Dar cum rămâne cu celălalt fir ? El continuă să aştepte până când observă că ambele obiecte eveniment sunt semnalate. Chiar dacă la început a detectat că hAutoResetEvent1 era semnalat, acum vede acest obiect ca nesemnalat.

Aşa cum am menţionat anterior, este important de reţinut ca WaitForMultipleObjects funcţionează în mod atomic. Atunci când verifică starea obiectelor nucleu, nici un alt fir nu poate schimba starea acestora. Acest lucru previne apariţia blocajelor. Altfel ne putem imagina ce s-ar putea întâmpla dacă un fir vede ca hAutoResetEvent1 este nesemnalat şi resetează evenimentul la starea nesemnalată şi apoi cecălalt fir vede că hAutoResetEvent2 este semnalat şi îl resetează la nesemnalat. Ambele fire ar fi blocate : un fir va aştepta un obiect care este deţinut ce celălalt fir, şi viceversa. WaitForMultipleObjects ne asigură că acest lucru nu va avea loc.

Dacă fire multiple de execuţie aşteaptă un singur obiect nucleu, ce fir va hotărî sistemul că trebuie trezit atunci când obiectul devine semnalat ? Răspunsul Microsoft la această întrebare este : “Algoritmul este corect.”. Microsoft nu vrea să dezvăluie algoritmul intern folosit de sistem. Tot ce trebuie să ştim este că dacă mai multe fire de execuţie sunt îş aşteptare, fiecare ar trebui să aibă posibilitatea de a se trezi de fiecare dată când obiectul devine semnalat.

Acest lucru însemnă că prioritatea firelor nu are nici un efect : firul cu prioritatea cea mai mare nu va primi în mod obligatoriu obiectul. De asemenea înseamnă că firul care a aşteptat cel mai mult va primi obiectul. Este posibil ca un fir care are obiectul să facă o buclă şi să îl aibă din nou. Totuşi, acest lucru nu ar fi corect faţă de celelalte fire, aşa că algoritmul încearcă să prevină acest lucru. Dar nu există garanţii.

În realitate, alogoritmul folosit de Microsoft este simpla schemă „primul venit, primul ieşit”. Astfel, firul care a aşteptat cel mai mult primeşte obiectul. Totuşi, pot apărea acţiuni în sistem care să modifice acest comportament, făcându-l mai greu de ghicit. De aceea Microsoft nu explică exact modul de funcţionare al algoritmului. O astfel de acţiune este suspendarea unui fir. Dacă firul aşteaptă un obiect şi apoi este suspendat, sistemul uită că firul aşteaptă un obiect. Aceasta are loc deoarece nu există nici un motiv să planificăm un fir care este suspendat. Atunci când firul îşi reia execuţia mai târziu, sistemul crede că firul de abia a început să aştepte acel obiect.

Atunci când depanăm un proces, toate firele din proces sunt suspendate atunci când atingem puncte de oprire. Astfel, depanarea unui proces face algoritmul „primul intrat, primul ieşit” foarte imprevizibil deoarece foarte des firele sunt suspendate şi apoi îşi reiau execuţia.

Page 124: Programare Windows

124

In loc de încheiere Analizaţi cu atenţie proiectele puse la dispoziţie. Pe baza acestor proiecte construiţi aplicaţii simple în care să se vadă că aţi înţeles arhitectura bibliotecii MFC. Probleme

1. Creati o aplicatie de tip Win32 (Windows cu SDK) in care să tratati evenimentul clic stanga mouse. Daca tasta SHIFT este apsata veti desena in zona client un dreptunghi, daca tasta Control este apasata veti desena un cerc, iar daca nu este apasata nici o tasta veti desena un dreptunghi “umplut” cu culoarea rosie. La clic dreapta mouse schimbati titlul ferestrei in “Clic dreapta”.

2. Creati o aplicatie de tip dialog (cu MFC, proiect AppWizard(exe)) in care sa aveti aceeasi functionalitate ca la problema 1.

3. Creati o aplicatie Windows bazata pe dialog. In caseta de dialog a aplicatiei veti adauga controale de editare (2) si butoane (2) necesare astfel incat sa realizati conversia din radiani in grade si invers. Fiecare buton va executa una din conversiile din grade in radiani sau din radiani in grade.

4. Rezultatul va fi afisat intr-un control static incorporat in acel dialog. 5. Aceeasi problema ca la 3, dar veti folosi un control de editare, un check box (grade / radiani) si un

singur buton. Check box-ul va indica tipul de transformare pe care doriti sa o faceti. 6. Aceeasi problema ca la 4, dar in loc de check box veti folosi doua butoane radio. Un buton radio

va indica transformarea din grade in radiani, celalalt din radiani in grade. 7. Creati o aplicatie bazata pe dialog in care veti incorpora urmatoarele controale: un combo box ce

va contine literele HDD din calculator, un list box ce va afisa directoarele ce se gasesc pe un HDD. Cand selectam in combo box un HDD, in list box sa apara lista directoarelor. De asemenea caseta de dialog va mai contine si un control de editare in care veti plasa articolul selectat din list box sau din combo box.

8. Aceeasi problema ca la 6, dar articolele din list box si combo box sa fie colorate (textul sa aiba culoare). Culorile se vor alege folosind o functie ce genereaza valori pentru culori, in mod aleator.

9. Creati o aplicatie bazata pe dialog in care sa incorporati doua controale unul de tip treeview si altul de tip list view. Aplicatia trebuie sa “imite” Explorer (sau wincmd).

10. Aceeasi problema ca la 8, dar aplicatia este de tip SDI. Se va folosi clasa CSplitterWnd. Analizati si codul generat de o aplicatie de tip SDI, “like Explorer”.

11. Sincronizare. Creati o aplicatie in care sa aveti pe linga firul primar inca doua fire de lucru. Functionalitate: Firul primar va deschide un fisier, firele de lucru vor numara caracterele din acel fisier, respectiv cuvintele (spatiu se considera separator pentru cuvinte) si in final firul primar va afisa cate cuvinte si cate caractere sunt in acel fisier.

12. Reluati problemele 3, 4 si 5 in care sa folositi mecanismul de transmitere a informatiei de la control la ecran si invers, mecanism numit DDX (Dynamic Data Exchange). Pentru indicatii puteti descarca si o aplicatie de pe pagina (pdiag) care care foloseste DDX si explicatiile sunt date in cod. Urmariti comentariile.

Page 125: Programare Windows

125

Cuprins Introducere.....................................................................................................................................................................1 Categorii de mesaje .....................................................................................................................................................11 GetMessage .................................................................................................................................................................16 SendMessage ...............................................................................................................................................................20 Bucla de mesaje “ascunsă” ..........................................................................................................................................22 CObject........................................................................................................................................................................29 CCmdTarget::OnCmdMsg ..........................................................................................................................................32 CDocument..................................................................................................................................................................39 CView..........................................................................................................................................................................39 Harta de mesaje ...........................................................................................................................................................40 Construirea hărţii de mesaje ........................................................................................................................................40 Înţelegerea comenzilor ................................................................................................................................................41 Crearea unei aplicaţii Windows...................................................................................................................................43 Controale clasice..........................................................................................................................................................49 Clasa CButton ..............................................................................................................................................................50 Clasa CListBox ............................................................................................................................................................57 Clasa CComboBox (control combo box) .....................................................................................................................61 Atributele Contextului de Dispozitiv...........................................................................................................................65 Modul MM_TEXT ......................................................................................................................................................67 Setarea originilor .........................................................................................................................................................68 Desenarea pe ecran ......................................................................................................................................................69 Crearea unui meniu......................................................................................................................................................70 Incărcarea şi afişarea unui meniu ................................................................................................................................71 Raspunsul la comenzile din meniu ..............................................................................................................................72 Intervale pentru comenzi .............................................................................................................................................72 Actualizarea articolelor intr-un meniu.........................................................................................................................73 Modificarea programatica............................................................................................................................................75 Meniul system .............................................................................................................................................................76 Meniuri contextuale. Mesajul WM_CONTEXTMENU.............................................................................................76 Obiecte nucleu .............................................................................................................................................................78 Ce sunt obiectele nucleu ? ...........................................................................................................................................78 Contorul de resurse......................................................................................................................................................78 Securitate .....................................................................................................................................................................78 Crearea unui obiect nucleu ..........................................................................................................................................79 Închiderea unui obiect nucleu......................................................................................................................................80 Partajarea obiectelor nucleu dincolo de graniţele procesului.......................................................................................80 Moştenirea identificatorilor obiectelor ........................................................................................................................81 Obiecte cu nume ..........................................................................................................................................................82 Spaţiile de nume Terminal Server ...............................................................................................................................84 Funcţia CreateProcess..................................................................................................................................................89 Oprirea execuţiei unui proces ......................................................................................................................................89 Funcţia ExitProcess .....................................................................................................................................................89 Funcţia TerminateProcess............................................................................................................................................89 Fire de execuţie............................................................................................................................................................91 Când creăm un fir de execuţie .....................................................................................................................................91 Când nu trebuie să creeăm un fir de execuţie ..............................................................................................................91 Firele de execuţie în MFC ...........................................................................................................................................91 Funcţia de fir de execuţie în SDK................................................................................................................................92 Funcţia firului de execuţie în MFC..............................................................................................................................92 Crearea unul fir de execuţie de lucru în MFC..............................................................................................................93 Crearea unui fir de execuţie cu interfaţă cu utilizatorul în MFC .................................................................................94 Suspendarea şi repornirea firelor de execuţie ..............................................................................................................95 Punerea unui fir de execuţie în starea de „sleep”.........................................................................................................95

Page 126: Programare Windows

126

Terminarea unui fir de execuţie în SDK......................................................................................................................96 Fire de execuţie, procese şi priorităţi ...........................................................................................................................98 Prioritatea proceselor şi a firelor de execuţie...............................................................................................................99 Clasele de prioritate ale proceselor..............................................................................................................................99 Programarea priorităţilor ...........................................................................................................................................101 Sincronizarea firelor de execuţie ...............................................................................................................................102 Secţiuni critice în SDK ..............................................................................................................................................102 Secţiuni critice în MFC..............................................................................................................................................107 Sincronizarea firelor de execuţie folosind obiecte nucleu .........................................................................................108 Evenimente în SDK...................................................................................................................................................108 Evenimente în MFC...................................................................................................................................................111 Mutexuri în SDK .......................................................................................................................................................113 Probleme legate de abandonare .................................................................................................................................115 Mutexuri în MFC.......................................................................................................................................................115 Semafoare în SDK.....................................................................................................................................................116 Semafoare în MFC.....................................................................................................................................................117 Clasele CSingleLock şi CMultiLock .........................................................................................................................118 Funcţiile Wait ............................................................................................................................................................120 Efectele secundare ale stării de aşteptare...................................................................................................................122 Bibliografie Charles Petzold- Programming Windows J. Prossie – Programming Windows with MFC Second Edition J. Richter – Advanced Windows MSDN


Recommended