Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

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

4.93/5 (87 votes)
6 Jan 2010CPOL22 min read 150K   2.7K  
Demonstration of seven new programming technologies.

w7dl.png

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.

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.

tbar_recent.png

tbar_toolbar.png

tbar_progress.png

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:

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.

C++
// 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):

C++
// 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):

C++
// 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

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.

libr_sample.png

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:

C++
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:

C++
IShellLibrary *picturesLibrary;  
hr = SHLoadLibraryFromKnownFolder(FOLDERID_PicturesLibrary, 
     STGM_READ, IID_PPV_ARGS(&amp;picturesLibrary));  
// picturesLibrary points to the user's picture library
 
IShellItemArray *pictureFolders;   
hr = pslLibrary-&gt;GetFolders(LFF_FORCEFILESYSTEM, 
                 IID_PPV_ARGS(&amp;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:

C++
// 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:

C++
// 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.

C++
// 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

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, and
  • WM_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.

C++
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

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.

sensor_permission.png

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

C++
// 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;
C++
// 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;
C++
// 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):

C++
// 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:

sensor_location.png

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

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:

anim_sample.png

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

C++
// 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;
   }
};
C++
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;
   }
};
C++
MyAnimationTimeEventHandler matH;
MyAnimationManagerEventHandler maEH;
C++
// Interface declarations
IUIAnimationManager* am = 0;
IUIAnimationTransitionLibrary* amtr = 0;
IUIAnimationStoryboard* amb = 0;
IUIAnimationTimer* amt = 0;
IUIAnimationVariable* amv[2] = {0,0};
C++
// 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.

C++
// 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

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:

You should call the CreateFactory functions with LoadLibrary()/GetProcAddress to ensure that your application will run in previous OS versions.

Create the Interface

C++
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:

C++
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:

C++
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.

C++
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:

C++
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 HBITMAPs 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:

C++
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:

To measure the text dimensions, you have to create the IDWriteTextLayout* (IDWriteFactory::CreateTextLayout) which represents the formatted text's attributes:

C++
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.

rib_sample.png

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:

rib_paint.png

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:

rib_contextual.png

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:

C++
// 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;
        }
    };
C++
// 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;
    }
  };
C++
// 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)