10 августа 2010 г.

Не будьте беспомощны: скрытие/показ кнопок окон в панели задач

Этот пост - добавление к моему посту 90% кода в интернете - говно.

На примере вопроса "Как скрыть окно из Панели задач" я покажу, как вы можете использовать свои голову и руки, чтобы успешно (и самостоятельно) решить эту проблему. Да это бредовый пост, но меня уже просто взбесило, что никто и не чешется что-то делать, когда им тыкаешь в подробное описание.

Когда начинающий задаёт такой вопрос на форуме, он обычно получает три вида ответов:
  • (60%) RTFM (aka "читать факи").
  • (39%) Код. Причём в 90% случаев код взят по принципу "бери этот код, он у меня работает, я гарантирую это!".
  • (1%) Ссылку на MSDN или аналогичное описание.
Я не буду разбирать подход, основанный на чтении FAQ или задании вопроса на форумах, ибо, как мы узнали в предыдущий раз, такой подход ведёт к говно-коду (в частности - решений из 90-х). Положим, мы хотим самостоятельно найти решение какой-то простой проблемы, в данном случае - показ/скрытие кнопки на Панели задач. Как мы будем действовать?

Шаг 1 - найти официальную документацию

Для этого мы используем поисковик (я предпочитаю Google, чего и вам советую). Формулируем ключевые слова для своей проблемы. К примеру, в нашем случае это будет "taskbar buttons" (не знаете английский?) или что-то такое. Далее, вы делаете поиск документации. В некоторых случаях вы можете сузить область поиска, указав сайт, на котором лежит документация. Если вы ищете что-то о Delphi, то ограничивайте поиск сайтом docwiki.embarcadero.com, если по продуктам Microsoft - то msdn.microsoft.com, если ещё что-то - то используйте соответствующие поправки.

Итак, поиск.

Все ссылки вида social.msdn.microsoft.com - это типа форумов. Отметаем сразу.

Итак, что мы получили? Ссылка 1 - это guideline-ы для Панели задач. Тоже весьма полезное чтиво, но сейчас интересует не это. Пропускаем.

Ссылка 2 - рассказ о Панели задач в рамках раздела "Расширение Панели задач". Во, похоже на то, что нам надо. Пробегая раздел глазами, встречаем подразделы "Managing Taskbar Buttons" и "Modifying the Contents of the Taskbar". Оно? Оно.

Шаг 2 - чтение и анализ документации

Для удобства, я приведу цитату из документации:
Managing Taskbar Buttons

The Shell creates a button on the taskbar whenever an application creates a window that isn't owned. To ensure that the window button is placed on the taskbar, create an unowned window with the WS_EX_APPWINDOW extended style. To prevent the window button from being placed on the taskbar, create the unowned window with the WS_EX_TOOLWINDOW extended style. As an alternative, you can create a hidden window and make this hidden window the owner of your visible window.

The Shell will remove a window's button from the taskbar only if the window's style supports visible taskbar buttons. If you want to dynamically change a window's style to one that doesn't support visible taskbar buttons, you must hide the window first (by calling ShowWindow with SW_HIDE), change the window style, and then show the window.

The window button typically contains the application icon and title. However, if the application does not contain a system menu, the window button is created without the icon.

If you want your application to get the user's attention when the window is not active, use the FlashWindow function to let the user know that a message is waiting. This function flashes the window button. Once the user clicks the window button to activate the window, your application can display the message.

Modifying the Contents of the Taskbar

Version 4.71 and later of Shell32.dll adds the capability to modify the contents of the taskbar. From an application, you can now add, remove, and activate taskbar buttons. Activating the item does not activate the window; it shows the item as pressed on the taskbar.

The taskbar modification capabilities are implemented in a Component Object Model (COM) object (CLSID_TaskbarList) that exposes the ITaskbarList interface (IID_ITaskbarList). You must call the ITaskbarList::HrInit method to initialize the object. You can then use the methods of the ITaskbarList interface to modify the contents of the taskbar.
Откуда напрямую следуют:
  1. Правила, согласно которым окна появляются на Панели задач.
  2. Официальный интерфейс по управлению кнопками в Панели задач.
