According to MSDN the window handle passed to IDBPromptInitialize::PromptDataSource will be used this way:
hWndParent [in]
The parent window handle for dialog boxes to be displayed. The dialog box will always be centred within this window.
Unfortunately it doesn’t work like this: If the parent form is on a secondary monitor the dialog is shown on the primary monitor on the side adjacent to the secondary monitor. It works fine when the parent form is on the primary monitor.
This is the code I used:
class function TConnectionInfoRec.EditConnectionString(_ParentHandle: HWND; var _ConnectionString: string): Boolean; var DataInit: IDataInitialize; DBPrompt: IDBPromptInitialize; DataSource: IUnknown; InitStr: PWideChar; s: WideString; begin DataInit := CreateComObject(CLSID_DataLinks) as IDataInitialize; if _ConnectionString <> '' then begin s := _ConnectionString; DataInit.GetDataSource(nil, CLSCTX_INPROC_SERVER, PWideChar(s), IUnknown, DataSource); end; DBPrompt := CreateComObject(CLSID_DataLinks) as IDBPromptInitialize; Result := Succeeded(DBPrompt.PromptDataSource(nil, _ParentHandle, DBPROMPTOPTIONS_PROPERTYSHEET, 0, nil, nil, IUnknown, DataSource)); if Result then begin InitStr := nil; DataInit.GetInitializationString(DataSource, True, InitStr); _ConnectionString := InitStr; end; end;
Called as
procedure TForm1.b_TestClick(Sender: TObject); var s: string; begin if TConnectionInfoRec.EditConnectionString(Self.Handle, s) then ed_Result.Text := s; end;
I asked on Google+ for help but nobody could find an error in my code, so it apparently is a bug in Windows.
So I set out to find a workaround: Start a background thread (since the foreground thread is blocked by showing that dialog) that finds the dialog and moves it to the correct place.
Unfortunately GetActiveWindow didn’t return this window, so I resorted to GetForegroundWindow and checked the window’s process id with GetThreadProcessId to make sure I got the correct window. While this worked, it left a bad smell. Using EnumThreadWindow as suggested by Attila Kovacs also worked, so I used that instead.
But while writing this blog post, I reread the GetActiveWindow documentation and noticed the reference to GetGUIThreadInfo which sounded as if it might do exactly what I was looking for. And lo and behold, it actually does. So here is the solution I eventually used:
type TMoveWindowThread = class(TNamedThread) private FParentHandle: HWND; FParentCenterX: Integer; FParentCenterY: Integer; procedure CenterWindow(wHandle: hwnd); protected procedure Execute; override; public constructor Create(_ParentHandle: HWND); end; { TMoveWindowThread } constructor TMoveWindowThread.Create(_ParentHandle: HWND); begin FreeOnTerminate := True; FParentHandle := _ParentHandle; inherited Create(False); end; procedure TMoveWindowThread.CenterWindow(wHandle: hwnd); var Rect: TRect; WindowCenterX: Integer; WindowCenterY: Integer; MoveByX: Integer; MoveByY: Integer; begin GetWindowRect(wHandle, Rect); WindowCenterX := Round(Rect.Left / 2 + Rect.Right / 2); WindowCenterY := Round(Rect.Top / 2 + Rect.Bottom / 2); MoveByX := WindowCenterX - FParentCenterX; MoveByY := WindowCenterY - FParentCenterY; MoveWindow(wHandle, Rect.Left - MoveByX, Rect.Top - MoveByY, Rect.Right - Rect.Left, Rect.Bottom - Rect.Top, False); end; procedure TMoveWindowThread.Execute; var Rect: TRect; MaxTickCount: DWORD; ThreadInfo: TGUIThreadinfo; begin inherited; GetWindowRect(FParentHandle, Rect); FParentCenterX := Round(Rect.Left / 2 + Rect.Right / 2); FParentCenterY := Round(Rect.Top / 2 + Rect.Bottom / 2); ThreadInfo.cbSize := SizeOf(ThreadInfo); MaxTickCount := GetTickCount + 10000; // 10 Seconds should be plenty while MaxTickCount > GetTickCount do begin Sleep(50); if GetGUIThreadInfo(MainThreadID, ThreadInfo) then begin if ThreadInfo.hwndActive <> FParentHandle then begin CenterWindow(ThreadInfo.hwndActive); Exit; end; end; end; end;
This thread is started from EditConnectionString by inserting the following code:
// ... DBPrompt := CreateComObject(CLSID_DataLinks) as IDBPromptInitialize; if _ParentHandle <> 0 then begin // This is a hack to make the dialog appear centered on the parent window // According to https://msdn.microsoft.com/en-us/library/ms725392(v=vs.85).aspx // the dialog should automatically be centered on the passed parent handle, // but if the parent window is not on the primary monitor this does not work. // So, we start a background thread that waits for the dialog to appear and then // moves it to the correct position. TMoveWindowThread.Create(_ParentHandle); end; Result := Succeeded(DBPrompt.PromptDataSource(nil, _ParentHandle, DBPROMPTOPTIONS_PROPERTYSHEET, 0, nil, nil, IUnknown, DataSource)); // ...
Unfortunately the dialog briefly still appears in the wrong position. I found no way around that.
The code is in my dzlib library in unit u_dzConnectionString.