Corso Microsoft Visual C++

Corso Microsoft Visual C++

 

 

 

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).

 

 

 

 

Corso Microsoft Visual C++

Documentazione per il corso:

“Sviluppo Applicazioni in ambiente Windows con Microsoft Visual C++ 6.0”

Autore: David Bandinelli

Revisione: 1.1

Bibliografia:
“Programming Windows with MFC” di J. Prosise
“Programming Windows” di C. Petzold
Microsoft MSDN Library

Parte 1 : Dall’SDK ad MFC

 

Introduzione alla programmazione in ambiente Windows

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.

Programmare con l’SDK

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.


 

Introduzione a MFC

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.

 

La prima applicazione MFC in dettaglio

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:

  • Selezionare File -> New e scegliere un nuovo progetto di tipo “Win32 Application
  • Indicare che si vuole creare un progetto vuoto scegliendo “Empty Project
  • Selezionare Project -> Settings ed indicare l’opzione “Use MFC in a shared DLL” per comunicare al compilatore che si vuole linkare dinamicamente la libreria MFC all’eseguibile

 

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).

La “Mappa dei Messaggi”

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)

 

Parte 2 : Le classi MFC e l’interfaccia di Windows

Gli elementi base di MFC: Disegnare tramite un “Device Context”, gestione di mouse e tastiera, gestione dei menu.

 

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.

Il “Device Context”

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:

  • Creazione di un oggetto device context
  • Creazione di eventuali oggetti che modificano le caratteristiche del disegno (come CPen e CBrush)
  • Salvataggio del vecchio oggetto associato al device context
  • Utilizzo delle funzioni di disegno con le nuove caratteristiche tramite il device context
  • Ripristino delle condizioni iniziali del device context
  • Rilascio del device context (distruzione); implicita quando la funzione OnPaint esce di visibilita’.

 

Le “Message Box”

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).

 

Il metodo PostNCDestroy

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;
}

 

L’intercettazione di mouse e tastiera

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.

La gestione dei menu di un’applicazione MFC

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 di base e le “dialog box”

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:

  • Nel .h della classe che implementa la finestra si aggiungono gli oggetti che si vorranno visualizzare nella finestra sotto forma di oggetti membro di tipo variabile a seconda della classe di oggetto (es. CButton m_myButton, CListbox m_myListBox ecc.)  
  • Al momento della creazione della una finestra si creano anche i controlli che sono contenuti nella finestra (chiamando i metodi create delle varie classi MFC che implementano i controlli come CButton, CListbox ecc.).
  •  Nella mappa dei messaggi vanno aggiunti gli eventi generati dai controlli utilizzati; ad esempio il controllo bottone genera un evento ON_BN_CLICKED che viene generato al momento della pressione.
  • Gli eventi dei controlli che si desidera gestire vanno associati alle corrispondenti routine di gestione (es. OnButtonClicked).
  • Le routine di gestione vanno implementate all’interno dell’applicazione.

 

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)

Le “Dialog Box”

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:
 

  • Sebbene sia possibile fare tutto manualmente e’ bene in questo caso avvalersi dell’aiuto dell’ambiente di sviluppo per la creazione della struttura base della dialog e quindi dal menu Insert selezionare New Form, scegliere come classe base CDialog e specificare il nome; il Visual Studio provvedera’ a creare il file di risorse, ed i file .cpp e .h per la nostra nuova dialog.
  • A questo punto possiamo aprire il resource editor e modificare l’aspetto della nostra dialog box aggiungendo i controlli di input necessari (da notare che i bottoni OK e CANCEL con la relativa gestione base sono gia’ stati implementati dallo Wizard.
  • Una volta aggiunti i controlli di input graficamente e assegnato loro un nome (es. IDC_EDITNAME) e’ necessario aggiungere nel .h tante variabili membro della classe CMyDialog quanti sono i campi associati ai controlli di input.
  • E’ necessario poi effettuare l’associazione tra le variabili membro ed i controlli di input e definire se devono essere effettuati controlli di validazione sull’input inserito; i due metodi possibili per queste operazioni sono illustrati successivamente da alcuni brani di codice tratti dall’esempio 2_2_4.
  • Una volta preparata la dialog box e’ possibile richiamarla dall’applicazione istanziando un oggetto di tipo CMyDialog in maniera “modale” (la dialog non si chiude fino a che non si preme Ok o Cancel e non si puo’ passare ad altre finestre) oppure “non modale” la dialog puo’ esssere chiusa e trattata come le finestre tradizionali.

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”

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.


Le classi di utilita’ MFC e la “serializzazione”

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).