Отсюда сразу видно, что у нас есть два способа осуществить, скажем, скрытие окна с Панели задач:
  1. Нарушить условия, при которых окно показывается на Панели задач.
  2. Вручную указать Панели задач скрывать наше окно.

Шаг 3 - написание или поиск заголовочников

Окей, если вы взяли первый подход, то все заголовочники у нас уже есть - это модуль Windows. Если вы взяли второй подход, то... то вам нужно их ещё найти. В частности, нам нужно определение интерфейса ITaskbarList.

Где искать заголовочники:
  1. В Delphi. Да, иногда бывает. Запускайте поиск "ITaskbarList" в "*.pas" файлах в папке Delphi.
  2. В JWAPI. Аналогично, но поиск по папке JWAPI.
  3. В интернете. Надо понимать, что качество этих заголовочников может быть... не очень.
  4. В Platform SDK. Окей, владельцам новых Delphi повезло - в их состав входят более-менее свежие заголовочники. В этом случае делаем поиск "ITaskbarList" в "*.h" файлах в папке Delphi. Всем остальным? Качать и ставить Platform SDK или MSDN.
Понятно, что в первых трёх случаях у вас на руках будут Delphi файлы. В последнем - только C-шные исходники. Поэтому...

Шаг 3а - конвертация заголовочников

В файле ShObjIdl.h вы найдёте такие строки:
/* interface ITaskbarList */
/* [object][uuid] */ 


EXTERN_C const IID IID_ITaskbarList;

#if defined(__cplusplus) && !defined(CINTERFACE)
    
    MIDL_INTERFACE("56FDF342-FD6D-11d0-958A-006097C9A090")
    ITaskbarList : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE HrInit( void) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE AddTab( 
            /* [in] */ __RPC__in HWND hwnd) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE DeleteTab( 
            /* [in] */ __RPC__in HWND hwnd) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE ActivateTab( 
            /* [in] */ __RPC__in HWND hwnd) = 0;
        
        virtual HRESULT STDMETHODCALLTYPE SetActiveAlt( 
            /* [in] */ __RPC__in HWND hwnd) = 0;
        
    };
    
#else  /* C style interface */
  ...

#ifdef __cplusplus

class DECLSPEC_UUID("56FDF344-FD6D-11d0-958A-006097C9A090")
TaskbarList;
#endif
Переводя это на Delphi, получаем (не знаете C?):
const
  CLSID_TaskbarList: TGUID = '{56FDF344-FD6D-11d0-958A-006097C9A090}';

type
  ITaskbarList = interface
  ['{56FDF342-FD6D-11d0-958A-006097C9A090}']
     procedure HrInit; safecall;
     procedure AddTab(hwnd: Cardinal); safecall;
     procedure DeleteTab(hwnd: Cardinal); safecall;
     procedure ActivateTab(hwnd: Cardinal); safecall;
     procedure SetActiveAlt(hwnd: Cardinal); safecall;
  end;
Обратите внимание на safecall. Я уже много раз про него рассказывал.

Шаг 4 - вызов кода

Итак, если вы решили пойти способом модификации условий, то вы можете:
  • Сбросить WS_EX_APPWINDOW
  • Установить WS_EX_TOOLWINDOW
  • Установить окну владельца в терминах системы (можно невидимого)
Все пункты обычно выполняются перегрузкой CreateParams. Иногда - вызовом Get/SetWindowLong(Ptr). Если вы посмотрите на FAQ или на форумы, то увидите, как этот принцип реализуется в коде.

Если вы пошли способом ручного управления, то вы создаёте ITaskbarList, инициализируете его (вызовом HrInit) и вызываете метод DeleteTab для своего окна. Достаточно прозрачно.

Шаг 5 - адаптация к Delphi

Ну и нужно учесть специфику Delphi.

