Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #125
See Also: Dr.Bob's Delphi Papers and Columns

Fun with the Windows 7 Taskbar
In this article I will show how we can manipulate the Windows 7 taskbar, and also how to solve an issue when running an application with the “run as Administrator” option, but still wanting the taskbar to be able to send messages to it.

Windows 7 Support
Delphi 2010 includes a number of Windows 7 specific features. One important point is to be able to correctly identify Windows 7 (compared to Windows Vista or XP for example), which can be done as follows:

  if CheckWin32Version(6, 1) then
    ShowMessage('Windows 7')
  else
  if CheckWin32Version(6) then
    ShowMessage('Windows Vista');

Windows 7 is enhanced with new APIs in C and COM, but not specifically for .NET, so we can still use them in native applications.

Taskbar buttons
Windows 7 offers support to manipulate the application “taskbar” button. This is done by four interfaces, with the very informative names ITaskbarList, ITaskbarList2, ITaskbarList3, ITaskbarList4.
We need to add the ComObj and ShlObj units to the uses clause, and can then work with the four interface types:

  private
    { Private declarations }
    TaskbarList: ITaskbarList;
    TaskbarList2: ITaskbarList2;
    TaskbarList3: ITaskbarList3;
    TaskbarList4: ITaskbarList4;

We can call CreateComObject passing the CLSID_TaskBarList and then use that to extract all four interfaces so we can use them from that point on.

  procedure TFormWorkshop.FormCreate(Sender: TObject);
  begin
    if CheckWin32Version(6, 1) then
    begin
      ShowMessage('Windows 7');
      TaskbarList := CreateComObject(CLSID_TaskbarList) as ITaskbarList;
      TaskbarList.HrInit;
      Supports(TaskbarList, IID_ITaskbarList2, TaskbarList2);
      Supports(TaskbarList, IID_ITaskbarList3, TaskbarList3);
      Supports(TaskbarList, IID_ITaskbarList4, TaskbarList4);
    end
    else
    begin
      if CheckWin32Version(6) then
        ShowMessage('Windows Vista is too old')
      else
        ShowMessage('Your version of Windows is too old...');
      Application.Terminate
    end
  end;

Before we actually start to use these interfaces, let’s first explorer their definitions, to see what’s possible when we want to use them.
The initial interface ITaskbarList can be used to add, delete, and activate tabs in the taskbar. It also contains the HrInit function that we must call to initiate our working with the taskbar.

  ITaskbarList = interface(IUnknown)
    [SID_ITaskbarList]
    function HrInit: HRESULT; stdcall;
    function AddTab(hwnd: HWND): HRESULT; stdcall;
    function DeleteTab(hwnd: HWND): HRESULT; stdcall;
    function ActivateTab(hwnd: HWND): HRESULT; stdcall;
    function SetActiveAlt(hwnd: HWND): HRESULT; stdcall;
  end;

We can use this for example to remove an application from the taskbar, by passing the Application.Handle, or the Application.MainForm.Handle (in case the MainFormOnTaskBar is set to true) to these methods:

  procedure TFormWorkshop.btnDeleteTabClick(Sender: TObject);
  var
    FormHandle: THandle;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;
    TaskbarList.DeleteTab(FormHandle);
  end;

  procedure TFormWorkshop.btnAddTabClick(Sender: TObject);
  var
    FormHandle: THandle;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;
    TaskbarList.AddTab(FormHandle);
  end;

I leave it as exercise for the reader to try the ActivateTab and SetActiveAlt methods (who only activate the tab on the taskbar, and not the window itself, by the way).
The ITaskbarList2 interface can be used to mark a windows as a full screen window.

  ITaskbarList2 = interface(ITaskbarList)
    [SID_ITaskbarList2]
    function MarkFullscreenWindow(hwnd: HWND;
      fFullscreen: BOOL): HRESULT; stdcall;
  end;

