Одним словом, назревает необходимость в создании универсальной библиотеки для работы со всем этим многообразием.
Встречаем qtdocktile
Общее для всех доков
Прежде всего необходимо выделить список возможностей, которые являются общими для всех доков:
- Бейджи
- Индикатор прогресса
- Меню
- Сигнализация
Весь этот функционал является базовым и так или иначе поддерживается и в семёрке, и в макоси, и в убунте. Именно на основе него будет строится базовое API qtdocktile, а все платформозависимые расширения будут добавляться по мере развития библиотеки и не будут являться обязательными.
Архитектура библиотеки
Для максимальной гибкости и расширяемости я решил, что реализации каждого конкретного дока будут представлять из себя обычные Qt плагины — это позволяет добавлять поддержку новых API без перекомпиляции всей библиотеки, а в случае невозможности использовать ту или иную реализацию, плагин просто не запустится. Плагины загружает специальный синглтон-менеджер. Каждый плагин сообщает менеджеру, может ли он работать в данном окружении или нет, в результате чего менеджер может вызывать нужные методы лишь у тех плагинов, которые являются работоспособными в данной среде.
Пользователь же работает с простым классом QtDockTile, который является оберткой над менеджером. В результате чего можно безопасно создавать любое количество экземпляров QtDockTile — они не нарушат работу дока.
Для меню дока будет использоваться обычное Qtшное QMenu. Нужно лишь помнить об ограничениях, которые выставляет та или иная платформа.
Примерное использование библиотеки
m_tile->setMenu(ui->menu); connect(ui->pushButton, SIGNAL(clicked()), m_tile, SLOT(alert())); connect(ui->lineEdit, SIGNAL(textChanged(QString)), m_tile, SLOT(setBadge(QString))); connect(ui->horizontalSlider, SIGNAL(valueChanged(int)), m_tile, SLOT(setProgress(int)));
Как можно заметить, оно очень простое! Но написать простое API это ещё полбеды, теперь нужно реализовать поддержку всех платформ:
Внимание! Дальше будет много технических подробностей, если вам они не интересны, то сразу можете переходить к чтению заключения.
Реализация плагина для Unity
Как ни странно, но для Unity получилась наиболее короткая и лаконичная реализация. Всё апи строится на отправки достаточно простых dbus сообщений:
void UnityLauncher::sendMessage(const QVariantMap &map) { QDBusMessage message = QDBusMessage::createSignal(appUri(), "com.canonical.Unity.LauncherEntry", "Update"); QVariantList args; args << appDesktopUri() << map; message.setArguments(args); if (!QDBusConnection::sessionBus().send(message)) qWarning("Unable to send message"); }
Где appUri — это уникальное название приложения, в данной реализации просто совпадающее с именем процесса, а appDesktopUri — это запись вида application://$appUri.desktop.
Для того, чтобы изменить значение на бейдже достаточно отправить такое сообщение:
QVariantMap map; map.insert(QLatin1String("count"), count); map.insert(QLatin1String("count-visible"), count > 0); sendMessage(map);
Аналогично для индикатора прогресса и сигнализации, с меню чуть поинтереснее: необходимо воспользоваться классом DBusMenuExporter при создании передав ему appUri и указатель на QMenu. Вот и всё API, теперь давайте перечислим ограничения:
Ограничения Unity Launcher API
- Бейджи только цифровые и только больше 0. В противном случае выводится 0
- Экспортированное меню не показывает подменю, поэтому лучше их избегать.
- Если меню экспортируется еще и в appmenu, то оно не появится в доке
- В реализации DBusMenuExporter есть баг в результате чего состояние checked у меню инвертируется
Ну и последнее: для работы API обязательно необходимо наличие в /usr/share/applications .desktop значка для приложения. Кстати, Unity API позволяет добавлять в меню постоянные пункты, которые работают когда приложение не запущено, выглядит это примерно так:
X-Ayatana-Desktop-Shortcuts=NewWindow; [NewWindow Shortcut Group] Name=Open a New Window Name[ast]=Abrir una ventana nueva Name[bn]=Abrir una ventana nueva Name[ca]=Obre una finestra nova Name[da]=Åbn et nyt vindue Name[de]=Ein neues Fenster öffnen Name[es]=Abrir una ventana nueva Name[fi]=Avaa uusi ikkuna Name[fr]=Ouvrir une nouvelle fenêtre Name[gl]=Abrir unha nova xanela Name[he]=פתיחת חלון חדש Name[hr]=Otvori novi prozor Name[hu]=Új ablak nyitása Name[it]=Apri una nuova finestra Name[ja]=新しいウィンドウを開く Name[ku]=Paceyeke nû veke Name[lt]=Atverti naują langą Name[nl]=Nieuw venster openen Name[ro]=Deschide o fereastră nouă Name[ru]=Открыть новое окно Name[sv]=Öppna ett nytt fönster Name[ug]=يېڭى كۆزنەك ئېچىش Name[uk]=Відкрити нове вікно Name[zh_CN]=新建窗口 Name[zh_TW]=開啟新視窗 Exec=firefox -new-window TargetEnvironment=Unity
И на закуску пара скриншотов:
Unity:

