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:
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
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 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.
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:
- Display a manipulation toolbar:
- Handle the taskbar as a progress bar:
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.
const wchar_t* OurAppID = L"You.Software.Sub.Version";
unsigned long uumsg = 0;
vector<wstring> SomeRecentDocuments;
SetCurrentProcessExplicitAppUserModelID(OurAppID);
uumsg = RegisterWindowMessage(L"TaskbarButtonCreated");
if (msg == uumsg)
{
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
SHAddToRecentDocs(SHARD_PATHW,0);
for(unsigned int i = 0 ; i < SomeRecentDocuments.size() ; i++)
{
SHAddToRecentDocs(SHARD_PATHW,(void*)SomeRecentDocuments[i].c_str());
}
ICustomDestinationList* cdl = 0;
CoCreateInstance(CLSID_DestinationList,NULL,CLSCTX_INPROC_SERVER,
__uuidof(ICustomDestinationList),(void**)&cdl);
cdl->SetAppID(OurAppID);
UINT MaxCount = 0;
IObjectArray* oa = 0;
hr = cdl->BeginList(&MaxCount,__uuidof(IObjectArray),(void**)&oa);
hr = cdl->AppendKnownCategory(KDC_RECENT);
oa->Release();
odl->Release();
tb->Release();
}
Step by Step Taskbar Toolbar (Error handling removed for simplicity):
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
HBITMAP hB = SomeBitmap();
LoadTransparentToolbarImage(hAppInstance,_T("WM7_TOOLBAR"),0xFFFFFFFF);
unsigned int nI = 2; HIMAGELIST tlbi = ImageList_Create(bi.bmHeight,bi.bmHeight,ILC_COLOR32,nI,0);
ImageList_Add(tlbi,hB,0);
tb->ThumbBarSetImageList(MainWindow,tlbi);
DeleteObject(hB);
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):
ITaskbarList3* tb = 0;
CoCreateInstance(CLSID_TaskbarList,0,CLSCTX_INPROC_SERVER,
__uuidof(ITaskbarList3),(void**)&tb);
tb->SetProgressState(MainWindow,TBPF_NORMAL);
tb->SetProgressValue(MainWindow,35,100);
tb->Release();
Read More
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.
Fig 2.1 : Our "W7Downloads" library contains two folders, W7DL and G:\TEMP.
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));
IShellItemArray *pictureFolders;
hr = pslLibrary->GetFolders(LFF_FORCEFILESYSTEM,
IID_PPV_ARGS(&pictureFolders));
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:
GetSaveFolder(SaveF); 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:
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);
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.
hr = SHAddFolderPathToLibrary(sl,SaveF.c_str());
sl->Commit();
To add more directories, we use the same function.
Read More
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)
{
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);
}
for(int i = 0 ; i < ni ; i++)
{
LONG x = ti[i].x;
LONG y = ti[i].y;
x /= 100;
y /= 100;
POINT p = {x,y};
ScreenToClient(hh,&p);
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
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.
Figure 4.1: Permission dialog displayed when an application tries to access a sensor.
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
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):
class MySensorManagerEvents : public ISensorManagerEvents
{
private:
unsigned long ref;
public:
MySensorManagerEvents()
{
ref = 0;
AddRef();
}
....
HRESULT __stdcall OnSensorEnter(
ISensor *pSensor,
SensorState state)
{
InvalidateRect(MainWindow,0,0);
UpdateWindow(MainWindow);
return S_OK;
}
};
MySensorManagerEvents sme;
class MySensorEvents : public ISensorEvents
{
private:
unsigned long ref;
public:
MySensorEvents()
{
ref = 0;
AddRef();
}
...
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)
{
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;
CoCreateInstance(CLSID_SensorManager,0,CLSCTX_ALL,
__uuidof(ISensorManager),(void**)&sm);
sm->SetEventSink(&sme);
GUID pguid[2];
pguid[0] = SENSOR_EVENT_DATA_UPDATED;
ISensorCollection* ic = 0;
sm->GetSensorsByCategory(SENSOR_CATEGORY_LIGHT,&ic);
ULONG pC = 0;
ic->GetCount(&pC);
if (pC)
{
ic->GetAt(0,&LightSensor);
if (LightSensor)
{
LightSensor->GetID(&LightSensorID);
hr = LightSensor->SetEventSink(&ses);
hr = LightSensor->SetEventInterest(pguid,1);
}
}
if (LightSensor)
{
ISensorDataReport* d = 0;
LightSensor->GetData(&d);
if (!d) error(...)
PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_R4;
d->GetSensorValue(SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX,&pv);
d->Release();
}
You have to release any ISensor
interface when you get the OnLeave
callback.
Step by Step Location Query (Error handling removed for simplicity):
ILocation* lm = 0;
CoCreateInstance(CLSID_Location,0,CLSCTX_ALL,__uuidof(ILocation),(void**)&lm);
ILocationReport* lmr = 0;
IID REPORT_TYPES[] = { IID_ILatLongReport }; 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);
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);
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:
Fig 4.2: GPSDirect 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
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:
Fig. 5.1 : Sine animation while downloading.
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:
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):
class MyAnimationManagerEventHandler : public IUIAnimationManagerEventHandler
{
private:
unsigned long ref;
public:
MyAnimationManagerEventHandler()
{
ref = 1;
}
...
HRESULT __stdcall OnManagerStatusChanged(
UI_ANIMATION_MANAGER_STATUS newStatus,
UI_ANIMATION_MANAGER_STATUS previousStatus
)
{
if (newStatus == UI_ANIMATION_MANAGER_IDLE)
{
PostMessage(MainWindow,WM_USER + 1,0,0);
}
return S_OK;
}
};
class MyAnimationTimeEventHandler : public IUIAnimationTimerEventHandler
{
private:
unsigned long ref;
public:
MyAnimationTimeEventHandler()
{
ref = 1;
}
...
HRESULT __stdcall OnPostUpdate()
{
return S_OK;
}
HRESULT __stdcall OnPreUpdate()
{
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;
IUIAnimationManager* am = 0;
IUIAnimationTransitionLibrary* amtr = 0;
IUIAnimationStoryboard* amb = 0;
IUIAnimationTimer* amt = 0;
IUIAnimationVariable* amv[2] = {0,0};
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);
am->SetManagerEventHandler(&maEH);
IUIAnimationTimerUpdateHandler* amth = 0;
am->QueryInterface(__uuidof(IUIAnimationTimerUpdateHandler),(void**)&amth);
amt->SetTimerUpdateHandler(amth,UI_ANIMATION_IDLE_BEHAVIOR_DISABLE);
amth->Release();
amt->SetTimerEventHandler(&matH);
And now, we need to create some variables, create the storyboard, and load it with those variables along with their transitions.
am->CreateAnimationVariable(0.0f,&amv[0]); am->CreateAnimationVariable(0.0f,&amv[1]);
am->CreateStoryboard(&amb);
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();
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 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:
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;
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
__uuidof(ID2D1Factory),0,(void**)&d2_f);
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)
{
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);
}
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; 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)
{
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);
}
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)
{
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;
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:
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};
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();
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
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.
Fig 7.1: Visual Ribbon Creator Ribbon.
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:
Fig 7.2 : MS Paint Ribbon.
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:
Fig 7.3: The
Turbo Play contextual ribbon is displayed only if an MIDI music track is selected, to show the Score Editor.
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
an IUIFramework
.- 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 for typeID == UI_VIEWTYPE_RIBBON
and verb == UI_VIEWVERB_CREATE
, then query the passed IUnknown
interface for an IUIRibbon.* - In
IUIApplication::OnViewChanged()
, check for typeID == UI_VIEWTYPE_RIBBON
and verb == UI_VIEWVERB_SIZE
, and call IUIRibbon::
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 call IUIFramework::
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:
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;
}
...
virtual HRESULT __stdcall UpdateProperty(UINT32 commandId,REFPROPERTYKEY key,
const PROPVARIANT *currentValue,PROPVARIANT *newValue)
{
if (key == UI_PKEY_Enabled)
{
if (commandId == 100)
UIInitPropertyFromBoolean(key,FALSE,newValue);
else
UIInitPropertyFromBoolean(key,TRUE,newValue);
}
if (key == UI_PKEY_RecentItems)
{
return 0;
}
if (key == UI_PKEY_ContextAvailable) {
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)
{
if (verb == UI_EXECUTIONVERB_EXECUTE)
{
if (ctype == UI_COMMANDTYPE_ACTION)
{
SendMessage(MainWindow,WM_COMMAND,commandId,0);
}
if (ctype == UI_COMMANDTYPE_RECENTITEMS)
{
if (!currentValue)
return 0;
SendMessage(MainWindow,WM_COMMAND,15000 + currentValue->intVal,0);
}
if (ctype == UI_COMMANDTYPE_FONT)
{
}
if (ctype == UI_COMMANDTYPE_COLORANCHOR)
{
return S_OK;
}
};
class MyUIApplication : public IUIApplication
{
private:
HWND hh; int refNum;
public:
MyUIApplication(HWND hP)
{
refNum = 1;
hh = hP;
}
...
virtual HRESULT __stdcall OnCreateUICommand(UINT32 commandId,
UI_COMMANDTYPE typeID,IUICommandHandler **commandHandler)
{
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;
}
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)
{
if (!u_r)
{
if (view)
view->QueryInterface(__uuidof(IUIRibbon),(void**)&u_r);
}
}
if (typeID == UI_VIEWTYPE_RIBBON && verb == UI_VIEWVERB_SIZE)
{
UINT32 cy = 0;
if (u_r)
{
u_r->GetHeight(&cy);
LastRibbonHeight = cy;
Update(); }
return S_OK;
}
return S_OK;
}
};
CoCreateInstance(CLSID_UIRibbonFramework,0,CLSCTX_ALL,
__uuidof(IUIFramework),(void**)&u_f);
u_app = new MyUIApplication(hh);
hr = u_f->Initialize(hh,u_app);
if (FAILED(hr))
{
u_f->Release();
u_f = 0;
return false;
}
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.