Пункт 1 - это запутанное управление окнами в старых Delphi, где кнопка окна на Панели задач на самом деле не была кнопкой от окна! Это была кнопка от невидимого окна Application. Это же кривое поведение может быть доступно и в новых Delphi с MainFormOnTaskbar равным False. Иными словами, вам нужно знать, какой режим активен в вашем приложении, и чем Application.Handle отличается от Form1.Handle.

Пункт 2 - ну, это опционально, но вы можете использовать CreateComObject вместо ручного вызова CoCreateInstance:
uses
  ComObj;

...

begin
  FTaskbar := CreateComObject(CLSID_TaskbarList) as ITaskbarList;
  FTaskbar.HrInit;
end;

Пункт 3 - Delphi часто автоматически выполняет за вас вызов CoInitialize(Ex), но когда она это не делает, многие просто не знают, что это нужно делать.

Шаг 6 - готовое решение

Например (вариант для MainFormOnTaskbar = True):
type
  TForm1 = class(TForm)
  protected
    procedure CreateWnd; override;
  end;

...

procedure TForm1.CreateWnd;
var
  Taskbar: ITaskbarList;
begin
  inherited;

  Taskbar := CreateComObject(CLSID_TaskbarList) as ITaskbarList;
  Taskbar.HrInit;
  Taskbar.DeleteTab(Handle);
end;
Вы можете использовать и другой подход. Я не хочу и не буду расписывать все варианты решения - просто используйте свою голову.

Заключение

Как видите, это простая задачка на 15 минут максимум. Однако, чтобы сделать это, мне понадобилось:
  • Хорошее знание Delphi:
    • Я знал про CreateParams (если вы выбрали путь со сменой условий).
    • Я знал про пересоздание окон, поэтому повесил код с ITaskbarList на CreateWnd, а не на (более простой, но неверный вариант) FormCreate.
    • Я знал про CreateComObject (хотя моё знание COM очень поверхностно, поэтому я могу где-то наврать).
    • Я знал про Application.Handle <> Form1.Handle и MainFormOnTaskbar.
    • Я знал про safecall в Delphi и обработку ошибок в COM.
  • Знание C, чтобы переводить заголовочники.
  • Знание WinAPI (Get/SetWindowLong, если вы взяли этот путь).
  • Знание работы с битовой логикой (смена битовых флагов в CreateParams или SetWindowLong).
  • Знание COM, чтобы успешно работать с интерфейсами и делать их перевод.
  • Знание английского, чтобы читать документацию и выполнять поиск.
  • Знание мест, где можно получить информацию (сайт MSDN, Wiki Embarcadero).
  • Знание доступных решений (в нашем случае - JWAPI).
Это хорошая задачка, которая показывает как могут действовать ваши скиллы вместе. По сути, этот пост стал также хорошей иллюстрацией к предыдущему посту. Иллюстрацией в том плане, что он показывает, что вам недостаточно прочитать книжку по Delphi, чтобы стать программистом.

Кажется сложным? Ну, возьмите тогда компонент ;)

Примечание: цель этого поста - НЕ дать готовый код "бери это, он 100% рабочий, я гарантирую это". Цель поста - показать как вы можете использовать свою голову, когда скопированный код не работает. Смысл в том, чтобы попытаться понять, как работает код и что он делает. А потом увидеть, что идёт не так. Может быть, вы применяете код не к тому окну (помним Application.Handle против MainForm.Handle?), может быть, вы вызываете код в неверное время - и так далее.