Progress
The interface ITaskbarList3 is by far the one with the most options. The relevant definition parts are as follows:

  type
    THUMBBUTTON = record
      dwMask: DWORD;
      iId: UINT;
      iBitmap: UINT;
      hIcon: HICON;
      szTip: packed array[0..259] of WCHAR;
      dwFlags: DWORD;
    end;

    TThumbButton = THUMBBUTTON;

  const
    THBF_ENABLED        =  $0000;
    THBF_DISABLED       =  $0001;
    THBF_DISMISSONCLICK =  $0002;
    THBF_NOBACKGROUND   =  $0004;
    THBF_HIDDEN         =  $0008;
    THBF_NONINTERACTIVE = $10;

  // THUMBBUTTON mask
    THB_BITMAP          =  $0001;
    THB_ICON            =  $0002;
    THB_TOOLTIP         =  $0004;
    THB_FLAGS           =  $0008;
    THBN_CLICKED        =  $1800;

  const
    TBPF_NOPROGRESS    = 0;
    TBPF_INDETERMINATE = $1;
    TBPF_NORMAL        = $2;
    TBPF_ERROR         = $4;
    TBPF_PAUSED        = $8;

    TBATF_USEMDITHUMBNAIL   = $1;
    TBATF_USEMDILIVEPREVIEW = $2;

  type
    ITaskbarList3 = interface(ITaskbarList2)
      [SID_ITaskbarList3]
      function SetProgressValue(hwnd: HWND; ullCompleted: ULONGLONG;
        ullTotal: ULONGLONG): HRESULT; stdcall;
      function SetProgressState(hwnd: HWND; tbpFlags: Integer): HRESULT; stdcall;
      function RegisterTab(hwndTab: HWND; hwndMDI: HWND): HRESULT; stdcall;
      function UnregisterTab(hwndTab: HWND): HRESULT; stdcall;
      function SetTabOrder(hwndTab: HWND; hwndInsertBefore: HWND): HRESULT; stdcall;
      function SetTabActive(hwndTab: HWND; hwndMDI: HWND;
        tbatFlags: Integer): HRESULT; stdcall;
      function ThumbBarAddButtons(hwnd: HWND; cButtons: UINT;
        pButton: PThumbButton): HRESULT; stdcall;
      function ThumbBarUpdateButtons(hwnd: HWND; cButtons: UINT;
        pButton: PThumbButton): HRESULT; stdcall;
      function ThumbBarSetImageList(hwnd: HWND; himl: HIMAGELIST): HRESULT; stdcall;
      function SetOverlayIcon(hwnd: HWND; hIcon: HICON;
        pszDescription: LPCWSTR): HRESULT; stdcall;
      function SetThumbnailTooltip(hwnd: HWND; pszTip: LPCWSTR): HRESULT; stdcall;
      function SetThumbnailClip(hwnd: HWND; var prcClip: TRect): HRESULT; stdcall;
    end;

We can show a progressbar in the taskbar, by calling the SetProgressState followed by the SetProgressValue API. This is using the TaskbarList3 interface.

  procedure TFormWorkshop.btnProgressClick(Sender: TObject);
  var
    FormHandle: THandle;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;

    if Assigned(TaskbarList3) then
      TaskbarList3.SetProgressState(FormHandle, TBPF_NORMAL);
    Progress := Progress + 1;
    if Assigned(TaskbarList3) then
      TaskbarList3.SetProgressValue(FormHandle, Progress, 10);
  end;

To stop the display of the progress indicator in the taskbar button, we can call the SetProgressState method again, this time passing the TBPF_NOPROGRESS flag.

  procedure TFormWorkshop.btnNoProgressClick(Sender: TObject);
  var
    FormHandle: THandle;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;
    if Assigned(TaskbarList3) then
      TaskbarList3.SetProgressState(FormHandle, TBPF_NOPROGRESS);
    Progress := 0;
  end;

ImageList –icon
We can also place an icon as overlay on the taskbar, but only if the taskbar button is big (not if it’s small, since then the icon will not show up).
For this, it’s easy to use a TImageList with icons so we can assign these to the icons that we pass to the SetOverlayIcon method. Note that you can always find example icons in the C:\Program Files\Common Files\CodeGear Shared\Images\Icons directory on your machine.

  procedure TFormWorkshop.btnIconClick(Sender: TObject);
  var
    FormHandle: THandle;
    MyIcon: TIcon;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;
    MyIcon := TIcon.Create;
    try
      ImageList1.GetIcon(0, MyIcon);
      if Assigned(TaskbarList3) then
        TaskbarList3.SetOverlayIcon(FormHandle,
          MyIcon.Handle, PChar('Delphi2010'));
    finally
      MyIcon.Free;
    end;
  end;

