Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
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.