Thursday, March 31, 2011

Windows 7 Taskbar Extensions in Qt: Tab Thumbnails

In this post I'll explain how I implemented this Windows 7 feature using Qt with MINGW32.

For this purpose I have created a simple browser with tabs.


The main blockers implementing this using Qt with MINGW32 are the missing defintions of various important Windows 7 SDK functions, interfaces, enums, constants and structures (e.g. ITaskbarList3, ITaskbarList4, CLSID_TaskbarList, STPFLAG, DWMWINDOWATTRIBUTE, and DWM functions (DwmInvalidateIconicBitmaps, DwmSetIconicThumbnail, DwmSetIconicLivePreviewBitmap, DwmSetWindowAttribute)).

In order to fix this I had to define them mingw friendly in my project. The DWM functions are loaded during runtime from dwmapi.dll.

In the win7utils.h and win7utils.cpp you can see the final result.

Initializing

Firstly the program has to register the TaskbarButtonCreated message. This will create a taskbar button for this application. Without this it is impossible to use the new Windows 7 features.


After having the taskbar button created the application has to initialize an ITaskbarList3 or ITaskbarList4 interface in order to be able to access the new features.

In our event filter:

bool MyClass::eventFilter(void *message_, long *result)
{
    static unsigned int taskBarCreatedId = WM_NULL;

    MSG* message = static_cast(message_);

    if (taskBarCreatedId == WM_NULL) {
        taskBarCreatedId = RegisterWindowMessage(L"TaskbarButtonCreated");
        return false;
    }

    if (message->message == taskBarCreatedId &&
        message->hwnd == parent->winId()) 
        //very important to check to which window this message is address
        //since it is possible to get a dozen of them
        //the parent can be a QMainWindow or any other QWidget which acts as a 
        //tab container
    {
        //init the ITaskbarList3  interface
        //announce that the interface is ready
        return true;
    }
    //...
}



Adding tabs

The most important ITaskbarList3 functions to work with in this case are: RegisterTab, SetTabActive, SetTabOrder and UnregisterTab.

You should not register the window or widget which contains these tabs as a tab.

First thing to do is to capture all messages sent to the application. This can be achieved by substituting the current application's event filter with ours.
This will route all messages to our own function.

// MyClass.h
class MyClass {
//..
  static bool eventFilter(void *message_, long *result);
  static QCoreApplication::EventFilter m_oldEventFilter;
//..
};

//MyClass.cpp
QCoreApplication::EventFilter TabsManager::m_oldEventFilter = NULL;

MyClass::MyClass {
//..
   m_oldEventFilter = qApp->setEventFilter(&MyClass::eventFilter);
}

Before registering a widget as a tab it is necessary to set two attributes to the window handle. This can be achieved by setting the DWMWA_FORCE_ICONIC_REPRESENTATION and DWMWA_HAS_ICONIC_BITMAP to true:

void EnableWidgetIconicPreview(QWidget* widget) {
    BOOL enable = TRUE;

    DwmSetWindowAttribute(
        widget->winId(),
        DWMWA_FORCE_ICONIC_REPRESENTATION,
        &enable,
        sizeof(enable));

    DwmSetWindowAttribute(
        widget->winId(),
        DWMWA_HAS_ICONIC_BITMAP,
        &enable,
        sizeof(enable));
}

This code will make the application receive WM_DWMSENDICONICTHUMBNAIL and WM_DWMSENDICONICLIVEPREVIEWBITMAP messages when a thumbnail is requested for the registered widget.

Another important thing which is related to Qt, you SHOULD NOT REGISTER AS A TAB A WIDGET WHICH IS BEING USED IN THE MAINWINDOW OR DIALOG.
This will not work.
The solution is to create a new blank widget, register it, and map the real widget to this blank widget. The thumbnails in the taskbar will be provided by the real widgets but the messages will be addressed to the blank ones.

So each time when a WM_DWMSENDICONICTHUMBNAIL or WM_DWMSENDICONICLIVEPREVIEWBITMAP message is received, it is addressed to the blank registered widget, not the real one.

The steps:
1) Create a new QWidget*
2) Map this created QWidget to the real QWidget is being used. This is apt to you.
3) Set iconic preview enabled to the created QWidget
4) Register the created QWidget as a tab
5) Set tab order
6) Set it as the active tab

//ITaskbarList3*  m_taskbarHandler;
//QMap<WId, QWidget*> m_widgetMap;
//QWidget* m_parent;