KDE (Icon Tasks):

При написании плагина я пользовался наработками Torkve для qutIM'а.
Реализация плагина для Macos X
Тоже не возникло особенных сложностей, для экспорта меню в Qt уже есть специальный метод, о нём уже много было сказано здесь.
Бейдж же элементарно средствами Cocoa устанавливается, нужно лишь QString преобразовать в NSString, послать сообщение доку и позаботится об очисте памяти.
const char *utf8String = badge.toUtf8().constData(); NSString *cocoaString = [[NSString alloc] initWithUTF8String:utf8String]; [[NSApp dockTile] setBadgeLabel:cocoaString]; [cocoaString release];
Чуть сложнее оказалось сделать индикатор прогресса: API дока не имеет встроенного метода, зато имеется метод для рисования своего изображения в иконке дока. Чтобы сильно не заморачиваться я просто взял реализацию индикатора из QtCreator'а, благо лицензия LGPL спокойно позволяет такой финт ушами.
Скриншот

Реализация для Windows 7 Taskbar'а
И напоследок самое вкусное! Если в других системах процесс написания плагинов прошёл более-менее гладко, то для самой популярной настольной операционки все оказалось далеко не таким безоблачным, пришлось про себя поминать разными нехорошими словами Билл Гейтса, Стива Балмера и безымянных программистов, которые бережно разложили различные грабли! По ходу написания в голове не раз возникали фразы must die, wtf и тому подобное вплоть до старого доброго windos.
Тут есть и странные нечитаемые типы типа LPCSTR вместо wchar_t * и венгерская нотация во все поля и великий и ужасный COM, одним словом, стиль кода просто ужасен. А ещё есть здесь проблема в ABI в результате которой невозможно прилинковать С++ библиотеку, собранную MS компилятором к коду, собираемому minGW. Ну и само API несколько странное из за чего пришлось идти на некоторые костыли. Плюс ко всему примеры jump lists'ов содержат использование библиотеки ATL, которая есть лишь в платной студии и нам ну совершенно не подходит по этой причине.
Для решения проблем с ABI мы с dtf решили сделать минимальную Си обёртку над COM API таскбара чтобы в будущем была возможность линковаться с ней динамически из любого компилятора.
API у нее получилось весьма простым, сама обертка не зависит от Qt и её можно использовать из чего угодно, хотя она написана и совершенно не в стиле winAPI.
... EXPORT void setApplicationId(const wchar_t *appId); EXPORT void setOverlayIcon(HWND winId, HICON icon, wchar_t *description = 0); EXPORT void clearOverlayIcon(HWND winId); EXPORT void setProgressValue(HWND winId, int percents); EXPORT void setProgressState(HWND winId, ProgressState state); ...
Начал я с самого простого: решил сделать индикатор прогресса, код для него уже был написан товарищем dtf, поэтому с переносом особенных сложностей не возникло.
//получаем указатель на таскбар static ITaskbarList3 *windowsTaskBar() { ITaskbarList3 *taskbar; if(S_OK != CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, (void**)&taskbar)) return 0; return taskbar; } ... // устанавливаем значение void setProgressValue(HWND winId, int progress) { ITaskbarList3 *taskbar = windowsTaskBar(); if (!taskbar) return; taskbar->HrInit(); taskbar->SetProgressValue(winId, progress, 100); taskbar->SetProgressState(winId, progress ? TBPF_NORMAL : TBPF_NOPROGRESS); taskbar->Release(); } // устанавливаем тип индикации void setProgressState(HWND winId, ProgressState state) { TBPFLAG flags; ITaskbarList3 *taskbar = windowsTaskBar(); if (!taskbar) return; taskbar->HrInit(); switch (state) { default: case ProgressStateNone : flags = TBPF_NOPROGRESS; break; case ProgressStateNormal : flags = TBPF_NORMAL; break; case ProgressStatePaused : flags = TBPF_PAUSED; break; case ProgressStateError : flags = TBPF_ERROR; break; case ProgressStateIndeterminate : flags = TBPF_INDETERMINATE; break; } taskbar->SetProgressState(winId, flags); taskbar->Release(); }
Бейдж я реализовал через метод setOverlayIcon, а саму иконку я рисовал и превращал в HICON средствами Qt
QPixmap WindowsTaskBar::createBadge(const QString &badge) const { QPixmap pixmap(overlayIconSize()); QRect rect = pixmap.rect(); rect.adjust(1, 1, -1, -1); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); QPalette palette = window()->palette(); painter.setBrush(palette.toolTipBase()); QPen pen = painter.pen(); pen.setColor(palette.color(QPalette::ToolTipText)); painter.setPen(pen); QString label = QFontMetrics(painter.font()).elidedText(badge, Qt::ElideMiddle, rect.width()); painter.drawRoundedRect(rect, 5, 5); painter.drawText(rect, Qt::AlignCenter | Qt::TextSingleLine, label); return pixmap; }
В итоге в бейдж пока влазит лишь 2 символа. Размер иконки выставляется через QStyle::pixelMetrics, мной было выяснено, что другие реализации overlayIcon просто рисуют иконку 16х16 и не заботятся о dpi, поэтому у меня на мониторе иконка получается размазанной.
А теперь самое интересное — реализация jump lists'ов. Вот уж где старина Билли услышал заочно много ласковых слов в свой адрес!
Мытарство номер 1 — сериализация QAction'а с учетом ограничений API
У каждого действия есть название, команда, которая исполняется при нажатии на действие и опционально путь до иконки в формате ico и описание. Причем всё это нужно передавать в виде сишных wide char строк, а значит и самостоятельно следить за временем их жизни. Ну и конечно нужно как-то организовать обратный вызов, что тоже не очевидно, ибо нужно вызывать метод trigger у QAction'а, что тоже не выглядит простым на первый взгляд.
В нашу сишную обёртку мы будем передавать массив структур такого содержания:
struct ActionInfo { const char *id; wchar_t *name; wchar_t *description; wchar_t *iconPath; ActionType type; void *data; //вот тут главная хитрость - через этот указатель мы будем реализовывать обратный вызов и заодно следить за временем жизни всех наших строк. }; typedef void (*ActionInvoker)(void*); //указатель на функцию, аргументом её является тот самый void *data
Теперь давайте раскроем тайну void *data:
typedef QVector<ActionInfo> ActionInfoList; //будем использовать возможности С++, чтобы избежать ручного слежения за временем жизни нашего массива typedef QVector<wchar_t> WCharArray; //аналогично для wchar_t * static WCharArray toWCharArray(const QString &str) { WCharArray array(str.length() + 1); str.toWCharArray(array.data()); return array; } struct Data { Data(QAction *action) : action(action), icon(action->icon()), id(QUuid::createUuid().toByteArray()), name(toWCharArray(action->text())), description(toWCharArray(action->toolTip())), iconPath(toWCharArray(icon.filePath())) { } QWeakPointer<QAction> action; TemporaryIcon icon; QByteArray id; WCharArray name; WCharArray description; WCharArray iconPath; }; void invokeQAction(void *pointer) { Data *data = reinterpret_cast<Data*>(pointer); if (data->action) { qDebug() << data->action.data(); data->action.data()->trigger(); } }
Вот такая вот сериализация действий получилась. Я постарался свести количество ручных new и delete к минимуму — всё происходит автоматически. Именно такой подход является залогом того, что ваши волосы будут гладкими и шелковистыми!
Теперь давайте вспомним об ограничениях платформы и поймем, какие действия мы можем сериализовывать, а какие лучше проигнорировать. Итак в jump lists'ах нет подменю, нет здесь и disabled и checkable пунктов, в общее число пунктов ограничено 20ью. Зато есть разделители, получается что-то вот такое:
if (!action->menu() && action->isVisible() && action->isEnabled() && !action->isCheckable()) list.append(serialize(action)); ... ActionInfo JumpListsMenuExporterPrivate::serialize(QAction *action) { Data *data = new Data(action); ActionType type = action->isSeparator() ? ActionTypeSeparator : ActionTypeNormal; ActionInfo info = { data->id.constData(), data->name.data(), data->description.data(), data->iconPath.data(), type, data }; return info; }
Для того, чтобы отображались иконки, пришлось создать свою реализацию временных файлов, QTemporaryFile не очень нам подходит, ибо монопольно влалеет файлом. Её отдельно я рассматривать не буду: там всё очень просто и понятно.
Мытарство номер 2 — заполнение jumpLists'ов
Чтобы заполнить jump lists'ы нужно вызвать метод beginList
void JumpListsManager::beginList() { if (m_destList) return; ICustomDestinationList *list; HRESULT res = CoCreateInstance(CLSID_DestinationList, 0, CLSCTX_INPROC_SERVER, IID_ICustomDestinationList, (void**)&list); if (FAILED(res)) { return; } UINT maxSlots; m_destList = list; m_destList->SetAppID(m_appId); m_destList->BeginList(&maxSlots, IID_IObjectArray, (void**)&m_destListContent); m_destListContent->Release(); IObjectArray *objArray; CoCreateInstance(CLSID_EnumerableObjectCollection, 0, CLSCTX_INPROC_SERVER, IID_IObjectArray, (void**)&objArray); objArray->QueryInterface(IID_IObjectCollection, (void**)&m_destListContent); objArray->Release(); }
Потом этот список заполнить
void JumpListsManager::addTask(ActionInfo *info) { if (!m_destList) return; IShellLinkW *task; HRESULT res = CoCreateInstance(CLSID_ShellLink, 0, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&task); if (FAILED(res)) return; task->SetDescription(info->description); task->SetPath(L"rundll32.exe"); task->SetArguments(makeArgs(info).c_str()); if (info->iconPath) task->SetIconLocation(info->iconPath, 0); IPropertyStore *title; PROPVARIANT titlepv; res = task->QueryInterface(IID_IPropertyStore, (void**)&title); if (FAILED(res)) { task->Release(); return; } InitPropVariantFromString(info->name, &titlepv); title->SetValue(PKEY_Title, titlepv); title->Commit(); PropVariantClear(&titlepv); res = m_destListContent->AddObject(task); title->Release(); task->Release(); m_actionInfoMap.insert(std::make_pair(info->id, info)); //обратите внимание на этот словарик: в нем хранится соответствие между id и указателем на действие. } ... void JumpListsManager::addSeparator() { IShellLinkW *separator; IPropertyStore *propStore; PROPVARIANT pv; HRESULT res = CoCreateInstance(CLSID_ShellLink, 0, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&separator); if (FAILED(res)) return; res = separator->QueryInterface(IID_IPropertyStore, (void**)&propStore); if (FAILED(res)) { separator->Release(); return; } InitPropVariantFromBoolean(TRUE, &pv); propStore->SetValue(PKEY_AppUserModel_IsDestListSeparator, pv); PropVariantClear(&pv); propStore->Commit(); propStore->Release(); res = m_destListContent->AddObject(separator); separator->Release(); }
И вызвать метод commitList
void JumpListsManager::commitList() { if (!m_destList) return; m_destList->AddUserTasks(m_destListContent); m_destList->CommitList(); m_destList->Release(); m_destListContent->Release(); m_destList = 0; m_destListContent = 0; }
Многословно, не находите? Но увы, придется стиснуть зубы и продолжить строчить сотни строк кода, иначе работать ничего не будет, но мы же настоящие мужики и трудностей не боимся? А раз не боимся, то давайте реализуем обратный вызов!
Мытарство номер 3 — Реализация обратного вызова
Итак, что мы имеем? Активация пункта в jumpList'е вызывает команду с некоторым набором аргументов. Но как же нам через неё сказать, что мы хотим найти actionInfo с определенным id и совершить обратный вызов?
Мы с dtf долго думали над этим и он предложил сделать всё через rundll, который способен вызвать определённый метод из библиотеки с заданными аргументами.
В результате родился метод, который принимает id действия, открывает сокет на 42042 порту и передает в него полученное id, а библиотека слушает этот сокет и получив id спокойно делает обратный вызов и наш искомый QAction вызывается!
std::wstring JumpListsManager::makeArgs(ActionInfo *info) { std::wstring args = m_wrapperPath; #ifdef _WIN64 args += L",_RundllCallback@28 "; // WARNING: TEST ME! // ptr×3 + int #else args += L",_RundllCallback@16 "; #endif // Convert to a wchar_t* size_t origsize = strlen(info->id) + 1; const size_t newsize = 64; size_t convertedChars = 0; wchar_t buffer[newsize]; mbstowcs_s(&convertedChars, buffer, origsize, info->id, _TRUNCATE); args += buffer; return args; }
И последний метод: реализация функции, которую вызывает rundll
EXPORT void CALLBACK RundllCallback(HWND hwnd, HINSTANCE hinst, LPSTR cmdLine, int cmdShow); void CALLBACK RundllCallback(HWND, HINSTANCE, LPSTR cmdLine, int) { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); SOCKET sk; sk = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sk == INVALID_SOCKET) { WSACleanup(); return; } sockaddr_in sai; sai.sin_family = AF_INET; sai.sin_addr.s_addr = inet_addr("127.0.0.1"); sai.sin_port = htons(Handler::port); if (connect(sk, reinterpret_cast<SOCKADDR*>(&sai), sizeof(sai)) == SOCKET_ERROR) { WSACleanup(); return; } std::string cmd = cmdLine; send(sk, cmd.c_str(), cmd.size(), 0); closesocket(sk); WSACleanup(); }
Всё, кода на сегодня хватит, можно вздохнуть спокойно, давайте подведем итоги:
Ограничения в windows реализации
- Только два знака в бейдже
- В результате наших манипуляций пропадают последние файлы из jump lists'ов
- Не поддерживаются действия — переключатели и неактивные действия
- Не поддерживаются подменю
Скриншот:

Заключение
Библиотека получилась очень простой в использовании и простой в расширении. Пока она покрывает лишь базовые возможности, которые есть на всех платформах. В дальнейшем мы будем думать как добавить платформозависимые расширения.
Чтобы меню гарантированно экспортировалось без проблем в док оно должно следующим пунктам удовлетворять:
- Не иметь подменю
- Не иметь переключаемых или отключенных пунктов
- Пунктов должно быть немного
- Не должно изменяться после того, как было установлено через setMenu
И ещё пара замечаний:
- Корректная работа дока возможна лишь в случае single application, используйте док совместно с Qt Single Application или другими подобными средствами
- В бейджах лучше использовать положительные числа меньше 100
В остальных случаях что-то будет доступно не во всех платформах. В принципе, это не смертельно, но нужно помнить об этом!
Спасибо Torkve за помощь в реализации Unity плагина, dtf за огромную помощь в реализации Windows плагина и разработчиков QtCreator'а за помощь в реализации Macos X версии.
Исходный код можно получить на github'е. Исправления и улучшения приветствуются.
ЗЫ
Есть ли желающие реализовать Dockmanager API?

