views:

738

answers:

3

I have a few old apps I've written (in Delphi) which for various reasons use a system tray icon. Most are using AppControls TacTrayIcon or some other similar component.

Here's my question: How does one control the position of a tray icon? (i.e. where it is, say, relative to the system time -- 1st position/"slot", 2nd position/"slot", etc). I recall seeing a demo (C#, if memory serves) that allowed the user to "shift icon to the left" and "shift icon to the right", but don't recall how it was done.

I'd like to allow the user to select what position they want to icon to appear in, for Windows 2000 - Windows 7. (I understand Windows 7 handles system tray stuff a little differently, but haven't tested that out yet).

Thanks for any and all help.

+7  A: 

There is no documented or supported way for programs to control the positions of their shell notification icons. There's not even anything guaranteeing they will appear at all, or if they do appear, that they will appear anywhere near the clock, such that your positioning instructions would make any sense.

(I used to use a program that hijacked some or all of the icons and optionally displayed them in its own window instead of in the area near the clock. It was TraySaver, by Mike Lin. The source is available if you wish to see how his hack worked.)

The icon's position is not under your control. My advice to you is to not try to make it your program's responsibility, especially if nobody has actually requested such functionality from your program in the first place. If people want to control your program's icon location, they probably want to control other programs' icon locations, in which case the problem is bigger than you anyway.

Rob Kennedy
Thanks for a legitimate answer, rather than just judgement/condemnation of the question -- its refreshing and appreciated. This is actually a feature users are requesting, though I agree w/you re: the problem being bigger than just my app. I'll take a look at the TraySaver code. Thanks again.
Jamo
+4  A: 

Accessing and modifying the shell notification area is hackish but possible. You first need to find the top level window:

var
  Wnd: HWND;
begin
  Wnd := FindWindow('Shell_TrayWnd', nil);
  if IsWindow(Wnd) then
    EnumChildWindows(Wnd, @FindTrayWnd, 0);
end;

then enumerate its children to find the tray notification area:

function FindTrayWnd(AWnd: HWND; AParam: LPARAM): BOOL; stdcall;
var
  ClassName: string;
begin
  SetLength(ClassName, 64);
  SetLength(ClassName, GetClassName(AWnd, PChar(ClassName), 64));
  Result := True;
  if AnsiCompareText(ClassName, 'TrayNotifyWnd') = 0 then begin
    EnumChildWindows(AWnd, @FindToolbar, 0);
    Result := False;
  end;
end;

then enumerate its children to find the standard Windows toolbar with the notification icons. Windows messages are used to get or set toolbar properties. Since the toolbar lives in another process you need to employ ReadProcessMemory() and WriteProcessMemory() for all messages that involve a buffer of some sort (like getting the button text or button info):

function FindToolbar(AWnd: HWND; AParam: LPARAM): BOOL; stdcall;
const
  VMFLAGS = PROCESS_VM_OPERATION or PROCESS_VM_READ or PROCESS_VM_WRITE;
var
  ClassName: string;
  i, ButtonCount: integer;
  ProcessId, BytesRead: Cardinal;
  ProcessHandle: THandle;
  ExplorerButtonInfo: PTBButton;
  ButtonInfo: array of TTBButton;
begin
  SetLength(ClassName, 64);
  SetLength(ClassName, GetClassName(AWnd, PChar(ClassName), 64));
  if AnsiCompareText(ClassName, 'ToolbarWindow32') = 0 then begin
    GetWindowThreadProcessId(AWnd, @ProcessId);
    ProcessHandle := OpenProcess(VMFLAGS, FALSE, ProcessId);
    ExplorerButtonInfo := VirtualAllocEx(ProcessHandle, nil, SizeOf(TTBButton),
      MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE);
    if ExplorerButtonInfo <> nil then try
      ButtonCount := SendMessage(AWnd, TB_BUTTONCOUNT, 0, 0);
      SetLength(ButtonInfo, ButtonCount);
      for i := 0 to ButtonCount - 1 do begin
        SendMessage(AWnd, TB_GETBUTTON, i, LPARAM(ExplorerButtonInfo));
        ReadProcessMemory(ProcessHandle, ExplorerButtonInfo, @ButtonInfo[i],
          SizeOf(TTBButton), BytesRead);
      end;
      // manipulate the button info, use WriteProcessMemory() and SendMessage()
      // to repopulate the toolbar

    finally
      VirtualFreeEx(ProcessId, ExplorerButtonInfo, SizeOf(TTBButton),
        MEM_RELEASE);
    end;
    Result := False;
  end else
    Result := True;
end;

You should be able to identify the button of your notification icon via its name, then delete that button, then insert it at the desired position. All error handling omitted, but this should get you started.

mghie
mghie, thanks so much for taking the time to post this, and also for your kind and clarifying commments above -- I really appreciate both. (Very much so!). As well, your point is noted re: "your customer requests control over something that isn't meant to be controlled." It seems strange to me that this isn't already "controllable" by the user directly in Windows, even if not programmatically by applications, like Chen refers to in the article you linked to.
Jamo
+1  A: 

Look at this article on CodeProject, hope it helps.

This solution have very limited compatibility, and not recommended at all.

ThinkJet
Ah -- thanks! THAT is the project I'd seen before, but was having trouble finding. Point noted re: "not recommended at all" though; will study it with caution. ; )
Jamo
Ок. Take attention on reading memory of other process and user privileges required for this action. Another thing is antivirus software which monitors such activity.
ThinkJet