23 комментария :

  1. Обратите внимание, что при таком подходе, поскольку вы стартуете от документации, то этим вы автоматически отметаете проблемы вроде использования недокументированных возможностей.

    ОтветитьУдалить
  2. И ещё: часто задают вопросы вида "как сделать прогресс на кнопке в Панели задач в Vista/Win7" или "как задать окно предпросмотра окна в Vista/Win7". Как нетрудно догадаться, описание этого функционала находится в двух шагах от нашего случая.

    Я рекомендую вам разобраться с этим вопросом по аналогии, как самостоятельное упражнение.

    ОтветитьУдалить
  3. Вообще, если говорить строго, то первый вариант решения со стилями - не решение проблемы "как мне убрать ЭТО окно". Потому что этот вариант не убирает ЭТО окно - он меняет окно на другое (которое не будет показываться) изменением стиля.

    ОтветитьУдалить
  4. Не-а... Думаю предлагается просто добавить/изменить флаги...
    Окно же не станет от этого "другим"...

    "как сделать прогресс на кнопке в Панели задач в Vista/Win7" - для XP? о__О
    Хм... Хорошая идея.....)))

    ОтветитьУдалить
  5. Пробовал способ в шаг№6 - на панели задач программа остается видимой.

    Если написать Form1.visible:=false - работает. Но некоторые антивирусы распознают это уже , как вирус. В частности AVAST.

    А вот рабочий код

    procedure TForm1.Button1Click(Sender: TObject);
    begin
    // nStyle - глобальная переменная , в которой хранится старое значение стиля окна
    nStyle:=GetWindowLong(Application.Handle, GWL_EXSTYLE or WS_EX_TOOLWINDOW);
    ShowWindow(Application.Handle, SW_HIDE);

    // устанавливаем новый стиль
    SetWindowLong(Application.Handle, GWL_EXSTYLE,
    GetWindowLong(Application.Handle, GWL_EXSTYLE) or WS_EX_TOOLWINDOW);
    ShowWindow(Application.Handle, SW_SHOW);
    end;

    procedure TForm1.Button2Click(Sender: TObject);
    begin
    ShowWindow(Application.Handle, SW_HIDE);
    // возвращаем окну старый стиль и показываем кнопку на панели задач.
    SetWindowLong(Application.Handle, GWL_EXSTYLE, nStyle);
    ShowWindow(Application.Handle, SW_SHOW);
    end;

    ОтветитьУдалить
  6. Специально же написал: "вариант для MainFormOnTaskbar = True".

    ОтветитьУдалить
  7. (спрашивается: для кого написано примечание?)

    ОтветитьУдалить
  8. "95% людей - идиоты."

    ОтветитьУдалить
  9. На днях как раз искал на это ответ. Присвоение окну стиля ToolWindow и прикрепление окна к родительской невидимой форме, хоть эти способы и советуются в MSDN, показались сложными. Путем поочередного перебора функций Windows, возвращающих HWnd, нашел GetWindow с параметром GW_OWNER, чтоб получить Application.Handle нужного окна. Окно брал то, что лежит на переднем плане.

    Код:

    var
    Wnd, WndApp: THandle;
    ...
    Wnd := Windows.GetForegroundWindow;
    WndApp := Windows.GetWindow(Wnd, GW_OWNER);
    ShowWindow(Wnd, SW_HIDE);
    ShowWindow(WndApp, SW_HIDE);

    Я не претендую на оригинальность, но, елки-палки, так же намного проще. А на форумах я, хоть убей, не нашел этого нигде. Наверное, плохо искал.

    ОтветитьУдалить
  10. >> Обратите внимание на safecall. Я уже много раз про него рассказывал.

    Поясните почему? Вообще во всех примерах с ITaskbarList для Делфи (в том числе JEDI) пишут через stdcall, вот так:

    > function HrInit: HResult; stdcall;

    З.Ы. На Делфи7 уже сутки мучаюсь заставить заработать SetProgressValue, делаю прям на дефолтном VCL проекте, и пока не понял в чём фокус (уже накалывался на фокусы взаимодействия API-функций с VCL) Попробую сделать чистый WinAPI проект что ли...
    Найду - расскажу)

    ОтветитьУдалить
  11. Если кратко: stdcall с HRESULT двоично совместим с safecall. Отличие в том, что для safecall магия компилятора автоматически вставляет обработку ошибок, конвертируя коды ошибок в исключения. Т.е. не нужно писать конструкции вида "if FunctionFailed then HandleError".

    Подробнее.

    ОтветитьУдалить
  12. Благодарю!)) Может наверное всё же стоит сделать версии и так и так. Только как их отличать? Добавить к названию доп.инфо, навроде ITaskbarList4_stdcall и ITaskbarList4_safecall? Но совместимость пропадёт, плохой идей. Вручную ставить какие нужно? Всё равно "не айс"...

    На чистом API работает как ни в чём ни бывало, забавно, значит точно VCL сопротивляется. "Как всё пзнвательно..." (С)

    ОтветитьУдалить
  13. Просто вызывать SetProgressValue и прочие можно только после того как "кнопка" приложения создалась и появилась на таскбаре, иначе просто ничего не произойдёт. О том что кнопка создалась и появилась сообщается с помощью WM_TASKBARBUTTONCREATED.

    Получается оно динамически (кстати почему, на разных ОС значение может не совпадать что ли?):

    Var WM_TASKBARBUTTONCREATED: UINT;
    ...
    WM_TASKBARBUTTONCREATED:=RegisterWindowMessage('TaskbarButtonCreated');
    // ChangeWindowMessageFilter - вроде не обязательно?

    Единственное что не пойму - почему во всех примерах RegisterWindowMessage суют в WM_CREATE окна (хоть и главного программы)? Оно разное для разных окон? Потоков? Почему не вызывать один раз в точке входа в программу?

    ОтветитьУдалить
  14. Не существует никакой хранимой таблицы пользовательских сообщений. Она создаётся заново вызовами RegisterWindowMessage после каждого перезапуска системы. Иными словами, числовые коды сообщений будут каждый раз разными. А если они совпадают - это случайность.

    RegisterWindowMessage можно вставлять в любое место до момента использования его результата. Очевидно, чем меньше промежуток между получением значения и его использованием - тем лучше. Если вы используете её результат в цикле сообщений окна, то логично располагать вызов RegisterWindowMessage на любое событие вида OnCreate этого окна.

    ОтветитьУдалить
  15. >> Очевидно, чем меньше промежуток между получением значения и его использованием - тем лучше.

    Чем же лучше? Если значение WM_TASKBARBUTTONCREATED не меняется в течение допустим хотя бы сеанса пользователя?

    >> Если вы используете её результат в цикле сообщений окна, то логично располагать вызов RegisterWindowMessage на любое событие вида OnCreate этого окна.

    Допустим у меня создаётся 5 окон всегда и 5 опционально. Мне вот не кажется логичным вызывать RegisterWindowMessage() для получения одной и той же константы десять раз.
    Разве если программа теоретически собирается использовать такую "константу" не логичнее получать значение при инициализации?

    хех... Case отказывается есть WM_TASKBARBUTTONCREATED, если это переменная. Ну да, правильно, запамятовал. Но и с If как-то не хотелось бы городить...

    ОтветитьУдалить
  16. > Чем же лучше?

    Очевидно, в некоторых случаях до создания WM_TASKBARBUTTONCREATED дело не дойдёт. Например, запуск второго экземпляра программы заблокирован. Или программу запустили с параметрами, которые говорят "выполни операцию и выйди". Или произошла ошибка инициализации окна. Нет сильно большого смысла делать операции, которые в итоге оказываются не нужны.

    > Допустим у меня создаётся 5 окон всегда и 5 опционально. Мне вот не кажется логичным вызывать RegisterWindowMessage() для получения одной и той же константы десять раз.

    В такой ситуации - конечно, лучше инициализировать её один раз, а не пять раз. Но исходно-то мы говорили про одно окно.

    P.S. Зря не подписываетесь - непонятно, с одним человеком тут разговор или с двумя.

    ОтветитьУдалить
  17. Все это полумеры знание английского и всего прочего что автор написал, это принципы так называемого datamaininga поиск информации.
    Насчет что 90% кода это гавно тут вы неправы, в каждом случае такой код подсказка где капать.
    Можно много не знать что на самом деле не является преступление но если вы на практике чтото освоили и оно работает, без шизы с WinAPi то кто там будет разбиратся из пользователей.
    Если я выложу все приложения которые я написал за 25 лет то все будут орать тут неправильно там не по фуншую, а мерилом истины является не гуру Delphi а пользователь и кол-во проданных программ. Чем более дружелюбно(в рамках самой меры понятия) и удобно(тут или ехать или шашечки ну в общем можно до бесконечности шлифовать).
    Я както выложил свое коммерческое приложение(склад-торговля которое до сих пор меня исправно кормит) исходники на обсуждение нареканий было много, но никто не задал вопрос по существу как народу нравится и прочее, все полезли в код искать изьяны)

    ОтветитьУдалить
  18. Копая инет нарыл компонент который бесплатный(но благодарности принимаются в любом виде)), это обертка ITaskbarList3 работает с ОС Vista и выше http://www.webdelphi.ru/wp-content/uploads/downloads/2011/02/TTaskbar-v.1.31.zip ('TaskBar от BuBa Group', SeregaAltmer, http://buba-group.ru)
    в нем реализована добавление/удаление, прогрессбар кнопки на панель задач проверял под Delphi XE3 работает в Win7, в XP не пашет но и не ругается приложение, там стоит заглушка. Можете скачать, если кому интересно по подобию раздербанить и вставить туда ITaskbarList для winxp, чтобы пахало только без прогрессбаров. В свое время помню ковыряли функции(PUT и прочее) макроассемблера и PL1 для ES ЭВМ(распечатки партянки метровые с консоли на вц) вот это была песня, на экзамен можно было ходить по асемблеру хоть со всеми конспектами - книгами (типа автор Вострикова) и прочее если не шаришь, никакие тебе книжки не помогут, также и интернет - надо мозг включать иногда, а не просто копипастить. Хотя и это тоже часто помогает, как в анекдоте "трясти надо, что думать" или как в кино "пилите Шура, пилите") А вообще пора российский портал делать, где народ выкладывает свои наработки , кому поделится охота. А то смотрю все пишут пишут и не делятся, а потом бац и нету программера - помер и проекты платные - бесплатные, все без исходников. Щас вот один такой восстанавливаю исходники Delphi +FireBird. А у чела 3 программы на софт портале продается до сих пор, а исходников то нет.

    ОтветитьУдалить
  19. Что-то попробовал на новом проекте на Берлине на семёрке - не работают "Сбросить WS_EX_APPWINDOW" и "Установить WS_EX_TOOLWINDOW"...
    То нет кнопки но с какая-то хрень творится с окном, то кнопка на месте что ни делай. Хотя я на том компе через TeamViewer, может это он чудит.

    ОтветитьУдалить
    Ответы
    1. Ежели скажете что-то конкретное, чтобы хрустальный шар не доставать, то может что кто подсказать сможет.

      Удалить
  20. >> ... в файле ShObjIdl.h вы найдёте ... переводя это на Delphi получаем ... внимание на safecall ... рассказывал ...

    Если вам не сложно, есть три вопроса.
    * Почему при описании типа в квадратных скобках нельзя указать саму константу CLSID_TaskbarList? Почему значение GUID приходится дублировать?
    * Что будет если случайно указать stdcall (мне переводили один интерфейс и вроде нормально работал с stdcall, да и этот при замене на stdcall работает)? Откуда именно видно что в данных конкретных местах нужен именно safecall, где вы про него рассказывали? Что-то не могу нагуглить, слишком много у вас статей))
    * Почему даже в самых новых Делфи нет довольно многих интерфейсов? Кого и где пнуть чтоб добавили в следующей версии те что мне нужны? Разве нельзя просто (полу)автоматически перевести всё что есть в оригинальных "h"? Или хотя бы список составить какие бывают, какие ещё не описаны и постепенно переводить...
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. И да, почему у вас "procedure"? Там же всюду явно возвращается HRESULT...

      Удалить
    2. Пардон, скопипастил не вчитываясь. Разумеется иселось ввиду не CLSID_TaskbarList, а IID_ITaskbarList (просто обычно сверху пишут IID, потом тип, а потом уже CLSID).
      Кстати в JEDI явно наследуется от "IUnknown", а так же с "function" с "stdcall". А моего интерфейса у них нету. Странно, IInitializeWithFile я новым не назвал бы...

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.