void MyClass::addTab(QWidget* widget) {

    QWidget* tab_widget = new QWidget();

    //enable iconic preview
    EnableWidgetIconicPreview(tab_widget->winId(), true);

    //map it
    m_widgetMap[tab_widget->winId()] = widget;


    //register it
    m_taskbarHandler->RegisterTab(tab_widget->winId(), m_parent->winId());
    m_taskbarHandler->SetTabOrder(tab_widget->winId(), NULL);
    m_taskbarHandler->SetTabActive(NULL, tab_widget->winId(), 0);
}

Processing the received messages

In our own event filter we'll receive many messages, the most important ones are:

The message code provided by the RegisterWindowMessage function. When the "TaskbarButtonCreated" message was registered - When this happens our tab manager will initialize the ITaskbarList3 interface.

WM_DWMSENDICONICTHUMBNAIL - from MSDN: "instructs a window to provide a static bitmap to use as a thumbnail representation of that window."

WM_DWMSENDICONICLIVEPREVIEWBITMAP - from MSDN: "Instructs a window to provide a static bitmap to use as a live preview (also known as a Peek preview) of that window."

WM_ACTIVATE - when a thumbnail was clicked.

WM_CLOSE - when a thumbnail is about to be closed.

Providing the static bitmap

When a WM_DWMSENDICONICTHUMBNAIL message is received first thing it is checked if this is addressed to one of our registered widgets. This check can be done by comparing widget->winId() to message->hwnd. If they match we have to provide a static bitmap.
The created bitmap is set by calling the DwmSetIconicThumbnail function

Providing the live preview

Basically is almost the same as in the first case, except that the DwmSetIconicLivePreviewBitmap function is called.

An example:
case WM_DWMSENDICONICTHUMBNAIL:
     //check if this is message is addressed to one of our widgets
     if (!m_widgetMap.contains(message.hwnd)) return false;
     
     //get the real widget
     widget = m_widgetMap[message.hwnd];

     QPixmap thumbnail = QPixmap::grabWidget(widget).scaled(size, Qt::KeepAspectRatio);

     //QPixmap::Alpha in case the image has transparent regions
     HBITMAP hbitmap = thumbnail.toWinHBITMAP(QPixmap::Alpha);

     DwmSetIconicThumbnail(id, hbitmap, 0);
     DeleteObject(hbitmap);
     return true;

case WM_DWMSENDICONICLIVEPREVIEWBITMAP:
     //check if this is message is addressed to one of our widgets
     if (!m_widgetMap.contains(message.hwnd)) return false;
     
     //we want to grap the main window and show as a live preview
     widget = parent;

     QPixmap thumbnail = QPixmap::grabWidget(widget).scaled(size, Qt::KeepAspectRatio);

     HBITMAP hbitmap = thumbnail.toWinHBITMAP(QPixmap::NoAlpha);

     DwmSetIconicLivePreviewBitmap(id, hbitmap, 0);
     DeleteObject(hbitmap);
     return true;

case WM_ACTIVATE :
     if (LOWORD(message->wParam) == WA_ACTIVE) {
        //check if this is message is addressed to one of our widgets
        if (!m_widgetMap.contains(message->hwnd)) return false;
   
         //get the real widget
         widget = m_widgetMap[message->hwnd];
         
         //announce that widget was activated
         //..
     }
     //route message further
     return false;

case WM_CLOSE :
      //The same as for WM_ACTIVE except ..
      //announce that widget is about to be removed
      //..
      
      return false;


Updating the tab

When the content of the tab has changed, the thumbnail bitmap will not change by itself. That is why is needed to call DwmInvalidateIconicBitmaps. This will update the taskbar thumbnail for the specified tab.


The source code can be downloaded from here.

4 comments:

Xiluembo said...

Nice tutorial! I'm going to try to implement those features into KDE apps :) (first trying it with dolphin, then konqueror, then other apps)

Xiluembo said...

The blank widget can be restored or maximized when right-clicking the thumbnail, what can be done to map this to maximize/restore the container window instead?

Nicoale Ghimbovschi said...

Yeah, you are right. That I see as a problem. Haven't noticed that before. I'll try to investigate that. Thanks !

Xiluembo said...

To circumvent this, I've used SendMessage to redirect the message (WM_SYSCOMMAND) into the main window for maximize/restore/minimize etc... except for SC_CLOSE param.