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.
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.
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.
Look at this article on CodeProject, hope it helps.
This solution have very limited compatibility, and not recommended at all.
Related
In a Delphi 10.4.2 Win32 VCL Application, on Windows10 X64 (German language) I set the shortcuts for some menu items programmatically:
mRasterizedDoubleSize.Shortcut := VK_ADD;
mRasterizedHalveSize.Shortcut := VK_SUBTRACT;
mRasterizedResetToOriginalSVGSize.Shortcut := VK_NUMPAD0;
This results in the following menu at run-time:
("ZEHNERTASTATUR" is German for NUMERIC KEYPAD)
Why "Zehnertastatur" (numeric keypad) is not shown for the third menu item?
How can I show "ZEHNERTASTATUR" (NUMERIC KEYPAD) for the menu item shortcut assigned with VK_NUMPAD0?
I have filed a Quality Report for this bug in Vcl.Menus: https://quality.embarcadero.com/browse/RSP-33296
Please vote for it!
EDIT: I have tried Andreas' advice, but it does work only programmatically at run-time, not at design-time in the Object Inspector:
mRasterizedResetToOriginalSVGSize.Caption := mRasterizedResetToOriginalSVGSize.Caption + #9 + '0 (NUMPAD) ';
Isn't there a function that translates the word "NUMPAD" into the current system language at-run-time?
EDIT2: I have tried this to get the word for the VK_NUMPAD0 shortcut, but it only gives back the same "0" without the "NUMPAD" suffix:
var s: TShortCut;
s := ShortCut(VK_NUMPAD0, []);
CodeSite.Send('TformMain.FormCreate: ShortCutToText(s)', ShortCutToText(s));
EDIT3: I now have debugged Vcl.Menus: The bug seems to be in Vcl.Menus.ShortCutToText: While VK_ADD ($6B) is correctly translated by GetSpecialName(ShortCut), VK_NUMPAD0 ($60) is NOT being translated by GetSpecialName(ShortCut)!
EDIT4: I have found the solution:
function MyGetSpecialName(ShortCut: TShortCut): string;
var
ScanCode: Integer;
KeyName: array[0..255] of Char;
begin
Result := '';
ScanCode := Winapi.Windows.MapVirtualKey(LoByte(Word(ShortCut)), 0) shl 16;
if ScanCode <> 0 then
begin
if Winapi.Windows.GetKeyNameText(ScanCode, KeyName, Length(KeyName)) <> 0 then
Result := KeyName;
end;
end;
var s: System.Classes.TShortCut;
s := ShortCut(VK_NUMPAD0, []);
CodeSite.Send('ShortCutToText', MyGetSpecialName(s));
One approach is like this:
Use a TActionList. This is good practice in general. Define your actions within this list, and then simply map them to menu items, buttons, check boxes, etc. The action list facility is one of the very best parts of the VCL IMHO.
Now, create an action named aResetZoom with Caption = 'Reset zoom'#9'Numpad 0' and NO ShortCut. Put this on the menu bar.
Then, create a new action named aResetZoomShortcut with the same OnExecute (and possibly the same OnUpdate) and shortcut Num 0 (set at design time or programmatically at run time). Don't put this on the main menu.
The result:
and the action is triggered when I press numpad 0 (but not the alpha 0).
There are many variants to this approach. Maybe you can make it work with a single action with no ShortCut but with Num 0 in its SecondaryShortCuts list. Or you can use the form's KeyPreview and OnKeyPress properties instead of the "dummy" action.
Many options. Choose the one that is best suited for your particular scenario.
Bonus remarks
Please note it is perfectly possibly to set captions with tabs at design time using the Object Inspector. See example video.
You can probably do localisation using the Win32 GetKeyNameText function. The following code is adapted from the VCL:
var
name: array[0..128] of Char;
begin
FillChar(name, SizeOf(name), 0);
GetKeyNameText(MapVirtualKey(VK_NUMPAD0, 0) shl 16, #name[0], Length(name));
// string(name) now is 'NUM 0' on my system
That being said, personally I don't mind if shortcut descriptions are non-localized or manually localised -- like the rest of the application.
Update
A clarification on how to use the localisation code:
procedure TForm5.FormCreate(Sender: TObject);
var
name: array[0..128] of Char;
NameAsANormalString: string;
begin
FillChar(name, SizeOf(name), 0);
GetKeyNameText(MapVirtualKey(VK_NUMPAD0, 0) shl 16, #name[0], Length(name));
NameAsANormalString := name;
ShowMessage(name);
end;
produces
on my system.
I need to make some changes to a TaskDialog before it is shown to the user. It's fairly simple to use Windows API calls to work with each of the controls on the dialog box. I need to be more sure which button I have found. I would have expected to find a place where I could read the result the button would give if pressed.
in other words, if I pressed a button that would cause a return value (in Delphi, it's called a modal result) of 100, I would have expected there to be an API call I could call to find out what the button's "return value" would be. I haven't yet found any such call.
I don't want to rely on the button text..
Here's what I have so far.
function EnumWindowsProcToFindDlgControls(hWindow: HWND; _param:LPARAM): BOOL; stdcall;
var
sClassName:string;
hBMP:THandle;
i:integer;
begin
SetLength(sClassName, MAX_PATH);
GetClassName(hWindow, PChar(sClassName), MAX_PATH);
SetLength(sClassName, StrLen(PChar(sClassName)));
if sClassName='Button' then
begin
// always 0...
i:=GetDlgCtrlID(hWindow);
if (i=100) or (i=102) then
begin
hBmp := LoadImage(HInstance, 'DISA', IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE or LR_LOADTRANSPARENT );
SendMessage(hWindow, BM_SETIMAGE, WPARAM(IMAGE_BITMAP), LPARAM(hBmp));
end;
end;
// keep looking
Result:=true;
end;
procedure TForm2.TaskDialog1DialogConstructed(Sender: TObject);
begin
EnumChildWindows(TaskDialog1.Handle, #EnumWindowsProcToFindDlgControls, 0);
end;
I suspect it's not entirely "respectable" to do things like this with a dialog.
This is a Delphi 10 Win32 application using Delphi's VCL TTaskDialog component which is a wrapper around Windows task dialog feature. before it's shown, the OnConstructed event fires, executing this code.
Thank you for your help!
Win32 buttons do not have "return values", which is why there is no API to retrieve such a value from them. What you are thinking of is strictly a VCL feature.
In Win32 API terms, a button can have a control ID, and in the case of MessageBox(), for example, standard ID values like IDOK, IDCANCEL, etc are assigned to the dialog buttons. When a button is clicked and the dialog is closed, the button's control ID is used as the function return value.
But task dialogs do not use control IDs, which is why you do not see any assigned to the dialog buttons.
To identify a particular task dialog button, I can think of two ways:
during child enumeration, retrieve each button's caption text (GetWindowText()), and compare that to captions you are interested in. Just know that the standard buttons (from the TTaskDialog.CommonButtons property) use localized text, which does not make this a well-suited option for locating standard buttons unless you have control over the app's locale settings.
send the dialog a TDM_ENABLE_BUTTON message to temporarily disable the desired button that has a given ID, then enumerate the dialog's controls until you find a disabled child window (using IsWindowEnabled()), and then re-enable the control. You can then manipulate the found window as needed.
For Task Dialog messages and Task Dialog Notifications that operate on buttons (like TDN_BUTTON_CLICKED, which triggers the TTaskDialog.OnButtonClicked event), the standard buttons use IDs like IDOK, IDCANCEL, etc while custom buttons (from the TTaskDialog.Buttons property) use their ModalResult property as their ID.
You can send TDM_ENABLE_BUTTON directly via SendMessage() for standard buttons, or via the TTaskDialogBaseButtonItem.Enabled property for custom buttons.
For #2, this works when I try it:
uses
Winapi.CommCtrl;
function FindDisabledDlgControl(hWindow: HWND; _param: LPARAM): BOOL; stdcall;
type
PHWND = ^HWND;
begin
if not IsWindowEnabled(hWindow) then
begin
PHWND(_param)^ := hWindow;
Result := False;
end else
Result := True;
end;
procedure TForm2.TaskDialog1DialogConstructed(Sender: TObject);
var
hButton: HWND;
begin
// common tcbOk button
SendMessage(TaskDialog1.Handle, TDM_ENABLE_BUTTON, IDOK, 0);
hButton := 0;
EnumChildWindows(TaskDialog1.Handle, #FindDisabledDlgControl, LPARAM(#hButton));
SendMessage(TaskDialog1.Handle, TDM_ENABLE_BUTTON, IDOK, 1);
if hButton <> 0 then
begin
// use hButton as needed...
end;
// custom button
TaskDialog1.Buttons[0].Enabled := False;
hButton := 0;
EnumChildWindows(TaskDialog1.Handle, #FindDisabledDlgControl, LPARAM(#hButton));
TaskDialog1.Buttons[0].Enabled := True;
if hButton <> 0 then
begin
// use hButton as needed...
end;
end;
To translate my application;
I am using one or more textfile(s) where the button captions, warnings, error messages are translated to the language which the user can select from a list of languages.
Translating of warnings and error- and other messages can be specific, so these should be translated by me.
I wonder if it is possible to retrieve captions for buttons like OK, Cancel, Abort, Close are retrievable from windows according to the selected display language of windows.
EDIT
the following function seem to do what I want
function GetButtonCaption(const ButtonType: Integer): WideString;
var
hDll: THandle;
Buffer: WideString;
BufferLen: Integer;
begin
Result := '';
hDll := LoadLibrary('User32.dll');
if hDLL <> 0 then
begin
SetLength(Buffer, 255);
BufferLen := LoadStringW(hdll, ButtonType, PWideChar(Buffer), Length(Buffer));
if BufferLen <> 0 then
Result := Copy(Buffer, 1, BufferLen);
FreeLibrary(hDll);
end;
end;
there was a list of some constants as example to pass
const
OK_CAPTION = 800;
CANCEL_CAPTION = 801;
ABORT_CAPTION = 802;
RETRY_CAPTION = 803;
IGNORE_CAPTION = 804;
YES_CAPTION = 805;
NO_CAPTION = 806;
CLOSE_CAPTION = 807;
So far it does what I am searching for.
At this point need to find where to get those identifiers/constants.
Edit2:
I found the identifiers, these are stored at the string table of user32.dll.
Until now I can view the string table using visual studio, don't know if it is possible via Delphi.
It seems allso that, other files (dll's), like shell32.dll has it's own string table.
Finally, I think I have my answer, except someone know a better way to get these informaion.
I'm using this Delphi 7 code to detect if Internet Explorer is running:
function IERunning: Boolean;
begin
Result := FindWindow('IEFrame', NIL) > 0;
end;
This works on 99% of the systems with IE 8,9 and 10.
But there are some systems (unfortunately none of mine, but I have two beta testers which have such systems, both Win7 x64 SP1) where FindWindow() returns 0 for IEFrame, even if IE is in memory.
So I've coded an alternate method to find the window:
function IERunningEx: Boolean;
var WinHandle : HWND;
Name: array[0..255] of Char;
begin
Result := False; // assume no IE window is present
WinHandle := GetTopWindow(GetDesktopWindow);
while WinHandle <> 0 do // go thru the window list
begin
GetClassName(WinHandle, #Name[0], 255);
if (CompareText(string(Name), 'IEFrame') = 0) then
begin // IEFrame found
Result := True;
Exit;
end;
WinHandle := GetNextWindow(WinHandle, GW_HWNDNEXT);
end;
end;
The alternate method works on 100% of all systems.
My question - why is FindWindow() not reliable on some of the systems?
I'm guessing that FindWindow is declared to return a WinHandle, which is a THandle, which is an Integer, which is signed. (At least, I think this was the case many years ago when I programmed in Delphi.)
If IE has a window handle with the top bit set then it will be negative so your test will return False:
Result := FindWindow('IEFrame', NIL) > 0;
Window handles don't usually have the top bit set, but I don't know that it's impossible.
According to Delphi Help, FindWindow(ClassName,WindowName) does not search child windows. This could be the reason for the 1% failures. Maybe in those two beta tester's systems the IEFrame window had WS_CHILD style set.
This would explain why the GetTopWindow/GetNextWindow loop works. GetTopWindow(hWndParent) retrieves the child window at the top of the Z order, and GetNextWindow(hWnd,Direction) retrieves the next child window in the Z order.
This could be tested by calling FindWindowEx(hWndParent,hWndChild,ClassName,WindowName),
to see if it works where FindWindow() fails.
The code below sometimes works: it inserts text from tEdit, but only in "Notepad", "Word", "ICQ". Such software like Firefox or Google Chrome doesn't work with it.
What should I do?
var
Pos: TPoint;
Target: HWND;
...
if not GetCursorPos(Pos) then
RaiseLastOSError;
Target := WindowFromPoint(Pos);
if Target<>0 then
SendMessage(Target, EM_REPLACESEL, ord(True), LPARAM(PChar(Edit1.Text)));
That's it! I have found the code i needed
procedure SendText(ds:string);
var
TI: TInput;
KI: TKeybdInput;
i: integer;
begin
TI.Itype := INPUT_KEYBOARD;
for i := 1 to Length(ds) do
begin
KI.wVk := Ord(UpCase(ds[i]));
KI.dwFlags := 0;
TI.ki := KI;
SendInput(1, TI, SizeOf(TI));
KI.dwFlags := KEYEVENTF_KEYUP;
TI.ki := KI;
SendInput(1, TI, SizeOf(TI));
end;
end;
But the problem is - i can't copy russian(cyrilic) symbols using SendInpit(Edit1.Text); Any suggestions?
It doesn't work in Firefox and Chrome because the edit boxes you see in them are rendered by the HTML engines in the browser and not by the operating system. They are called "windowless controls", and thus do not have a window handle associated with them.
As far as the operating system is concerned, the webpage is one big HWND with a webpage painted inside it, and some of the painted elements just happen to look and act like controls thanks to the HTML engine.
You cannot target such controls with SendMessage(). Depending on exactly what you plan to do, there may be another, more direct way to automate the browser. But using SendMessage() is definitely not the way to go.
AFAIR, Firefox editboxes aren't really Windows native editboxes but something different.
I can be wrong, but you cannot treat those as normal editboxes. You need to get their window
handle (well, if they have an window handle) and send the message to that.
And I'm talking about editboxes of Firefox (address bar and search bar) itself not the ones
rendered out of HTML.
There are utilities on Windows Platform SDK (download from Microsoft) that can help you identify
the correct target for your SendMessage calls.
You can do this with MSAA. Here is an example: http://www.transl-gunsmoker.ru/2009/08/blog-post.html And in WinSDK there is an analog of WinSpy for MSAA which is called AccExplorer32.