software-on-demand-ita.com

Giuseppe Pischedda

Software Engineer

software-on-demand-ita.com

Giuseppe Pischedda

Software Engineer

The old new style of MFC applications :
Windows 10/8/7 look and feel with Windows Ribbon Framework



     In rete sono disponibili una miriade di tutorials su come
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

UI e Source Code

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.

Ribbon Markup

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

Da xml a Windows Ribbon

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

Struttura del Ribbon
Usare il Windows Ribbon Framework in un’ applicazione MFC old-style.

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.
Step 2: In MFC Application Wizard andiamo Avanti
Step 3: In Application Type scegliamo Single Document e MFC Standard
Step 4: Andiamo sempre avanti fino a Generated Classes e quindi Finish.
Mandiamo in esecuzione l’applicazione generata da AppWizard. Sarà un qualcosa del genere: (piuttosto datata come UI).

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.
Step 6: In solution expolorer apriamo il file MainFrm.cpp e commentiamo nella funzione OnCreate il codice per la creazione della toolbar e le funzioni relative al docking della stessa.
Step 7 : Nello stesso file, nella funzione PreCreateWindow, aggiungiamo il codice per eliminare il menu principale dell’applicazione
Step 8 : Eseguiamo l’applicazione. Non ci sono più nel il MainMenu né la Toolbar.
Creazione del file xml per il ribbon Aggiungiamo un nuovo elemento al progetto. Un file XML. Diamo un nome esplicito. Io l’ho chiamato nativeribbonmarkup.xml.
Eliminiamo il contenuto del file xml ed iniziamo a dichiarare il Windows 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>

Creazione delle risorse

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"
Creazione della classe Windows Ribbon

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>

La classe che deve interagire con il ribbon via COM deve discendere :
  • 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;

	}


};

Nel file NativeRibbon.cpp implementiamo le funzioni per la gestione del ribbon.

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

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


Apriamo ora il file MainFrm.cpp e dichiariamo come extern le funzioni contenute nei file NativeRibbon .h e NativeRibbon.cpp.

/* 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)
	{

	CComPtr spPropertyStore;
	
	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();
	}
     }

Giuseppe Pischedda 2014


Ehi, se ti va puoi offrirmi un caffè.


Ehi, se ti va puoi offrirmi un caffè.

Informativa estesa art.13 d.lgs. 196/03 ed art. 13 del Regolamento UE 2016/679 del 27 aprile 2016 (privacy)