Thursday, January 27, 2011

How to find the position of a taskbar button on Windows 7 or Vista

In this article I'm going to show you how to find the position of your application's taskbar button on the Windows 7 taskbar. The example code is Qt/C++ but it easy to adapt it for another language.
First of all we have to find the taskbar window (its HWND). We do this by searching for a specific window class:
HWND hwndTrayWnd = ::FindWindowW(L"Shell_TrayWnd", NULL);
Then we must find a child window which contains all the buttons:

HWND hwndTaskList = 0;
::EnumChildWindows(hwndTrayWnd, EnumShellWindows, (LPARAM)&hwndTaskList);
This functions is used to enumerate child windows:
static CALLBACK BOOL EnumShellWindows(HWND hwnd, LPARAM userParam)
{
    wchar_t buffer[255];
    HWND *hwndTaskList = (HWND *)userParam;

    GetClassNameW(hwnd, buffer, sizeof buffer);
    if (std::wstring(buffer) == L"MSTaskListWClass") {
        *hwndTaskList = hwnd;
        return FALSE;
    }
    return TRUE;
}
Now things start to be more tricky. The buttons aren't actual controls (windows), they are just pictures drawn on the taskbar. It means that we cannot enumerate and compare them. We have to capture the screen and analyze it. Thanks to Qt taking a capture of any windows is a one liner:
QImage taskListImage = QPixmap::grabWindow(hwndTaskList).toImage();
Finding a button for an active window is easy, because an active button has a light border around it with a few white pixels just below the top-left corner:

It means that if our windows is the active one we just have to find a button with the white pixels.
static int findActiveButtonX(const QImage& tasklistImage)
{
    const QRgb white = qRgb(255, 255, 255);
    for (int x = SPACING; x < tasklistImage.width(); x += (BUTTON_WIDTH + SPACING)) {
        if (tasklistImage.pixel(x, ROUND_CORNER_HEIGHT) == white) {
            return x;
        }
    }
    return -1;
}
If we are looking for an inactive button then there is no other way but to compare the icons.
static int findInactiveButtonX(const QIcon& appIcon, const QImage& tasklistImage)
{
    QImage iconImage = appIcon.pixmap(32, 32).toImage();

    for (int x = SPACING; x < tasklistImage.width(); x += (BUTTON_WIDTH + SPACING)) {
        QImage otherIcon = tasklistImage.copy(x + ICON_LEFT_MARGIN, ICON_TOP_MARGIN, 32, 32);
        if (compareImages(iconImage, otherIcon))
            return x;
    }
    return -1;
}

static bool compareImages(const QImage& a, const QImage& b)
{
    for (int x = 0; x < a.width(); ++x) {
        for (int y = 0; y < a.height(); ++y) {
            QRgb rgb = a.pixel(x, y);
            // Test only opaque pixels
            if (qAlpha(rgb) == 255) {
                if (rgb != b.pixel(x, y))
                    return false;
            }
        }
    }
    return true;
}
When we have the x position of our button within the taskbar list window, then the last step is to convert this value to global (screen) coordinate:
::RECT rect;
::GetWindowRect(hwndTaskList, &rect);
QRect buttonRect(rect.left + buttonX, rect.top + 1, BUTTON_WIDTH, BUTTON_WIDTH);
And that's it. Now we can for example display a notification directly above the taskbar button making it look more logical to the user.

1 comment:

  1. Better is to use the Microsoft MSAA API. Use AccChecker_v2.0_x86 ->AccCheckUI.exe to display this object tree. This object tree contains the taskbar buttons, their position and their title.

    ReplyDelete