I riassunti , gli appunti i testi contenuti nel nostro sito sono messi a disposizione gratuitamente con finalità illustrative didattiche, scientifiche, a carattere sociale, civile e culturale a tutti i possibili interessati secondo il concetto del fair use e con l' obiettivo del rispetto della direttiva europea 2001/29/CE e dell' art. 70 della legge 633/1941 sul diritto d'autore
Le informazioni di medicina e salute contenute nel sito sono di natura generale ed a scopo puramente divulgativo e per questo motivo non possono sostituire in alcun caso il consiglio di un medico (ovvero un soggetto abilitato legalmente alla professione).
Documentazione per il corso:
Autore: David Bandinelli
Revisione: 1.1
Bibliografia:
“Programming Windows with MFC” di J. Prosise
“Programming Windows” di C. Petzold
Microsoft MSDN Library
Fino a pochi anni fa l’unica possibilita’ per sviluppare applicazioni in ambiente Windows di una certa complessita’, era quella di utilizzare il linguaggio C e l’SDK (Software Development Kit).
In pratica l’SDK di Windows era formato da una immensa collezione di API (Application Program Interface) che potevano essere richiamate all’interno di un programma C e che permettevano di realizzare applicazioni grafiche a prezzo pero’ di una notevole complessita’ e di una vasta gamma di conoscenze necessarie al programmatore per scrivere anche il piu’ banale programma.
Successivamente, l’avvento del Visual Basic ha permesso a tutti di poter sviluppare applicazioni Windows in maniera sicuramente piu’ veloce e piu’ facile, senza la necessita’ di dover acquisire l’enorme bagaglio di conoscenze necessario per programmare con l’SDK.
Fino alla versione 6.0 comunque il Visual Basic e’ rimasto un linguaggio semi-interpretato e quindi per applicazioni di una certa importanza, dove le prestazioni sono un fattore critico, non rappresentava certo la scelta ideale.
Per rispondere a questa esigenza di uno strumento piu’ performante e piu’ sicuro del Visual Basic ma che fosse piu’ semplice da utilizzare dell’SDK e soprattutto fosse in linea con i moderni concetti della programmazione Object Oriented, lo sviluppo in ambiente Windows si e’ orientato verso il linguaggio C++ utilizzato congiuntamente alla libreria di classi MFC.
Resta il fatto che la programmazione in ambiente Windows e’ notevolmente piu’ complessa di quella tradizionale, soprattutto a causa della complessa interfaccia utente, e quindi richiede un approccio sostanzialmente diverso da parte del programmatore.
In generale i normali programmi per ambienti tradizionali utilizzano una logica procedurale, ovvero l’esecuzione del programma parte da un main e continua a seconda del flusso dell’applicazione (cicli, chiamate a funzione, istruzioni if-then-else ecc.).
Le applicazioni Windows operano in maniera diversa, ovvero utilizzano un modello di programmazione chiamato “event driven”; che consiste nel gestire la reazione del programma di fronte ad eventi (es. la pressione del pulsante del mouse o la chiusura di una finestra) che vengono segnalati all’applicazione tramite messaggi inviati dal sistema operativo.
In sostanza un’applicazione Windows non ha un vero e proprio main, ma si limita ad aprire una o piu’ finestre e a posizionarsi in un ciclo di attesa (loop dei messaggi), aspettando che il sistema operativo invii messaggi all’applicazione in risposta ai vari eventi che si possono verificare durante l’esecuzione del programma.
I messaggi in arrivo diretti ad una applicazione vengono accodati in attesa dell’elaborazione e solitamente si continua l’esecuzione in questo modo fino all’arrivo di un messaggio di richiesta di terminazione del programma (WM_QUIT), che solitamente viene inviato quando l’utente preme il tasto ‘x’ su di una finestra, oppure seleziona la voce ‘Esci’ dalla barra dei menu.
La complessita’ di sviluppo delle applicazioni Windows deriva pertanto dalla necessita’ di prevedere in fase di progetto tutti i possibili messaggi che dovranno essere gestiti e che quindi richiederanno la scrittura di una routine di gestione apposita.
La libreria MFC semplifica questo processo prendendosi carico di gestire al suo interno gran parte dei messaggi che l’applicazione riceve e lasciando quindi al programmatore solo la scrittura della logica del programma.
Prima di arrivare a trattare le classi MFC e quindi la programmazione Windows in linguaggio C++ e’ istruttivo esaminare un semplice programma scritto in C tramite l’SDK per capire meglio la struttura ed il funzionamento delle applicazioni in ambiente Windows e la notevole differenza dalle applicazioni tradizionali.
Il seguente listato puo’ essere generato dallo Wizard di Visual Studio 6.0 selezionando File->New e scegliendo un nuovo progetto di tipo Win32Application.
Nella finestra del wizard selezionare “Hello World Application” per generare automaticamente il codice di una semplice applicazione Windows-SDK che stampa un messaggio a video ed e’ dotata di un piccolo menu e di una finestra di informazioni.
(listato 2_1_1)
#include "stdafx.h"
#include "resource.h"
#define MAX_LOADSTRING 100
// Variabili globali: istanza, titolo della finestra
// principale e nome dell’applicazione
HINSTANCE hInst;
TCHAR szTitle[MAX_LOADSTRING];
TCHAR szWindowClass[MAX_LOADSTRING];
// Prototipo delle funzioni utilizzate
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
// Questa e’ la funzione main delle applicazioni Windows
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// variabile di tipo messaggio
MSG msg;
// Inizializzazione stringhe globali
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_TESTSDK, szWindowClass, MAX_LOADSTRING);
// Si registrano le caratteristiche principali
// dell’applicazione
MyRegisterClass(hInstance);
// Inizializzazione dell’applicazione
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// LOOP dei messaggi
while (GetMessage(&msg, NULL, 0, 0))
{
// I messaggi vengono estratti dalla coda
// e passati alla funzione di callback
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
// si definiscono le caratteristiche dell’applicazione
// come icone, stile della finestra principale, colori
// font, menu ecc.
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_TESTSDK);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = (LPCSTR)IDC_TESTSDK;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
return RegisterClassEx(&wcex);
}
// La finestra principale dell’applicazione viene
// visualizzata
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance;
// Questa funzione crea la finestra principale
// dell’applicazione
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
// La finestra viene finalmente visualizzata
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
// Questa e’ la funzione detta di callback che gestisce I
// messaggi ricevuti dall’applicazione
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_DAVID, szHello, MAX_LOADSTRING);
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Gestisce la selezione di una voce dal
// menu:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
RECT rt;
GetClientRect(hWnd, &rt);
// Visualizza il messaggio nella finestra
DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
// si e’ ricevuto il messaggio di uscita
PostQuitMessage(0);
break;
default:
// I messaggi non gestiti vengono passati
// alla funzione di default
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// La dialog box di informazioni ha il suo gestore di
// messaggi
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break;
}
return FALSE;
}
Il cuore di una applicazione Windows SDK e’ la funzione WinMain; al suo interno viene richiamata RegisterClass per registrare la finestra principale dell’applicazione (la registrazione consiste nel definire alcune importanti caratteristiche di una finestra come l’indirizzo della routine di gestione dei messaggi, la presenza di icone e menu, i colori di sfondo ecc.).
Una volta registrata la finestra dell’applicazione e’ possibile creare fisicamente la finestra tramite la funzione CreateWindow che permette anche di definirne lo stile (posizione, dimensione, presenza di scrollbar laterali e/o orizzontali ecc.).
La finestra, una volta creata, non e’ visibile fino a che non vengono chiamate le funzioni ShowWindow e UpdateWindow.
Successivamente l’applicazione inizia il cosiddetto loop dei messaggi, ovvero si effettua un ciclo while che chiama le funzioni GetMessage, TranslateMessage e DispatchMessage le quali rispettivamente esaminano la coda dei messaggi e, se un nuovo messaggio e’ presente, lo interpretano e lo inviano alla corrispondente funzione di gestione (message handler).
Nel nostro esempio la funzione di gestione dei messaggi e’ la CALLBACK WndProc che si occupa di implementare la reazione dell’applicazione ad ognuno dei messaggi riconosciuti.
Ad esempio, in risposta al messaggio WM_PAINT (che viene inviato tutte le volte che e’ necessario ridisegnare la finestra), la funzione WndProc provvede a visualizzare il messaggio a video.
La funzione WndProc viene definita funzione di callback perche’ non viene chiamata esplicitamente dal programma ma e’ chiamata direttamente dal loop dei messaggi ogni volta che se ne presenti la necessita’ (ovvero e’ arrivato un nuovo messaggio).
I messaggi non gestiti direttamente dall’applicazione vengono passati alla funzione DefWindowProc per segnalare al sistema operativo che il messaggio non viene gestito e che quindi e’ sufficiente un trattamento di default da parte di Windows.
Il gestore del messaggio WM_DESTROY richiama la funzione PostQuitMessage per inviare un messaggio WM_QUIT alla coda dei messaggi e causare in questo modo la corretta terminazione dell’applicazione.
Il messaggio WM_DESTROY viene inviato ad una applicazione immediatamente prima della chiusura della finestra principale ed e’ a cura del programmatore inviare il messaggio WM_QUIT altrimenti l’applicazione non potrebbe terminare correttamente.
Lo scopo primario di MFC (Microsoft Foundation Classes) e’ quello di incapsulare all’interno di classi (piu’ di 200 nella versione 6.0) le API di Windows e di fornire inoltre un ambiente di programmazione Windows ad oggetti che aiuti il programmatore nel complesso meccanismo di sviluppo delle applicazioni dotate di interfaccia grafica.
Le classi MFC forniscono un architettura di sviluppo che semplifica la gestione dei compiti piu’ ripetitivi di ogni applicazione Windows come la gestione dei messaggi, ed offre inoltre alcuni potenti strumenti per separare l’implementazione dei dati di un programma dalla sua rappresentazione grafica raggiungendo un livello di astrazione sensibilmente maggiore rispetto all’SDK (Architettura Document/View).
E’ da considerare inoltre che l’utilizzo di MFC e del linguaggio C++ aggiunge alle applicazioni Windows tutti i benefici derivanti dalle tecniche OO come l’incapsulamento, l’ereditarieta’ e le funzioni virtuali.
E’ possibile infatti ereditare da una qualsiasi classe MFC e creare in poco tempo potenti elementi di interfaccia personalizzati ed avvalersi inoltre della libreria di template e container messi a disposizione del programmatore da STL.
Analizziamo adesso in dettaglio la struttura di una applicazione MFC tramite l’analisi di un semplice esempio che stampa al centro di una finestra il messaggio “Hello World!”.
Sebbene sia possibile utilizzare il wizard di Visual Studio per generare interamente lo scheletro di una applicazione MFC e’ utile scrivere tale programma senza l’ausilio dell’ambiente di sviluppo per prendere familiarita’ con l’architettura.
Per creare un nuovo progetto MFC senza utilizzare il Wizard e’ necessario:
A questo punto e’ possibile aggiungere un nuovo file .CPP o .H (in alternativa .HPP) ed iniziare a scrivere il codice.
(listato 2_1_2)
(Hello.h)
// classe principale applicazione MFC
class CMyApp : public CWinApp
{
public:
// il metodo InitInstance deve essere
// implementato in CMyApp
virtual BOOL InitInstance ();
};
// classe per la finestra dell’applicazione
class CMainWindow : public CFrameWnd
{
public:
// metodo per la creazione della finestra
// principale (e’ anche il costruttore!)
CMainWindow ();
protected:
// dichiarazione della mappa dei messaggi
// e delle funzioni di gestione dei msg
afx_msg void OnPaint ();
DECLARE_MESSAGE_MAP ()
};
(Hello.cpp)
// MFC richiede l’include di afxwin.h
#include <afxwin.h>
#include "Hello.h"
// Istanziamo un oggetto di tipo applicazione MFC
CMyApp myApp;
// Override del metodo InitInstance
BOOL CMyApp::InitInstance ()
{
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
// mappa dei messaggi gestiti dall’applicazione
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
ON_WM_PAINT ()
END_MESSAGE_MAP ()
// override della funzione di creazione della
// finestra principale necessaria per modificare
// il titolo (il costruttore della classe CmainWindow
// provvede a creare la finestra)
CMainWindow::CMainWindow ()
{
Create (NULL, "La mia prima applicazione MFC");
}
// gestione dell’evento (messaggio) WM_PAINT
void CMainWindow::OnPaint ()
{
// istanziamo un “device context”
CPaintDC dc (this);
// istanziamo una client area
CRect rect;
GetClientRect (&rect);
// adesso possiamo scrivere nella finestra
dc.DrawText ("Hello, World!", -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
Come si puo’ notare dal listato, una applicazione MFC non ha una funzione WinMain come l’esempio del capitolo precedente realizzato mediante l’SDK.
Le funzionalita’ del WinMain sono svolte da un oggetto di tipo applicazione che deve essere istanziato in maniera globale derivandolo dalla classe base CWinApp.
In ogni programma MFC deve esistere un solo oggetto di tipo applicazione e se si vuole che l’applicazione abbia una finestra e’ necessario effettuare l’implementazione del metodo InitInstance nella classe derivata (in questo caso CMyApp) per crearne una e poi visualizzarla tramite i metodi ShowWindow e UpdateWindow.
La funzione virtuale InitInstance e’ utilizzata per effettuare qualsiasi operazione di inizializzazione sia necessaria al momento della partenza dell’applicazione; la funzione InitInstance deve sempre terminare con il return TRUE.
Da notare che la finestra dell’applicazione viene creata dinamicamente all’interno di InitInstance tramite l’istruzione m_pMainWnd = new CMainWindow.
In questo modo viene creata una nuova istanza di un oggetto appartenente alla classe CMainWindow che avevamo derivato (nel .h) dalla classe base CFrameWnd e ne avevamo implementato il costruttore (l’istruzione Create).
La classe base CFrameWnd e’ una delle classi MFC che fornisce l’implementazione di una generica finestra che puo’ essere contenitore per altre finestre o controlli, oppure essere utilizzata come nell’esempio per potervi accedere direttamente.
Il metodo Create della classe CFrameWnd puo’ essere scritto in modo da modificare l’apparenza della finestra dell’applicazione; ad esempio per dotare la window di una scrollbar verticale e’ necessario specificare:
Create (NULL, "MFC", WS_OVERLAPPEDWINDOW | WS_VSCROLL);
L’applicazione di esempio deve gestire il refresh del messaggio “Hello World!” all’interno della propria finestra in risposta al messaggio WM_PAINT che gli viene inviato dal sistema operativo.
La gestione del messaggio WM_PAINT avviene all’interno del metodo OnPaint( ) della classe CMainWindow.
Per poter disegnare (o scrivere del testo come nell’esempio) all’interno di una finestra e’ necessario disporre di un oggetto denominato “Device Context” che serve ad astrarre cio’ che si vuole disegnare dal destinatario fisico del disegno (schermo o stampante indipendentemente dalla risoluzione, numero dei colori, modello della scheda video ecc.).
Non e’ possibile effettuare alcun tipo di output diretto su di una finestra senza l’utilizzo di un Device Context.
Una volta istanziato l’oggetto di tipo Device context e’ possibile richiamarne il metodo DrawText per inviare del testo ad un area rettangolare all’interno della finestra definita istanziando un oggetto di classe CRect.
Il metodo GetClientRect restituisce un riferimento ad un oggetto di tipo CRect che rappresenta la parte di una finestra su cui e’ possibile effettuare un output (e’ necessario questo passaggio intermedio dato che l’area di una finestra varia sempre a causa del ridimensionamento effettuato dall’utente e quindi non e’ possibile stabilire in maniera assoluta le coordinate e l’ampiezza dell’area utilizzabile).
Il loop dei messaggi e le funzioni di callback che venivano utilizzate con l’SDK sono state incapsulate all’interno di MFC e quindi il programmatore deve solo utilizzare le macro BEGIN_MESSAGE_MAP e END_MESSAGE_MAP per specificare quali saranno i messaggi gestiti dalla sua applicazione.
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
ON_WM_PAINT ()
END_MESSAGE_MAP ()
Quando si dichiara la mappa dei messaggi e’ necessario indicare sia la classe base che la classe derivata di cui si dovranno gestire i messaggi ed inoltre e’ necessario che la classe derivata contenga una funzione membro per ogni messaggio gestito (la CMainWindow risponde all’evento ON_WM_PAINT dichiarato all’interno della mappa dei messaggi, tramite la funzione OnPaint).
E’ necessario consultare la documentazione dell’MFC per il prototipo preciso della funzione membro che deve rispondere ad un determinato messaggio dato che alcune richiedono parametri in input come ad esempio:
WM_LBUTTONDOWN (Pulsante destro del mouse premuto)
che viene gestita da:
Afx_msg void OnLButtonDown (UINT nFlags, Cpoint point)
Per illustrare gli argomenti di questo capitolo verra’ utilizzato come esempio il listato 2_2_1, nel quale si trova l’implementazione di una semplice versione del gioco del tris.
Oltre ad essere un poco piu’ complessa dell’esempio “Hello World” questa applicazione ci permette di introdurre alcuni argomenti fondamentali come l’intercettazione del mouse e l’aggiunta e la gestione dei menu.
Data la lunghezza del listato ne riporteremo solo alcuni brani commentati ed invitiamo gli studenti ad esaminarlo con la massima attenzione autonomamente.
Come abbiamo gia’ visto nel capitolo precedente per poter disegnare su di una finestra e’ necessario disporre di un dispositivo astratto chiamato “Device Context”; per modificare le caratteristiche di cio’ che si disegna come ad esempio il colore o lo spessore del tratto, e’ necessario avvalersi di alcune classi MFC come CPen e CBrush che devono essere associate al device context corrente in questo modo:
Void CMainWindow::OnPaint()
{
// crea il device context
CPaintDC dc(this);
// Crea un nuovo oggetto di tipo CPen (un tratto
// solido completamente nero largo 16 pixel
CPen pen(PS_SOLID, 16, RGB(0, 0, 0));
// si salva la penna corrente associata al
// device context (e’ una buona norma)
CPen *pOldPen = dc.SelectObject(&pen);
// disegna una ellisse (utilizzando la nuova penna)
dc.Ellipse(0, 0, 200, 100);
// riassocia la penna salvata al device context
dc.SelectObject(pOldPen);
}
Dal questo brano di codice si comprende che la sequenza di passi per disegnare su di una window e’ sempre composta da:
Spesso e’ necessario comunicare all’utente di una applicazione tramite piccole finestre di avvertenza o di segnalazione dette “message box”.
Una message box puo’ essere indipendente dalla finestra in cui si trova e solitamente serve per comunicare errori, warning o informazioni all’utente (come ad esempio che la partita di tris e’ terminata nel listato di esempio).
La message box si definisce “finestra modale” ovvero rimane a video sopra le altre finestre fino a che l’utente non preme il pulsante (solitamente di OK) per confermare la lettura del messaggio.
MessageBox ("Pareggio!","Game Over",MB_ICONEXCLAMATION | MB_OK);
Il primo parametro della funzione MessageBox e’ il messaggio stesso, il secondo e’ il titolo della finestra ed il terzo e’ una combinazione di parametri che specificano il tipo di MessageBox (informativa, di errore o di avvertenza) ed la combinazione di bottoni presenti (in questo caso il solo bottone OK).
Nell’applicazione di esempio possiamo vedere che la classe che gestisce la finestra principale dell’applicazione (CMainWindow) richiede l’implementazione della funzione virtuale PostNCDestroy per distruggere esplicitamente l’oggetto di tipo finestra.
Questo avviene perche’ la classe CMainWindow non era stata derivata da CFrameWnd come nell’esempio precedente, ma dalla classe base CWnd che ha caratteristiche diverse e non effettua automaticamente il rilascio delle risorse allocate.
Virtual void PostNCDestroy
…
void CMainWindow::PostNcDestroy ()
{
delete this;
}
Come si puo’ gia’ intuire da quanto detto fino ad ora riguardo alla programmazione Windows, l’intercettazione dell’input proveniente dal mouse e dalla tastiera avviene tramite la gestione di appositi messaggi da parte dell’applicazione.
Ad esempio la pressione dei tasti del mouse ed il suo movimento sono comunicati dal sistema operativo alle applicazioni tramite una serie di messaggi come ad esempio WM_LBUTTONDOWN, che indica che il pulsante sinistro del mouse e’ stato premuto, oppure WM_MOUSEMOVE che indica che il mouse e’ stato mosso al di sopra della finestra dell’applicazione.
Il programma che vuole gestire questi eventi deve aggiungere alla mappa dei messaggi l’indicazione dei messaggi a cui e’ interessata e deve fornire le corrispondenti routine di gestione per gli eventi ricevuti.
Venendo alla nostra applicazione di esempio:
Nel .h ho abbiamo i seguenti prototipi:
// l’applicazione gestisce la pressione del tasto destro
// e sinistro del mouse e il doppio click del tasto
// sinistro
afx_msg void OnLButtonDown (UINT nFlags, CPoint point);
afx_msg void OnLButtonDblClk (UINT nFlags, CPoint point);
afx_msg void OnRButtonDown (UINT nFlags, CPoint point);
Nella mappa dei messaggi ho l’associazione tra eventi e corrispondenti routine di gestione:
BEGIN_MESSAGE_MAP (CMainWindow, CWnd)
…
ON_WM_LBUTTONDOWN ()
ON_WM_LBUTTONDBLCLK ()
ON_WM_RBUTTONDOWN ()
…
END_MESSAGE_MAP ()
e nel .cpp avro’ l’implementazione delle varie routine di gestione degli eventi legati al mouse:
// in questo caso l’implementazione della routine di
// gestione per l’evento double click del pulsante sinistro
void CMainWindow::OnLButtonDblClk (UINT nFlags, CPoint point)
{
…
CClientDC dc (this);
if (dc.GetPixel (point) == RGB (0, 0, 0))
ResetGame ();
…
}
Ogni messaggio derivante da una operazione di input da parte dell’utente su mouse e tastiera avra’ argomenti di input diversi che poi andranno utilizzati nell’implementazione della funzione di gestione; ad esempio il messaggio LBUTTONDBLCLK invia alla funzione di gestione un oggetto di tipo CPoint contenente le coordinate del punto in cui e’ stato effettuato il doppio click del mouse ed un intero contenente un flag che specifica informazioni addizionali come lo stato dell’altro pulsante oppure se alcuni tasti speciali della tastiera come CONTROL e SHIFT risultavano premuti al momento del doppio click.
Le coordinate vengono gia’ passate dal sistema operativo in maniera relativa rispetto alla finestra dell’applicazione e quindi possono essere facilmente utilizzate dal programmatore.
I menu sono una parte fondamentale di ogni applicazione Windows e spesso rappresentano un elemento irrinunciabile per ogni applicazione dotata di interfaccia grafica.
Ci sono vari modi per aggiungere un menu ad una applicazione MFC, ma in sostanza la differenza principale e’ tra memorizzare i menu in un file di risorse esterno oppure crearli a runtime all’interno del codice che inizializza l’applicazione.
Solitamente si tende a mantenere i menu all’interno del file di risorse che viene poi compilato separatamente dall’applicazione tramite il “Resource Compiler” per avere una maggiore facilita’ in caso di modifica successiva delle voci che compongono il menu.
Infatti nel caso in cui si utilizzi il file di risorse si puo’ intervenire con un normale editor di testo e cambiare le voci di menu senza andare a cercarle all’interno del sorgente dell’applicazione.
Inoltre l’ambiente di sviluppo Visual Studio mette a disposizione un tool chiamato “resource editor” che permette di intervenire facilmente sulla struttura dei menu di un’applicazione e di ricompilare il file di risorse senza conoscerne la struttura interna; il file di risorse puo’ ospitare al suo interno non solo menu, ma anche messaggi di testo, icone, bitmap e cursori utilizzati dall’applicazione Windows.
Per aggiungere un menu all’applicazione di esempio utilizzando il file di risorse e’ necessario effettuare i seguenti passi:
Selezionare dal menu Insert -> Resource e scegliere Menu premendo il tasto New.
A questo punto e’ possibile editare la struttura dei menu graficamente facendo doppio click sulla casella vuota per inserire un nuovo elemento in un menu esistente oppure crearne uno nuovo.
Una volta creata la struttura dei menu e’ necessario salvare il file di risorse (avra’ estensione .rc) e spostarlo tra i sorgenti del progetto per fare in modo che venga ricompilato quando si effettua il Build dell’eseguibile.
Per indicare nel sorgente che si utilizza un file di risorse esterno e’ necessario includere nel .h lo header file resource.h
#include <resource.h>
a questo punto dobbiamo, sempre nel .h, dichiarare il prototipo della funzione che verra’ richiamata per ogni voce di menu selezionata; ad esempio se abbiamo una voce ‘Exit’ all’interno del menu ‘File’ che e’ stata chiamata all’interno dell’editor di menu ID_EXIT, e’ necessario aggiungere il prototipo della seguente funzione di gestione:
afx_msg void OnExit();
nel .cpp e’ necessario aggiungere alla mappa dei messaggi il collegamento alla funzione OnExit( ) nel momento in cui viene ricevuto il messaggio che segnala che l’utente ha selezionato la voce ‘Exit’ dalla barra dei menu:
BEGIN_MESSAGE_MAP (CMainWindow, CWnd)
…
ON_COMMAND(ID_EXIT, OnExit)
…
END_MESSAGE_MAP ()
La funzione di gestione OnExit deve essere implementata all’interno della classe CMainWindow:
void CMainWindow::OnExit()
{
// si fa semplicemente terminare l’applicazione
PostMessage(WM_CLOSE, 0, 0);
}
A questo punto rimane da collegare il menu che abbiamo creato alla finestra dell’applicazione e per fare questo abbiamo varie alternative; una di queste consiste nell’aggiungere le seguenti righe di codice all’interno del costruttore della classe CMainWindow:
// Crea un nuovo oggetto di tipo menu
CMenu menu;
// il menu creato nel resource file viene associato
// all’oggetto menu creato
menu.LoadMenu(MAKEINTRESOURCE(IDR_MENU1));
SetMenu(&menu);
// si distacca il menu creato dall’oggetto menu in modo che
// non venga distrutto quando l’oggetto menu andra’ al di
// fuori dello spazio di visibilita’
menu.Detach();
Un metodo alternativo per collegare un menu ad una finestra consiste nel modificare l’istruzione Create della finestra passandogli l’identificativo del menu:
CreateEx (0, strWndClass, "Tris",
WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,rectDefault, MAKEINTRESOURCE(IDR_MENU1), NULL );
Altrimenti e’ possibile effettuare la creazione e/o la modifica di un menu completamente all’interno di un programma senza utilizzare il file di risorse; questa possibilita’ puo’ essere utilizzata ad esempi nel caso in cui si abbia una applicazione che ha due tipi di menu che possono essere scambiati a richiesta (ad esempio menu “abbreviati” per gli utenti principianti e menu “completi” per quelli avanzati).
Esempio:
// crea il menu di primo livello (es. File)
CMenu menuMain;
MenuMain.CreateMenu();
// crea le voci all’interno del menu (es. Exit)
Cmenu menuPopup;
MenuPopup.CreatePopupMenu();
// crea la voce ‘Exit’
MenuPopup.AppendMenu(MF_STRING, ID_EXIT, “Exit”);
// aggancia il sottomenu al menu principale ‘File’
Menumain.AppendMenu(MF_POPUP, (UINT) menuPopup.Detach(), “File”);
// visualizza il menu completo
SetMenu(&menuMain);
MenuMain.Detach();
I “controlli” possono essere considerati come i “mattoni” che servono a costruire l’interfaccia di qualsiasi applicazione Windows.
In generale un controllo e’ uno speciale tipo di finestra che permette di interagire con l’utente finale, sia in termini di input che di output; i piu’ comuni controlli utilizzati in pressoche’ tutte le applicazioni Windows sono bottoni, label, textbox, listbox, combo box e check box.
Naturalmente esistono molti altri controlli, ed altri se ne aggiungono via via che l’interfaccia delle varie versioni di Windows viene migliorata ed evoluta; il programmatore li puo’ utilizzare come oggetti preconfezionati (classi in MFC) tramite la loro interfaccia senza preoccuparsi di come siano stati implementati.
Per utilizzare i controlli di Windows all’interno delle applicazioni MFC in genere si procede in questo modo:
Ogni controllo ha le sue caratteristiche, attributi ed eventi che puo’ generare con relativi differenti prototipi delle routine di gestione; e’ chiaro quindi che e’ necessario leggere la documentazione di ogni tipo di controllo per poterlo utilizzare correttamente all’interno delle proprie applicazioni.
Nel listato 2_2_2 si trova un esempio di utilizzo di alcuni dei controlli principali (un bottone, una listbox, una label ecc.) che vongono impiegati in una semplice applicazione che visualizza in una listbox tutte le font di caratteri installate nel sistema.
Nel file .h vengono aggiunti come membri della classe finestra dell’applicazione i controlli che si utilizzeranno in questo modo:
CStatic m_wndLBTitle;
CListBox m_wndListBox;
CButton m_wndCheckBox;
CButton m_wndGroupBox;
CStatic m_wndSampleText;
CButton m_wndPushButton;
Nella mappa dei messaggi della nostra applicazione d’esempio vediamo quali siano gli eventi gestiti per ognuno dei controlli presenti nella finestra principale; per il bottone gestiamo l’evento OnPushButtonClicked, per la checkbox gestiamo OnCheckBoxClicked e per la listbox OnSelChange che viene attivato quando la selezione di uno degli elementi della listbox cambia.
Naturalmente possono esserci dei controlli nella finestra dell’applicazione per cui non gestiamo eventi; ad esempio il controllo label e’ utilizzato per visualizzare solo del testo statico e quindi non abbiamo la necessita’ di gestirne alcun evento.
BEGIN_MESSAGE_MAP (CMainWindow, CWnd)
// evento bottone 'Stampa' premuto
ON_BN_CLICKED (IDC_PRINT, OnPushButtonClicked)
// evento click sulla checkbox
ON_BN_CLICKED (IDC_CHECKBOX, OnCheckBoxClicked)
// evento cambio di elemento all'interno della listbox
ON_LBN_SELCHANGE (IDC_LISTBOX, OnSelChange)
END_MESSAGE_MAP ()
Nella routine di gestione dell’evento OnCreate della finestra dell’applicazione si chiama il metodo Create per ognuno degli oggetti di tipo controllo; in questo modo i controlli verranno finalmente visualizzati nella finestra e se ne potra’ anche definire le caratteristiche tramite gli argomenti passati al metodo Create (es. la caption per un bottone o una label).
// creazione del controllo label
m_wndLBTitle.Create ("Font", WS_CHILD | WS_VISIBLE | SS_LEFT, rect, this);
// creazione della listbox
m_wndListBox.CreateEx (WS_EX_CLIENTEDGE, "listbox", NULL,
WS_CHILD | WS_VISIBLE | LBS_STANDARD, rect, this, IDC_LISTBOX);
// creazione della checkbox
m_wndCheckBox.Create ("Mostra solo le font TrueType", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, rect, this, IDC_CHECKBOX);
// creazione del frame di visualizzazione della font
m_wndGroupBox.Create ("Esempio", WS_CHILD | WS_VISIBLE | BS_GROUPBOX, rect, this, (UINT) -1);
// creazione del testo font selezionata
m_wndSampleText.Create ("", WS_CHILD | WS_VISIBLE | SS_CENTER, rect, this, IDC_SAMPLE);
// creazione del bottone 'Stampa'
m_wndPushButton.Create ("Stampa", WS_CHILD | WS_VISIBLE |
WS_DISABLED | BS_PUSHBUTTON, rect, this, IDC_PRINT);
Da notare che il metodo Create e’ diverso per ogni controllo a seconda degli argomenti necessari e che ognuno ha bisogna di una struttura di tipo CRect per definirne posizione e dimensione all’interno della finestra.
A questo punto vanno implementate le routine di gestione specificate all’interno della mappa dei messaggi:
void CMainWindow::OnPushButtonClicked ()
{
MessageBox ("Aspetto di essere implementata",
"Error", MB_ICONINFORMATION | MB_OK);
}
Questa e’ l’implementazione della routine di risposta all’evento click del bottone di ‘Stampa’; chiaramente in questo caso il programma non fa niente se non visualizzare un messaggio ma l’importante e’ vedere come e’ strutturata la routine di gestione.
// evento cambiamento di font selezionata nella listbox
void CMainWindow::OnSelChange ()
{
int nIndex = m_wndListBox.GetCurSel ();
…
CString strFaceName;
m_wndListBox.GetText (nIndex, strFaceName);
…
}
Piu’ interessante appare l’implementazione della routine di risposta all’evento OnSelChange della listbox di cui e’ stato riportato solo una porzione.
Il metodo GetCurSel restituisce l’indice dell’elemento correntemente selezionato all’interno della listbox e tale indice viene utilizzato congiuntamente al metodo GetText per recuperare il testo dell’elemento selezionato ed appoggiarlo in una variabile di tipo CString.
Una delle potenzialita’ piu’ interessanti di MFC e’ rappresentata dal fatto che e’ possibile creare controlli “customizzati” ereditando dalle classi base dei controlli predefiniti in modo da arricchirne le funzionalita’.
L’architettura MFC rende questa operazione sufficientemente semplice in quanto permette di specificare un meccanismo di “reindirizzamento” dei messaggi dalla classe base a quella derivata; ad esempio se ho derivato un controllo listbox proprietario in cui voglio gestire il double click in maniera diversa da quella della listbox tradizionale e’ sufficientemente ereditare una nuova classe CMyListbox da CListBox e specificare nella mappa dei messaggi che l’evento double click avra’ un trattamento speciale (che naturalmente dovra essere implementato) rispetto al controllo base.
L’esempio 2_2_3 serve ad esemplificare questa tecnica, implementando un controllo label a cui e’ possibile cambiare il colore del testo; stranamente nel controllo base non e’ possibile infatti intervenire direttamente sul colore.
class CColorStatic : public CStatic
{
protected:
COLORREF m_clrText;
COLORREF m_clrBack;
CBrush m_brBkgnd;
public:
CColorStatic ();
void SetTextColor (COLORREF clrText);
void SetBkColor (COLORREF clrBack);
protected:
afx_msg HBRUSH CtlColor (CDC* pDC, UINT nCtlColor);
DECLARE_MESSAGE_MAP ()
};
Dalla definizione della classe del nostro controllo custom si puo’ notare che abbiamo aggiunto due nuovi metodi SetTextColor e SetBkColor; nella mappa dei messaggi del nuovo controllo dovremo specificare solo quali sono gli eventi che vogliamo gestire in maniera differente dal controllo base:
BEGIN_MESSAGE_MAP (CColorStatic, CStatic)
// rispetto al controllo base il controllo derivato
// gestisce autonomamente il messaggio CtlColor
ON_WM_CTLCOLOR_REFLECT ()
END_MESSAGE_MAP ()
Il messaggio speciale ON_WM_CTLCOLOR_REFLECT, serve a segnalare ad MFC che il nostro controllo derivato avra’ una sua routine di gestione di questo evento e che non vogliamo il trattamento di default riservato alla classe base.
(common controls)
Nelle applicazioni Windows di solito i controlli non vengono posizionati nella finestra principale dell’applicazione, ma in speciali finestre progettate per la raccolta dell’input da parte dell’utente dette “dialog box”.
Un esempio puo’ essere la finestra di salvataggio dei file che troviamo in tutte le applicazioni Windows oppure la finestra in cui si definiscono le opzioni di stampa.
Per inserire una dialog box in una applicazione MFC e’ necessario ereditare una propria classe dalla classe base CDialog, inserire i controlli all’interno della dialog, ed eventualmente implementare alcune funzioni virtuali che gestiscono il comportamento della dialog.
In sintesi i passi da seguire per aggiungere ad una applicazione una semplice dialog box che richiede l’input di un campo test e di uno numerico (listato 2_2_4), sono questi:
Vediamo la definizione della classe CMyDialog tratta dal listato 2_2_4 con la dichiarazione delle variabili membro che dovranno ospitare il risultato dell’input sui controllli della dialog:
class CMyDialog : public CDialog
{
public:
// costruttore standard
CMyDialog(CWnd* pParent = NULL);
// variabili membro associate ai controlli di
// input
CString m_strNome;
int m_intNumero;
…
}
Per poter associare i controlli alle variabili membro appena dichiarate essitono due metodi; il primo consiste nell’effettuare l’override delle funzioni virtuali OnInitDialog e OnOk per associare direttamente al controllo (in questo caso IDC_EDITNOME) la variabile membro (m_strNome) sia quando la dialog viene inizializzata, sia quando il pulsante Ok viene premuto e quindi la dialog verra’ chiusa.
// nel .h (CMyDialog.h)
virtual BOOL OnInitDialog ();
virtual void OnOK();
// nel .cpp (CMyDialog.cpp)
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
SetDlgItemText(IDC_EDITNOME, m_strNome);
return TRUE;
}
void CMyDialog::OnOK()
{
GetDlgItemText(IDC_EDITNOME, m_strNome);
CDialog::OnOK();
}
Altrimenti e’ possibile lasciare il lavoro di associazione / validazione dell’input direttamente ad MFC utilizzando il metodo DoDataExchange della classe CDialog; la funzione DDX_Text associa una variabile membro ad un controllo e la funzione DDV_MinMaxInt controlla che il valore inserito nel controllo di input sia all’interno di un range specificato.
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDITNUMERO, m_intNumero);
DDV_MinMaxInt(pDX, m_intNumero, 0, 10);
}
Nell’applicazione principale la dialog viene richiamata in maniera “modale” e si puo’ testare il valore di ritorno per determinare se l’utente ha premuto il pulsante Ok o Cancel; una volta usciti dalla dialog e’ possibile recuperare il contenuto delle variabili membro ed utilizzare il risultato dell’input effettuato all’interno della dialog.
CMyDialog dlg;
if (dlg.DoModal() == IDOK)
{
// l'utente ha premuto il pulsante OK nella dialog
// visualizziamo il risultato dell'input nella dialog
CString nome;
nome = dlg.m_strNome;
MessageBox(NULL,nome,"Messaggio",MB_OK);
}
Nonostante l’associazione tra controlli e variabili membro all’interno di una dialog box possa essere lasciata ad MFC, e’ possibile comunque manipolare direttamente i controlli presenti all’interno di una dialog ottenendone un puntatore e chiamandone i metodi tramite questo puntatore:
// ottiene un puntatore al controllo Edit
CWnd* pWnd = GetDlgItem(IDC_EDITNOME);
// accede al controllo Edit tramite il puntatore e lo rende
// inattivo
pWnd->EnableWindow(FALSE);
// ottiene un puntatore ad una ListBox
CListBox *pListBox = (CListbox *) GetDlgItem(IDC_LIST);
PListBox->AddString(“TEST”);
si puo’ interagire con un controllo all’interno della dialog anche in altri modi, ad esempio tramite il metodo DDX_Control della dialog che effettua il collegamento tra una variabile di tipo CListBox ed un controllo ListBox all’interno della dialog:
DDX_Control (pDX, IDC_LIST, m_ListBox);
m_listBox.AddString(“TEST”);
Fogli di proprieta’ (“Property Sheets”)
I fogli di proprieta’ sono particolari tipologie di dialog box in cui le pagine di controlli sono suddivise in piu’ sezioni dette “tabs”.
Le “common dialogs” sono finestre di tipo dialog box che vengono usate praticamente in ogni applicazione Windows (come la finestra “Save as …”) e che quindi sono state “pacchettizzate” in un un unico controllo che al suo interno incorpora tutti gli altri controlli costituenti e nasconde all’utilizzatore la complessita’ della loro implementazione.
In sostanza e’ possibile, utilizzando le common dialogs, incorporare nelle proprie applicazioni finestre complete di gestione di particolari tipi di input standard (salvataggio dei file, settaggio della stampante, selezione di un colore ecc.) scrivendo pochissime righe di codice che servono solo a personalizzare minimamente il contenuto di questi utili oggetti preconfezionati.
Un alternativa all’utilizzo della libreria STL (Standard Template Library) e’ quella di utilizzare le collezioni di classi “container” e di utilita’ fornite da MFC; infatti all’interno di MFC si trovano una serie di classi che mette a disposizione container, strutture dati ed ua classe stringa (CString).
Dato che le funzionalita’ di queste classi rappresentano, sotto molti aspetti, un sottoinsieme di quello che viene fornito da STL si consiglia per quanto possibile, di utilizzare STL all’interno delle proprie applicazioni MFC (anche per non dover imparare ad utilizzare tutta una nuova libreria di classi per fare piu’ o meno le stesse cose).
Uno dei concetti piu’ importanti contenuti all’interno delle classi di utilita’ MFC e’ quello della serializzazione; questo procedimento consiste nel poter salvare e leggere da disco i dati persistenti di una applicazione MFC in maniera semplice ed indipendente da come i dati sono organizzati.
Il cuore del concetto di serializzazione consiste nella possibilita’ di rendere “serializzabile” una classe, ovvero di poter avere a disposizione una serie di meccanismi che permettono di memorizzare e rileggere da supporto magnetico i dati contenuti all’interno di una classe semplicemente utilizzando gli operatori << e >> tramite l’utilizzo di un oggetto appartenente alla classe CArchive.
Vediamo qualche esempio tratto dal listato 2_2_5:
Per rendere una classe “serializzabile” come prima cosa e’ necessario derivarla dalla classe base CObject ed inserire all’interno della definizione della classe la macro DECLARE_SERIAL; la classe serializzabile deve inoltre avere un metodo Serialize che riceve in input un reference ad un oggetto di tipo CArchive che sara’ il vero e proprio file su cui andremo a salvare il contenuto della classe Ctest:
class CTest : public CObject
{
DECLARE_SERIAL (CTest)
protected:
CString m_strMsg;
public:
CTest() { }
void set_msg(CString s) { m_strMsg = s; }
CString get_msg() { return m_strMsg; }
void Serialize (CArchive& ar);
};
Nel .cpp in cui andremo ad implementare i metodi della nostra classe serializzabile e’ necessario specificare la macro IMPLEMENT_SERIAL e scrivere il metodo Serialize in cui andremo a scrivere e leggere all’interno dell’oggetto CArchive i membri della classe che vogliamo rendere persistenti:
IMPLEMENT_SERIAL(CTest, CObject, 1)
void CTest::Serialize(CArchive &ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
// SAVE
ar << m_strMsg;
else
// LOAD
ar >> m_strMsg;
}
All’interno dell’applicazione principale utilizziamo la nostra classe serializzabile all’interno di due comandi di menu Load e Save; nel metodo Save viene creato il file su cui salvare il contenuto della classe e viene chiamato il metodo Serialize che provvede a scrivere sul file.
Nel metodo Load il file viene aperto in lettura e di nuovo viene invocato il metodo Serialize per recuperare il contenuto dei membri persistenti della classe dal supporto magnetico:
void CMy2_2_5App::OnSave()
{
CTest ct;
ct.set_msg("pino");
CFile file("CTest.txt", CFile::modeWrite | CFile::modeCreate);
CArchive ar (&file, CArchive::store);
ct.Serialize(ar);
}
void CMy2_2_5App::OnLoad()
{
CTest ct;
CFile file("CTest.txt", CFile::modeRead);
CArchive ar (&file, CArchive::load);
ct.Serialize(ar);
MessageBox(NULL,ct.get_msg(),"Msg",MB_OK);
}
Come gia’ detto nei primi capitoli, MFC non e’ solo un’interfaccia Object Oriented verso le API di Windows, ma e’ anche tutta una serie di meccanismi applicativi e di tecniche di sviluppo (il cosiddetto “framework”), di cui l’architettura Document/View (Documento/Vista) costituisce una parte fondamentale.
L’architettura applicativa Document/View si puo’ definire come un insieme di classi e di tecniche di programmazione che permette di astrarre la rappresentazione dei dati di una applicazione (Document) dalla sua visualizzazione grafica all’interno di una finestra (View).
Il vantaggio di questo tipo di architettura, oltre a quello intrinseco di separare i dati dall’interfaccia grafica, e’ rappresentato dal fatto che il framework MFC praticamente si occupa di gestire molti aspetti dell’applicazione (soprattutto in termini di interfaccia grafica) lasciando quindi al programmatore il compito di scrivere il codice concentrandosi sulle classi che gli servono per risolvere il problema specifico.
Per fare un esempio, l’architettura Document/View permette di gestire il salvataggio e la lettura dei dati di un’applicazione senza praticamente dover scrivere codice, e gestisce inoltre in maniera trasparente altre situazioni come la richiesta di salvare prima di uscire se un documento e’ stato modificato ecc.
Un altro vantaggio nell’utilizzo di applicazioni MFC Document/View consiste nell’elevato grado di astrazione della vista rispetto al dispositivo fisico di output; infatti effettuare operazioni di output su di una vista permette di raggiungere l’indipendenza rispetto al fatto che poi la vista sia mappata fisicamente sullo schermo oppure sulla stampante, rendendo l’implementazione di routine di stampa un compito ben piu’ semplice che in passato.
Una applicazione Document/View MFC puo’ inoltre aggiornare il registro, specificando la propria estensione dei file in modo che sia sufficiente fare doppio click su di un file salvato dall’applicazione per poterlo caricare ed eseguire allo stesso tempo l’applicazione (come succede quando si fa doppio click su di un file .doc di Word); e’ inoltre possibile aprire direttamente un nuovo file semplicemente trascinandolo all’interno della finestra dell’applicazione (drag & drop open).
La struttura base dell’architettura di un’applicazione Document/View e’ formata da una “finestra container” o “frame window” (CFrameWnd) che rappresenta la finestra principale dell’applicazione, la quale agisce da contenitore per una o piu’ “viste” (CView) che non sono altro che rappresentazioni dei dati del programma memorizzati all’interno di un oggetto appartenente alla classe “documento” (CDocument).
Le applicazioni Document/View possono essere di tipo SDI (Single Document Interface), ovvero che possono gestire una singola vista per volta di un oggetto documento all’interno della finestra contenitore, e MDI (Multiple Document Interface) ovvero dotate di viste multiple su oggetti documento multipli.
E’ chiaro che l’implementazione e la gestione di un’applicazione MDI e’ ben piu’ complessa di una SDI e probabilmente anche piu’ difficile da comprendere per l’utilizzatore finale; percio’ all’interno di questi capitoli dedicati all’architettura Document/View ci concentreremo maggiormente sulle applicazioni SDI.
Il framework MFC permette inoltre di creare a runtime documenti, viste e finestre container tramite il processo chiamato “Dynamic Object Creation”
Grazie alla possibilita’ di instradamento dei messaggi effettuata da MFC le varie componenti di una applicazione Document/View possono processare i messaggi ricevuti indipendentemente; ogni messaggio in arrivo verra’ prima inviato alla vista, se questa non ha una routine di gestione andra’ all’oggetto Document e successivamente alla finestra container CFramewnd e su fino all’oggetto globale Application (Ad esempio e’ l’oggetto applicazione, che solitamente gestisce gli eventi OnFileNew e OnFileSave per una applicazione Document/View).
Per chiarire il funzionamento dell’architettura Document/View andremo ad analizzare un’applicazione di esempio (listato 2_2_6), che e’ molto semplice ma contiene al suo interno tutti i concetti fondamentali.
Grazie al framework MFC e’ possibile generare un’applicazione di questo tipo limitando al minimo la quantita’ di codice scritto manualmente utilizzando il Wizard dell’ambiente di sviluppo.
Avvalendosi del wizard e’ possibile delegare al Visual Studio la generazione di tutto lo scheletro di una applicazione SDI base gia’ fornita di tutti le funzionalita’ base descritte nel capitolo precedente, come salvataggio/caricamento dei documenti, menu dei comandi pregenerato con relativa mappa dei messaggi, predisposizione per le funzioni di stampa ecc.
Per creare lo scheletro di una applicazione SDI utilizzando il code wizard e’ necessario seguire questi passi:
Se proviamo ad eseguire lo scheletro dell’applicazione generato dal Visual Studio possiamo vedere quanta parte di lavoro sia gia’ stata svolta dall’ambiente di sviluppo in termini di interfaccia e funzioni comuni.
Utilizzando come modello l’applicazione di esempio andiamo ad esaminare quali sono le classi utilizzate da una tipica applicazione SDI:
La prima classe che troviamo e’ quella utilizzata per istanziare l’applicazione stessa; l’oggetto applicazione viene creato in maniera globale nello stesso modo che avevamo visto negli esempi precedenti (ad esempio CSquaresApp MyApp), ma troviamo una differenza nell’implementazione del metodo InitInstance dell’oggetto CSquaresApp dato che l’inizializzazione dell’architettura Document/View richiede una serie di operazioni aggiuntive:
// anche CSquaresAPP e’ derivata da CWinApp
class CSquaresApp : public CWinApp
BOOL CSquaresApp::InitInstance()
{
// crea la chiave nel registro se non esistente
// e carica le impostazioni di default (gestito
// da MFC)
SetRegistryKey("SDISquares");
LoadStdProfileSettings();
// inizializza l’applicazione di tipo
// Document/View SDI creando un nuovo template e
// specificando l’id del file di risorse, il nome
// della classe Document, il nome della classe della
// finestra container ed il nome della classe vista
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CSquaresDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CSquaresView));
AddDocTemplate(pDocTemplate);
// abilita l’apertura dell’applicazione in seguito
// al doppio click su di un file salvato
EnableShellOpen();
RegisterShellFileTypes(TRUE);
// riceve eventuali parametri da linea di comando
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// processa i parametri ricevuti da linea di comando
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// visualizza la finestra e container
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
// abilita l’apertura di nuovi documenti semplicemente
// facendo drag e drop all’interno della finestra
// dell’applicazione
m_pMainWnd->DragAcceptFiles();
return TRUE;
}
La differenza piu’ importante sta nella creazione del template di tipo CSingleDocTemplate, che in pratica comunica al compilatore che stiamo creando una nuova applicazione di tipo Document/View e che quindi vogliamo utilizzare la struttura finestra container / vista / documento.
La finestra principale dell’applicazione che agisce da container per la vista e’ derivata dalla classe base CFrameWnd; la classe CFrameWnd e’ diversa da CWnd che avevamo utilizzato negli esempi precedenti in quanto la classe CWnd e’ semplicemente un oggetto di tipo finestra Windows senza particolari caratteristiche mentre la classe CFrameWnd rappresenta un tipo particolare di finestra gia’ progettato per agire da container per altre finestre e dotato di tutta una serie di funzionalita’ orientate verso l’architettura Document/View.
Si tratta in sostanza di una finestra “intelligente” che provvede al suo interno a gestire molti dei compiti richiesti dall’architettura Document/View (ad esempio quando viene ridimensionata la finestra container automaticamente viene ridimensionata anche la vista ecc.).
Se osserviamo l’implementazione della classe CMainFrame, vediamo che oltre al codice generato automaticamente dal wizard non e’ stato necessario scrivere una sola riga di codice per personalizzare il comportamento della finestra container che va gia’ benissimo cosi’ per la nostra semplice applicazione.
class CMainFrame : public CFrameWnd
La classe CSquaresDoc derivata da CDocument rappresenta la struttura dati di una applicazione Document/View ed include le “operazioni” che e’ possibile effettuare sui dati; e’ da notare che non e’ presente nessuna informazione riguardo a come questi dati verranno poi visualizzati nella finestra dato che questo compito e’ demandato interamente all’oggetto vista.
class CSquaresDoc : public Cdocument
{
…
public:
void SetSquare (int i, int j, COLORREF color);
COLORREF GetSquare (int i, int j);
COLORREF GetCurrentColor();
virtual ~CSquaresDoc();
protected:
COLORREF m_clrCurrentColor;
COLORREF m_clrGrid[4][4];
afx_msg void OnColorRed();
afx_msg void OnColorYellow();
afx_msg void OnColorGreen();
afx_msg void OnColorCyan();
afx_msg void OnColorBlue();
afx_msg void OnColorWhite();
afx_msg void OnUpdateColorRed(CCmdUI* pCmdUI);
afx_msg void OnUpdateColorYellow(CCmdUI* pCmdUI);
afx_msg void OnUpdateColorGreen(CCmdUI* pCmdUI);
afx_msg void OnUpdateColorCyan(CCmdUI* pCmdUI);
afx_msg void OnUpdateColorBlue(CCmdUI* pCmdUI);
afx_msg void OnUpdateColorWhite(CCmdUI* pCmdUI);
DECLARE_MESSAGE_MAP()
}
Generalmente nell’implementazione della classe derivata dalla CDocument e’ necessario effettuare l’override del metodo OnNewDocument per effettuare le necessarie operazioni di inizializzazione nel momento in cui un nuovo documento viene creato (il metodo OnNewDocument viene chiamato automaticamente dal framework MFC nel momento in cui si selezione la voce ‘New’ dal menu ‘File’);e’ inoltre necessario implementare anche il metodo Serialize per fare in modo che la nostra classe derivata da CDocument sia “serializzabile” (vedi capitolo 2.3).
BOOL CSquaresDoc::OnNewDocument()
void CSquaresDoc::Serialize(CArchive& ar)
La classe CSquaresView e’ derivata dalla classe base CView ed e’ quello che si definisce la “vista” della nostra applicazione; come gia’ accennato lo scopo della vista e’ quello di fornire una rappresentazione dei dati contenuti nella classe documento e di interagire con l’utente dell’applicazione (ovvero intercettare l’input da mouse e/o tastiera).
Si possono associare piu’ viste ad uno stesso oggetto documento (dato che possono esistere diversi modi di rappresentare graficamente gli stessi dati), ma una vista appartiene sempre ad un solo documento.
Questo legame e’ mantenuto dal framework MFC ed il programmatore puo’ accedere per puntatore all’oggetto documento collegato ad una vista tramite il metodo GetDocument.
class CSquaresView : public CView
{
…
public:
CSquaresDoc* GetDocument();
…
inline CSquaresDoc* CSquaresView::GetDocument()
{ return (CSquaresDoc*)m_pDocument; }
}
In genere, dato che la vista si occupa di dare una rappresentazione grafica ad un documento, il metodo piu’ importante dell’oggetto CView e’ quello che risponde all’evento OnDraw, ovvero alla richiesta di disegnare il contenuto della vista.
Questo metodo, fornito come implementazione della funzione virtuale OnDraw, e’ chiamato direttamente dal framework MFC tutte le volte che sia necessario ridisegnare il contenuto della vista :
void CSquaresView::OnDraw(CDC* pDC)
{
CSquaresDoc* pDoc = GetDocument();
…
COLORREF color = pDoc->GetSquare (i, j);
…
pDC->FillRect (rect, &brush);
…
}
E’ da notare due cose nell’implementazione del metodo OnDraw; la prima e’ il modo in cui la vista ottiene accesso al documento utilizzando un puntatore di tipo CSquaresDoc restituito dal metodo GetDocument, e la seconda e’ il fatto che le funzioni di disegno (come FillRect) lavorino su di un device context generico che viene passato alla funzione di disegno della vista dal framework MFC (questo ci suggerisce che se il device context fosse associato ad un oggetto fisico di tipo “stampante” non ci sarebbe da modificare niente all’interno del metodo OnDraw per poter ottenere una stampa cartacea).
La classe CSquaresView, come gia’ detto, si deve occupare anche della gestione dell’input (in questo caso della pressione del tasto sinistro del mouse che comporta la colorazione di uno dei quadrati); e’ da notare che questo evento e’ gestito normalmente tramite la mappa dei messaggi come avevami visto negli esempi dei capitoli precedenti.
L’unica particolarita’ consiste nel fatto che, come prima cosa, viene richiamato il metodo OnLButtonDown della classe base CView (dato che anche la classe base deve essere notificata dell’evento):
void CSquaresView::OnLButtonDown(UINT nFlags, CPoint point)
{
CView::OnLButtonDown(nFlags, point);
…
}
Le viste “scrollabili” ed altri tipi di viste
La vista che abbiamo utilizzato nel programma di esempio (derivata da CView) non dispone di funzionalita’ di scroll del contenuto della finestra; se avessimo voluto disporre di tale funzionalita’ avremmo dovuto derivare la nostra classe vista da un’altra classe denominata CScrollView; la classe CscrollView si occupa di gestire la maggior parte delle operazioni necessarie per gestire lo scorrimento del contenuto della vista tanto da poter utilizzare una generica funzione OnDraw sia su di una vista scrollabile, sia su di una vista fissa.
Esistono inoltre altri tipi di viste che e’ possibile utilizzare all’interno dell’architettura Document/View, le piu’ importanti sono:
CHtmlView utilizzata per visualizzare documenti HTML all’interno della vista
CTreeView utilizzata per visualizzare documenti in maniera gerarchica
E’ possibile inoltre crearsi tipi personalizzati di viste per particolari esegenze applicative semplicemente derivando una propria classe vista dalla classe base CCtrlView.
In questo capitolo evidenzieremo solo le caratteristiche principali delle applicazioni MDI ma non ci addentreremo nei dettagli relativi allo sviluppo data la complessita’ di questo tipo di applicazioni (anche se in fondo la maggior parte delle applicazioni Windows che vengono realizzate sono proprio di questo tipo: vedi i pacchetti che compongono Microsoft Office e molte altri software commerciali).
Una applicazione MDI puo’ supportare viste multiple sullo stesso documento, piu’ documenti dello stesso tipo attivi allo stesso tempo e perfino tipi multipli di documento visualizzati tramite viste differenti.
Queste sono le caratteristiche principali di una applicazione MDI:
Fortunatamente il framework MFC offre un notevole supporto al programmatore nella realizzazione di applicazioni MDI, mantendo oltretutto le stesse modalita’ impiegate per lo sviluppi di applicazioni SDI, con alcune differenze:
Implementare funzionalita’ di stampa in applicazioni Windows tradizionali (SDK) e’ sempre stato un compito di una certa difficolta’, che rendeva la progettazione dei report e delle stampe una attivita’ piuttosto onerosa per il programmatore.
Tutto questo per i molti fattori che entrano in gioco quando si tratta di stampare in un ambiente Windows, come la paginazione, il fatto di poter essere in grado di annullare una stampa in corso ed anche perche’ spesso si tratta di stampare output grafico e non solo testo.
Inoltre altro codice andava scritto se si voleva dotare le applicazioni anche della funzionalita’di “Preview di stampa”.
L’architettura MFC attenua moltissimo la difficolta’ di implementazione di procedure di stampa nelle applicazioni Windows, in particolare se utilizzano l’architettura Document/View.
Infatti MFC mette a disposizione del programmatore un device context generico che puo’ essere rediretto facilmento su stampante, si prende cura della possibile interruzione della stampa in corso e gestisce perfino in maniera molto semplice la funzionalita’ di preview.
Sebbene il metodo OnDraw visto nel capitolo 2.5 permetta di inviare l’output sia a video che su stampante e’ spesso necessario utilizzare l’altro metodo chiamato OnPrint in quanto comunque quando si stampa si devono gestire informazioni aggiuntive come magari il numero di pagina o una intestazione.
I passi necessari per effettuare la stampa da una applicazione MFC document/view sono I seguenti:
Effettuare l’override della funzione virtuale OnPreparePrinting della classe CView.
BOOL CMyView::OnPreparePrinting(CPrintInfo* pInfo)
{
pInfo->SetMaxPage(10);
return DoPreparePrinting(pInfo);
}
In questa forma minima la funzione OnPreparePrinting non fa altro che aprire la dialog box relativa alle impostazioni di stampa e ritornare un puntatore ad un device context di tipo stampante; e’ consigliato inoltre chiamare la funzione SetMaxPage per comunicare al sottosistema di stampa il numero massimo di pagine.
Dato che il numero di pagine puo’ non essere noto a priori e spesso dipende dalla dimensione del documento si puo’ omettere tale parametro nella OnPreparePrinting e sfruttare l’evento successivo OnBeginPrinting per calcolare il numero effettivo di pagine:
void CMyView::OnBeginPrinting(CDC *pDC, CprintInfo *pInfo)
{
int nPageHeight = pDC->GetDeviceCaps(VERTRES);
int nDocLength = GetDocument()->GetDocLength();
int nMaxPage = max (1, (nDocLength + (NpageHeight –1)) / nPageHeight);
pInfo->SetMaxPage(nMaxPage);
}
L’evento OnBeginPrinting viene attivato subito prima di iniziare la stampa, quindi il sistema ha gia’ allocato le risorse necessarie ed e’ quindi possibile calcolare con esattezza il numero delle pagine da stampare.
Una volta chiamate queste due funzioni di preparazione della stampa, per ogni pagina, vengono chiamati i metodi:
void CMyView::OnPrepareDC(CDC *pDC, CprintInfo *pInfo)
{
}
e
void CMyView::OnPrint(CDC *pDC, CprintInfo *pInfo)
{
// ad esempio
PrintHeader(pDC);
PrintPageNumber(pDC, pInfo->m_nCurPage);
OnDraw(pDC);
}
l’override di queste due funzioni virtuali e’ necessario solamente nel caso in cui si voglia stampare qualcosa oltre il contenuto di default (cio’ che viene stampato nel metodo OnDraw), come ad esempio il numero di pagina oppure un’intestazione.
Alla fine del processo di stampa viene chiamato il metodo:
void CMyView::OnEndPrint(CDC *pDC, CprintInfo *pInfo)
{
}
Se deve essere effettuata qualche operazione al termine della stampa puo’ essere utile effettuare l’override del metodo OnEndPrint.
Se ad un’applicazione MFC document/view e’ stata aggiunt la funzionalita’ di stampa, la fpossibilita’ di effettare anche il preview e’ implementabile in maniera veramente semplice collegando la voce di menu “Print Preview” al metodo CView::OnFilePrintPreview.
Questa e’ la mappa dei messaggi tratta dall’applicazione di esempio 2_2_7 che mostra quanto poco codice sia necessario aggiungere per implementare la funzionalita’ di preview di stampa:
ON_COMMAND(ID_FILE_PRINT_PREVIEW,CView::OnFilePrintPreview)
Spesso capita di dover sviluppare applicazioni che devono eseguire operazioni non in risposta a determinati input da parte dell’utente, ma ad intervalli regolari; un esempio puo’ essere un’applicazione che effettua il monitoraggio delle risorse di un server ogni 5 minuti, oppure un programma che effettua la pulizia di alcuni file temporanei ogni n secondi ecc.
In questi casi e’ necessario avere a disposizione un oggetto chiamato “timer” la cui funzione e’ quella di “svegliare” un’applicazione una volta trascorso un certo intervallo di tempo (solitamente espresso in millisecondi).
Per gestire i timer con MFC e’ necessario utilizzare due funzioni CWnd::SetTimer e CWnd::KillTimer il cui significato e’ abbastanza ovvio.
Ogni volta che l’intervallo fissato e’ trascorso il timer deve comunicarlo all’applicazione e per fare questo possono essere utilizzati due metodi:
Gestire il messaggio WM_TIMER probabilmente e’ il metodo piu’ semplice, ma se si hanno timer multipli in un’applicazione e’ preferibile utilizzare il metodo della funzione di callback.
NOTA BENE: Windows non e’ un sistema operativo “real time” quindi un timer settato ad un intervallo di 500 millisecondi non mi da nessuna garanzia che scattera’ con la massima precisione ogni 500 millisecondi; diciamo piuttosto che la media, dopo un centinaio di attivazioni, convergera’ verso tale valore.
Per creare un timer si utilizza la funzione SetTimer passandogli un numero identificativo del timer, l’intervallo in millisecondi ed il metodo di gestione (NULL come ultimo parametro indica l’utilizzo del messaggio WM_TIMER altrimenti e’ necessario passare il nome della funzione di callback):
// in genere il timer viene creato al momento
// della creazione della finestra principale
Int CMainWindow::OnCreate (LPCREATESTRUCT lpcs)
{
…
// creiamo il timer 1 con intervallo 1 secondo
if (!SetTimer (1, 1000, NULL))
{
MessageBox(“Errore nella SetTimer”);
Return –1;
}
return 0;
}
// dichiariamo nella message map il messaggio del timer
BEGIN_MESSAGE_MAP(CMainWindow, CFrameWnd)
…
ON_WM_TIMER()
ON_WM_CLOSE()
…
END_MESSAGE_MAP()
// funzione che risponde al messaggio WM_TIMER
// da notare che input riceve l’identificativo del timer
Void CMainWindow::OnTimer(UINT nTimerId)
{
// operazioni da svolgere ogni secondo
}
// il timer viene distrutto prima della chiusura
// della finestra dell’applicazione
Void CMainWindow::OnClose()
{
KillTimer(1);
}
Per settare un timer in modo da utilizzare la funzione di callback:
SetTimer (1, 1000, TimerProc);
Void CALLBACK TimerProc (HWND hWnd, UINT nMsg,
UINT nTimerId, DWORD dwTime)
{
// il parametro hWnd contiene l’handle della
// finestra in cui era stato definito il timer
// nMsg contiene l’id di messaggio WM_TIMER
// nTimerId contiene l’identificativo del timer
// dwTime contiene il numero di millisecondi
// passati dal momento dell’avvio di Windows (!)
}
Una applicazione Windows generica trascorre la maggior parte del suo tempo ad elaborare i vari messaggi in arrivo dal sistema operativo in risposta ad operazioni di input da parte dell’utente finale.
Quando nella coda non sono disponibili messaggi una applicazione si mantiene in uno stato detto di “idle” (inattivita’); per ottimizzare l’efficienza di una applicazione si puo’ pensare di utilizzare questo momento di inattivita’ per portare avanti delle elaborazioni in background che non hanno bisogno di interagire con l’utente (ad esempio la pulizia di file temporanei o la scrittura di informazioni non critiche in un file di log ecc.).
La classe applicazione MFC CWinApp mette a disposizione per questo tipo di elaborazioni una routine di gestione chiamata OnIdle in cui si puo’ posizionare del codice da eseguire durante il ciclo di “idle”.
// chiamata automaticamente durante il ciclo di idle
// lCount indica il numero di volte che la funzione
// di idle e’ stata chiamata dall’ultimo messaggio
// processato dall’applicazione
BOOL CMyApp::OnIdle (LONG lCount)
{
// ATTENZIONE se si omette la chiamata al metodo
// OnIdle di CWinApp si compromette il funzionamento
// di tutta l’applicazione
CWinApp::OnIdle(lCount)
// adesso possiamo eseguire le nostre operazioni in
// background
DoIdleProcessing();
Return TRUE;
}
Sebbene la tecnica dell’utilizzo del ciclo di idle possa apparire interessante e conveniente nelle applicazioni moderne in genere si tende ad utilizzare la tecnica del “multithreading” che vedremo nel prossimo capitolo; puo’ capitare comunque di utilizzare questa tecnica se le operazioni da compiere in background sono semplici e non si vuole utilizzare risorse inutilmente per creare un nuovo thread, oppure se la nostra applicazione deve girare ancora in ambiente 16 bit (Windows 3.11 e precedenti) dove non esiste supporto per il multithreading.
(WriteProfileString / GetProfileString)
(Ctime / Ctime::GetCurrentTime)
Nell’ambiente Windows a 32 bit un’applicazione in esecuzione viene definita “processo”, ed ogni processo contiene uno o piu’ “thread” in esecuzione; un thread puo’ essere definito come un percorso di elaborazione all’interno del codice di un programma che si porta dietro i suoi dati e le sue risorse (stack, registri, heap e cosi’ via).
Solitamente nelle applicazioni tradizionali si ha un solo thread che tende quindi ad identificarsi completamente con il processo; un sistema operativo si definisce “multitasking” se e’ in grado di eseguire piu’ processi contemporaneamente mentre un’applicazione si definisce “multithread” se e’ formata da piu’ thread in esecuzione contemporanea (parallela).
Nonostante gli evedenti benefici di questo tipo di applicazioni (un thread puo’ occuparsi di interagire con l’utente mentre un altro effettua le necessarie elaborazioni e calcoli dando la possibilita’ al programma di rimanere sempre pronto a rispondere alle richieste di input dell’utente e non bloccandosi durante le elaborazioni), i programmi “multithread” sono molto difficili da scrivere rispetto ai programmi tradizionali ed e’ necessario che il programmatore inizi a ragionare in maniera “parallela”.
Il problema risiede nel fatto che si e’ portati a ragionare come programmatori in maniera “sincrona”, mentre per definizione i programmi multithread sono “asincroni” e quindi e’ necessario gestire la sincronizzazione fra il task principale dell’applicazione ed i vari sottothread per evitare tutta una serie di errori che possono scaturire dal parallelismo (ad esempio il programma principale tenta di accedere ai risultati di una elaborazione quando il thread che elabora non e’ ancora terminato, oppure le risorse necessarie ad un thread per un’elaborazione sono “bloccate” da un altro ecc.).
La programmazione multithread si porta dietro inoltre tutta una serie di concetti e considerazioni che non possono essere ignorate qualora si decida di utilizzare questa tecnica:
Ad un thread puo’ essere utile assegnare una priorita’ maggiore rispetto ad un altro perche’ l’elaborazione che sta svolgendo e’ di importanza critica per l’applicazione.
L’applicazione principale deve avere a disposizione meccanismi per gestire la sincronizzazione dei vari thread ed inoltre devono essere usati strumenti per regolamentare l’accesso alle risorse tra i vari thread in esecuzione; Windows supporta i seguenti meccanismi di sincronizzazione:
L’esempio 2_3_1 dimostra un possibile utilizzo per un’applicazione multithread; in questo esempio un thread si occupa di effettuare un complesso calcolo matematico mentre il thread principale si occupa di gestire l’interfaccia utente.
Se proviamo infatti a far calcolare al programma il numero di primi da 2 a 100.000.000 il thread che effettua il calcolo impieghera’ un certo tempo ma possiamo notare che, grazie all’architettura multithread, l’interfaccia utente non e’ bloccata fino alla fine del calcolo ma rimane utilizzabile (ovvero risponde agli input dell’utente).
// alla pressione del comando start viene avviato il nuovo
// thread
void CSieveDlg::OnStart()
{
…
// al thread di calcolo viene passata una struttura
// contenente i parametri necessari per il calcolo
THREADPARMS* ptp = new THREADPARMS;
ptp->nMax = nMax;
ptp->hWnd = m_hWnd;
// oltre ai parametri viene passato il nome della
// funzione che implementa il thread vero e proprio
AfxBeginThread (ThreadFunc, ptp);
}
// mappa dei messaggi dell’applicazione principale
BEGIN_MESSAGE_MAP(CSieveDlg, CDialog)
…
ON_MESSAGE (WM_USER_THREAD_FINISHED, OnThreadFinished)
END_MESSAGE_MAP()
// l’evento OnThreadFinished viene attivato al termine
// dell’elaborazione del thread grazie al messaggio
// definito nella message map (WM_USER_THREAD_FINISHED)
// il messaggio viene inviato dal thread stesso una volta
// terminata l’elaborazione per fare in modo che
// l’applicazione principale possa visualizzare il
// risultato
LONG CSieveDlg::OnThreadFinished (WPARAM wParam, LPARAM lParam)
{
// una volta finito il thread di calcolo visualizziamo
// il risultato
SetDlgItemInt (IDC_RESULT, (int) wParam);
GetDlgItem (IDC_START)->EnableWindow (TRUE);
return 0;
}
// il vero e proprio thread di calcolo
UINT ThreadFunc (LPVOID pParam)
{
// lettura dei parametri ricevuti
THREADPARMS* ptp = (THREADPARMS*) pParam;
int nMax = ptp->nMax;
HWND hWnd = ptp->hWnd;
delete ptp;
// si effettua il calcolo
int nCount = Sieve (nMax);
// si segnala all’applicazione principale che
// l’elaborazione e’ terminata
::PostMessage (hWnd, WM_USER_THREAD_FINISHED, (WPARAM) nCount, 0);
return 0;
}
L’architettura corrente per l’accesso ai dati messa a disposizione da Microsoft per i vari ambienti Windows e’ definita MDAC (Microsoft Data Access Components), ed e’ attualmente distribuita in un pacchetto arrivato alla versione 2.7
I componenti di accesso ai dati includono ActiveX Data Objects (ADO) che verra’ trattato in dettaglio nel capitolo 3.4, OLE DB e Open Data Base Connectivity (ODBC).
Applicazioni scritte utilizzando i vari linguaggi Microsoft (Visual C++, Visual Basic, ASP ecc.) possono utilizzare questi componenti per accedere ad una vasta gamma di fonti dati, sia relazionali che non relazionali, attraverso reti locali (LAN) oppure attraverso Internet.
ActiveX Data Objects (ADO)
ADO fornisce una libreria di accesso ai dati ad alte prestazioni e dalle basse richieste in termini di risorse come memoria e disco.
Fornisce inoltre un’interfaccia di facile utilizzo verso OLE DB, il quale si occupa dello strato sottostante e del vero e proprio accesso ai dati; ADO e’ realizzato tramite la tecnologia COM per poter essere impiegato alll’interno di qualsiasi applicazione indipendentemente dal linguaggio con cui e’ scritta.
OLE DB
OLE DB e’ un set di interfacce che espongono dati da una varieta’ di fonti relazionali e non relazionali, attraverso l’utilizzo della tecnologia COM.
Fornisce inoltre un’interfaccia verso i driver ODBC per garantire uniformita’ di supporto e di accesso verso una vasta gamma di DBMS relazionali.
Open Database Connectivity (ODBC)
L’interfaccia ODBC di Microsoft permette alle applicazioni di poter accedere ai piu’ popolari database relazionali tramite un metodo di uniforme ed indipendente dal vero e proprio DBMS che contiene i dati.
Tramite la tecnologia ODBC e’ possibile creare applicazioni che utilizzano statement SQL che non devono essere modificate nel momento in cui viene cambiato il DBMS sottostante.
Tra le varie tecnologie di accesso ai database elaborate da Microsoft nel corso delle varie versioni Windows e’ interessante soffermarsi su quella chiamata OLE DB.
OLE DB definisce un insieme di interfacce COM che forniscono alle applicazioni un metodo uniforme di accesso ai dati memorizzati nelle piu’ diverse forme.
L’architettura OLE DB rende trasparente alle applicazioni che accedono alla fonte dati tramite ADO, gli ulteriori strati intermedi necessari per arrivare dallo statement SQL all’interno del programma all’effettivo interfacciamento con il DBMS ed al recupero dei dati attraverso una rete locale o perfino attraverso Internet.
Un driver OLE DB che permette ad ADO di interagire direttamente con una fonte dati e’ definito “OLE DB Provider”.
Consideriamo ad esempio i seguenti scenari che possono verificarsi nel momento in cui si voglia effettuare una query:
Come si puo’ vedere da questi esempi, un’applicazione che utilizza ADO puo’ effettuare interrogazioni su una vasta gamma di fonti dati (non necessariamente database relazionali) senza effettuare cambiamenti di rilievo alla struttura dell’applicazione ed al modo di utilizzare gli oggetti ADO; tutto questo e’ possibile grazie all’architettura OLE DB.
ADO (ActiveX Data Object) e’ una libreria basata su tecnologia COM che fornisce agli sviluppatori un potente modello di programmazione per accedere ad una grande varieta’ di fonti dati attraverso interfacce di sistema OLE DB.
L’utilizzo piu’ comune di ADO e’ per effettuare query e aggiornamenti sulle tabelle dei piu’ diffusi database relazionali (Oracle, SQL Server, DB2, Informix ecc.) tramite il linguaggio SQL.
ADO e’ formato da una serie di nove oggetti e di quattro “collections” attraverso i quali e’ possibile utilizzarne le funzionalita’:
Oggetto o Collezione |
Descrizione |
Connection object |
Rappresenta la connessione tra un applicazione client ed una generica fonte dati |
Command object |
Usato per definire comandi SQL che poi vengono applicati ad una fonte dati |
Recordset object |
Rappresenta l’insieme dei record risultato di uno statement SQL oppure l’intero contenuto di una tabella di un database; ogni recordset e’ formato da righe e da colonne (campi della tabella) |
Record object |
Rappresenta una singola riga all’interno di un recordset |
Stream object |
Rappresenta un flusso generico di dati binari o di tipo testo |
Parameter object |
Contiene i parametri da associare ad una query o stored procedure che richiede argomenti dinamici |
Field object |
Rappresenta una colonna (campo) all’interno di un oggetto Recordset |
Property object |
Rappresenta un container di proprieta’ di ADO definite dinamicamente a seconda delle caratteristiche del provider con cui si accede alla fonte dati |
Error object |
Contiene i dettagli relativi agli errori di accesso ai dati |
Fields collection |
Contiene tutti gli oggetti Field di un Recordset o di un Record |
Properties collection |
Contiene tutti gli oggetti Property di una particolare istanza di un oggetto |
Parameters collection |
Contiene tiutti gli oggetti Parameter di un oggetto Command |
Errors collection |
Contiene tutti gli oggetti Error avvenuti durante una operazione sulla fonte dati |
Il seguente diagramma mostra graficamente la gerarchia degli oggetti che compongono ADO:
L’esempio 2_3_4 provvede ad illustrare le caratteristiche fondamentali di ADO illustrando come avviene la connessione ad un DB Access, l’apertura di un recordset su di una tabella del db e lo scorrimento di tutti i record della tabella che vengono utilizzati per riempire una listbox.
Per poter utilizzare ADO all’interno di una applicazione C++ e’ necessario importare la libreria ADO (msado15.dll) tramite l’utilizzo della nuova direttiva di preprocessore import; inoltre per evitare collisioni tra ADO ed MFC e’ necessario specificare la clausola no_namespace e rinominare la define EOF in EndOfFile.
#import
"c:\Program Files\CommonFiles\System\ADO\msado15.dll"
no_namespace rename("EOF", "EndOfFile")
Il primo passo per poter accedere ad un data base tramite ADO e’ quello di dichiarare ed istanziare una variabile di tipo puntatore a Connection:
// dichiariamo un puntatore ad un oggetto ADO Connection
// e ad un oggetto ADO RecordSet
_ConnectionPtr pConn("ADODB.Connection");
_RecordsetPtr pRst("ADODB.Recordset");
// stringa di connessione ad Access
// apriamo la connessione tramite l’utilizzo del metodo
// Open dell’oggetto Connection
pConn->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=d:\\test.mdb;", "", "", adConnectUnspecified);
Dato che ADO puo’ connettersi a qualsiasi fonte dati e’ necessario specificare all’interno del metodo Open dell’oggetto Connection a quale database ci vogliamo connettere (OLEDB Provider) e quale e’ il nome della fonte dati che identifica il vero e proprio db.
Nel caso di Microsoft Access come identificativo della fonte dati possiamo utilizzare direttamente il nome del file mdb che contiene il database, ma se ad esempio volessimo utilizzare ADO per accedere a SQL Server avremmo dovuto fornire dei parametri di connessione diversi:
// stringa di connessione per una fonte dati SQL Server
Provider=sqloledb;Data Source='MySqlServer'
Initial Catalog='pubs';Integrated Security='SSPI'
Una volta inizializzato l’oggetto ADO Connection e’ possibile inizializzare un oggetto ADO RecordSet utilizzando il puntatore alla connessione:
// apre il recordset
pRst->Open(
"countries", // nome tabella db
_variant_t((IDispatch *) pConn, true),
adOpenStatic, // recordset statico
adLockReadOnly, // la tabella e’ lockata
adCmdTable); // stiamo leggendo una tabella
se si vuole aprire un recordset che contiene il risultato di una SELECT SQL e’ necessario modificare gli argomenti del metodo Open in questo modo:
pRst->Open(
"SELECT * FROM countries ", // SQL
_variant_t((IDispatch *) pConn, true),
adOpenStatic,
adLockReadOnly,
1); // stiamo passando un’istruzione SQL
una volta che il recordset e’ aperto abbiamo a disposizione tutto l’insieme dei record della tabella (o dello stament SQL) e possiamo quindi scorrere e posizionarci all’interno del RecordSet:
// si posiziona sul primo elemento del recordset
pRst->MoveFirst();
while (!pRst->EndOfFile)
{
// Legge il campo ‘Country’ dal recordset
vtCountry =
pRst->GetFields()->GetItem("country")->GetValue();
pRst->MoveNext();
}
e’ necessario sempre ricordarsi di chiudere sia l’oggetto recordset che la connessione una volta terminate le operazioni sul database:
pRst->Close();
pConn->Close();
Per poter eseguire uno statement SQL che effettui una modifica al database (INSERT,UPDATE o DELETE) e’ necessario utilizzare l’oggetto ADO Command:
// inizializza lo statement SQL di aggiornamento
bstr_t strSQLChange("UPDATE Titles SET Type = "
"'self_help' WHERE Type = 'psychology'");
_ConnectionPtr pConnection("ADODB.Connection");
_CommandPtr pCmdChange("ADODB.Command");
_RecordsetPtr pRstTitles("ADODB.RecordSet");
// apertura connessione
…
// apertura del recordset
pRstTitles->Open ("Titles",
_variant_t((IDispatch *) pConnection,true), adOpenStatic, adLockOptimistic, adCmdTable);
// Inizializza l’oggetto Command con lo statement SQL
pCmdChange->ActiveConnection = pConnection;
pCmdChange->CommandText = strSQLChange;
// esegue l’oggetto Command sul recordset pRstTitles
ExecuteCommand(pCmdChange, pRstTitles);
In alternativa il metodo execute puo’ essere applicato direttamente all’oggetto Connection passandogli una stringa contenente lo statement SQL da eseguire:
pConnection->Execute(strSQLRestore, NULL, adExecuteNoRecords);
// Dopo l’esecuzione di uno statement SQL che modifica un
// recordset e’ possibile rigenerare il recordset chiamando
// il metodo Requery
pRstTitles->Requery(adCmdUnknown);
L’acronimo COM (Component Object Model) identifica una serie di tecnologie elaborate da Microsoft che permetteno di costruire “oggetti” indipendenti da qualsiasi linguaggio di programmazione.
Il linguaggio C++ e’ fortemente orientato al concetto di “riuso” di codice sia in forma sorgente che sotto forma di librerie di classi gia’ precompilate; l’unico svantaggio e’ rappresentato dal fatto che tali classi siano utilizzabili solo all’interno di programmi C++ e quindi di conseguenza da programmatori C++.
La tecnologia COM spinge all’estremo il concetto di “riuso di moduli eseguibili”, permettendo di costruire “oggetti COM”, scritti con un qualsiasi linguaggio di programmazione, che una volta eseguiti potranno offrire funzionalita’ ad un altro programma scritto in un qualsiasi linguaggio di programmazione.
Questo significa che un programma C++ potra’ ususfruire dei servizi messi a disposizione da un oggetto COM scritto in Visual Basic e viceversa.
Le funzioni “messe a disposizione” da un oggetto COM vengono chiamate “metodi”; piu’ metodi vengono raggruppati in “interfacce” che possono essere accedute tramite “puntatori ad interfacce”.
I metodi di un oggetto COM vengono raggruppati in interfacce diverse a seconda della funzione svolta dai vari metodi (ad esempio un’interfaccia IMath puo’ contenere i metodi Add e Subtract), quindi un oggetto COM puo’ avere piu’ di un’interfaccia.
Ogni oggetto COM deve comunque implementare una particolare interfaccia denominata “IUnknown” che deve contenere sempre i metodi QueryInterface, AddRef e Release; questi metodi servono rispettivamente per:
QueryInterface Ritorna un puntatore ad un altra interfaccia dell’oggetto
AddRef Serve per segnalare che un client sta utilizzando l’oggetto
Release Serve per segnalare che un client sta rilasciando l’oggetto
Gli oggetti COM sono identificati univocamente tramite valori a 128 bit; il Class Id identifica l’oggetto COM e deve essere utilizzato per crearne un’istanza, e l’Interface Id identifica una delle interfacce all’interno di un oggetto COM.
Esempio:
// dichiariamo un puntatore all’interfaccia
IMath *pMath;
// creiamo un’istanza dell’oggetto COM tramite
// Class ID e Interface ID e assegniamo l’indirizzo
// dell’interfaccia IMath al puntatore pMath
CoCreateInstance(CLSID_Object, NULL, CLSCTX_SERVER, IID_IMath, (void **) &pMath);
// a questo punto possiamo utilizzare l’oggetto COM
// tramite l’interfaccia pMath
Int sum;
PMath->Add (2, 2, &sum);
// rilasciamo l’interfaccia
PMath->Release();
Un programma eseguibile che implementa al suo interno un oggetto COM e’ chiamato “COM Server”; per poter utilizzare un oggetto implementato all’interno di un COM Server e’ necessario che tale oggetto sia “registrato” (ovvero esista all’interno del registro di sistema un’associazione tra il CLASS ID ed il nome dell’eseguibile al cui interno e’ implementato l’oggetto COM).
In una prima release della tecnologia COM era possibile, da un client, istanziare solo oggetti COM che erano registrati sulla stessa macchina, ma successivamente fu introdotta un’evoluzione (chiamata DCOM, Distributed COM) che permetteva ad un oggetto di girare su di un qualsiasi COM server all’interno di una rete.
Tramite un meccanismo chiamato “marshaling” la locazione dell’oggetto DCOM e’ completamente trasparente al programma che lo utilizza, nel senso che e’ la tecnologia COM che risolve la chiamata al metodo remoto sia che l’ogggetto si trovi sullo stesso computer, all’interno di una LAN oppure su di un server remoto a cui si accede tramite Internet.
Object Linking and Embedding (OLE)
E’ importante sottolineare che la tecnologia COM deriva dal meccanismo chiamato OLE (Object Linking and Embedding); questa tecnologia che nacque insieme alla prime versioni di Windows permetteva di includere oggetti creati da un’applicazione all’interno di un’altra che agiva da contenitore (un caso classico consisteva nell’includere un foglio Excel all’interno di un documento Microsoft Word, e secondo la terminologia OLE si definiva Excel un “OLE Server” e Word un “OLE Container”).
La tecnologia OLE, prima di essere inclusa all’interno del mondo COM, si e’ evoluta in quelli che venivano definiti “Active Documents” ovvero una tecnica che permetteva ad applicazioni di tipo “Active Document Containers” (come Microsoft Internet Explorer) di incorporare al suo interno documenti creati da “Active Document Serves” come Word ed Excel.
Un’altra delle tecnologie COM-based di Microsoft che vale la pena di menzionare e’ denominata “ActiveX”; questo tipo di tecnologia e’ nota soprattutto perche’ permette di realizzare controlli particolari (denominati controlli ActiveX) i quali sono oggetti COM a tutti gli effetti che hanno la particolarita’ di poter essere utilizzati indifferentemente all’interno di un’applicazione VC++ o Visual Basic e allo stesso tempo all’interno di una pagina Internet (e possono essere manipolati tramite linguaggi di scripting come VBScript).
Il ruolo di MFC all’interno della tecnologia COM, e’ quello di fornire al programmatore VC++ un’infrastruttura applicativa che semplifichi il piu’ possibile la realizzazione di oggetti COM (che siano ActiveX o OLE Server o Active Documents).
Senza il supporto di MFC la realizzazione di un oggetto COM si rivelerebbe un compito di estrema difficolta’ dato che lo standard COM prevede che l’oggetto implementi tutta una serie di interfacce (e di metodi) predefinite che regolamentano il protocollo di chiamata e di accesso all’oggetto.
Con “automazione” si definisce una tecnologia “COM based” che permette ad un’applicazione Windows di esporre le proprie feature ad un’applicazione client scritta in Visual Basic, VBA oppure VBScript; indirettamente e’ possibile utilizzare l’automazione anche tramite il linguaggio C++.
Esporre le proprie feature significa che il comportamento e le funzionalita’ di un applicazione possono essere “programmati” da un’applicazione esterna tramite l’utilizzo di un linguaggio di scripting.
Le applicazioni che offrono questa possibilita’ sono definite “automation servers”, mentre quelle che ne fanno uso vengono denominate “automation clients”.
In genere l’automazione viene usata per permettere ad una applicazione di interagire con uno dei pacchetti di Office; ad esempio per esportare i dati in un foglio Excel oppure in un documento Word, oppure per realizzare un grafico appoggiandosi alla potente capacita’ di Excel di produrre grafici.
Solitamente un oggetto COM espone interfacce e metodi, mentre un “automation server” espone metodi e proprieta’ (attributi).
La scrittura di client di automazione e’ veramente semplice se si utilizzano linguaggi che supportano direttamente al loro interno tale tecnologia (nascondendo all’utente tutta la complessita’ COM che sta sotto) come il Visual Basic e il VB Script:
(esempio Visual Basic)
Dim math as Object
Set math = CreateObject(“Math.Object”)
// impostiamo una proprieta’ dell’oggetto
Math.base = 10
// accediamo al metodo add dell’oggetto
Sum = math.add(2,2)
Set math = nothing
Questo esempio scritto in Visual Basic mostra l’estrema semplicita’ con cui si puo’ accedere ad un automation server chiamato Math.Object.
Il linguaggio Visual C++ non dispone di tutti i meccanismi del Visual Basic che facilitano la scrittura di automation client, rendendo quindi la scrittura di tali applicazioni un compito molto piu’ complesso (e spesso anche inutile dato che si puo’ scrivere tali client con linguaggi piu’ adatti e gia’ orientati allo “scripting”).
Dove l’accoppiata MFC / Visual C++ si rivela vincente e’ nella scrittura di server di automazione, la quale diventa un’operazione alquanto semplice in VC++ grazie soprattutto allo Wizard che si occupa di generare gran parte del codice necessario a costruire l’infrastruttura di automazione.
In effetti la creazione di un semplicissimo server di automazione come quello utilizzato dall’esempio scritto in Visual Basic e’ molto semplice se ci si avvale del potente supporto offerto da MFC e dallo Wizard; i passi da seguire per la sua creazione sono:
Long CautoMath::Add(long a, long b)
{
return a + b;
}
Double CautoMathDoc::GetPi()
{
return 3.1415926;
}
void CautoMathDoc::SetPi(double newValue)
{
SetNotSupported();
}
Set mathObj = CreateObject(“Automath.Object”)
Sum = mathObj.add(2,2)
Msgbox (sum)
Msgbox (“pi = “ + cstr(mathObj.pi))
Per eseguire il codice VBS e’ sufficiente eseguire un doppio click sul file sorgente, dato che il motore di scripting e’ integrato nei sistemi operativi Windows 2000 e Windows XP.
Per un esempio piu’ complesso (e significativo) di automazione si prega di esaminare l’esempio 2_3_3 che si compone di due applicazioni; un automation server che disegna un grafico a torta ed un semplice automation client da cui e’ possibile variare i valori del grafico disegnato dal server direttamente durante l’esecuzione.
La Active Template Library (ATL) e’ una collezione di template che fa parte di Microsoft Visual C++ e che serve per facilitare il compito di creare oggetti COM.
Sebbene sia possibile creare oggetti COM (come abbiamo visto nei capitoli precedenti) tramite la scrittura manuale di codice C++ oppure tramite l’utilizzo di MFC e’ consigliato l’utilizzo di ATL per applicazioni professionali dato che il codice generato da ATL e’ particolarmente performante ed efficiente.
Inoltre, lo wizard contenuto nel VC++ permette di creare oggetti COM utilizzando ATL in un tempo brevissimo rispetto ad altre metodologie di sviluppo, dato che si occupa di generare tutto il codice di base e le interfacce richieste dallo standard COM.
Le interfacce personalizzate dall’utente possono essere facilmente realizzate con ATL, tramite l’utilizzo di un linguaggio di definizione delle interfacce denominate IDL (Interface Definition Language).
Per realizzare un semplice oggetto COM utilizzando ATL e’ possibile utilizzare l’application Wizard di Visual C++, il quale si occupa della creazione di tutta l’infrastruttura ATL e di tutte le interfacce richieste dallo standard COM.
Nell’esempio 2_3_5 e’ riportato il COM server che offre solamente un metodo per adddizionare due numeri e che e’ stato realizzato con i seguenti passi:
[in] long Num1, [in] long Num2, [out] long *ReturnVal
STDMETHODIMP CFirst_ATL::AddNumbers(long Num1, long Num2, long *ReturnVal)
{
// TODO: Add your implementation code here
*ReturnVal = Num1 + Num2;
return S_OK;
}
Private Sub Command1_Click()
Dim objTestATL As SIMPLE_ATLLib.First_ATL
Dim lngReturnValue As Long
objTestATL.AddNumbers 5, 7, lngReturnValue
MsgBox "La somma di 5 + 7 vale: " & lngReturnValue
End Sub
2_1_1 “Hello World” versione SDK
2_1_2 “Hello World” versione MFC
2_2_1 Menu e “device context”
2_2_2 I controlli di base
2_2_3 “Customizzare” un controllo
2_2_4 Controlli e Dialog box
2_2_5 La serializzazione
2_2_6 Applicazioni SDI
2_2_7 Stampa e preview di stampa con MFC
2_3_1 Programmazione multithread
2_3_2 Automation server
2_3_3 Automation server + client
2_3_4 Accesso al DB tramite ADO
2_3_5 COM Server con ATL
ADO ActiveX Data Object
API Application Program Interface
ASP Active Server Pages
ATL Active Template Library
COM Component Object Model
DBMS Data Base Management System
IDL Interface Definition Language
LAN Local Area Network
MDAC Microsoft Data Access Components
MDI Multiple Document Interface
MFC Microsoft Foundation Classes
ODBC Open Data Base Connectivity
OLE Object Linking and Embedding
OO Object Oriented
OOP Object Oriented Programming
SDI Single Document Interface
SDK Software Development Kit
STL Standard Template Library
VBA Visual Basic for Application
WSH Windows Scripting Host
Fonte: http://www.davidbandinelli.it/appunti/appunti_mfc.doc
Sito web da visitare: http://www.davidbandinelli.it
Autore del testo: indicato nel documento di origine
Il testo è di proprietà dei rispettivi autori che ringraziamo per l'opportunità che ci danno di far conoscere gratuitamente i loro testi per finalità illustrative e didattiche. Se siete gli autori del testo e siete interessati a richiedere la rimozione del testo o l'inserimento di altre informazioni inviateci un e-mail dopo le opportune verifiche soddisferemo la vostra richiesta nel più breve tempo possibile.
I riassunti , gli appunti i testi contenuti nel nostro sito sono messi a disposizione gratuitamente con finalità illustrative didattiche, scientifiche, a carattere sociale, civile e culturale a tutti i possibili interessati secondo il concetto del fair use e con l' obiettivo del rispetto della direttiva europea 2001/29/CE e dell' art. 70 della legge 633/1941 sul diritto d'autore
Le informazioni di medicina e salute contenute nel sito sono di natura generale ed a scopo puramente divulgativo e per questo motivo non possono sostituire in alcun caso il consiglio di un medico (ovvero un soggetto abilitato legalmente alla professione).
"Ciò che sappiamo è una goccia, ciò che ignoriamo un oceano!" Isaac Newton. Essendo impossibile tenere a mente l'enorme quantità di informazioni, l'importante è sapere dove ritrovare l'informazione quando questa serve. U. Eco
www.riassuntini.com dove ritrovare l'informazione quando questa serve