La “serializzazione”

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); 
}

 

 

Introduzione all’architettura “Document View”

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).

 

Le applicazioni SDI (Single Document Interface)

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:

  • Selezionare File -> New per creare un nuovo progetto e scegliere “MFC App Wizard” come tipo di applicazione.
  • A questo punto si apriranno alcune finestre in cui e’ possibile specificare che tipo di scheletro vogliamo generare; nel nostro caso selezionare “Single Document” e lasciare attivata la checkbox “Document/View Support”.
  • Nella seconda finestra del wizard non selezionare alcun supporto per database.
  • Nella terza finestra lasciare attivata la voce “None” e deselezionare anche la voce “ActiveX Controls”.
  • Nella quarta finestra selezionare le feature che si desidera includere automaticamente nell’applicazione; per mantenere semplice il codice se non si hanno esigenze particolari di stampa o di interfaccia deselezionare tutte le opzioni.
  • Nella quinta e ultima finestra del wizard lasciare tutte le opzioni di default.
  • Nella sesta e ultima finestra e’ presente un riepilogo dei file .cpp e .h che verranno generati automaticamente dal Visual Studio ed e’ possibile eventualmente cambiare i nomi proposti per classi e sorgenti.
  • Finalmente lo scheletro dell’applicazione viene generato, ed e’ ora possibile intervenire nel codice prodotto per personalizzare il comportamento dell’applicazione; da notare che viene generato anche il file di risorse (.rc) che contiene tutti i menu, i messaggi a video, le icone e le dialog box dell’applicazione.

 

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.

             

Le applicazioni MDI (Multiple Document Interface)

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:

  • si possono avere piu’ documenti aperti in editing allo stesso tempo mentre le applicazioni SDI richiedono che il documento corrente venga chiuso prima di poterne aprire un altro.
  • Talvolta si possono avere applicazioni MDI che supportano diversi tipi di documento contemporaneamente (ad esempio un gestionale integrato che permette di lavorare allo stesso tempo su di un testo, un foglio di calcolo, un archivio, un grafico).
  • Le applicazioni MDI dispongono di un menu’ Windows che offre dei comandi per selezionare la vista attiva e disporre le varie finestre contenenti le varie viste.
  • Le applicazioni MDI che supportano diversi tipi di documento contemporaneamente devono gestire diversi tipi di menu (uno per ogni tipologia di documento).
  • Le applicazioni SDI sono dotate di una sola finestra contenitore e della vista, mentre le applicazioni MDI hanno all’interno della finestra contenitore altre finestre che contengono le diverse viste e che possono essere ognuna ridimensionata, dockata, minimizzata, chiusa ecc.

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:

  • Le applicazioni MDI derivano la finestra principale (quella che fa da container a tutte le altre) da CMDIFrameWnd invece che da CFrameWnd
  • Le finestre multiple che contengono le varie viste vengono derivate da CMDIChildWindow
  • Il template da cui vengono create le applicazioni MDI e’ denominato CMultiDocTemplate invece di CSingleDocTemplate
  • Le applicazioni MDI hanno in genere piu’ di un menu nel file di risorse, e solitamente gestiscono menu diversi per quando nessun documento e’ aperto e quando e’ aperto almeno un documento

 

 

