Display Multiple Balloons conditionally using Windows API - System Tray Icon in Delphi - delphi

I wrote a program that can identify outdated software in a Windows System and update them by interacting with the user.
It has a Software Updater Program which displays a System Tray Icon and show Balloon Tips about Available / Downloading Updates and Software installed in the System.
The problem is It can't show multiple Balloon Tips when each task is processing by it. Such as, when an update is available for a Software, it should remember user showing a balloon like An update for Software Name is available. and when user choose to download and minimize it to system tray again, the balloon tip should again show something like Updates are downloading...Click to view the Progress of Downloads.
However I like to know how can I do this by using only one System Tray Icon?
Can I use the NIM_MODIFY Flag again and again to change the Balloon Tip according to the current state of the Program?
I searched about this and I found some examples, but for Visual Studio and C++.
That's how I tried to show Multiple Tips when the Program is running:
unit MainForm-1;
...
const
NIF_INFO = $10;
NIF_MESSAGE = 1;
NIF_ICON = 2;
NOTIFYICON_VERSION = 3;
NIF_TIP = 4;
NIM_SETVERSION = $00000004;
NIM_SETFOCUS = $00000003;
NIIF_INFO = $00000001;
NIIF_WARNING = $00000002;
NIIF_ERROR = $00000003;
NIN_BALLOONSHOW = WM_USER + 2;
NIN_BALLOONHIDE = WM_USER + 3;
NIN_BALLOONTIMEOUT = WM_USER + 4;
NIN_BALLOONUSERCLICK = WM_USER + 5;
NIN_SELECT = WM_USER + 0;
NINF_KEY = $1;
NIN_KEYSELECT = NIN_SELECT or NINF_KEY;
TRAY_CALLBACK = WM_USER + $7258;
PNewNotifyIconData = ^TNewNotifyIconData;
TDUMMYUNIONNAME = record
case Integer of
0: (uTimeout: UINT);
1: (uVersion: UINT);
end;
TNewNotifyIconData = record
cbSize: DWORD;
Wnd: HWND;
uID: UINT;
uFlags: UINT;
uCallbackMessage: UINT;
hIcon: HICON;
szTip: array [0..127] of Char;
dwState: DWORD; /
dwStateMask: DWORD;
szInfo: array [0..255] of Char;
DUMMYUNIONNAME: TDUMMYUNIONNAME;
szInfoTitle: array [0..63] of Char;
dwInfoFlags: DWORD;
end;
type
MainForm-1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
IconData: TNewNotifyIconData;
procedure SysTrayIconMessageHandler(var Msg: TMessage); message TRAY_CALLBACK;
procedure AddSysTrayIcon;
procedure ShowBalloonTips;
procedure DeleteSysTrayIcon;
public
end;
var
MainForm-1: TForm;
implementation
uses
ShellAPI...,.....,;
procedure MainForm-1.SysTrayIconMessageHandler(var Msg: TMessage);
begin
case Msg.lParam of
WM_MOUSEMOVE:;
WM_LBUTTONDOWN:;
WM_LBUTTONUP:;
WM_LBUTTONDBLCLK:;
WM_RBUTTONDOWN:;
WM_RBUTTONUP:;
WM_RBUTTONDBLCLK:;
NIN_BALLOONSHOW:;
NIN_BALLOONHIDE:;
NIN_BALLOONTIMEOUT:
NIN_BALLOONUSERCLICK:;
end;
end;
procedure MainForm-1.AddSysTrayIcon;
begin
IconData.cbSize := SizeOf(IconData);
IconData.Wnd := AllocateHWnd(SysTrayIconMessageHandler);
IconData.uID := 0;
IconData.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
IconData.uCallbackMessage := TRAY_CALLBACK;
IconData.hIcon := Application.Icon.Handle;
IconData.szTip := 'Software Updater is running';
if not Shell_NotifyIcon(NIM_ADD, #IconData) then
ShowMessage('System Tray Icon cannot be created.');
end;
procedure MainForm-1.DisplayBalloonTips;
var
TipInfo, TipTitle: string;
begin
IconData.cbSize := SizeOf(IconData);
IconData.uFlags := NIF_INFO;
if ssHelperState = UpdatesAvailable then TipInfo := 'Updates are available to the programs installed on your Computer' + ' Click to see details.';
if ssHelperState = UpdatesDownloading then TipInfo := 'Updates are downloading in the background. Click to view the details.';
strPLCopy(IconData.szInfo, TipInfo, SizeOf(IconData.szInfo) - 1);
IconData.DUMMYUNIONNAME.uTimeout := 2500;
if ssHelperState = UpdatesAvailable then TipTitle := 'Updates are Available...';
if ssHelperState = UpdatesDownloading then TipTitle := 'Downloading the Updates...';
strPLCopy(IconData.szInfoTitle, TipTitle, SizeOf(IconData.szInfoTitle) - 1);
IconData.dwInfoFlags := NIIF_INFO;
Shell_NotifyIcon(NIM_MODIFY, #IconData);
{Following code is for testing purpose.}
IconData.DUMMYUNIONNAME.uVersion := NOTIFYICON_VERSION;
if not Shell_NotifyIcon(NIM_SETVERSION, #IconData) then
ShowMessage('Setting the Version is Failed.');
end;
procedure MainForm-1.DeleteSysTrayIcon;
begin
DeallocateHWnd(IconData.Wnd);
if not Shell_NotifyIcon(NIM_DELETE, #IconData) then
ShowMessage('Unable to delete System Tray Icon.');
end;
procedure MainForm-1.FormCreate(Sender: TObject);
begin
AddSysTrayIcon;
ShowBalloonTips;
end;
procedure MainForm-1.FormDestroy(Sender: TObject);
begin
DeleteSysTrayIcon;
end;
...
end.
But, this is failing and I keep getting the same Balloon Tip (First One) again and again when the Program is running.......
I don't know how to use NIN_BALLOONSHOW and NIN_BALLOONHIDE Flags correctly. So, Thanks in Advance for Your Important Help.

Why are you declaring everything manually? Delphi 2009 already has declarations for the Shell_NotifyIcon() API. They are in the ShellAPI unit. It declares just about everything you are trying to use, except for the uVersion field (that was added in Delphi 2010). You are not using the guidItem and hBalloonIcon fields, so let's not worry about them here. The uTimeout field exists, and since it is wrapped in a union with uVersion, the data size does not change, so you can just use uTimeout when you want to use uVersion (or you can define your own union and type-cast the field, but that is overkill). You certainly do not need to redeclare the entire API.
You are reusing the same IconData variable each time you call Shell_NotifyIcon(), which is fine, but you are not clearing the szTip and szInfoTitle fields if your helper state is not UpdatesAvailable or UpdatesDownloading, so the tray icon keeps displaying the last tip/balloon you have set. You need to clear those fields when you don't need tips/balloons anymore.
NIN_BALLOONSHOW and NIN_BALLOONHIDE are not flags. They are notifications that are sent to your tray icon's registered HWND. To receive the notifications, you need to fill in the Wnd and uCallbackMessage fields and enable the NIF_MESSAGE flag.
Also, you need to handle the WM_TASKBARCREATED message. If Explorer gets restarted for any reason (crashes, or is killed by the user), the Taskbar gets re-created, so you have to re-add your tray icon again.
Also, make sure your message handler passes any unhandled window messages to DefWindowProc(), or you can lock up the system, or at least your app.
And lastly, Delphi 2009 is a Unicode version of Delphi, but there are some sections of your code that are not handling Unicode correctly. Specifically, when populating szTip and szInfoTitle using StrPLCopy(), you need to use Length() instead of SizeOf(). The copy is expressed in number of characters, not number of bytes.
With that said, try something more like this:
unit MainForm1;
interface
uses
..., ShellAPI;
type
eHelperState = (Idle, UpdatesAvailable, UpdatesDownloading);
MainForm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
TaskbarCreatedMsg: UINT;
IconData: NOTIFYICONDATA;
IconAdded: Boolean;
ssHelperState: eHelperState;
procedure SysTrayIconMessageHandler(var Message: TMessage);
procedure AddSysTrayIcon;
procedure ShowBalloonTips;
procedure DeleteSysTrayIcon;
procedures SetHelperState(NewState: eHelperState);
...
end;
var
MainForm: TForm;
implementation
const
TRAY_CALLBACK = WM_USER + $7258;
{$IF RTLVersion < 21}
NOTIFYICON_VERSION_4 = 4;
{$IFEND}
procedure MainForm.FormCreate(Sender: TObject);
begin
TaskbarCreatedMsg := RegisterWindowMessage('TaskbarCreated');
IconData.cbSize := SizeOf(IconData);
IconData.Wnd := AllocateHWnd(SysTrayIconMessageHandler);
IconData.uID := 1;
AddSysTrayIcon;
end;
procedure MainForm.FormDestroy(Sender: TObject);
begin
DeleteSysTrayIcon;
DeallocateHWnd(IconData.Wnd);
end;
procedure MainForm.AddSysTrayIcon;
begin
IconData.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
IconData.uCallbackMessage := TRAY_CALLBACK;
IconData.hIcon := Application.Icon.Handle;
StrLCopy(IconData.szTip, 'Software Updater is running', Length(IconData.szTip));
IconAdded := Shell_NotifyIcon(NIM_ADD, #IconData);
if not IconAdded then
begin
ShowMessage('Unable to add System Tray Icon.');
Exit;
end;
if CheckWin32Version(5, 0) then
begin
IconData.{$IF RTLVersion >= 21}uVersion{$ELSE}uTimeout{$IFEND} := NOTIFYICON_VERSION_4;
if not Shell_NotifyIcon(NIM_SETVERSION, #IconData) then
ShowMessage('Unable to set version for System Tray Icon.');
end;
end;
procedure MainForm.DisplayBalloonTips;
var
Tip, InfoText, InfoTitle: string;
begin
if not IconAdded then Exit;
case ssHelperState of
UpdatesAvailable: begin
Tip := 'Updates are Available. Click to see details.';
InfoText := 'Updates are available to the programs installed on your Computer. Click to see details.';
InfoTitle := 'Updates are Available';
end;
UpdatesDownloading: begin
Tip := 'Downloading Updates. Click to see details.';
InfoText := 'Updates are downloading in the background. Click to see details.';
InfoTitle := 'Downloading Updates';
end;
else
Tip := 'Software Updater is running';
end;
IconData.uFlags := NIF_TIP or NIF_INFO;
StrPLCopy(IconData.szTip, Tip, Length(IconData.szTip));
StrPLCopy(IconData.szInfo, InfoText, Length(IconData.szInfo));
StrPLCopy(IconData.szInfoTitle, InfoTitle, Length(IconData.szInfoTitle));
IconData.uTimeout := 2500;
IconData.dwInfoFlags := NIIF_INFO;
if not Shell_NotifyIcon(NIM_MODIFY, #IconData) then
ShowMessage('Unable to update System Tray Icon.')
end;
procedure MainForm.DeleteSysTrayIcon;
begin
if IconAdded then
begin
IconAdded := False;
if not Shell_NotifyIcon(NIM_DELETE, #IconData) then
ShowMessage('Unable to delete System Tray Icon.');
end;
end;
procedures MainForm.SetHelperState(NewState: eHelperState);
begin
if ssHelperState <> NewState then
begin
ssHelperState := NewState;
DisplayBalloonTips;
end;
end;
procedure MainForm.SysTrayIconMessageHandler(var Message: TMessage);
begin
if Message.Msg = TRAY_CALLBACK then
begin
case LOWORD(Message.LParam) of
WM_MOUSEMOVE: begin
//...
end;
WM_LBUTTONDBLCLK,
NIN_BALLOONUSERCLICK: begin
// display status window...
end;
WM_CONTEXTMENU,
NIN_KEYSELECT,
NIN_SELECT: begin
// display popup menu at coordinates specified by Msg.WParam...
end;
NIN_BALLOONSHOW:;
NIN_BALLOONHIDE:;
NIN_BALLOONTIMEOUT:;
end;
end
else if (Message.Msg = TaskbarCreatedMsg) and (TaskbarCreatedMsg <> 0) then
begin
IconAdded := False;
AddSysTrayIcon;
DisplayBalloonTips;
end
else begin
Message.Result := DefWindowProc(IconData.Wnd, Message.Msg, Message.WParam, Message.LParam);
end;
end;
...
end.

Related

How to set `ElevationRequired` for `TBitBtn`

I need to mark a TBitBtn (not TButton), that the button action requires elevation. I set ElevationRequired to True, but I do not get the shield icon.
To reproduce, place a TButton and a TBitBtn on a form:
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.ElevationRequired := True;
BitBtn1.ElevationRequired := True;
end;
Button1 is displayed with shield icon, BitBtn1 is not.
This is not possible.
A VCL TBitBtn is an owner-drawn Win32 BUTTON control. You can see that here:
procedure TBitBtn.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params do Style := Style or BS_OWNERDRAW;
end;
Hence, a TBitBtn is not drawn by Windows but manually by the Pascal code in Vcl.Buttons.pas. Specifically, TBitBtn.DrawItem(const DrawItemStruct: TDrawItemStruct) does the painting.
And here you can see that there is no mentioning of ElevationRequired.
Hence, TBitBtn doesn't support this.
In general, don't use TBitBtn; use TButton to get the native Win32 button.
As ElevationRequired is not implemented for TBitBtn (see Andreas' answer). I ended up drawing the shield icon via this procedure (Vista+):
procedure MarkElevationRequired(ABitBtn: TBitBtn);
var
Icon: TIcon;
begin
Assert(Assigned(ABitBtn));
//---
try
Icon := TIcon.Create;
try
Icon.Handle := GetSystemIcon(SIID_SHIELD, TSystemIconSize.Small); //see WinApi.ShellApi
ABitBtn.Glyph.Assign(Icon);
finally
Icon.Free;
end;
except
//CreateSystemIcon throws an exception for <WinVista
end;
end;
with
/// Get system icon for SIID, see https://learn.microsoft.com/de-de/windows/win32/api/shellapi/ne-shellapi-shstockiconid
/// Works for Win Vista or better
/// see https://community.idera.com/developer-tools/b/blog/posts/using-windows-stock-icons-in-delphi
function GetSystemIcon(Id: integer; Size: TSystemIconSize = TSystemIconSize.Large;
Overlay: Boolean = False; Selected: Boolean = False): HICON;
var
Flags: Cardinal;
SSII: TSHStockIconInfo;
ResCode: HResult;
begin
if not TOSVersion.Check(6, 0) then
raise Exception.Create('SHGetStockIconInfo is only available in Win Vista or better.');
case Size of
TSystemIconSize.Large: Flags := SHGSI_ICON or SHGSI_LARGEICON;
TSystemIconSize.Small: Flags := SHGSI_ICON or SHGSI_SMALLICON;
TSystemIconSize.ShellSize: Flags := SHGSI_ICON or SHGSI_SHELLICONSIZE;
end;
if Selected then
Flags := Flags OR SHGSI_SELECTED;
if Overlay then
Flags := Flags OR SHGSI_LINKOVERLAY;
SSII.cbSize := SizeOf(SSII);
ResCode := SHGetStockIconInfo(Id, Flags, SSII);
if ResCode <> S_OK then
begin
if ResCode = E_INVALIDARG then
raise Exception.Create(
'The stock icon identifier [' + IntToStr(id) + '] is invalid')
else
raise Exception.Create(
'Error calling GetSystemIcon: ' + IntToStr(ResCode));
end
else
Result := SSII.hIcon;
end;

SHGetPathFromIDList (Delphi) - False Positive Issue

I am using Kaspersky Internet Security 2018. But when I compile my Delphi application with these codes, my anti-virus application will remove the compiled exe:
function BrowseForFolder(var dpFolder: String; dpTitle: String): Boolean;
var
dpBrowseInfo: TBrowseInfo;
dpDisplayName: array[0..255] of Char;
dpItemIDList: PItemIDList;
begin
FillChar(dpBrowseInfo, sizeof(dpBrowseInfo), #0);
with dpBrowseInfo do begin
hwndOwner := Application.Handle;
pszDisplayName := #dpDisplayName[0];
lpszTitle := PChar(dpTitle);
dpItemIDList := SHBrowseForFolder(dpBrowseInfo);
ulFlags := BIF_RETURNONLYFSDIRS and BIF_VALIDATE;
end;
if Assigned(dpItemIDList) then begin
if SHGetPathFromIDList(dpItemIDList, dpDisplayName) then begin
dpFolder := dpDisplayName;
Result := True;
end else begin
Result := False;
end;
end;
end;
What should I do to pop up "Browse folder" menu other than adding my app to whitelist?
I have tried every "Browse Folder" dialog types and I have realized that my anti-virus software only allows Vcl.FileCtrl.TSelectDirExtOpt (also it hates other FileCtrl dialogs).
So, I fixed my issue myself like this:
procedure TForm1.gözatDüğmesiClick(Sender: TObject);
begin
if not (menü4CB = '1') then begin
if not (SelectDirectory('Kurulum programının yedekleneceği klasörü seçin',
GetSpecialFolderPathFromCSIDL($0011), adres, [sdNewFolder, sdNewUI], nil) = False) then begin
adresÇubuğu.Text := adres;
end else begin
end;
end;
end;
Thank Remy Lebeau anyway for caring about me.

VirtualKeyboard not Show when focus Edit fields in Firemonkey project

I have a Firemonkey multi device project in Delphi 10 Seattle where the user can get a screen at the start of the app. Here the user needs to fill in 2 fields. But when I click on the edit fields the Virtual Keyboard isn't shown. If I skip this screen at start and call it later then the Virtual Keyboard is shown. This is done in the same way too.
I found sort of a solution:
When i click on the edit fields i call show VirtualKeyboard myself. The only problem is that the cursor isn't shown in the edit field.
Is there a way to place the cursor myself? Or does anyone know how to solve the Virtual Keyboard not showing problem in an other way?
Problem is on both Android and iOS
In the code below you can see the initial form create. The problem is when in ConnectFromProfile method the actCreateNewProfileExecute is called. There it will call a new form. In that form(TfrmProfile) the virtual keyboard isn't shown. I also call this form with another action and then it works fine.
procedure TfrmNocoreDKS.FormCreate(Sender: TObject);
begin
Inherited;
System.SysUtils.FormatSettings.ShortDateFormat := 'dd/mm/yyyy';
CheckPhone;
ConnectfromProfile;
if not Assigned(fProfileAction) then
ConnectDatabase
Else
lstDocuments.Enabled := False;
{$IFDEF ANDROID}
ChangeComboBoxStyle;
{$ENDIF}
end;
procedure TfrmNocoreDKS.ConnectfromProfile;
begin
fdmProfileConnection := TdmConnection.Create(nil);
fdmProfileConnection.OpenProfileDb;
fdmProfileConnection.LoadProfiles;
if fdmProfileConnection.Profiles.Count = 0 then
begin // Createdefault Profile
fProfileAction := actCreateNewProfileExecute;
end
else if fdmProfileConnection.Profiles.Count = 1 then
begin // one profile load connection;
fProfileAction := nil;
fCurrentProfile := fdmProfileConnection.Profiles.Items[0];
end
else
begin // multiple profiles choose connection;
fProfileAction := SelectProfileOnStartUp;
end;
end;
procedure TfrmNocoreDKS.FormShow(Sender: TObject);
begin
if Assigned(fProfileAction) then
fProfileAction(Self);
end;
procedure TfrmNocoreDKS.actCreateNewProfileExecute(Sender: TObject);
var
profilename, databasename, pathname: string;
prf: TfrmProfile;
begin
prf := TfrmProfile.Create(nil);
prf.Data := fdmProfileConnection.Profiles;
prf.ShowModal(
procedure(ModalResult: TModalResult)
begin
if ModalResult = mrOk then
begin
profilename := prf.edtProfilename.Text;
databasename := prf.edtDatabaseName.Text;
{$IFDEF IOS}
pathname := System.IOUtils.TPath.GetDocumentsPath;
{$ENDIF}
{$IFDEF ANDROID}
pathname := System.IOUtils.TPath.GetDocumentsPath;
{$ENDIF}
{$IFDEF WIN32}
pathname := ExtractFilePath(ParamStr(0)) + '\Data';
{$ENDIF}
FDSQLiteBackup1.Database := System.IOUtils.TPath.Combine(pathname,
'default.sqlite3'); // Default Database
FDSQLiteBackup1.DestDatabase := System.IOUtils.TPath.Combine(pathname,
databasename + '.sqlite3');
FDSQLiteBackup1.Backup;
fdmProfileConnection.AddProfile(databasename + '.sqlite3', profilename);
fdmProfileConnection.LoadProfiles;
fCurrentProfile := fdmProfileConnection.Profiles.Items[0];
connectDatabase;
end else
Application.Terminate;
end);
end;
Do not show any additional forms in MainForm.OnCreate/OnShow. Trying this on iOS 9.2 freeze app at "launch screen".
Instead of this, show new form asynchronously, like this:
procedure TForm4.FormShow(Sender: TObject);
begin
TTask.Run(procedure
begin
TThread.Synchronize(nil, procedure // work with visual controls - only throught Synchronize or Queue
begin
Form5:=TForm5.Create(Application);
Form5.ShowModal;
end)
end);
end;
of cource, you can separate this code to external procedures:
procedure ShowMyForm;
begin
Form5:=TForm5.Create(Application);
Form5.ShowModal;
end;
procedure TaskProc;
begin
TThread.Synchronize(nil, ShowMyForm);
end;
procedure TForm4.FormShow(Sender: TObject);
begin
TTask.Run(TaskProc);
end;
========
Another way - do not use any additional forms. Create frame and put it (at runtime) on MainForm with Align = Contents. After all needed actions - hide or release (due to ARC dont forget to set nil to frame variable) this frame.

How do I position a TOpenDialog

I have got a Delphi application which uses TOpenDialog to let the user select a file. By default, the open dialog is displayed centered on the current monitor which nowadays can be "miles" away from the application's window. I would like the dialog to be displayed centered on the TOpenDialog's owner control, failing that, I'd settle for the application's main window.
The following code kind of works, it is derived from TJvOpenDialog which gave me some hint on how to do it:
type
TMyOpenDialog = class(TJvOpenDialog)
private
procedure SetPosition;
protected
procedure DoFolderChange; override;
procedure WndProc(var Msg: TMessage); override;
end;
procedure TMyOpenDialog.SetPosition;
begin
var
Monitor: TMonitor;
ParentControl: TWinControl;
Res: LongBool;
begin
if (Assigned(Owner)) and (Owner is TWinControl) then
ParentControl := (Owner as TWinControl)
else if Application.MainForm <> nil then
ParentControl := Application.MainForm
else begin
// this code was already in TJvOpenDialog
Monitor := Screen.Monitors[0];
Res := SetWindowPos(ParentWnd, 0,
Monitor.Left + ((Monitor.Width - Width) div 2),
Monitor.Top + ((Monitor.Height - Height) div 3),
Width, Height,
SWP_NOACTIVATE or SWP_NOZORDER);
exit; // =>
end;
// this is new
Res := SetWindowPos(GetParent(Handle), 0,
ParentControl.Left + ((ParentControl.Width - Width) div 2),
ParentControl.Top + ((ParentControl.Height - Height) div 3),
Width, Height,
SWP_NOACTIVATE or SWP_NOZORDER);
end;
procedure TMyOpenDialog.DoFolderChange
begin
inherited DoFolderChange; // call inherited first, it sets the dialog style etc.
SetPosition;
end;
procedure TMyOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_ENTERIDLE: begin
// This has never been called in my tests, but since TJVOpenDialog
// does it I figured there may be some fringe case which requires
// SetPosition being called from here.
inherited; // call inherited first, it sets the dialog style etc.
SetPosition;
exit;
end;
end;
inherited;
end;
"kind of works" meaning that the first time the dialog is opened, it is displayed centered on the owner form. But, if I then close the dialog, move the window and open the dialog again, SetWindowPos doesn't seem to have any effect even though it does return true. The dialog gets opened at the same position as the first time.
This is with Delphi 2007 running on Windows XP, the target box is also running Windows XP.
The behaviour you describe I can reproduce only by passing a bogus value for the OwnerHwnd to the dialog's Execute method.
This window handle is then passed on to the underlying Windows common control and in fact you will have other problems with your dialogs if you do not set it to the handle of the active form when the dialog is shown.
For example when I call Execute and pass Application.Handle, the dialog always appears on the same window, in a rather bizarre location, irrespective of where my main form is.
When I call Execute and pass the handle to my main form, the dialog appears on top of the main form, slightly shifted to the right and down. This is true no matter which monitor the form is on.
I am using Delphi 2010 and I don't know whether or not you have the overloaded version of Execute available on your version of Delphi. Even if you don't have that available, you should still be able to create a derived class that will pass a more sensible value for OwnerHwnd.
Although I don't have conclusive 100% evidence that this is your problem, I think that this observation will lead you to a satisfactory resolution.
TJvOpenDialog is a descendant of TOpenDialog, hence you should run your placement call after the VCL centers the dialog. The VCL does it in response to a CDN_INITDONE notification. Responding to a WM_SHOWWINDOW message is too early, and in my tests the window procedure never receives a WM_ENTERIDLE message.
uses
commdlg;
[...]
procedure TJvOpenDialog.DoFolderChange;
begin
inherited DoFolderChange;
// SetPosition; // shouldn't be needing this, only place the dialog once
end;
procedure TJvOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_NOTIFY: begin
if POFNotify(Msg.LParam)^.hdr.code = CDN_INITDONE then begin
inherited; // VCL centers the dialog here
SetPosition; // we don't like it ;)
Exit;
end;
end;
inherited;
end;
or,
procedure TJvOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_NOTIFY: if POFNotify(Msg.LParam)^.hdr.code = CDN_INITDONE then
Exit;
end;
inherited;
end;
to have the dialog where the OS puts it, it actually makes sense.
I tried both examples without success ... but here is a symple solution:
type
TPThread = class(TThread)
private
Title : string;
XPos,YPos : integer;
protected
procedure Execute; override;
end;
TODialogPos = class(Dialogs.TOpenDialog)
private
Pt : TPThread;
public
function Execute(X,Y : integer):boolean; reintroduce;
end;
TSDialogPos = class(Dialogs.TSaveDialog)
private
Pt : TPThread;
public
function Execute(X,Y : integer):boolean; reintroduce;
end;
implementation
procedure TPThread.Execute;
var ODhandle : THandle; dlgRect : TRect;
begin
ODhandle:= FindWindow(nil, PChar(Title));
while (ODhandle = 0) do ODhandle:= FindWindow(nil, PChar(Title));
if ODhandle <> 0 then begin
GetWindowRect(ODhandle, dlgRect);
with dlgRect do begin
XPos:=XPos-(Right-Left) div 2;
YPos:=YPos-(Bottom-Top) div 2;
MoveWindow(ODhandle, XPos, YPos,Right-Left,Bottom-Top,True);
SetWindowPos(ODhandle, HWND_TOP, XPos, YPos, 0, 0, SWP_NOSIZE);
end
end;
DoTerminate;
end;
function TODialogPos.Execute(X,Y : integer):boolean;
begin
Pt:= TPThread.Create(False);
Pt.XPos := X;
Pt.YPos := Y;
if Self.Title <> '' then
Pt.Title := Self.Title
else begin
Self.Title := 'Open';
Pt.Title := Self.Title;
end;
Result:= inherited Execute;
Pt.Free;
end;
function TSDialogPos.Execute(X,Y : integer):boolean;
begin
Pt:= TPThread.Create(False);
Pt.XPos := X;
Pt.YPos := Y;
if Self.Title <> '' then
Pt.Title := Self.Title
else begin
Self.Title := 'Save';
Pt.Title := Self.Title;
end;
Result:= inherited Execute;
Pt.Free;
end;
...
Use it like (for example center Save Dilaog in Form1) the following code:
type
TForm1 = class(TForm)
...
...
dlgSave:=TSDialogPos.Create(self);
dlgSave.Filter := 'Symbol File (*.asy)|*.asy';
dlgSave.Options:=[ofHideReadOnly,ofExtensionDifferent,ofPathMustExist,
ofCreatePrompt,ofNoTestFileCreate,ofNoNetworkButton,
ofOldStyleDialog,ofEnableIncludeNotify,ofEnableSizing];
...
with dlgSave do begin
Title :='Copy : [ *.asy ] with Attributes';
InitialDir:= DirectoryList.Directory;
FileName:='*.asy';
end;
...
with Form1 do
if dlgSave.Execute(Left+Width div 2, Top+Height div 2) then begin
// your code
end;
...
dlgSave.Free
...

How to switch the 'current' directory of a TOpenDialog in an OnTypeChange handler? (is it possible at all?)

Depending on the chosen filter, I'd like the OpenDialog to 'look' in different directries.
Something like:
procedure TForm1.FileOpen1OpenDialogTypeChange(Sender: TObject);
// This does not work as intended...
var
Dialog: TOpenDialog;
FilterIndex: Integer;
FilterExt: string;
Path: string;
begin { TForm1.actFileOpenOpenDialogTypeChange }
Dialog := Sender as TOpenDialog;
FilterIndex := Dialog.FilterIndex;
FilterExt := ExtFromFilter(Dialog.Filter, FilterIndex);
GetIniPathForExtension(FilterExt, Path);
if DirectoryExists(Path) and
(Path <> IncludeTrailingPathDelimiter(Dialog.InitialDir)) then
begin
// those two statements don't have the desired effect
// but illustrate what is meant to happen:
Dialog.FileName := Path + '*' + FilterExt;
Dialog.InitialDir := Path;
end;
end; { TForm1.actFileOpenOpenDialogTypeChange }
I can't find any way to let the dialog update itself to the new directory.
I've tried calling OpenDialog.Execute, but that starts another OpenDialog without closing the current one...
Some time ago I have looked after exactly that sort of thing, but couldn't find a solution either. Nowadays I'm glad not to implement it anyway for the following reason:
Imagine a user executes the open dialog. He knows where to find the required file and navigates to that folder. Now he can't see the file and realizes that the filter is set wrong. He changes the filter and naturally expects the folder to stay the same.
Try and make some observations: in most of the cases a user first selects the folder and after that the file type.
While the below is not exactly elegant, tested with 2K, XP, Vista and 7, it seems to work. The idea is to use the dialog's behavior that, when a valid directory is entered into the file name box, if 'Open' button is pressed, the dialog switches to that folder.
It does not work with 'Vista style' dialogs, I don't have any acquaintance with the Common Item Dialog. So the UseLatestCommonDialogs must be set to false before showing a dialog. Also note that the OnTypeChange event is not fired when the dialog is initially launched, one can set the FilterIndex and InitialDir before showing the dialog.
type
TForm1 = class(TForm)
Button1: TButton;
OpenDialog1: TOpenDialog;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure OpenDialog1TypeChange(Sender: TObject);
procedure OpenDialog1FolderChange(Sender: TObject);
private
FDlgCleanUp: Boolean;
FDlgFocusCtrl: HWnd;
FSaveDlgFName: array [0..255] of Char;
public
end;
[...]
uses
CommDlg, Dlgs;
procedure TForm1.Button1Click(Sender: TObject);
begin
if OpenDialog1.Execute then
ShowMessage(OpenDialog1.FileName);
end;
type
TFileExt = (feText = 1, feRichText, feDocument);
const
FileExts: array [TFileExt] of string = ('txt', 'rtf', 'doc');
FileExtDesc: array [TFileExt] of string =
('text (*.txt)', 'rich text (*.rtf)', 'document (*.doc)');
procedure TForm1.FormCreate(Sender: TObject);
var
fe: TFileExt;
begin
OpenDialog1.Options := OpenDialog1.Options - [ofOldStyleDialog];
NewStyleControls := True;
UseLatestCommonDialogs := False;
OpenDialog1.Filter := '';
for fe := Low(FileExts) to High(FileExts) do
OpenDialog1.Filter := OpenDialog1.Filter +
FileExtDesc[fe] + '|*.' + FileExts[fe] + '|';
end;
function GetIniPathForExtension(const Ext: string): string;
begin
// Get corresponding path from an ini file....
Result := ExtractFilePath(Application.ExeName) + Ext;
end;
procedure TForm1.OpenDialog1TypeChange(Sender: TObject);
var
Dialog: TOpenDialog;
Dlg: HWnd;
Path: string;
begin
Dialog := Sender as TOpenDialog;
Dlg := GetParent(Dialog.Handle);
Path := GetIniPathForExtension(FileExts[TFileExt(Dialog.FilterIndex)]);
ForceDirectories(Path);
// remember what's in file name, have to put it back later
GetDlgItemText(Dlg, cmb13, #FSaveDlgFName, 256);
SendMessage(GetDlgItem(Dlg, cmb13), WM_SETREDRAW, 0, 0); // reduce flicker
FDlgFocusCtrl := GetFocus;
// set file name to new folder
SendMessage(Dlg, CDM_SETCONTROLTEXT, cmb13, Longint(PChar(Path)));
// weird OS: windows - the below is only necessary for XP. 2K, Vista and 7
// clicks fine without it, XP does not!
windows.SetFocus(GetDlgItem(Dlg, IDOK));
// do not cleanup here, with Vista and 7 folder change seems to happen
// asynchronously - it might occur later than setting the file name and that
// clears/reverts the edit box.
FDlgCleanUp := True;
// click 'Open' to change to folder
SendMessage(GetDlgItem(Dlg, IDOK), BM_CLICK, IDOK, 0);
end;
procedure TForm1.OpenDialog1FolderChange(Sender: TObject);
var
Dlg: HWnd;
begin
// set the file name and focus back
if FDlgCleanup then begin // do not intervene if we didn't cause the change
Dlg := GetParent((Sender as TOpenDialog).Handle);
SendMessage(GetDlgItem(Dlg, cmb13), WM_SETREDRAW, 1, 0);
SetDlgItemText(Dlg, cmb13, #FSaveDlgFName);
windows.SetFocus(FDlgFocusCtrl);
end;
FDlgCleanup := False;
end;
One possibility:
var
ShowAfterClose: boolean = false;
MemFilterIndex: integer;
procedure TForm1.Import1Click(Sender: TObject);
begin
//...
with OpenDialogImport do
repeat
if Execute then
begin
ReadImportedFile(FileName); //Do action
exit;
end else begin
if not ShowAfterClose then //Check ShowAfterClose
exit;
ShowAfterClose := false; //Set ShowAfterClose false
FilterIndex := MemFilterIndex; //Copy MemFilterIndex
end;
until false;
//...
end;
procedure TForm1.OpenDialogImportTypeChange(Sender: TObject);
begin
PostMessage(TOpenDialog(Sender).handle,
WM_KEYDOWN, VK_ESCAPE , 0); //Cancel dialog
TOpenDialog(Sender).InitialDir := 'C:\'; //Set new directory
MemFilterIndex := TOpenDialog(Sender).FilterIndex; //Remember filter index
ShowAfterClose := True; //ShowAfterClose = True
end;
I'll agree with everyone else to date... it's VERY BAD user interface design to change things without asking the user, and/or against the user's wishes.

Resources