Windows 7 Goodies in C++: Taskbar Progress and Status Indicators
Windows 7 Goodies in C++: Taskbar Progress and Status Indicators
Contents
- Introduction
- Using the New Taskbar Interfaces
- Showing a progress bar
- Showing an Overlay Status Icon
- Revision History
Introduction
One of the major changes to how the Windows 7 Taskbar operates is in an area that Microsoft calls peripheral status. This covers two types of status indicators: progress for long operations, and icons for important notifications. Apps can continue to use progress dialogs during long operations, but the Windows 7 Taskbar lets the app show a progress bar in its Taskbar button as well, so the user can see the progress indicator at a glance, without having to switch to the app.
Many apps also use the notification area to convey important status information. For example, Outlook shows an icon when you have new email. However, in Windows 7, notification area icons are hidden by default, so the notification area is no longer useful for displaying this kind of status. The Taskbar lets an app display a 16x16 icon that is overlaid on the existing icon in its Taskbar button. This prevents the notification area from getting too crowded, and keeps the status icon visually associated with the app that created it.
This article's sample app is a re-write of the file downloader from my article Using Internet Explorer to download files for you. The app shows the download progress in its Taskbar button, in addition to a traditional progress bar in the dialog. This app didn't have a notification area icon before, but for the purposes of demonstrating the API calls involved, it has commands for showing a status icon as well.
The sample code for this article was built with Visual Studio 2008, WTL 8.0, and the Windows 7 RC SDK.
Using the New Taskbar Interfaces
Here is a screen shot of the revised downloader app:
As with jump lists, progress indicators are exposed through a new COM interface.
The ITaskbarList3 interface
Before our app can use peripheral status features, we must create an ITaskbarList3
interface. ITaskbarList3
is a new interface in Windows 7 that provides access to many of the new Taskbar features. There is a bit more to this than just calling CoCreateInstance()
, however. We must wait for Windows to tell us that our Taskbar button has been created. Windows does this by sending our main window a registered message called TaskbarButtonCreated
. Once we receive that message, it's safe to create an ITaskbarList3
interface and call its methods.
The main dialog class has a private data member that holds the ID of this message. This member is initialized during the app's startup sequence, and used in the message map:
class CMainDlg : public CDialogImpl<CMainDlg>
{
public:
BEGIN_MSG_MAP(CMainDlg)
MESSAGE_HANDLER_EX(m_uTaskbarBtnCreatedMsg, OnTaskbarBtnCreated)
// other message handlers...
END_MSG_MAP()
LRESULT OnTaskbarBtnCreated ( UINT uMsg, WPARAM wParam, LPARAM lParam );
private:
static const UINT m_uTaskbarBtnCreatedMsg;
};
const UINT CMainDlg::m_uTaskbarBtnCreatedMsg =
RegisterWindowMessage ( _T("TaskbarButtonCreated") );
CMainDlg
also has a member m_pTaskbarList
, which is a CComPtr<ITaskbarList3>
.The message handler for the TaskbarButtonCreated
message calls CoCreateInstance()
to initialize this interface:
LRESULT CMainDlg::OnTaskbarBtnCreated ( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
m_pTaskbarList.Release();
m_pTaskbarList.CoCreateInstance ( CLSID_TaskbarList );
return 0;
}
It's possible for the Taskbar to send this message multiple times. For instance, if Explorer crashes and restarts, it will recreate the Taskbar, and notify all running apps again that their buttons have been created. To handle the case where we get multiple TaskbarButtonCreated
messages, we release any existing interface we might have, and create a new one.
If your app will run on versions of Windows before Windows 7, be aware that anyone could register a window message called TaskbarButtonCreated
and broadcast it. To protect against this, you should ignore the message if the OS is older than Windows 7. Here's the updated message handler that checks the OS version:
LRESULT CMainDlg::OnTaskbarBtnCreated ( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
DWORD dwMajor = LOBYTE(LOWORD(GetVersion()));
DWORD dwMinor = HIBYTE(LOWORD(GetVersion()));
// Check that the OS is Win 7 or later (Win 7 is v6.1).
if ( dwMajor > 6 || ( dwMajor == 6 && dwMinor > 0 ) )
{
m_pTaskbarList.Release();
m_pTaskbarList.CoCreateInstance ( CLSID_TaskbarList );
}
return 0;
}
There's one final detail to take care of: We have to tell Windows to allow the TaskbarButtonCreated
message to be sent to our window if our app is running elevated. We do this in our WM_INITDIALOG
handler:
BOOL CMainDlg::OnInitDialog ( HWND hwndFocus, LPARAM lParam )
{
CHANGEFILTERSTRUCT cfs = { sizeof(CHANGEFILTERSTRUCT) };
ChangeWindowMessageFilterEx ( m_hWnd, m_uTaskbarBtnCreatedMsg,
MSGFLT_ALLOW, &cfs );
// other initialization steps...
return TRUE:
}
We can do this all the time, because if our app isn't running elevated, the ChangeWindowMessageFilterEx()
call is a no-op.
Showing a Progress Bar
Now that we've seen how to create an ITaskbarList3
interface, let's look at the methods for showing a progress bar. There are two methods that control the position and appearance of the progress bar:
SetProgressState()
: Controls whether the bar is visible, and what state it's in: normal, indeterminate, paused, or error.SetProgressValue()
: Sets the position of the bar.
The prototype for SetProgressState()
is:
HRESULT SetProgressState ( HWND hwnd, TBPFLAG tbpFlags );
hwnd
is the HWND
of our window. We must pass our dialog's HWND
so the Taskbar knows which button to show the progress indicator in. tbpFlags
is a TBPFLAG
value indicating the state:
TBPF_NOPROGRESS
: The progress bar is not visible.TBPF_INDETERMINATE
: The progress bar shows indeterminate progress by running in marquee mode.TBPF_NORMAL
: The normal state (green in the default theme).TBPF_PAUSED
: The paused state (yellow in the default theme).TBPF_ERROR
: The error state (red in the default theme).
The last three states are analogous to the states that were added to the progress bar control in Vista.
The other method, SetProgresValue()
, sets the position of the bar, if the bar is visible. Its prototype is:
HRESULT SetProgressValue ( HWND hwnd, ULONGLONG ullCompleted, ULONGLONG ullTotal );
As before, hwnd
is the HWND
of our dialog. ullCompleted
represents the amount of work completed so far, and ullTotal
represents the total amount of work. The Taskbar will convert ullCompleted
to a percentage, and the progress bar will display that percentage. If the bar is currently in the indeterminate state, calling SetProgressValue()
will automatically put the bar into the normal state.
How the sample project uses the progress bar
Since the dialog has a progress bar as well, that progress bar and the Taskbar progress bar are typically in the same state and showing the same information. For instance, when a download starts, we reset the progress bar and clear any existing progress indicator:
void CMainDlg::OnStart ( UINT uCode, int nID, HWND hwndCtrl )
{
// Clear the taskbar progress bar.
if ( m_pTaskbarList )
m_pTaskbarList->SetProgressState ( m_hWnd, TBPF_NOPROGRESS );
// m_cProgress is a CProgressBarCtrl attached to the progress bar.
m_cProgress.SetState ( PBST_NORMAL );
m_cProgress.SetPos(0);
}
When the download thread reports progress, the dialog updates both progress indicators accordingly:
LRESULT CMainDlg::OnDownloadProgress ( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
ULONG ulProgress = (ULONG) wParam, ulProgressMax = (ULONG) lParam;
bool bIsMarquee = (m_cProgress.GetStyle() & PBS_MARQUEE) != 0;
if ( 0 == ulProgressMax )
{
// We don't know the size of the file, so put the progress bars into
// marquee mode to indicate an indeterminate state.
if ( m_pTaskbarList )
m_pTaskbarList->SetProgressState ( m_hWnd, TBPF_INDETERMINATE );
if ( !bIsMarquee )
{
m_cProgress.ModifyStyle ( 0, PBS_MARQUEE );
m_cProgress.SetMarquee ( TRUE, 100 );
}
}
else
{
// SetProgressValue automatically cancels the TBPF_INDETERMINATE state if
// the progress bar is in that state.
if ( m_pTaskbarList )
m_pTaskbarList->SetProgressValue ( m_hWnd, ulProgress, ulProgressMax );
if ( bIsMarquee )
m_cProgress.ModifyStyle ( PBS_MARQUEE, 0 );
m_cProgress.SetPos ( MulDiv ( ulProgress, 100, ulProgressMax ) );
}
return 0;
}
Here's how the progress bar looks during a download:
The app also uses the paused state to indicate that the download timed out, and the error state to indicate that the download stopped because of an error. For example, if you cancel the download, the progress bars look like this:
You can look at the CMainDlg::OnThreadExited()
function in the sample code to see how this is done.
Showing an Overlay Status Icon
An app can add an overlay icon to its Taskbar button by calling SetOverlayIcon()
:
HRESULT SetOverlayIcon ( HWND hwnd, HICON hIcon, LPCWSTR pszDescription );
As with the progress indicator methods, hwnd
is our dialog's HWND
. hIcon
is a 16x16 icon. You can also pass NULL
to remove the overlay icon altogether. pszDescription
is textual version of the status indicator; this string will be used by accessibility applications to convey the status to users that can't see the icon. The Taskbar makes its own copy of the icon, so the caller can destroy its HICON
immediately if it wants to.
There are two things to watch out for when using overlay icons:
- If the Taskbar is set to show small icons, then overlay icons are not shown at all.
- If several windows are grouped into one Taskbar button, only one icon can be shown at a time - the last app to call
SetOverlayIcon()
wins. But if one of the windows in the group removes its overlay icon, and a previous icon has not been removed, it is shown again.
To try out overlay icons in the sample app, click the Taskbar Icon button. You'll get a menu with three commands: Green, Red, and None. Green and Red each set a different icon, and None removes the icon. Here's how the green overlay icon looks:
And here's the code that handles the Green menu item:
void CMainDlg::OnGreenIcon ( UINT uCode, int nID, HWND hwndCtrl )
{
CIcon icon;
if ( NULL != icon.LoadIcon ( IDI_GREEN, 16, 16 ) && m_pTaskbarList )
m_pTaskbarList->SetOverlayIcon ( m_hWnd, icon, L"Green overlay icon" );
}
Since the Taskbar makes a copy of the icon we pass to SetOverlayIcon()
, we don't need to keep the HICON
around. The CIcon
destructor will free the icon for us.