Le funzionalita’ di stampa

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)

 

Parte 3 : Oltre l’interfaccia

La gestione dei timer e del ciclo “Idle”

 

I Timer

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:


  • inviando ad una finestra specificata il messaggio WM_TIMER
  • chiamando una funzione definita all’interno dell’applicazione detta “funzione di callback

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 (!)
}

 

Le elaborazioni durante il ciclo di “Idle”

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)

 

Gestione dei “Thread”

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:

Priorita’

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.

Sincronizzazione tra thread e tra thread e applicazione principale

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:

  • Sezioni critiche (l’accesso ad alcune risorse come variabili o strutture dati condivise tra piu’ thread vengono serializzate, ovvero un solo thread alla volta puo’ accedervi)
  • Mutex (contrazione di “mutuamente esclusivo”; aree di memoria condivise tra piu’ thread anche di processi differenti sono accessibili in maniera esclusiva da un solo thread per volta)
  • Eventi (possono essere definiti come comandi che vengono inviati ai thread per bloccarne o riprenderne l’esecuzione)
  • Semafori (i semafori sono utlizzati per regolamentare l’accesso a risorse a disponibilita’ limitata, ovvero risorse a cui non possano accedere piu’ di n thread; il semaforo si occupa di tenere il conteggio e quando il numero di accessi consentiti e’ finito impedisce ogni accesso alla risorsa fino a che un thread non segnala al semaforo che ha intenzione di rilasciarla)

 

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;
}

 

Introduzione alle tecniche di accesso ai dati

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.

ADO e gli OLE DB Provider

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:

  • I dati risiedono in un database relazionale per cui esiste un driver ODBC ma per cui non e’ disponibile un provider OLE DB nativo; l’applicazione utilizza ADO per parlare con l’OLE DB provider per ODBC il quale poi pensa ad utilizzare il driver  ODBC appropriato; il driver passa lo statement SQL al database il quale restituisce i dati richiesti.
  • I dati risiedono in in database Microsoft SQL Server per cui e’ disponibile un provider OLE DB nativo; l’applicazione utilizza ADO per parlare direttamente con l’OLE DB provider per SQL Server e nessun altro strato intermedio e’ necessario
  • I dati risiedono in Microsoft Exchange, il quale ha un OLE DB provider ma che, non essendo un DBMS relazionale, non ha un motore in grado di comprendere statement SQL; l’applicazione utilizza ADO per parlare con l’OLE DB Provider per Echange il quale si occupa di tradurre gli statement SQL in query che possano essere comprese da Exchange
  • I dati risiedono nel file system di Windows 2000 (NTFS) sotto forma di documento; l’accesso avviene tramite un provider OLE DB nativo che si appoggia al servizio di Microsoft Indexing Service il quale estre i dati richiesti dal proprio repository.

 
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.

 

Accesso ai database tramite ADO

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);

 

Gli oggetti COM

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();

 

COM Servers

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.

ActiveX

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).

 

COM e MFC

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.

 

Tecniche di “Automazione”

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:

  • Utilizzare MFC App Wizard per creare un nuovo progetto chiamato AutoMath; scegliere Single Document Interface (SDI) e cliccare sul supporto per l’automazione (selezionando Automation e scegliendo Full Server).
  • Nello step successivo (4) premere il bottone “advanced” ed indicare “AutoMath.Object” nel campo “File Type ID”.
  • Negli step successivi eliminare tutto quello che non serve (funzioni di stampa, toolbar ecc.)
  • Una volta generato lo scheletro dell’applicazione lanciare il Class Wizard da View -> Class Wizard e spostarsi nella pagina Automation; selezionare la classe CAutoMathDoc  e cliccare su ‘Add Method’; denominare il metodo ‘Add’, premere OK e poi su ‘Edit Code’ per aggiungere l’implementazione del metodo

 