To remove the icon, we simple have to pass 0 as argument to the SetOverlayIcon method:

  if Assigned(TaskbarList3) then
    TaskbarList3.SetOverlayIcon(FormHandle, 0, nil);

ThumbButtons
We can also add ThumbButtons to the taskbar tab. For this, we also need an ImageList, and one or more TThumbButtons:

  procedure TFormWorkshop.btnThumbButtonClick(Sender: TObject);
  var
    Button: TThumbButton;
    FormHandle: THandle;
  begin
    if not Application.MainFormOnTaskBar then
      FormHandle := Application.Handle
    else FormHandle := Application.MainForm.Handle;
    Button.iId := 42;;
    Button.iBitmap := 0;
    Button.dwMask := THB_FLAGS or THB_BITMAP or THB_TOOLTIP;
    Button.dwFlags :=  THBF_ENABLED or THBF_NOBACKGROUND;
    StrCopy(Button.szTip, PChar('Answer'));
    TaskbarList3.ThumbBarSetImageList(FormHandle, ImageList1.Handle);
    TaskbarList3.ThumbBarAddButtons(FormHandle, 1, @Button);
  end;

Note that this will show the button in the taskbar preview window, and if we leave the mouse hanging over the button, the tooltip will appear:
The final task is responding to a click on this taskbar button. For that, we have to listen to a WM_COMMAND. The best way to add this listener is to go to the code editor, place the cursor inside the TForm you’re working on, and press Alt+Space followed by “WMC” to find the WMCommand procedure, which should be added to the protected area:

  protected
    procedure WMCommand(var Message: TWMCommand); message WM_COMMAND;

Press Ctrl+Alt+C and implement WMCommand as follows:

  procedure TFormWorkshop.WMCommand(var Message: TWMCommand);
  begin
    if Message.NotifyCode = THBN_CLICKED then
    begin
      ShowMessage('You clicked on button #' +
        IntToStr(Message.ItemID));
    end;
    inherited;
  end;

Note that this will only work if the application is NOT run with Administrator rights. If you run the Delphi IDE with the “Run as Administrator” option and then spawn the application, the buttonclick will NOT be received or handled.

Administrator Fix
With help and suggestions from a few people, Alan Fletcher and Damian Undziakiewicz to be specific, I was able to solve the issue of the thumb buttons not responding to the click events when the application is run with Administrator rights (or spawned from an application such as Delphi or Total Commander, running with Administrator rights).
The problem is caused by the fact that the User Interface Privilege Isolation prevents the taskbar from sending messages to the application running as administrator. According to Microsoft: User Interface Privilege Isolation (UIPI) implements restrictions in the windows subsystem that prevents lower-privilege applications from sending window messages or installing hooks in higher-privilege processes. Higher-privilege applications are permitted to send window messages to lower-privilege processes. The restrictions are implemented in the SendMessage and related window message functions. Not all window messages that are sent from a lower-privilege process to a higher-privilege process are blocked. Generally, “read” type messages, for example WM_GETTEXT, can be sent from a lower-privilege to a higher-privilege window. However, write type messages, such as WM_SETTEXT, are blocked.
The solution to this problem is to call a function called ChangeWindowMessageFilter, which needs two parameters: message (the message that should be let through), action (set to MSGFLT_ALLOW to allow the message through). There is also a ChangeWindowMessageFilterEx, which can be used to enable the filter for a specific Window Handle.
The function ChangeWindowMessageFilter and ChangeWindowMessageFilterEx can be found in the user32.dll, so the import declaration is as follows:

  function ChangeWindowMessageFilter(msg: Cardinal; action: Word): BOOL; stdcall;
    external 'user32.dll';

And in the FormCreate we need to call it, ChangeWindowMessageFilter(WM_COMMAND, 1) in order to allow the taskbar to send the messages to the application itself. Even when running as administrator.
Note that this implicit import will make the application fail if the function is not found in user32.dll, which is OK, since that means it’s not Windows 7 anyway.


This webpage © 2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.