Windows 7: Exploration of 7 exciting new programming features in a single application!






4.93/5 (72 votes)
Demonstration of seven new programming technologies.
Introduction
Welcome to the Windows 7 world. I will try here to demonstrate a collection of seven, most appealing programming features that you can use to enhance both the appearance and the internals of your application!
This article is a candidate for the CodeProject Windows 7 contest. If you like this, please vote for me!
The Magic Number Seven
The article discusses these topics:
- Taskbar Extensions
- Libraries
- Touch API
- Sensor & Location API
- Animation Manager
- Direct2D and DirectWrite
- Ribbon
All these are used for the sample application, "Windows 7 Downloader", which downloads files. It uses the taskbar extensions to display a progress bar and toolbar, it stores the files to the library locations, draws progress and information with Direct2D, allows touching the screen to select a transfer, animates the transfer progress with the animation manager, changes the drawing depending on light conditions (using the Virtual Light Sensor tool from the SDK), and calculates and displays your location from a GPS sensor, if installed, drawing a virtual path from your location to the destination file's location using Google Maps. Finally, all this is presented with a ribbon interface.
Code pasted in this article is stripped of error checking functions for the sake of simplicity. You should always be checking the HRESULT
values of the functions! Also, most known code (like the IUnknown
implementation) is stripped from here as well. To get the full code, see the CPP files.
Includes:
- All-in-one sources
- TBAR.CPP - Taskbar extensions
- LIBR.CPP - Libraries
- TOUCH.CPP - Touch sample
- SENS.CPP - Sensor Explorer
- ANIM.CPP - Animation manager
- D2D.CPP - Direct2D sample
- RIB.CPP - Ribbon
- MAIN.CPP/RC/Hs - Main stuff
- Ribbon bitmaps
- Visual Studio 2008 solution and project files (VCPROJ and SLN)
- Visual Ribbon Creator project file (VRC)
- x86/x64 executables
Required Software
- Windows 7. If you still have RC1, most of the code will work with it - however, some definitions have changed from the RC1 SDK.
- Windows 7 SDK.
- Visual Studio 2008 or Visual Studio 2010.
Optional/Helpful Software
- Visual Ribbon Creator - my designer if you are bored to write Ribbon code by hand!
- GPS Sensor Driver - my driver to test the Sensor and Location API, with real data coming from any NMEA-compatible GPS that can connect via a serial port (USB or Bluetooth). If you do not own an actual GPS hardware, this driver can also simulate your location.
- CodePlex Touch Simulation. If you do not have a touch screen yet, this one contains a driver that can simulate touch gestures with the mouse.
- The Windows 7 Device Driver Kit to write your own Sensor driver.
Further Reading
- The Ribbon API documentation.
- The Sensor and Location API documentation.
- The Taskbar Extensions documentation.
- The Animation API documentation.
- The Direct2D documentation.
- The Libraries documentation.
- The Touch API documentation.
The APIs in General
But from a few exceptions, all APIs discussed in this article are COM features. That is, your application will continue to work even if not running under Windows 7 - so there is almost no reason not to implement them!
There are a few functions (for example, the one to create the Direct2D interface) that you should call them LoadLibrary
/GetProcAddress
- otherwise, your application will fail to initialize when not running under Windows 7.
Some of the features (in particular, Ribbon, Direct2D, and the Animation Manager) are also available in Vista through the Platform update for Vista SP2.
I. Taskbar Extensions
What are they?
The application uses the extensions to:
- Specify recent/frequent items for the taskbar right click (Jumplists).
- Display a toolbar to manipulate the application without switching to the application.
- Handle the taskbar as a progress bar.
|
|
|
Fig 1.1: Jumplists
|
Fig 1.2: Toolbar
|
Fig 1.3: Progress bar
|
Important note #1: All manipulation of the taskbar must occur after your application knows that the taskbar button has been created. Call RegisterWindowMessage
(L"TaskbarButtonCreated") to get a message ID, and when you receive this in your loop, then you do the taskbar stuff. Failure to do this will cost you some hours of debugging, as it happened to me :)
Important note #2: If you are running the application as administrator, a manipulation toolbar cannot be used since the Explorer runs in medium integrity and can't therefore send messages to your application. If you do want the Explorer to be able to communicate with your application while in High Integrity mode, you must use ChangeWindowMessageFilterEx
for both WM_COMMAND
and the message returned by RegisterWindowMessage(L"TaskbarButtonCreated")
.
Quick Steps:
- To specify recent items:
- Set a unique App ID by calling
SetCurrentProcessExplicitAppUserModelID()
. CoCreate
theITaskbarList3
.- Use
SHAddToRecentDocs()
to manipulate the recent documents. CoCreate
anICustomDestinationList
.- Display a manipulation toolbar:
CoCreate
theITaskbarList3
.- Add an image list to the taskbar with the member function
ThumbBarSetImageList()
. - Add buttons with the member function
ThumbBarAddButtons()
. - Your application will receive a
WM_COMMAND
when a button is clicked. - Handle the taskbar as a progress bar:
CoCreate
theITaskbarList3
.- Call the member
SetProgressState()
to set the state. - To update the state, call the member
SetProgressValue()
.
Step by Step Recent Items (Error handling removed for simplicity):
There are two known categories for which you do not need to define something, the recent and the frequent items. These items must be filenames for these categories. If you want to add any other sort of category, you have to implement your own IObjectArray
. For more, see the ICustomDestinationList::AppendCategory
documentation.
// Globals
const wchar_t* OurAppID = L"You.Software.Sub.Version";
unsigned long uumsg = 0;
vector<wstring> SomeRecentDocuments;
// In WinMain
// Set the AppID
SetCurrentProcessExplicitAppUserModelID(OurAppID);
// Get message
uumsg = RegisterWindowMessage(L"TaskbarButtonCreated");
// In Message Loop
if (msg == uumsg)
{
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
// Put the MRU
SHAddToRecentDocs(SHARD_PATHW,0);
for(unsigned int i = 0 ; i < SomeRecentDocuments.size() ; i++)
{
SHAddToRecentDocs(SHARD_PATHW,(void*)SomeRecentDocuments[i].c_str());
}
// Use the Destination List
ICustomDestinationList* cdl = 0;
CoCreateInstance(CLSID_DestinationList,NULL,CLSCTX_INPROC_SERVER,
__uuidof(ICustomDestinationList),(void**)&cdl);
// Set AppID
cdl->SetAppID(OurAppID);
// Recent Items begin list
UINT MaxCount = 0;
IObjectArray* oa = 0;
hr = cdl->BeginList(&MaxCount,__uuidof(IObjectArray),(void**)&oa);
// Add known category
hr = cdl->AppendKnownCategory(KDC_RECENT);
oa->Release();
odl->Release();
tb->Release();
}
Step by Step Taskbar Toolbar (Error handling removed for simplicity):
// Create the interface
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
// Load the toolbar
HBITMAP hB = SomeBitmap();
LoadTransparentToolbarImage(hAppInstance,_T("WM7_TOOLBAR"),0xFFFFFFFF);
unsigned int nI = 2; // 2 Images.
HIMAGELIST tlbi = ImageList_Create(bi.bmHeight,bi.bmHeight,ILC_COLOR32,nI,0);
// Add the bitmap
ImageList_Add(tlbi,hB,0);
tb->ThumbBarSetImageList(MainWindow,tlbi);
DeleteObject(hB);
// Add 2 buttons:
THUMBBUTTON tup[2];
int ids[] = {701,702};
wchar_t* txts[] = {L"Stop all",L"Start all"};
for(int i = 0 ; i < 2 ; i++)
{
THUMBBUTTON& tu = tup[i];
tu.dwMask = THB_FLAGS | THB_BITMAP | THB_TOOLTIP;
tu.iBitmap = i;
tu.iId = ids[i];
_tcscpy(tu.szTip,txts[i]);
tu.dwFlags = THBF_ENABLED | THBF_DISMISSONCLICK;
}
tb->ThumbBarAddButtons(MainWindow,2,tup);
tb->Release();
Step by Step Progress Bar (Error handling removed for simplicity):
// Create the interface
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
// Set the state
tb->SetProgressState(MainWindow,TBPF_NORMAL);
// To display progress
tb->SetProgressValue(MainWindow,35,100); // 35% progress
tb->Release();
Read More
- The Taskbar Extensions documentation.
- The ITaskbarList3 interface.
II. Libraries
What are libraries?
A Library is a way to tell Windows 7 where your files are stored. The problem with the "My Documents" folder is that it is only one; a user might want to store important files all over the hard disk. A Library tells Windows where your collection is, so it can be easily indexed and searched. So a Library can have many files, sorted as they were in a single folder, while, in reality, they are in different directories in the hard disk. It is basically a container of multiple directories; this reduces application complexity - for example, an application that needs to be notified when something in the files is changed, now only needs to monitor the library object, which automatically monitors all the items it contains.
For example, one might want to have all their photos from the Island of Crete in special locations (depending on when they visited the Island), but to be able to search them all at once, they will simply define a Library with all the photos, and searching and indexing and sorting of these photos will occur as they belonged to a single directory.
Because the Libraries are a Shell interface, it can be used with any sort of application, not just from our downloader.
Enabling the Selection of a Library in Save-as:
To enable the usage of libraries, you can normally call GetSaveFileName()
, but for more flexibility, you can use IFileSaveDialog
:
void SelectFileLocation(HWND hh,wstring& fn)
{
HRESULT hr = S_OK;
IShellItem* si = 0;
IFileSaveDialog* fs;
hr = CoCreateInstance(CLSID_FileSaveDialog,NULL,CLSCTX_INPROC,
__uuidof(IFileSaveDialog),(void**)&fs);
if (SUCCEEDED(hr))
{
FILEOPENDIALOGOPTIONS fop;
hr = fs->GetOptions(&fop);
fop |= FOS_OVERWRITEPROMPT | FOS_PATHMUSTEXIST | FOS_FORCESHOWHIDDEN;
hr = fs->SetOptions(fop);
hr = fs->SetFileName(fn.c_str());
if (SUCCEEDED(hr))
{
hr = fs->Show(hh);
if (SUCCEEDED(hr))
{
hr = fs->GetResult(&si);
}
}
fs->Release();
}
if (!si)
return;
LPWSTR ff = 0;
si->GetDisplayName(SIGDN_FILESYSPATH,&ff);
if (ff)
{
fn = ff;
CoTaskMemFree(ff);
}
si->Release();
}
The above function returns the user selection. If you select a library, then the library's Default Save Location is returned. (You can set this with IShellLibrary::GetDefaultSaveFolder) .
Enabling the Selection of a Library in File-Open:
This is different, because Libraries are not file system objects. You have to use IShellLibrary
, as MSDN states:
IShellLibrary *picturesLibrary;
hr = SHLoadLibraryFromKnownFolder(FOLDERID_PicturesLibrary,
STGM_READ, IID_PPV_ARGS(&picturesLibrary));
// picturesLibrary points to the user's picture library
IShellItemArray *pictureFolders;
hr = pslLibrary->GetFolders(LFF_FORCEFILESYSTEM,
IID_PPV_ARGS(&pictureFolders));
// pictureFolders contains an array of shell items that represent
// the folders found in the user's pictures library
Creating and Using a Library
Our application creates a "W7DL" Library. For each file you download with it (the default location is My documents\\W7DL), its directory is stored as an item in that Library so you can access all your downloads in a single destination:
First, create the Library if not existing:
// Creates a W7Downloader Library if not existing wstring SaveF;
GetSaveFolder(SaveF); // Gets default save folder, my documents\W7DL
// First, make sure the library exists
IShellLibrary* sl = 0;
SHCreateLibrary(__uuidof(IShellLibrary),(void**)&sl);
if (!sl)
return;
IShellItem* si = 0;
sl->SaveInKnownFolder(FOLDERID_Libraries,
L"W7Downloads",LSF_FAILIFTHERE,&si);
if (si)
si->Release();
si = 0;
sl->Release();
sl = 0;
The above code ensures that we have a Library. LFS_FAILIFTHERE
ensures that an existing Library won't be overwritten.
To now load the Library into an IShellFolder
, we use this code:
// Load the library
TCHAR ln[10000] = {0};
PWSTR LibraryFolderPath = 0;
SHGetKnownFolderPath(FOLDERID_Libraries,0,0,&LibraryFolderPath);
if (!LibraryFolderPath)
return;
swprintf_s(ln,10000,L"%s\\W7Downloads.library-ms",LibraryFolderPath);
// library-ms seems to be the extension for library files
CoTaskMemFree(LibraryFolderPath);
hr = SHLoadLibraryFromParsingName(ln,STGM_READWRITE,
__uuidof(IShellLibrary),(void**)&sl);
if (!sl)
return;
We now want to add our default save folder to this Library. Since it is the first directory we add, it will be marked as default.
// Add the SaveF folder to this library
hr = SHAddFolderPathToLibrary(sl,SaveF.c_str());
sl->Commit();
To add more directories, we use the same function.
Read More
- The Libraries documentation.
- The IShellLibrary interface.
III. Touch API
What is it?
The Touch API is a new API to process multiple contacts (touches) from a multi-touch capable surface. It supports two messages:
WM_GESTURE
, which passes information about a gesture if a touch is recognized as such, andWM_TOUCH
, which passes information about multiple touches.
Here we will focus on WM_TOUCH
.
Hey, does anyone have a Touch screen yet?
I am not sure, but I have good news for you. You can use your mouse (or multiple mice) to simulate touching, with the aid of the Codeplex Touch Simulation tool. This tool converts your mice to virtual touching devices. This video shows how to install and enable this simulator. Note that you must "disable" the mice function for the application to actually receive WM_TOUCH
.
Registration for WM_TOUCH
A window must register to be capable to receive WM_TOUCH
with RegisterTouchWindow. If that window has child windows, RegisterTouchWindow()
must be called for each of them separately.
Processing WM_TOUCH
The wParam
low-word contains the number of touches, and lParam
contains a handle. You use this handle to GetTouchInputInfo to get the touch information in an array of TOUCHINPUT structures. You must free this handle with CloseTouchInputHandle, or pass the message to DefWindowProc
for the cleanup.
The TOUCHINPUT
structure contains many elements. For the sake of simplicity, here, you will convert the screen coordinates to the client coordinates of our app. If a download is "touched", it is selected.
LRESULT TouchHandler(HWND hh,UINT mm,WPARAM ww,LPARAM ll)
{
// Get the touching
int ni = LOWORD(ww);
TOUCHINPUT* ti = new TOUCHINPUT[ni + 1];
if (!GetTouchInputInfo((HTOUCHINPUT)ll,ni + 1,ti,sizeof(TOUCHINPUT)))
{
delete[] ti;
return DefWindowProc(hh,mm,ww,ll);
}
// Process Messages
for(int i = 0 ; i < ni ; i++)
{
LONG x = ti[i].x;
LONG y = ti[i].y;
// Convert these /100 as MSDN says
x /= 100;
y /= 100;
// Convert to client
POINT p = {x,y};
ScreenToClient(hh,&p);
// Select downloads
for(unsigned int i = 0 ; i < Downloads.size() ; i++)
{
if (InRect(p.x,p.y,Downloads[i]->HitTest))
Downloads[i]->Selected = !Downloads[i]->Selected;
}
}
delete[] ti;
CloseTouchInputHandle((HTOUCHINPUT)ll);
return 0;
}
More complex messages include information on how the movement occurred, such as TOUCHEVENTF_DOWN
.
Read More
- Windows Touch API.
IV. Sensor & Location API
What is this?
The Sensor API is a new abstraction API to query values from sensors, i.e., devices that can generate data from a hardware source. Sensors can include GPS, Light Detectors, Temperature Detectors, Motion Detectors, Biometrics (fingerprint), and other stuff.
You can think of the Sensor API as a "raw input" method, from which you can get information from any device that has a sensor driver no matter what device that is.
The Locaton API is a reduction of the Sensor API which gets the PC's location from a GPS Sensor, if installed. It is useful if you only want to get an idea of the PC's location, and you do not need more data like satellite location, speed etc., which would also be returned by a GPS sensor.
Because sensors can provide user-sensitive data, sensors are disabled by default. An application can request to use a sensor, and this results in a Control Panel message to prompt the user to allow the sensor. You cannot enable sensor access programmatically even if running as Administrator. The user can also specify, through the Control Panel, which applications have access to selected sensors.
To use a sensor, a sensor driver must be installed. The SDK comes with a "Virtual Light Sensor" which you can use to simulate a light detector sensor. You can also use my own GPS Sensor Driver, a driver to test the Sensor and Location API with real data coming from any NMEA-compatible GPS that can connect via a serial port (USB or Bluetooth). If you do not own an actual GPS hardware, this driver can also simulate your location.
The application uses the Sensor API to query the virtual light sensor and to adjust the foreground and background colors of the client area (the more light, the more bold the letters appear). The application uses the Location API to query your PC for its location, and then it can display a map (using Google Maps) along with the destinations of your downloads (IPs are converted to GPS coordinates by using the http://www.hostip.info/ free service).
Quick Steps
#include <sensors.h>
and<sensorsapi.h>
#pragma comment(lib,"sensors.lib");
CoCreate
ISensorManager*- Implement ISensorManagerEvents and pass it to ISensorManager::SetEventSink() to get notified when a new sensor is available.
- Use ISensorManager::GetSensorsByCategory() to enumerate the sensors. You can use either a known category or a custom CLSID, or
SENSOR_CATEGORY_ALL
. Use the returned ISensorCollection to loop and GetAt() the needed ISensor. - Implement an ISensorEvents, and use ISensor::SetEventSink and ISensor::SetEventInterest to get notified when the sensor state changes or when the sensor data changes. You also get notified when the sensor is removed.
- Use ISensor::GetData to get an ISensorDataReport to query the sensor data.
The meaning of the sensor values depend on the sensor type. There are some predefined types and categories you can use, but it could be any category and type CLSID, as long as you know how to interpret it. For a custom sensor driver project, you can see my own 3DConnexion Sensor driver which maps as a sensor and a 3D Mouse 3DConnexion device.
Step by Step Light Query (Error handling removed for simplicity):
// Sensor Manager Events Implementation
class MySensorManagerEvents : public ISensorManagerEvents
{
private:
unsigned long ref;
public:
MySensorManagerEvents()
{
ref = 0;
AddRef();
}
// IUnknown
....
// ISensorManagerEvents
HRESULT __stdcall OnSensorEnter(
ISensor *pSensor,
SensorState state)
{
InvalidateRect(MainWindow,0,0);
UpdateWindow(MainWindow);
return S_OK;
}
};
MySensorManagerEvents sme;
// Sensor Events Implementation
class MySensorEvents : public ISensorEvents
{
private:
unsigned long ref;
public:
MySensorEvents()
{
ref = 0;
AddRef();
}
// IUnknown
...
// Sensor Events
HRESULT __stdcall OnEvent(ISensor *pSensor,
REFGUID eventID,IPortableDeviceValues *pEventData)
{
return S_OK;
}
HRESULT __stdcall OnDataUpdated(ISensor *pSensor,
ISensorDataReport *pNewData)
{
SensorDataUpdate(pSensor,pNewData);
return S_OK;
}
HRESULT __stdcall OnLeave(REFSENSOR_ID sensorID)
{
// Free sensors if released
if (sensorID == LightSensorID)
{
LightSensor->Release();
LightSensor = 0;
}
InvalidateRect(MainWindow,0,0);
UpdateWindow(MainWindow);
return S_OK;
}
HRESULT __stdcall OnStateChanged(ISensor *pSensor,SensorState state)
{
SensorDataUpdate(pSensor,0);
return S_OK;
}
};
MySensorEvents ses;
// Load the sensor
CoCreateInstance(CLSID_SensorManager,0,CLSCTX_ALL,
__uuidof(ISensorManager),(void**)&sm);
sm->SetEventSink(&sme);
GUID pguid[2];
pguid[0] = SENSOR_EVENT_DATA_UPDATED;
// Check to find light sensor
ISensorCollection* ic = 0;
sm->GetSensorsByCategory(SENSOR_CATEGORY_LIGHT,&ic);
ULONG pC = 0;
ic->GetCount(&pC);
if (pC)
{
// Get the first one
ic->GetAt(0,&LightSensor);
if (LightSensor)
{
LightSensor->GetID(&LightSensorID);
hr = LightSensor->SetEventSink(&ses);
hr = LightSensor->SetEventInterest(pguid,1);
}
}
// Do we have a light sensor?
if (LightSensor)
{
// Get the value
ISensorDataReport* d = 0;
LightSensor->GetData(&d);
if (!d) error(...)
// no data duh
PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_R4;
d->GetSensorValue(SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX,&pv);
d->Release();
// pv.fltVal has the LUX value
}
You have to release any ISensor
interface when you get the OnLeave
callback.
Step by Step Location Query (Error handling removed for simplicity):
// Create the ILocation
ILocation* lm = 0;
CoCreateInstance(CLSID_Location,0,CLSCTX_ALL,__uuidof(ILocation),(void**)&lm);
ILocationReport* lmr = 0;
IID REPORT_TYPES[] = { IID_ILatLongReport }; // We want a XYZ report
// Ask permissions. If result is E_ACCESSDENIED,
// then the user doesn't allow our app to use the location API.
hr = lm->RequestPermissions(MainWindow,REPORT_TYPES,1,TRUE);
LOCATION_REPORT_STATUS status = REPORT_NOT_SUPPORTED;
hr = lm->GetReportStatus(IID_ILatLongReport, &status);
if (status == REPORT_RUNNING)
hr = lm->GetReport(__uuidof(ILatLongReport),&lmr);
// We must have a running sensor to get valid report.
if (lmr)
{
ILatLongReport* llr = 0;
hr = lmr->QueryInterface(__uuidof(ILatLongReport),(void**)&llr);
if (llr)
{
DOUBLE xx = 0,yy = 0,zz = 0;
llr->GetLongitude(&xx);
llr->GetLatitude(&yy);
llr->GetAltitude(&zz);
// xx,yy,zz have the XYZ of the location
llr->Release();
}
lmr->Release();
}
lm->Release();
It is very important to note that the ILocation
interface is volatile. That is, you cannot get an ILocation
and use it throughout the application while the sensor information might change. For safety, you should release the ILocation
as long as it is not needed, and when you need Location Information again, instantiate it.
You can test the ILocation
code with my GPS driver:
This driver can use your GPS hardware (such as a Bluetooth GPS device), or simulate the information if no hardware exists.
Sensor Drivers
A sensor driver is a user mode driver (UMDF) that provides sensor information to user applications. The Windows 7 Device Driver Kit provides the "SensorSkeleton
" driver which you can use as a template to implement your own Sensor driver.
Read More
- The Sensor and Location API documentation.
- My Sensor Explorer project in CodeProject.
- Sensor Drivers in MSDN.
V. Animation Manager
What is this?
The Animation Manager is a new API to manipulate animations. It does not actually draw anything. Instead, it allows you to specify variables (objects) to animate, and a storyboard, which contains the variables to animate and the type of the animation to use (there are defined animations, and you can also define your own). Finally, the animation is done through either a timer (which we will be using here), or when triggered by the application itself:
So, using this API, you simply specify what to animate and how, and then you are given back the results of the math applied to your variables, and then you can use them to draw your objects. For example, if you have a 3D cube that rotates based on X,Y,Z values, you can define these to be manipulated linearly, logarithmically, or in another predefined or custom way, and then your callback functions are called with the results, based on a frame rate timer.
For example, this application draws and animates a sine function during the download. So it needs two variables: the "x", which linearly goes from 0 to 100 , and "y" , which is a sinusoid function that goes from -1 to 1. When values are updated (based on a timer), the application redraws the sine.
Quick Steps
The Animation API is a powerful API, and space here permits only a brief discussion. In short, here is what you have to do for a timer-driven application animation:
CoCreate
the IUIAnimationManager.CoCreate
the IUIAnimationTimer.CoCreate
the IUIAnimationTransitionLibrary.- Attach the timer to the manager by calling IUIAnimationManager::SetTimerUpdateHandler(), passing an IUIAnimationTimerUpdateHandler. You can get this interface by querying the IUIAnimationTimer.
- Implement an IUIAnimationManagerEventHandler event handler and pass it to IUIAnimationManager::SetTimerEventHandler.
- Create all the animation variables by calling IUIAnimationManager::CreateAnimationVariable(). There is an initial value, and after that, values can only be set by the transition.
- Create a storyboard by calling IUIAnimationManager::CreateStoryboard(). A storyboard is a storage that contains all the variables and their animation transitions.
- Call
IUIAnimationTransitionLibrary
methods to create standard transitions. The application here creates a linear transition variable (CreateLinearTransition) which represents the X axis in a sinusoid oscillation, and a sinusoidal transition variable (CreateSinusoidalTransitionFromRange) which represents the Y axis. - Get the current "animation time" by calling IUIAnimationTimer::GetTime(), then pass it to IUIAnimationStoryboard::Schedule(). This starts the animation.
- Your event handler OnPreUpdate is called when the variables are changed. Use IUIAnimationVariable::GetValue() to query the values of the variables, then use them to draw the objects based on their value.
- When the animation is done, you are notified. Recreate stuff to restart the animation.
Alternatively, you can setup key frames (time positions) in the animation and specify to loop between them, finitely or indefinitely.
Step by Step Timer Animation (Error handling removed for simplicity):
// Implementation of event handlers
class MyAnimationManagerEventHandler : public IUIAnimationManagerEventHandler
{
private:
unsigned long ref;
public:
MyAnimationManagerEventHandler()
{
ref = 1;
}
// IUnknown
...
// IUIAnimationManagerEventHandler
HRESULT __stdcall OnManagerStatusChanged(
UI_ANIMATION_MANAGER_STATUS newStatus,
UI_ANIMATION_MANAGER_STATUS previousStatus
)
{
if (newStatus == UI_ANIMATION_MANAGER_IDLE)
{
// Schedule the animation again
PostMessage(MainWindow,WM_USER + 1,0,0);
}
return S_OK;
}
};
class MyAnimationTimeEventHandler : public IUIAnimationTimerEventHandler
{
private:
unsigned long ref;
public:
MyAnimationTimeEventHandler()
{
ref = 1;
}
// IUnknown
...
// IUIAnimationTimerEventHandler
HRESULT __stdcall OnPostUpdate()
{
return S_OK;
}
HRESULT __stdcall OnPreUpdate()
// This is where we get the update values of the animated variables
{
// Request the y variable value
DOUBLE v1 = 0,v0 = 0;
if (amv[0])
amv[0]->GetValue(&v0);
if (amv[1])
amv[1]->GetValue(&v1);
void D2DrawAnimation(double,double);
D2DrawAnimation(v0,v1);
return S_OK;
}
HRESULT __stdcall OnRenderingTooSlow(UINT32 framesPerSecond)
{
return E_NOTIMPL;
}
};
MyAnimationTimeEventHandler matH;
MyAnimationManagerEventHandler maEH;
// Interface declarations
IUIAnimationManager* am = 0;
IUIAnimationTransitionLibrary* amtr = 0;
IUIAnimationStoryboard* amb = 0;
IUIAnimationTimer* amt = 0;
IUIAnimationVariable* amv[2] = {0,0};
// CoCreate elements
CoCreateInstance(CLSID_UIAnimationManager,0,CLSCTX_INPROC_SERVER,
__uuidof(IUIAnimationManager),(void**)&am);
CoCreateInstance(CLSID_UIAnimationTimer,0,CLSCTX_INPROC_SERVER,
__uuidof(IUIAnimationTimer),(void**)&amt);
CoCreateInstance(CLSID_UIAnimationTransitionLibrary,0,CLSCTX_INPROC_SERVER,
__uuidof(IUIAnimationTransitionLibrary),(void**)&amtr);
// Set the event handler
am->SetManagerEventHandler(&maEH);
// Timer Update Handler
IUIAnimationTimerUpdateHandler* amth = 0;
am->QueryInterface(__uuidof(IUIAnimationTimerUpdateHandler),(void**)&amth);
amt->SetTimerUpdateHandler(amth,UI_ANIMATION_IDLE_BEHAVIOR_DISABLE);
amth->Release();
// Set the timer event handler
amt->SetTimerEventHandler(&matH);
And now, we need to create some variables, create the storyboard, and load it with those variables along with their transitions.
// The Variables to animate (x,y of the point)
am->CreateAnimationVariable(0.0f,&amv[0]); // Start value 0
am->CreateAnimationVariable(0.0f,&amv[1]); // Start value 0
// Create the story board
am->CreateStoryboard(&amb);
// Create a linear and a sinusoidal transition for the variables
IUIAnimationTransition* trs = 0;
amtr->CreateSinusoidalTransitionFromRange(5,0.0f,1.0f,0.5f,
UI_ANIMATION_SLOPE_INCREASING,&trs);
amb->AddTransition(amv[1],trs);
trs->Release();
amtr->CreateLinearTransition(5,100,&trs);
amb->AddTransition(amv[0],trs);
trs->Release();
// Specify the time and start the animation
UI_ANIMATION_SECONDS se = 0;
if (SUCCEEDED(amt->GetTime(&se)))
hr = amb->Schedule(se);
amt->Enable();
After the above code, the animation starts for 5 seconds (as we have specified in the transitions). The X variable starts from 0 and goes to 100 (linearly), the Y variable starts from 0 and goes to 1.0 with a period of 0.5f (or 2Hz frequency). The application uses these values to animate a sine function while the download is in progress, while using color transparency to indicate the percentage of the download.
The Animation Manager allows you to use key frames to setup loop positions. For more information on this, see Creating a Storyboard.
Read More
- The Animation API documentation.
- Animation API Samples.
VI. Direct2D and DirectWrite
The Direct2D is a powerful ActiveX hardware-accelerated drawing API which replaces GDI and GDI+, providing enhanced features. The application draws its client area through Direct2D; however, the real power of this API is shown in applications that draw a lot of information, scroll in real time etc. An example of that kind of application is my Turbo Play. Direct2D also supports software rendering if hardware acceleration is not available. Direct2D can write to both an HWND or an HDC, allowing you to combine it, if needed, with GDI or GDI+. In addition, DirectWrite is provided to write high-quality text to the target.
Introduction
A short-step guide for using Direct2D is to:
#include <d2d1.h>
#include <dwrite.h>
- Create the ID2D1Factory* with D2D1CreateFactory.
- Call ID2D1Factory::CreateHwndRenderTarget to create an ID2D1HwndRenderTarget if you are writing to an HWND, or call ID2D1Factory::CreateDCRenderTarget to get an ID2D1DCRenderTarget* to write to a device context.
- Create the IDWriteFactory* by calling DWriteCreateFactory().
- Use the Render Target interface to draw. Call the method BeginDraw() before drawing, draw the data with the
ID2D1HwndRenderTarget
orID2D1DCRenderTarget
members, then use the method Flush() or EndDraw() to end the drawing.Flush()
will draw the result even if errors occur, whileEndDraw()
will draw the result only if no errors have occurred. Check the return value ofEndDraw()
- you may need to recreate the Direct2D object. - When your window is resized, call the method Resize() to update the Direct2D interface.
You should call the CreateFactory
functions with LoadLibrary()
/GetProcAddress
to ensure that your application will run in previous OS versions.
Create the Interface
ID2D1Factory* d2_f = 0;
ID2D1HwndRenderTarget* d2_RT = 0;
IDWriteFactory* d2_w = 0;
// Create the interface
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
__uuidof(ID2D1Factory),0,(void**)&d2_f);
// HWND Target Render
// First try hardware
D2D1_RENDER_TARGET_PROPERTIES def = D2D1::RenderTargetProperties();
def.type = D2D1_RENDER_TARGET_TYPE_HARDWARE;
RECT rc = {0};
GetClientRect(MainWindow,&rc);
d2_f->CreateHwndRenderTarget(def,
D2D1::HwndRenderTargetProperties(hh,D2D1::SizeU(
rc.right - rc.left,rc.bottom - rc.top)),&d2_RT);
if (!d2_RT)
{
// Try again
def.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
d2_f->CreateHwndRenderTarget(def,
D2D1::HwndRenderTargetProperties(hh,D2D1::SizeU(
rc.right - rc.left,rc.bottom - rc.top)),&d2_RT);
}
// DirectWrite
DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),(IUnknown**)&d2_w);
Direct2D Colors
Direct2D colors are represented by a D2D1_COLOR_F
which contains the ARGB values from 0 to 1.0. So, to convert to a D2D1_COLOR_F
from a classic ARGB:
D2D1_COLOR_F GetD2Color(unsigned long c)
{
D2D1_COLOR_F cc;
cc.a = GetAValue(c)/255.0f;
if (cc.a == 0)
cc.a = 1.0f; // Cope for RGB colors only
cc.r = GetRValue(c)/255.0f;
cc.g = GetGValue(c)/255.0f;
cc.b = GetBValue(c)/255.0f;
return cc;
}
Direct2D Fonts
Direct2D Fonts are represented by IDWriteTextFormat*
. You create this interface by calling IDWriteFactory::CreateTextFormat()
, passing the face name, size, and characteristics:
FLOAT fs = 12.0f;
wstring fc = L"Tahoma";
IDWriteTextFormat* fo2d_n = 0;
d2_w->CreateTextFormat(fc.c_str(),0,DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,DWRITE_FONT_STRETCH_NORMAL,fs,L"",&fo2d_n);
Direct2D Brushes
Direct2D supports various styles of brushes. For the sake of simplicity, this application uses the classic Solid color brush.
D2D1SolidColorBrush* GetD2SolidBrush(unsigned long c)
{
ID2D1SolidColorBrush* b = 0;
D2D1_COLOR_F cc = GetD2Color(c);
d2_RT->CreateSolidColorBrush(cc,&b);
return b;
}
Note that there is no "pen" ; each function that draws shapes accepts a stroke style and width.
Direct2D Images
The easiest way to supply an image to Direct2D is through the Windows Imaging Component. WIC can convert an HBITMAP
to an IWICBitmap*
and then we use the render target's CreateBitmapFromWicBitmap()
to convert it to a format that Direct2D likes. If direct conversion fails, then we can convert it to the 32bppPBGRA
format using WIC:
void DrawImage(int x1,int y1,HBITMAP hB,float Op,bool HasAlpha,void** Cache)
{
BITMAP bo;
GetObject(hB,sizeof(bo),&bo);
WICBitmapAlphaChannelOption ao = WICBitmapUseAlpha;
IWICBitmap* wb = 0;
IWICImagingFactory* pImageFactory = 0;
CoCreateInstance(CLSID_WICImagingFactory,0,CLSCTX_ALL,
__uuidof(IWICImagingFactory),(void**)&pImageFactory);
pImageFactory->CreateBitmapFromHBITMAP(hB,0,ao,&wb);
ID2D1Bitmap* b = 0;
pRT->CreateBitmapFromWicBitmap(wb,0,&b);
if (!b)
{
// Convert it
IWICFormatConverter* spConverter = 0;
pImageFactory->CreateFormatConverter(&spConverter);
if (spConverter)
{
spConverter->Initialize(wb,GUID_WICPixelFormat32bppPBGRA,
WICBitmapDitherTypeNone,NULL,0.f,WICBitmapPaletteTypeMedianCut);
pRT->CreateBitmapFromWicBitmap(spConverter,0,&b);
spConverter->Release();
}
if (wb)
{
wb->Release();
wb = 0;
}
D2D1_RECT_F r;
r.left = (FLOAT)x1;
r.top = (FLOAT)y1;
r.right = (FLOAT)(x1 + bo.bmWidth);
r.bottom = (FLOAT)(y1 + bo.bmHeight);
pRT->DrawBitmap(b,r,Op);
// Release interfaces ...
}
Using this function is easy, but in practice, you should convert all your HBITMAP
s to ID2D1Bitmap*
once and then use them directly with DrawBitmap()
, to avoid unnecessary overhead each time the bitmap has to be drawn.
Direct2D Shapes
Use methods exposed from ID2D1RenderTarget (which is the parent class of ID2D1HwndRenderTarget
and ID2D1DCRenderTarget
) to draw:
Draw
/FillEllipse
DrawLine
Draw
/FillRectangle
DrawText
This class supports many other forms of drawing, like layers, paths, mesh etc.
Direct2D Polygons
The following sample shows how to use DrawGeometry() to create a polygon by a set of points:
void Polygon(POINT*p,int n,bool Close)
{
// Convert POINT to D2D1_POINT_2F
D2D1_POINT_2F* pt = new D2D1_POINT_2F[n];
for(int i = 0 ; i < n ; i++)
{
pt[i].x = (FLOAT)p[i].x;
pt[i].y = (FLOAT)p[i].y;
}
ID2D1SolidColorBrush* b = GetD2SolidBrush(c);
ID2D1PathGeometry* pg = 0;
ID2D1GeometrySink* pgs = 0;
pD2DFactory->CreatePathGeometry(&pg);
if (pg)
{
pg->Open(&pgs);
if (pgs)
{
D2D1_POINT_2F fb;
fb.x = (FLOAT)pt[0].x;
fb.y = (FLOAT)pt[0].y;
// Use D2D1_FIGURE_BEGIN_FILLED for filled
D2D1_FIGURE_BEGIN fg = D2D1_FIGURE_BEGIN_HOLLOW;
D2D1_FIGURE_END fe;
if (Close)
fe = D2D1_FIGURE_END_CLOSED;
else
fe = D2D1_FIGURE_END_OPEN;
pgs->BeginFigure(fb,fg);
for(int i = 1 ; i < n ; i++)
{
D2D1_POINT_2F fu;
fu.x = pt[i].x;
fu.y = pt[i].y;
pgs->AddLine(fu);
}
pgs->EndFigure(fe);
pgs->Close();
pgs->Release();
}
if (b)
pRT->DrawGeometry(pg,b,1);
pg->Release();
if (b)
b->Release();
delete[] pt;
}
DirectWrite Text
The steps to draw text are:
- Create the font as discussed above.
- Set the text alignment by calling
IDWriteTextFormat::
SetParagraphAlignment and SetTextAlignment. - Call
ID2D1RenderTarget::
DrawText.
To measure the text dimensions, you have to create the IDWriteTextLayout* (IDWriteFactory::
CreateTextLayout) which represents the formatted text's attributes:
POINT GetD2TextSize(IDWriteTextFormat* ffo,wchar_t* txt,int l = -1)
{
POINT p = {0};
// Create a layout
IDWriteTextLayout* lay = 0;
d2_w->CreateTextLayout(txt,l == -1 ? wcslen(txt) : l,ffo,1000,1000,&lay);
DWRITE_TEXT_METRICS m = {0};
float fx = lay->GetMaxWidth();
lay->GetMetrics(&m);
lay->Release();
// Save the metrics
int wi = (int)m.widthIncludingTrailingWhitespace;
if (m.widthIncludingTrailingWhitespace > (float)wi)
wi++;
int he = (int)m.height;
if (m.height > (float)he)
he++;
p.x = wi;
p.y = he;
return p;
}
Read More
VII. Ribbon
The last chapter of this article describes the most important added API to the Win32 collection, in my opinion. The Windows 7 Ribbon effectively transforms your application from "old" style to "new" style. Let's be honest; an application that uses Direct2D, Sensors, Taskbar Lists etc., won't be much noticed by the average-user as a serious upgrade; but a ribbon which replaces the old application toolbar and menu will definitely draw attention.
Basically, the ribbon is an area that contains
- An application menu
- A quick toolbar
- An optional help button
- Tabs
A tab is an area that contains groups of other controls. These controls can be buttons, checkboxes, dropdown combos, font selection/color selection controls, drop down buttons, and more. All this is stored in an XML configuration file, and it's compiled to a binary file with an SDK tool called UICC.EXE. UICC.exe can also generate .h and .rc files to include them to your existing .rc script, so the images for the ribbon are loaded. The ribbon only supports 32-bit BMP images.
A ribbon tab can be permanently displayed, or displayed optionally. Tabs and groups support "ApplicationMode
", which is simply a binary value that indicates the bits that should be on for the tab or group to be displayed. So, a tab with ApplicationMode
== 0 will be always displayed, whereas a tab with ApplicationMode
== 3 (11b) will be displayed when the mode bit 0 or 1 is set.
A ribbon can contain more complex items:
Tabs can also be "contextual". This is similar to the application mode, but there is a special focus to the tab so the user notices that there is additional content available, depending on the application context:
Quick Steps:
- Prepare the ribbon XML. You can either edit that file with Notepad, or have a tool (such as VRC) generate the binary for you. For more details on the XML format the ribbon wants, see my my Ribbon article in CodeProject.
CoCreate
anIUIFramework
.- Implement an IUIApplication and pass it to IUIFramework::Initialize().
- Call
IUIFramework::LoadUI()
to load the ribbon data. - In the
IUIApplication::
OnCreateUICommand() callback, implement a IUICommandHandler interface (for each command) and pass it back. - In
IUIApplication::
OnViewChanged(), check fortypeID == UI_VIEWTYPE_RIBBON
andverb == UI_VIEWVERB_CREATE
, then query the passedIUnknown
interface for an IUIRibbon.* - In
IUIApplication::OnViewChanged()
, check fortypeID == UI_VIEWTYPE_RIBBON
andverb == UI_VIEWVERB_SIZE
, and callIUIRibbon::
GetHeight() to get the ribbon height. Each time the ribbon is resized, this callback is called. - Each time a ribbon element needs information from you, the
IUICommandHandler::
UpdateProperty is called. If you want to force a property to be updated, you callIUIFramework::
InvalidateUICommand(). - Each time a command is issued (e.g., a button is pressed),
IUICommandHandler::
Execute is called.
The IUIRibbon interface also allows you to save/load persisting ribbon size to/from a stream.
Step by Step Ribbon Creation
Implementation of the Command Handler, implementation of IUIApplication
, Ribbon creation, and initialization:
// Implement a command handler for all commands
// IUICommandHandler for Implementation
class MyUICommandHandler : public IUICommandHandler
{
private:
class MyUIApplication* app;
UINT32 cmd;
UI_COMMANDTYPE ctype;
int refNum;
public:
MyUICommandHandler(class MyUIApplication* uapp,UINT32 c,UI_COMMANDTYPE ty)
{
refNum = 1;
app = uapp;
cmd = c;
ctype = ty;
}
// IUnknown
...
// IUICommandHandler
virtual HRESULT __stdcall UpdateProperty(UINT32 commandId,REFPROPERTYKEY key,
const PROPVARIANT *currentValue,PROPVARIANT *newValue)
{
// This is called when a property should be updated
if (key == UI_PKEY_Enabled)
{
// Enable or Disable commands here, lets say we want to disable Command ID 100
if (commandId == 100)
UIInitPropertyFromBoolean(key,FALSE,newValue);
else
UIInitPropertyFromBoolean(key,TRUE,newValue);
}
if (key == UI_PKEY_RecentItems)
{
// Display here the recent items.
// The ribbon wants a SAFEARRAY
// See RI.CPP for details on this
return 0;
}
if (key == UI_PKEY_ContextAvailable) // Contextual Tabs update
{
unsigned int gPR = (unsigned int)UI_CONTEXTAVAILABILITY_NOTAVAILABLE;
if (ShouldThatTabShow(commandID))
gPR = (unsigned int)UI_CONTEXTAVAILABILITY_ACTIVE;
UIInitPropertyFromUInt32(key, gPR, newValue);
}
return S_OK;
}
virtual HRESULT __stdcall Execute(UINT32 commandId,UI_EXECUTIONVERB verb,
const PROPERTYKEY *key,const PROPVARIANT *currentValue,
IUISimplePropertySet *commandExecutionProperties)
{
// This is called when there are actions
if (verb == UI_EXECUTIONVERB_EXECUTE)
{
if (ctype == UI_COMMANDTYPE_ACTION)
{
// Button Pressed
SendMessage(MainWindow,WM_COMMAND,commandId,0);
}
if (ctype == UI_COMMANDTYPE_RECENTITEMS)
{
if (!currentValue)
return 0;
// Recent Items
SendMessage(MainWindow,WM_COMMAND,15000 + currentValue->intVal,0);
}
// This is called on a font choosing
if (ctype == UI_COMMANDTYPE_FONT)
{
// See code for details
}
if (ctype == UI_COMMANDTYPE_COLORANCHOR)
{
// See code for details, this is called on color selection
return S_OK;
}
};
// Implement an IUIApplication
class MyUIApplication : public IUIApplication
{
private:
HWND hh; // Our Window
int refNum;
public:
MyUIApplication(HWND hP)
{
refNum = 1;
hh = hP;
}
// IUnknown
...
// IUIApplication callbacks
// These have to be implemented to handle notifications from a control
virtual HRESULT __stdcall OnCreateUICommand(UINT32 commandId,
UI_COMMANDTYPE typeID,IUICommandHandler **commandHandler)
{
// Create a command handler
if (!commandHandler)
return E_POINTER;
MyUICommandHandler * C = new MyUICommandHandler(this,commandId,typeID);
*commandHandler = (IUICommandHandler*)C;
return S_OK;
}
virtual HRESULT __stdcall OnDestroyUICommand(UINT32 commandId,
UI_COMMANDTYPE typeID,IUICommandHandler *commandHandler)
{
return S_OK;
// Do NOT release your MyUICommandHandler! It is released from the control!
}
virtual HRESULT __stdcall OnViewChanged(UINT32 viewId,UI_VIEWTYPE typeID,
IUnknown *view,UI_VIEWVERB verb,INT32 uReasonCode)
{
if (typeID == UI_VIEWTYPE_RIBBON && verb == UI_VIEWVERB_CREATE)
{
// This is where the ribbon is created, get a pointer to an IUIRibbon.
if (!u_r)
{
if (view)
view->QueryInterface(__uuidof(IUIRibbon),(void**)&u_r);
}
}
if (typeID == UI_VIEWTYPE_RIBBON && verb == UI_VIEWVERB_SIZE)
{
// Ribbon Resized, Update Window
UINT32 cy = 0;
if (u_r)
{
u_r->GetHeight(&cy);
LastRibbonHeight = cy;
Update(); // Main Window Resize
}
return S_OK;
}
return S_OK;
}
};
// CoCreate the ribbon
CoCreateInstance(CLSID_UIRibbonFramework,0,CLSCTX_ALL,
__uuidof(IUIFramework),(void**)&u_f);
// Initialize with the IUIApplication callback
u_app = new MyUIApplication(hh);
hr = u_f->Initialize(hh,u_app);
if (FAILED(hr))
{
u_f->Release();
u_f = 0;
return false;
}
// Load the Markup
hr = u_f->LoadUI(hAppInstance,_T("APPLICATION_RIBBON"));
if (FAILED(hr))
{
u_f->Release();
u_f = 0;
return false;
}
Setting/Querying Properties
To set command properties, you first invalidate the command with InvalidateUICommand. This causes the ribbon to call your UpdateProperty method, in which you can test which value requests need changing. For example, you would need to check for propertykey UI_PKEY_Enabled
to enable or disable commands, UI_PKEY_RecentItems
to change the recent items, UI_PKEY_ContextAvailable
to set the contextual tabs etc. Here are all the state properties.
Read More
Possible Bugs?
This is a list of bugs I have found so far in the Windows 7 APIs. Perhaps, these are my code's bugs; feel free to comment and advise me.
ILocation
throws exceptions when used repeatedly or from a callback. For safety, use it immediately when needed and then release it.ILocation
does not return the Z (altitude), even if this property is properly returned from the sensor driver.
Limitations
- The ribbon can't be dynamically generated or changed by an application, since UICC is required.
- Sensor gets permanently disabled for the app once you say "no" to the sensor dialog.
- Ribbon only accepts 24-bit bitmaps. It should allow JPGs, PNGs etc.
- Ribbon forces to have the images inside the same module that contains the ribbon. This forces you to re-include the images to each localized DLL you make.
- Ribbon does not notify you when the user selects a tab, and you cannot reorder the tabs in runtime.
- You cannot remove items from the recent document list programmatically.
- For the recent items to appear in the jump list, your application must have registered as the default opener for the extension. For example, if you open *.JXX files, you must register the JXX extension to open with your application in order for the jumplist to include recent files of that type.
Acknowledgements
Special thanks to these Microsoft staff that cooperated with me to test the various Windows 7 features:
- Ryan Demopoulos
- Nicolas Brun
- Kyle Marsh
- Jaime Rodriguez
History
- 06 - 01 - 2010: Fixed some typos, updated source code.
- 25 - 11 - 2009: First release.