Long CautoMath::Add(long a, long b)
{
return a + b;
}

  • Se si desidera si possono aggiungere altri metodi analoghi con le stesse modalita’ (es. Subtract, Multiply ecc)
  • Sempre all’interno della pagina Automation del Class Wizard cliccare su ‘Add Property’ e selezionare get/set per creare una nuova proprieta’ denominata pi

 

Double CautoMathDoc::GetPi()
{
return 3.1415926;
}

void CautoMathDoc::SetPi(double newValue)
{
SetNotSupported();
}

  • Una volta definiti i metodi e le proprieta’ e’ sufficiente compilare l’automation server ed eseguirlo almeno una volta per fare in modo che venga registrato.
  • A questo punto e’ possibile da un linguaggio di scripting come Visual Basic o Windows Scripting Host testare il server di automazione creato; per effettuare il test con il VBScript creare un nuovo file di testo con estensione .vbs e scrivere del codice di questo tipo:

 

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.


Introduzione ad ATL

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:

  • Creare un nuovo progetto VC++ e selezionare “ATL COM App Wizard”; dare un nome al progetto come “Simple_ATL”.
  • Nella dialog successiva specificare che si vuole creare un oggetto COM di tipo “Server DLL” e lasciare invariate tutte le altre opzioni.
  • Una volta che il wizard ha creato i file di base per il nostro progetto selezionare Insert->New ATL Object dal menu; tra i vari oggetti proposti scegliere “Simple Object” e cliccare sul pulsante “Next
  • Nella dialog successiva indicare come short name “First_ATL” e poi cliccare sul tab “Attributes”; impostare Interface->Custom e Aggregation->No e premere OK per fare in modo che il wizard crei il nostro oggetto ATL.
  • Se osserviamo il “Class View” per il nostro progetto possiamo vedere un’interfaccia denominata “IFirst_ATL”; fare click  del pulsante destro su questa interfaccia e selezionare “Add Method
  • Il nome del metodo sara’ AddNumber e come parametri possiamo specificare:

 

[in] long Num1, [in] long Num2, [out] long *ReturnVal

  • Una volta specificata l’interfaccia IDL per il nostro metodo e’ necessario fornirne l’implementazione; per fare questo dal Class View espandere (+) l’albero dell’interfaccia e fare doppio click su “AddNumbers”; il codice del metodo verra’ generato, e sara’ possibile quindi aggiungere l’istruzione di somma:
 

STDMETHODIMP CFirst_ATL::AddNumbers(long Num1, long Num2, long *ReturnVal)
{
// TODO: Add your implementation code here
*ReturnVal = Num1 + Num2;

            return S_OK;
}

  • a questo punto il nostro oggetto COM minimale realizzato tramite ATL e’ pronto per essere compilato (Build); una volta generata la DLL il VC++ provvedera’ a registrare l’oggetto COM in modo da poterlo utilizzare da altre applicazioni.
  • Per testare l’oggetto COM dal Visual Basic, aprire un nuovo progetto VB e dal menu Project->References selezionare la libreria “Simple ATL 1.0 Type Library” per aggiungere un riferimento all’oggetto COM all’interno del progetto Visual Basic.
  • Adesso e’ possibile scrivere ed eseguire il seguente codice Visual Basic che utilizza il metodo AddNumbers del nostro oggetto COM:

 

Private Sub Command1_Click()
Dim objTestATL As SIMPLE_ATLLib.First_ATL

    Set objTestATL = New First_ATL

   
    Dim lngReturnValue As Long
   
    objTestATL.AddNumbers 5, 7, lngReturnValue
   
    MsgBox "La somma di 5 + 7 vale: " & lngReturnValue
End Sub


 Parte 4 : Appendice

Indice dei listati a corredo del testo

 

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

 

Glossario

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.

 

Corso Microsoft Visual C++

 

 

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).

 

Corso Microsoft Visual C++

 

"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

 

Argomenti

Termini d' uso, cookies e privacy

Contatti

Cerca nel sito

 

 

Corso Microsoft Visual C++