The old new style of MFC applications :
Windows 10/8/7 look and feel with Windows Ribbon Framework
usare il famosissimo controllo Ribbon di Microsoft Office in applicazioni native Win32 mentre invece poco o nulla si trova per fare la stessa cosa in MFC.
Naturalmente se stiamo sviluppando un’applicazione MFC è possibile usare il controllo incluso (peraltro dal 2008) nella libreria Visual C++ CMFCRibbonBar.
La classe CMFCRibbonBar però non è un wrapper del ribbon nativo di Win7 e Win8 / 8.1. Essa è in realtà un componente CPane di MFC sviluppato appositamente per funzionare "da ribbon" e dare l’impressione di avere a che fare col controllo ribbon (nativo) di Microsoft Office 2007 style.
Noi vogliamo invece sfruttare il "ribbon nativo" messo a disposizione dalle API di Win7, 8, 8.1 .
Notare bene che, ovviamente, la nostra applicazione potrà essere eseguita esclusivamente in sistemi operativi che supportano il ribbon nativo o più precisamente quei s.o. che supportano il Windows Ribbon Framework: ovvero da Win7 in avanti per intenderci (niente da fare quindi da XP compreso in giù).
Cos’è il Windows Ribbon Framework.
Il Windows Ribbon Framework è una collezione di API Win32 per il supporto di nuove funzionalità di interfaccia utente. Qui sotto qualche link interessante sull’argomento:
Windows Ribbon Documentation on MSDN
Windows Ribbon Guidelines on MSDN
MSDN Forum on Windows Ribbon development
A suo tempo, si è voluto separare UI (User Interface) e codice sorgente, allo scopo di favorire la creatività dei progettisti di UI, senza che questi dovessero avere la minima conoscenza di alcun linguaggio di sviluppo. Tecnicamente si tratta di "declarative programming", un approccio già arcinoto a chi sviluppa Silverlight o WPF etc. In WPF(Windows Presentation Foundation) ad esempio viene utilizzato XAML per la UI e C# o VB.Net per il code-behind.
A partire da Win7 è stato concesso gli sviluppatori Windows un completo framework ( alle cui funzioni si accede tramite l’interfaccia COM IUIFRamework ) basato proprio sul concetto di separazione tra UI (User Interface) e il codice (code-behind). La UI viene disegnata da Windows in base alla "descrizione degli attributi" contenuti in un file xml based mentre si userà il linguaggio C++/COM per il code-behind.
Il ribbon, le tabs i gruppi e tutti i relativi controlli, anche molto sofisticati, vengono creati semplicemente dichiarandoli in un file xml che deve aderire al cosiddetto Ribbon Markup. Il markup è definito in un file di schema : uicc.xsd. In base a tale schema, lo strumento uicc.exe esegue la validazione del nostro file xml. Invito a leggere la documentazione su Windows Ribbon Framework per avere un quadro di quali sono i possibili attributi da inserire nel file dichiarativo del nostro ribbon (e come usarli).
Lo strumento uicc.exe (tra gli strumenti di Visual Studio e dell’ SDK) si occupa, oltre che di compilare il nostro file xml, di creare i file di risorse da includere poi nel nostro progetto.
Il file xml deve essere compilato in formato binary (con estensione bml). Quest’ultimo dovrà essere inserito come risorsa Win32 (nell’exe o in una DLL ), la quale risorsa sarà caricata in memoria a runtime e darà vita al Windows Ribbon grazie alla tecnologia COM (Component Object Model). Windows Ribbon infatti non è un controllo Win32 classico ma un oggetto COM (non un ActiveX) che le applicazioni possono istanziare, caricare in memoria e agganciare al top dell’area client di una finestra di windows per consentire all’utente di interagire. Inoltre non è possibile aggiungere al ribbon controlli Win32 che non siano espressamente consentiti dallo schema previsto da Microsoft (tanto per dire un nostro controllo derivato o owner draw).
Prerequisiti:
Visual Studio 2013 Professional o Community Edition per avere il supporto MFC.
Creazione del progetto:
Step 1: Creiamo un nuovo progetto Visual C++. Il progetto sarà di tipo MFC -> MFC Application. Diamo un nome al progetto per esempio MFCNativeRibbonDemo.Eliminazione dei controlli Old-Style:
Step 5: In solution explorer apriamo il file MainFrm.h e commentiamo la riga CToolbar in quanto non vogliamo una "vecchia" toolbar bensì un ribbon.
Il minimo da dichiarare nel file xml per avere un semplice ribbon è un tag Application, un command e una view:
<Application xmlns="http://schemas.microsoft.com/windows/2009/Ribbon"> <Application.Commands> <Command Name="TabHome" Symbol="cmdTabHome" Id="30000" /> </Application.Commands> <Application.Views> <Ribbon> <Ribbon.Tabs> <Tab CommandName="TabHome"> </Tab> </Ribbon.Tabs> </Ribbon> </Application.Views> </Application>
In solution explorer facciamo click destro -> properties sul file xml nativeribbonmarkup.xml. In General alla riga Item Type scegliamo Custom Build Tool e quindi facciamo click su Apply. Questo attiva la funzione Custom Build Tool in cui alla riga Command Line specifichiamo il comando :
uicc.exe nativeribbonmarkup.xml nativeribbonmarkup.bml /header:nativeribbonmarkup.h /res:nativeribbonmarkup.rc
e alla riga Outputs :
nativeribbonmarkup.bml;nativeribbonmarkup.rc;nativeribbonmarkup.h
In pratica non stiamo facendo altro che dire allo strumento uicc.exe di creare dal file xml un file binario (.bml) un header file (.h) e un file di risorse compilato (.rc). Il linker poi lo inserirà per noi tra le risorse.
E' necessario aggiungere al file di risorse predefinito (nel nostro caso MFCNativeRibbonDemo.rc) le inclusioni dei file nativeribbonmarkup.h e nativeribbonmarkup.rc creati dallo strumento ucc.exe.
In solution expolorer facciamo click destro su MFCNativeRibbonDemo.rc e scegliamo View Code. Nel file che viene aperto da Visual Studio aggiungiamo le inclusioni.
#include "nativeribbonmarkup.h" #include "nativeribbonmarkup.rc"
Dopo aver creato le risorse per il ribbon dobbiamo implementare il codice per comunicare con esso. Creiamo una nuova classe C++. Diamo un nome. Io l’ho chiamata CNativeRibbon. Click su Finish. Ora, dato che in realtà non si tratterà di una classe classica ma invece una classe COM cancelleremo il contenuto predefinito dentro la dichiarazione di classe. Stessa cosa nel file di implementazione.
Essendo una classe che ha a che fare con COM dobbiamo aggiungere al file di intestazione (NativeRibbon.h) i file di inclusione relativi a COM e a Windows Ribbon Framework:
#include <atlbase.h> #include <atlcom.h> #include <initguid.h> #include <uiribbon.h>
- dalla classe CComObjectRootEx
per la gestione delle funzionalità COM - dall’interfaccia IUIApplication per il callback dei metodi di Windows Ribbon Framework
- dall’interfaccia IUICommandHandler per lo scambio dei comandi da e verso il ribbon.
//NativeRibbon.h #pragma once #include "stdafx.h" #include <atlbase.h> #include <atlcom.h> #include <initguid.h> #include <uiribbon.h> #include "resource.h" IUIFramework* g_pFramework = NULL; UINT m_wRibbonHeight = 0; BOOL OFFICE_2013_STYLE = false; class CNativeRibbon : public CComObjectRootEx, public IUIApplication, public IUICommandHandler { public: BEGIN_COM_MAP(CNativeRibbon) COM_INTERFACE_ENTRY(IUIApplication) COM_INTERFACE_ENTRY(IUICommandHandler) END_COM_MAP() STDMETHOD(OnViewChanged)(UINT32 nViewID, __in UI_VIEWTYPE typeID, __in IUnknown* pView, UI_VIEWVERB verb, INT32 uReasonCode) { HRESULT hr; // The Ribbon size has changed. if (verb == UI_VIEWVERB_SIZE) { CComQIPtr pRibbon = pView; if (!pRibbon) return E_FAIL; UINT ulRibbonHeight; // Get the Ribbon height. hr = pRibbon->GetHeight(&ulRibbonHeight); if (FAILED(hr)) return hr; m_wRibbonHeight = ulRibbonHeight; ::InvalidateRect(NULL, NULL, TRUE); } return E_NOTIMPL; } STDMETHOD(OnDestroyUICommand)(UINT32 commandId, __in UI_COMMANDTYPE typeID, __in_opt IUICommandHandler* pCommandHandler) { return E_NOTIMPL; } STDMETHOD(OnCreateUICommand)(UINT32 nCmdID, __in UI_COMMANDTYPE typeID, __deref_out IUICommandHandler** ppCommandHandler) { if (nCmdID == pencilButton) { return QueryInterface(IID_PPV_ARGS(ppCommandHandler)); } else if (nCmdID == ID_FILE_EXIT) { return QueryInterface(IID_PPV_ARGS(ppCommandHandler)); } return E_NOTIMPL; } STDMETHODIMP Execute(UINT nCmdID, UI_EXECUTIONVERB verb, __in_opt const PROPERTYKEY* key, __in_opt const PROPVARIANT* ppropvarValue, __in_opt IUISimplePropertySet* pCommandExecutionProperties) { HRESULT hr = S_OK; switch (verb) { case UI_EXECUTIONVERB_EXECUTE: if (nCmdID == pencilButton) { MessageBox(NULL, L"Pencil Button was clicked",L"Pencil Button Handler", MB_OK); } else if (nCmdID == ID_FILE_EXIT) { ::PostQuitMessage(0); } break; } return hr; } STDMETHODIMP UpdateProperty(UINT nCmdID, __in REFPROPERTYKEY key, __in_opt const PROPVARIANT* ppropvarCurrentValue, __out PROPVARIANT* ppropvarNewValue) { return E_NOTIMPL; } };
//NativeRibbon.cpp #include "stdafx.h" #include "NativeRibbon.h" #include <atlbase.h> #include <atlcom.h> #include <initguid.h> #include <uiribbon.h> #include <Propvarutil.h> HRESULT InitRibbon(HWND hWindowFrame) { HRESULT hr = ::CoCreateInstance(CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&g_pFramework)); if (FAILED(hr)) { return hr; } CComObjectApriamo ora il file MainFrm.cpp e dichiariamo come extern le funzioni contenute nei file NativeRibbon .h e NativeRibbon.cpp.*pApplication = NULL; hr = CComObject ::CreateInstance(&pApplication); if (FAILED(hr)) { return hr; } hr = g_pFramework->Initialize(hWindowFrame, pApplication); if (FAILED(hr)) { return hr; } g_pFramework->LoadUI(GetModuleHandle(NULL), L"APPLICATION_RIBBON"); if (FAILED(hr)) { return hr; } } BOOL DestroyRibbon() { if (g_pFramework) { g_pFramework->Destroy(); g_pFramework->Release(); g_pFramework = NULL; return S_OK; } return E_NOTIMPL; }
/* Extern Declarations */ /*************************************************/ CComModule _Module; //COM extern HRESULT InitRibbon(HWND hWindowFrame); extern BOOL DestroyRibbon(); extern BOOL OFFICE_2013_STYLE; /*************************************************/
Dobbiamo dichiarare una variabile globale (_Module) di tipo CComModule in quanto serve alle librerie COM. Sempre nello stesso file nel costruttore aggiungiamo la chiamata di inizializzazione alle librerie COM e nel distruttore la deallocazione di COM e del ribbon.
// CMainFrame construction/destruction CMainFrame::CMainFrame() { // TODO: add member initialization code here CoInitialize(NULL); } CMainFrame::~CMainFrame() { DestroyRibbon(); CoUninitialize(); }
Ancora nello stesso file aggiungiamo la funzione di inizializzazione del ribbon:
//Native Ribbon Loading HRESULT hr = InitRibbon(m_hWnd); if (FAILED(hr)) { return FALSE; }
Infine nella funzione PreCreateWindow aggiungiamo lo stile WS_CLIPCHILDREN alla finestra frame per evitare problemi di repaint.
Eseguiamo l’applicazione e vediamo in azione il Windows Ribbon Framework in MFC.
Dopo avere aggiunto qualche altra funzionalità alla nostra applicazione ecco il risultato finale:
Personalizzare lo stile del Ribbon
E' possibile settare lo stile del ribbon manipolando tre properietà:- UI_PKEY_GlobalBackgroundColor
- UI_PKEY_GlobalHighlightColor
- UI_PKEY_GlobalTextColor
Modificandole è possibile avere un Windows Ribbon totalmente personalizzato. Io per esempio ho voluto renderlo somigliante a quello di Microsoft Office 2013.
//Office 2013 Ribbon Style //............................... //.............................. if (OFFICE_2013_STYLE) { CComPtrspPropertyStore; if (SUCCEEDED(g_pFramework->QueryInterface(&spPropertyStore))) { PROPVARIANT propvarBackground; PROPVARIANT propvarHighlight; PROPVARIANT propvarText; UI_HSBCOLOR BackgroundColor = UI_HSB(0x295, 0x20, 0xFF); UI_HSBCOLOR HighlightColor = UI_HSB(0x00, 0x55, 0xFF); UI_HSBCOLOR TextColor = UI_HSB(0x00, 0x00, 0x00); InitPropVariantFromUInt32(BackgroundColor, &propvarBackground); InitPropVariantFromUInt32(HighlightColor, &propvarHighlight); InitPropVariantFromUInt32(TextColor, &propvarText); spPropertyStore->SetValue(UI_PKEY_GlobalBackgroundColor, propvarBackground); spPropertyStore->SetValue(UI_PKEY_GlobalHighlightColor, propvarHighlight); spPropertyStore->SetValue(UI_PKEY_GlobalTextColor, propvarText); spPropertyStore->Commit(); } }