Quantcast
Channel: twm's blog
Viewing all 650 articles
Browse latest View live

Fixing SelectDirectory function

$
0
0

The Delphi VCL comes with several overloaded SelectDirectory functions declared in FileCtrl, one of which uses the ShBrowseForFolder Windows API function. It passes a callback function which tries to do the following:

  1. Position the dialog on the monitor on which the application’s main form is shown.
  2. Center the dialog on that monitor
  3. Select the given directory in the tree view.

Unfortunately in Delphi 2007 it fails for 2.5 of these points and even in Delphi 10.1 Berlin only one bug has been fixed:

  1. Since it fails to take into account that a monitor left or on top of the primary monitor has negative coordinates, the dialog will appear on the primary monitor if the application’s main form is located on such a monitor. This has been fixed in Delphi 10.1 (possibly earlier, I didn’t check)
  2. Centering the dialog on the monitor fails, at least on both of my computers running Windows 8.1, but I seem to remember that the same problem occurs on several other computers running Windows 7 and XP. This is still the case with Delphi 10.1.
  3. Selecting the given directory works, kind of, but if the tree view contains many entries, the selected entry will not be visible. You’ll have to scroll down to see it. This is still the case with Delphi 10.1

So, why is that? The main problem is that Delphi tries to change the dialog before it is fully visible. Setting the position fails because of this as well as making the selected directory visible.

My bugfix isn’t pretty, I must admit. It just defers these changes until the dialog is fully visible. This has the disadvantage the the user will see it popping up at the wrong place first before its position is corrected and the selected directory becomes visible.

But here it comes anyway:

First, you need to copy the code of SelectDirectory from FileCtrl, the one with the following signature:

function SelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts; Parent: TWinControl): Boolean;

In addition you need the function SelectDirCB which is declared immediately above SelectDirectory.

Copy it to a separate unit (or, if you are brave, directly modify FileCtrl).

Above these functions, add the following code (which is based on the existing TSelectDirCallback class declared an implemented in FileCtrl but heavily modified):


uses
  Windows,
  SysUtils,
  FileCtrl,
  Controls,
  Consts,
  ShlObj,
  ActiveX,
  Dialogs,
  Forms,
  Classes,
  u_dzVclUtils;

// ....

type
  TSelectDirCallback = class(TObject)
  private
    FParent: TWinControl;
    FDirectory: string;
    FInitialized: Boolean;
    FPositioned: Boolean;
  protected
    function SelectDirCB(Wnd: HWND; uMsg: UINT; lParam, lpData: lParam): Integer;
  public
    constructor Create(const ADirectory: string; _Parent: TWinControl);
  end;

{ TSelectDirCallback }

constructor TSelectDirCallback.Create(const ADirectory: string; _Parent: TWinControl);
begin
  inherited Create;
  FParent := _Parent;
  FDirectory := ADirectory;
  FInitialized := False;
  FPositioned := False;
end;

function TSelectDirCallback.SelectDirCB(Wnd: HWND; uMsg: UINT; lParam, lpData: lParam): Integer;

  procedure SetDialogPosition;
  var
    Rect: TRect;
    Monitor: TMonitor;
    ltwh: TRectLTWH;
    RefLtwh: TRectLTWH;
    frm: TCustomForm;
  begin
    GetWindowRect(Wnd, Rect);
    if Assigned(FParent) then begin
      frm := GetParentForm(FParent);
      Monitor := Screen.MonitorFromWindow(frm.Handle);
      RefLtwh.Assign(frm.BoundsRect);
    end else begin
      if Assigned(Application.MainForm) then
        Monitor := Screen.MonitorFromWindow(Application.MainForm.Handle)
      else
        Monitor := Screen.MonitorFromWindow(0);
      RefLtwh.Assign(Monitor.BoundsRect);
    end;
    ltwh.Assign(Rect);
    ltwh.Left := RefLtwh.Left + RefLtwh.Width div 2 - ltwh.Width div 2;
    ltwh.Top := RefLtwh.Top + RefLtwh.Height div 2 - ltwh.Height div 2;
    TMonitor_MakeFullyVisible(Monitor, ltwh);
    SetWindowPos(Wnd, 0, ltwh.Left, ltwh.Top, ltwh.Width, ltwh.Height, SWP_NOZORDER);
  end;

  procedure SelectDirectory;
  begin
    if FDirectory <> '' then begin
      // we use PostMessage to asynchronously select the directory
      PostMessage(Wnd, BFFM_SETSELECTION, Windows.wParam(True), Windows.lParam(PChar(FDirectory)));
    end;
  end;

begin
  Result := 0;
  if uMsg = BFFM_INITIALIZED then begin
    FInitialized := True;
    // It's too early to set the dialog position.
    // That only works once the dialog is visible.
    // But we must select the current directory, once here and once again
    SelectDirectory;
  end else if uMsg = BFFM_VALIDATEFAILED then begin
    MessageDlg(Format(SInvalidPath, [PChar(lParam)]), mtError, [mbOK], 0);
    Result := 1;
  end else if uMsg = BFFM_SELCHANGED then begin
    if FInitialized and not FPositioned then begin
      FPositioned := True;
      SetDialogPosition;
      // The first call to SelectDirectory only selects it but does not scrol the dialog
      // to make it visible. That's what this second call is for.
      SelectDirectory;
    end;
  end;
end;

Then modify SelectDirectory to pass the additional parameter Parent to the constructor:

// Initialization of the BrowseInfo record is done above
      SelectDirCallback := TSelectDirCallback.Create(Directory, Parent);
      try
        BrowseInfo.lParam := Integer(SelectDirCallback);

The TRectLTWH record as well as the TMonitor_MakeFullyVisible procedure is declared in u_dzVclUtils.

Ok, so, what does this do?

First of all, it does not try to position the dialog when receiving the BFFM_INITIALIZED message. In that state, the dialog isn’t yet visible and apparently cannot be positioned correctly. But it’s still necessary to set the selected directory at this time. My code also sets the FInitialized flag so it knows which of the multiple BFFM_SELCHANGED message to use to do the rest. And that’s actually all there is to it: When handling the first BFFM_SELCHANGED message after FInitialized was set, it sets the dialog position and selects the directory again.

Some details:

Fixing the first bug (not taking the Monitor.Left / .Top coordinates into account is done here:

    ltwh.Left := RefLtwh.Left + RefLtwh.Width div 2 - ltwh.Width div 2;
    ltwh.Top := RefLtwh.Top + RefLtwh.Height div 2 - ltwh.Height div 2;

Note that RefLtwh is initialized with the Monitor’s Left, Top, Width and Height properties.

Fixing the second bug is also part of that code and it works because the dialog is already visible.

Fixing the third bug involves using

PostMessage(Wnd, BFFM_SETSELECTION, Windows.wParam(True), Windows.lParam(PChar(FDirectory)));

rather than SendMessage and posting this messages twice. Once for selecting the directory and once again to let the dialog scroll so it becomes visible.

In addition to fixing these bugs, this code adds a feature: It tries to centre the dialog on the form passed as parent. No idea why Borland/Codegear/Embarcadero didn’t do that.

You can find this bugfix in my dzlib svn repository on SourceForge


Fixing the SelectDirectory fix

$
0
0

In my last blog post I wrote

My bugfix isn’t pretty, I must admit. It just defers these changes until the dialog is fully visible. This has the disadvantage the the user will see it popping up at the wrong place first before its position is corrected and the selected directory becomes visible.

David Millington commented on my G+ post:

I haven’t looked at the code to see how the dialog itself is created or shown, but is it possible to either create it invisibly (not shown), position it, then show via ShowWindow or something, or hook its window proc to intercept its initial move message, or set its transparency to 0 and move and then make opaque, or some other hack?

Which got me thinking. Why not indeed try to hide the dialog while it hasn’t been positioned yet and show it once that has happened?

I tried it but it didn’t work: The dialog still became visible before my code could set its position correctly.

After some more debugging it turned out that the problem isn’t actually that the dialog isn’t visible when the BFFM_INITIALIZED message is sent. The problem is, that the dialog changes its size after the BFFM_INITIALIZED message was handled, so setting its position there got it wrong because it calculated it based on the wrong size. So, setting it again was necessary.

But what if I hook the WindowProc (aka subclass the window), wait for the WM_SIZE message being sent and only then set the dialog position?

That seems to be the solution. I have tried it on my Windows 8.1 machine and it worked every single time. Some preliminary code is already in the dzlib svn, but it needs to be polished some more until I can present it here.

The ultimate bugfix for SelectDirectory

$
0
0

OK, here it comes, the ultimate bugfix for the FileCtrl.SelectDirectory function. ;-)

I blogged about it before: The SelectDirectory function in Delphi’s FileCtrl unit has several bugs. My first approach on fixing these, while working, was ugly because the user could see that the dialog position changed after it was shown initially at a different position. My initial guess why the code in FileCtrl failed to set the position correctly was wrong. It’s not because the dialog is not yet visible, but because its size is being changed later in the initialization process so the size used to calculate its position centered on the monitor was wrong.

My bugfix works like this:

  • During the initialization subclass the window of the dialog by setting a new WindowProc function.
  • Wait for the first WM_SIZE message.
  • Now, that the dialog has its final size, position it
  • Un-subclass the window, we don’t need the WindowProc any more

But there is more:

Since the caller passed a Parent to SelectDirectory, why not use that parameter? The user expects modal dialogs to pop up on top of the window he is currently using, so why not position the dialog centered on the Parent rather than the monitor? To do that correctly we also must position it so that it is fully visible on a particular monitor. Nobody wants a dialog crossing monitor borders or being partially obscured by the task bar. For that I have added some overloaded TMonitor_MakeVisible procedures to u_dzVclUtils and call one of them after centering the dialog on the parent.

Of course, the bug of the given directory not being visible, which was already fixed in my first try, is still fixed.

Here is the code:

///<summary>
/// Fixes the SelectDirectory function of Delphi 2007 (not sure whether it needs fixing
/// in later versions)</summary>
unit u_dzSelectDirectoryFix;

interface

uses
  Windows,
  SysUtils,
  FileCtrl,
  Controls;

///<summary>
/// Bugixed version of the FilCtrl SelectDirectory function with identical parameters
/// The following bugs have been fixed:
/// 1. Positioning the dialog works for all tested monitor combinations. This means
///    not only that the correct monitor is being selected (which is already fixed
///    Delphi 10.1 (and possibly earlier) but also that it is correctly centered
///    on that monitor.
/// 2. The given directory is not only selected but the tree view is also scrolled
///    to make the entry visible.
/// In addition to that, if passing a Parent parameter <> nil, the dialog will be
/// centered on that parent (or the form the parent belongs to), taking the monitor
/// work area into account. </summary>
function SelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil): Boolean;

///<summary>
/// Same as SelectDirectory above but with a different name so it can be called explicitly rather
/// than relying on the order of units in the uses clause. </summary>
function dzSelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil): Boolean;

implementation

uses
  Consts,
  ShlObj,
  ActiveX,
  Dialogs,
  Forms,
  Messages,
  Classes,
  u_dzVclUtils;

type
  TSelectDirCallback = class(TObject)
  private
    FWndProcInstanceStub: Pointer;
    FWndProcPrevious: TFNWndProc;
    FWnd: HWND;
    FParent: TWinControl;
    FDirectory: string;
    FInitialized: Boolean;
    FPositioned: Boolean;
    procedure WndProcSubClassed(var _Msg: TMessage);
    procedure SetDialogPosition;
    procedure SubClass(_Wnd: HWND);
    procedure UnsubClass;
  protected
    function SelectDirCB(_Wnd: HWND; _uMsg: UINT; _lParam, _lpData: lParam): Integer;
  public
    constructor Create(const _Directory: string; _Parent: TWinControl);
  end;

{ TSelectDirCallback }

constructor TSelectDirCallback.Create(const _Directory: string; _Parent: TWinControl);
begin
  inherited Create;
  FParent := _Parent;
  FDirectory := _Directory;
end;

// subclass the given window by replacing its WindowProc

procedure TSelectDirCallback.SubClass(_Wnd: HWND);
begin
  if FWndProcPrevious <> nil then
    Exit;
  FWnd := _Wnd;
  FWndProcPrevious := TFNWndProc(GetWindowLong(_Wnd, GWL_WNDPROC));
  FWndProcInstanceStub := MakeObjectInstance(WndProcSubClassed);
  SetWindowlong(_Wnd, GWL_WNDPROC, NativeInt(FWndProcInstanceStub));
end;

// un-subclass the window by restoring the previous WindowProc

procedure TSelectDirCallback.UnsubClass;
begin
  if FWndProcPrevious <> nil then begin
    SetWindowlong(FWnd, GWL_WNDPROC, NativeInt(FWndProcPrevious));
    FreeObjectInstance(FWndProcInstanceStub);
    FWndProcPrevious := nil;
    FWndProcInstanceStub := nil;
  end;
end;

// The WindowsProc method set by sublcassing the window.
// Waits for the first WM_SIZE message, sets the dialog position
// and un-subclasses the window.

procedure TSelectDirCallback.WndProcSubClassed(var _Msg: TMessage);
begin
  if (_Msg.Msg = WM_SIZE) then begin
    SetDialogPosition;
    _Msg.Result := CallWindowProc(FWndProcPrevious, FWnd, _Msg.Msg, _Msg.WParam, _Msg.lParam);
    UnsubClass;
  end;
  _Msg.Result := CallWindowProc(FWndProcPrevious, FWnd, _Msg.Msg, _Msg.WParam, _Msg.lParam);
end;

procedure TSelectDirCallback.SetDialogPosition;
var
  Rect: TRect;
  Monitor: TMonitor;
  ltwh: TRectLTWH;
  RefLtwh: TRectLTWH;
  frm: TCustomForm;
begin
  GetWindowRect(FWnd, Rect);
  if Assigned(FParent) then begin
    // this is new: Center on the parent form if a parent was given
    frm := GetParentForm(FParent);
    Monitor := Screen.MonitorFromWindow(frm.Handle);
    RefLtwh.Assign(frm.BoundsRect);
  end else begin
    if Assigned(Application.MainForm) then
      Monitor := Screen.MonitorFromWindow(Application.MainForm.Handle)
    else
      Monitor := Screen.MonitorFromWindow(0);
    RefLtwh.Assign(Monitor.BoundsRect);
  end;
  ltwh.Assign(Rect);
  ltwh.Left := RefLtwh.Left + RefLtwh.Width div 2 - ltwh.Width div 2;
  ltwh.Top := RefLtwh.Top + RefLtwh.Height div 2 - ltwh.Height div 2;
  TMonitor_MakeFullyVisible(Monitor, ltwh);
  SetWindowPos(FWnd, 0, ltwh.Left, ltwh.Top, 0, 0, SWP_NOZORDER or SWP_NOSIZE);
end;

function TSelectDirCallback.SelectDirCB(_Wnd: HWND; _uMsg: UINT; _lParam, _lpData: lParam): Integer;

  procedure SelectDirectory;
  begin
    if FDirectory <> '' then begin
      // we use PostMessage to asynchronously select the directory
      PostMessage(_Wnd, BFFM_SETSELECTION, Windows.WParam(True), Windows.lParam(PChar(FDirectory)));
    end;
  end;

begin
  Result := 0;
  if _uMsg = BFFM_INITIALIZED then begin
    // Subclass the window to catch the WM_SIZE message when it is automatically being resized
    // later in the initialization process. Only then it is possible to get the final size
    // and position it correctly.
    SubClass(_Wnd);
    FInitialized := True;
    // Selecting the directory here only selects the entry but does not necessarily make
    // it visible. So we set it here and again further below.
    SelectDirectory;
  end else if (_uMsg = BFFM_VALIDATEFAILEDW) or (_uMsg = BFFM_VALIDATEFAILEDA) then begin
    // default code copied from FileCtrl
    MessageDlg(Format(SInvalidPath, [PChar(_lParam)]), mtError, [mbOK], 0);
    Result := 1;
  end else if _uMsg = BFFM_SELCHANGED then begin
    if FInitialized and not FPositioned then begin
      FPositioned := True;
      // The first call to SelectDirectory only selects it but does not scroll the tree view
      // to make it visible. That's what this second call is for.
      SelectDirectory;
    end;
  end;
end;

// This is the actual callback function passed to the Windows API. lpData is the TSelectDirCallback
// object we created. Here we simply call its SelectDirCB method.

function SelectDirCB(Wnd: HWND; uMsg: UINT; lParam, lpData: lParam): Integer stdcall;
begin
  Result := TSelectDirCallback(lpData).SelectDirCB(Wnd, uMsg, lParam, lpData);
end;

function SelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil): Boolean;
begin
  Result := dzSelectDirectory(Caption, Root, Directory, Options, Parent);
end;

// This is copied from FileCtrl, mostly unchanged. I removed the WITH statement though.

function dzSelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil): Boolean;
var
  BrowseInfo: TBrowseInfo;
  OldErrorMode: Cardinal;
  ShellMalloc: IMalloc;
  IDesktopFolder: IShellFolder;
  Eaten, Flags: LongWord;
  CoInitResult: HRESULT;
  SelectDirCallback: TSelectDirCallback;
  WindowList: Pointer;
  Buffer: PChar;
  RootItemIDList, ItemIDList: PItemIDList;
begin
  Result := False;
  if not DirectoryExists(Directory) then
    Directory := '';
  FillChar(BrowseInfo, SizeOf(BrowseInfo), 0);
  if (ShGetMalloc(ShellMalloc) = S_OK) and (ShellMalloc <> nil) then begin
    Buffer := ShellMalloc.Alloc(MAX_PATH * SizeOf(Char));
    try
      RootItemIDList := nil;
      if Root <> '' then begin
        SHGetDesktopFolder(IDesktopFolder);
        IDesktopFolder.ParseDisplayName(Application.Handle, nil,
          POleStr(Root), Eaten, RootItemIDList, Flags);
      end;

      // fill BrowseInfo
      if (Parent = nil) or not Parent.HandleAllocated then
        BrowseInfo.hwndOwner := Application.Handle
      else
        BrowseInfo.hwndOwner := Parent.Handle;
      BrowseInfo.pidlRoot := RootItemIDList;
      BrowseInfo.pszDisplayName := Buffer;
      BrowseInfo.lpszTitle := PChar(Caption);
      BrowseInfo.lpfn := SelectDirCB;
      BrowseInfo.ulFlags := BIF_RETURNONLYFSDIRS;
      if sdNewUI in Options then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_NEWDIALOGSTYLE;
      if not (sdNewFolder in Options) then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_NONEWFOLDERBUTTON;
      if sdShowEdit in Options then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_EDITBOX;
        if not (sdNewUI in Options) and (sdShowShares in Options) then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_SHAREABLE;
      if sdShowFiles in Options then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_BROWSEINCLUDEFILES;
      if sdValidateDir in Options then
        BrowseInfo.ulFlags := BrowseInfo.ulFlags or BIF_VALIDATE;

      SelectDirCallback := TSelectDirCallback.Create(Directory, Parent);
      try
        BrowseInfo.lParam := lParam(SelectDirCallback);
        // Not sure if this is necessary. Delphi 2007 does it, Delphi 10.1 doesn't
        if sdNewUI in Options then begin
          CoInitResult := CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
          if CoInitResult = RPC_E_CHANGED_MODE then
            BrowseInfo.ulFlags := BrowseInfo.ulFlags and not BIF_NEWDIALOGSTYLE;
        end;
        try
          WindowList := DisableTaskWindows(0);
          OldErrorMode := SetErrorMode(SEM_FAILCRITICALERRORS);
          try
            ItemIDList := ShBrowseForFolder(BrowseInfo);
          finally
            SetErrorMode(OldErrorMode);
            EnableTaskWindows(WindowList);
          end;
        finally
          if sdNewUI in Options then
            CoUninitialize;
        end;
      finally
        SelectDirCallback.Free;
      end;
      Result := ItemIDList <> nil;
      if Result then begin
        ShGetPathFromIDList(ItemIDList, Buffer);
        ShellMalloc.Free(ItemIDList);
        Directory := Buffer;
      end;
    finally
      ShellMalloc.Free(Buffer);
    end;
  end;
end;

end.

The unit u_dzSelectDirectoryFix is part of my dzlib in the svn repository on SourceForge.

GExperts 1.38 experimental twm 2016-05-07

$
0
0

Delphi Custom Container Pack updated for Delphi 10.1 Berlin

$
0
0

I just updated the Custom Container Pack sources to support Delphi 10.1 Berlin. It was mostly a matter of creating the packages for the “new” version. I also used the latest version of the Delphiversions.inc file.

It now compiles and installs. I have not tested it extensively.

Custom Containers Pack (CCPack) is an integrated tool and component mini-library to produce and maintain composite controls (or simply “composites”) and other containers (forms, data modules and frames). The process of building composite components looks like ActiveForm and Frame creating, but the result is the native VCL component. You can create new composites just as usual forms.

Here is the original documentation in RTF format.

It was originally developed by Sergey Orlik who posted the source code to code central

Delphi7Help4BDS updated for Delphi 10.1 Berlin

$
0
0

I have just updated my Delphi7Help4BDS expert to support Delphi 10.1 Berlin.

It allows to reconfigure F1 + any of the modifier keys Shift, Ctrl, Alt and Alt+Ctrl to call either a help file (you will need the old winhelp viewer that Microsoft dropped with Vista(?)), chm file or internet url. It comes preconfigured with a few example internet urls for searching with Google, Bing, the Embarcadero docwiki or MSDN.

This shows the result of Alt+F1 when the cursor was on the keyword "Integer":

Delphi7HelpForBdsExpert

To install it, get the sources from SourceForge, open the package for your Delphi version, compile and install it. You should then find a new entry in the IDE’s Help menu called “Configure Delphi7HelpForBds” which gets you a dialog where you can configure the actions for each of the key combinations.

Delphi7HelpForBdsWizard

Delphi IDE Explorer Expert for Delphi 6 to 10.1 Berlin

$
0
0

I wrote this Delphi IDE Explorer Expert when I was trying find the button for showing and hiding the background for the Firemonkey mobile form designer and turn the bloody thing off for good.

Later, I ported it back to Delphi 2007 to 2010 and also added support for Delphi 10 Seattle.

Then I started improving GExperts (or actually made GExperts improve the Delphi IDE dialogs), so I needed it also for Delphi 6 to 2006.

Now I have added support for Delphi 10.1 Berlin. You can find the sources on SourceForge.

Oh, did I mention, that it accepts user input even if the IDE shows a modal dialog? Very useful feature.

DelphiIDEExplorer-for-Berlin

Getting an old package wizard to work

$
0
0

I tried to compile the SarchWiz project by Corbin Dunn, which he describes in the corresponding article which in turn Ondrey Kelle mentioned in his blog post More Subversion.

(The links to code central and edn above are the ones Embarcadero created from the original links to code central and bdn when they were still operated by Borland. Luckily they did not change the article IDs so it was only a matter of converting the urls to the new adresses. Allan Bauer’s example code is here, btw.)

The first problem I ran into was that the original wizard was written for Delphi 5 (yes 5, not XE5). So some package and unit names had changed.

  • The VCL/RTL etc. .dcp files no longer have the version sufix (only the corresponding .bpl files do), so the package requires vcl, vclie and inet rather than vcl50, vclie50 and inet50
  • The dsnide50 package is now called dsignide
  • The unit DsgnIntf no longer exists, but apparently nothing was used from it, so I just removed it from the uses list.

Then, there is TDockableForm, the ancestor of the TSearchWizForm class used in the wizard. Apparently the declarations of LoadWindowState and SaveWindowState have changed. In Delphi 5 the Desktop parameter was a TMemIniFile, nowadays it’s a TCustomIniFile.

// old
procedure LoadWindowState(Desktop: TMemIniFile); override;
procedure SaveWindowState(Desktop: TMemIniFile; isProject: Boolean); override;

// new
procedure LoadWindowState(Desktop: TCustomIniFile); override;
procedure SaveWindowState(Desktop: TCustomIniFile; isProject: Boolean); override;

The last change to get it to compile was changing the WebBrowserBeforeNavigate2 method. In Delphi 5 some parameters were declared as var, they now are const:

// old
procedure WebBrowserBeforeNavigate2(Sender: TObject;
  const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
  Headers: OleVariant; var Cancel: WordBool);

// new
procedure WebBrowserBeforeNavigate2(ASender: TObject;
  const pDisp: IDispatch; const URL, Flags, TargetFrameName, PostData,
  Headers: OleVariant; var Cancel: WordBool);

So, I compiled and installed the package without errors, and … nothing happened. Since it is an ancient wizard, I looked into the help menu and found Help -> Help Wizards -> Web Search, which is the menu entry added by the wizard. Clicking on it still didn’t do anything.

After putting a breakpoint in the wizard’s execute method (and setting the host application to the Delphi ide), I found that the window is actually being created and shown. It’s listed in the IDE’s Window menu and I eventually found it on the screen: It was far down behind the task bar (which fortunately in Windows 8.1 is transparent). So, switching to it and pressing Alt+Space gave me the system menu and selecting Maximize finally brought it somewhere sensible. It turned out that the problem was this code:

procedure TSearchWizard.Execute;
begin
  if SearchWizForm = nil then
  begin
    SearchWizForm := TSearchWizForm.Create(Application);
    SearchWizForm.Left := 10;
    SearchWizForm.Top := Application.MainForm.Top +
      Application.MainForm.Height + 10;
  end;

  FocusWindow(SearchWizForm);
end;

Application.MainForm is no longer the small window it was in Delphi 5 which contained only the menu, tool bars and component palette but the whole Window, so setting the form’s top position to MainForm.Top + MainForm.Height + 10 no longer positions it below the component palette but below the normal screen area.

After changing it to simply 10, the wizard window is displayed and can be used.

It’s very strange to see Altavista, Deja.com, Excite and Northern Light in the list of search engines. Apparently Altavista now gets redirected to Yahoo, deja.com is now Google groups, excite.com still exists, but search.excite.com does not work, and northernlight.com is no longer a search engine. I changed the excite entry to the new url and it worked (ignoring some JavaScript errors).

This is what it looks like when sarching excite for “delphi programming” when the wizard is docked to the Delphi 10 IDE:

SearchWiz

Now, you might ask why I went through all this, just to have a web search window in the IDE? It’s connected with my Delphi7Help4Bds expert. It currently uses the Welcome Screen to display its results when configured to call a url. I thought about creating my own browser window and – you know that if you looked at the code – I had started to build something based on some code I got from Simon Stuart before he stopped publishing stuff under open source licenses and shut down his blog in 2012. And since I was unable to get it to work, I googled for other stuff using similar techniques. So I came across Ondrey Kelles’s post and the links there took me to this ancient wizard code. Isn’t it strange what turns life can take?


dzDelphiPaths tool

$
0
0

Somebody in the German forum Delphi Praxis asked for a tool to display the Delphi library paths. Apparently such a tool didn’t exist yet but it could sometimes come in rather handy, so I wrote it.

dzDelphiPaths

It doesn’t do much, only scans the registry for installed Delphi versions (Starting with Delphi 5, I am not sure about the registry path’s for the older versions. Feel free to send me patches.) and gives you a list of all entries in the Libraries branch. In addition to a list by Delphi version it also gives you a list by entry for all Delphi versions as you can see in the screen shot. That way you can easily compare the same path in different Delphi installations.

It’s on SourceForge.

Thank you, GExperts contributors

$
0
0

I would like to thank those people who have contributed to GExperts during the time that I have been maintaining most of it. There have been various contributions ranging from feature suggestions with partial implementations, good bug reports, to bug fixes that worked out of the box.

Thank-You

You might have had the impression that the recent activity has been a one man show, which would not be too far from the truth, but I really appreciate the contributions of others.

GExperts 1.38 experimental twm 2016-05-26

$
0
0

Known IDE Packages in Delphi

$
0
0

Prompted by a comment in this Google+ post I had a look at what is actually listed in [HKEY_CURRENT_USER\Software\Borland|Codegear|Embarcadero\BDS|Delphi\#.0\Known IDE Packages] and found some interesting entries:

In Delphi 6 and 7 there is direct60.bpl and direct70.bpl which can probably simply be removed nowadays. The server used by “Delphi Direct” which was the equivalent of the current “Welcome Page” no longer exists. Removing this package results in the Environment Options dialog having one less tab:

DelphiDirect

I never understood the purpose of Delphi Direct. Even back in the 1990ies there wasn’t any interesting content delivered to it. In my opinion it was totally wasted by Borland.

Then there is delphiclxide60.bpl and delphiclxide70.bpl. These packages apparently contain the designer for CLX applications (anybody remember theses?). When removed, there are still some CLX items in the File->New->Other menu and these can still be used, but you will no longer be able to visually design these forms and dialogs. Also the File->New->CLX Application entry will be gone. In Delphi 6 this results in some designtime packages not being able to load, in Delphi 7 it doesn’t but that might just be because I removed them earlier.

CLXNew

Since I doubt that anybody is still developing CLX applications, it’s probably safe to remove these entries as well.

Moving on to the “modern” IDE, which was introduced with Delphi 8 (which I don’t own, so I’ll look at Delphi 2005 instead):

The startpageide90.bpl package is the equivalent of Delphi Direct mentioned above. It is still there in Delphi 10.1 Berlin as startpageide230.bpl. It has been suggested to remove this package because it is hardly useful. I haven’t done that because my delphi7help4bds expert uses it to display HTML content.

There is caliberide90.bpl or Borland.Caliber.IDE100.bpl as it is called in Delphi 2006. After removing it in Delphi 2006 I found nothing missing. Caliber apparently is a requirement management tool now owned by MicroFocus (who acquired Borland after they spun off their development tools as CodeGear). I’m not sure what exactly it does or did in Delphi 2005 and 2006 but it is no longer there in Delphi 2007 and later. The same applies to TGIDE90.BPL which is the “Together IDE integration”. It’s the UML modelling tool Borland acquired in 2003.

Another interesting entry is historyide90.bpl. It apparently implements the history tab (at the bottom of the editor, to the right of the Code and Design tabs).

HistoryTab

It’s still there in Delphi 10.1 Berlin.

Some other observation: Starting with Delphi 2006 the key “Known IDE Packages” has subkeys, one of them is “Delphi”, others, that were in use at some time are:

  • CSharp
  • DelphiDotNet
  • CBuilder

There might have been others but I recently stopped installing anything but the Delphi personality even though my (employer’s) subscription is on Rad Studio just in case.

I’ll stop here with listing those packages. Just some comment about enabling and disabling them:

Apparently they don’t get loaded if the entry does not have a value. Some of the packages have a value of (Untitled) which apparently means “nobody bothered to give them a name but we need a value so we added something generic”. If you remove this value (set it to an empty string) the package will no longer be loaded. But it’s probably not a good idea to disable them by removing the value. I did my tests by creating a new “unused” subkey and moving these packages there, including the description.

I’ll probably write another post on this topic at a later time. I’m also thinking about writing a tool to disable/enable entries in Known IDE Packages.

Clearing a TTreeView

$
0
0

Note to self: If you want to clear the items that were added at design time to a TTreeView, you must make sure it has a handle. The following does not work (in Delphi 2007):

constructor TForm1.Create(Owner: TCoponent);
begin
  inherited;
  TreeView1.Items.Clear;
end;

Adding a TreeView1.HandleNeeded makes it work:

constructor TForm1.Create(Owner: TCoponent);
begin
  inherited;
  TreeView1.HandleNeeded
  TreeView1.Items.Clear;
end;

Displaying huge text files

$
0
0

Sometimes programmers have to deal with larger than usual text files. Examples are log files or XML dumps of databases. For my tests, I used a dump of the English Wikipedia in XML format. This file is 48 gigabytes in size and as I found out only today contains 789,577,286 lines of text.

dzLargeTextViewer-Line708754410

If you google for “large text viewer” you get quite a few hits, but many of these are not what they advertise.

Others kind of work but have various shortcomings, e.g. Large Text File Reader which allows you to only display the first n lines of a file. But don’t try to enter too large a number because it will then just hang. Then there is Log Expert which was suggested by this answer on StackOverflow but apparently tries to load the whole file into memory, nearly crashing my Windows installation.

I found the following viewers which seem to work:

LTFViewer from the now defunct swiftgear.com site (Link therefore goes to archive.org). This works but seems to be rather slow in reading the file. Closed it after about an hour when it had read about 200 million lines.

glogg which is a log file viewer with searching and filtering. It’s multi platform which usually means that the Windows version is barely usable. Glogg is not too bad on this account. It took quite a while loading the file which ran in the background. Unfortunately while it is loading the file, you cannot browse it. It only displays the first page, scrolling is impossible. You can use the filter function though. It took about one hour to read the 48 GB file using 1 GB of memory in the process. Once it has loaded it, it supports e.g. incremental search, which, in a file of this size takes quite a while.

Another option is a file viewer written in JavaScript at www.readfileonline.com. I had my doubts about this but since it works locally and browses the file in batches of a given size, it can be very fast. It even has a search function, but of course that one has limits when talking about a file of 48 gigabytes.

Yours truly also wrote such a tool, back when I received a huge XML file from a customer, also a database dump, and had to resort to Linux and the less command line tool to view it. dzLargeTextViewer doesn’t have many features. It theoretically allows you to display files up to MaxInt64 lines. I have tested it with the aforementioned Wikipedia dump. The first version ran out of memory after around 75 million lines. The second version used 1 GB of memory and could index about 300 million lines. The third version now has read all nearly 790 million lines and displays them, using only 3.5 MB of memory. To achieve that, it created a 6 GB index file. It stores that file as <Filename>.LineIndex so it can be reused when the same file is opened again. My tool assumes ansi strings, though, while glogg reads the file as UTF-8.

GExperts 1.38 experimental twm 2016-06-05

$
0
0

This is another test release before Erik is going to do an official 1.39 release. Please report any bugs you may find (preferentially in the GExperts community on Google+ or the bug tracker on SourceForge)

Again, I have built installers for each Delphi version. These installers should install everything that is necessary, including the files for the Code Formatter.

Erik and I fixed various bugs and added a few minor features:

  • The Backup Project Expert now uses the current project name for the ZIP file (contributed by xander xiao)
  • Again the Backup Project Expert now has an option to recourse into subdirectories, optionally ignoring __history and __restore, and can ignore backup files (*.~*). Also it ignores .svn, .git and .hg subdirs.
  • The preview in the Code Formatter configuration dialog now uses SynEdit for syntax highlighting
  • The To Do Expert now reads the numeric todo priority (e.g. specified in the Delphi Edit-Todo dialog). We no longer need the “todo 1” .. “todo 5” tokens.
    GExperts-ToDo-Expert
  • The Editor Popup Menu configuration dialog no longer allows duplicates (contributed by Achim Kalwa)
  • Yet another unicode bug was fixed, this time in the Macro Templates Expert (contributed by Denis Bisson)
  • There is now a stand alone version of the PE Information Expert which, as all the existing stand alone versions of experts requires the GExperts DLL. It can be called with a filename as parameter and also supports drag & drop.

Here are the links:


Snapping windows to monitor halves / quadrants

$
0
0

You probably know about the Windows 7+ feature to snap a window to the left or right side of the monitor via Windows+Left / Windows+Right hotkey. You might even know that Windows 10 extended this to snap a window to the top or bottom and even to one of the quadrants of your monitor. (I for one had read about that Windows 10 feature but had already forgotten it, since I don’t plan to upgrade in the near future.)

But what if you don’t have Windows 10? Can this be done programmatically? Of course it can! Just add appropriate Actions, assign the shortcuts to your form and write the code. Or set the form’s KeyPreview property to true and put the code into the FormKeyDown event handler. Unfortunately this requires some rather tiresome copy and paste plus clicking on the object inspector for each of the forms. And, of course, copy and paste is evil, because it violates the DRY principle: If you make a mistake once, you will have to correct it in all the copies you have made in the meantime.

Enter TForm_ActivatePositoning: A utility function which I have added to dzLib today:

Just by calling

TForm_ActivatePositioning(Self);

you can add this functionality to every form in every project you’ll ever write.

Ok, so what does it do?

It subclasses the window (that is: Replaces its WindowProc (Wow, I just learnt that there is a new approach to subclassing, must have missed that for a few years)) to intercept the CM_CHILDKEY message which is sent by the VCL whenever any child control of a form receives a WM_KEYDOWN message:

type
  TFormPositioningActivator = class(TWindowProcHook)
  private
    FModifier: TShiftState;
    procedure CmChildKey(var _Msg: TMessage);
    function TheForm: TForm;
  protected
    procedure NewWindowProc(var _Msg: TMessage); override;
  public
    constructor Create(_Form: TForm; _Modifier: TShiftState);
  end;

constructor TFormPositioningActivator.Create(_Form: TForm; _Modifier: TShiftState);
begin
  inherited Create(_Form);
  FModifier := _Modifier;
end;

procedure TFormPositioningActivator.CmChildKey(var _Msg: TMessage);
var
  Key: Word;
begin
  Key := (_Msg.WParamLo and $FF);
  if GetModifierKeyState = FModifier then begin
    case Key of
      VK_LEFT, VK_NUMPAD4: TForm_MoveTo(TheForm, dwpLeft);
      VK_RIGHT, VK_NUMPAD6: TForm_MoveTo(TheForm, dwpRight);
      VK_UP, VK_NUMPAD8: TForm_MoveTo(TheForm, dwpTop);
      VK_DOWN, VK_NUMPAD2: TForm_MoveTo(TheForm, dwpBottom);
      VK_PRIOR, VK_NUMPAD9: TForm_MoveTo(TheForm, dwpTopRight);
      VK_NEXT, VK_NUMPAD3: TForm_MoveTo(TheForm, dwpBottomRight);
      VK_HOME, VK_NUMPAD7: TForm_MoveTo(TheForm, dwpTopLeft);
      VK_END, VK_NUMPAD1: TForm_MoveTo(TheForm, dwpBottomLeft);
    else
      Exit; //==> exit, so Result doesn't get set to 1
    end;
    _Msg.Result := 1;
  end;
end;

procedure TFormPositioningActivator.NewWindowProc(var _Msg: TMessage);
begin
  if _Msg.Msg = CM_CHILDKEY then
    CmChildKey(_Msg);

  inherited NewWindowProc(_Msg);
end;

function TFormPositioningActivator.TheForm: TForm;
begin
  Result := TForm(FCtrl);
end;

function TForm_ActivatePositioning(_Form: TForm; _Modifier: TShiftState = [ssCtrl, ssAlt]): TObject;
begin
  Result := TFormPositioningActivator.Create(_Form, _Modifier);
end;

It uses the existing TWindowProcHook class which is already used for drag&drop and autocomplete (wow, was that really two years ago?) so the actual code is quite simple, as you can see above.

I did not use the Windows key as modifier but rather Ctrl+Alt because Microsoft says we should not use the Windows key. But I’m not quite sure about this. Maybe I could instead check for the Windows version to see if those hotkeys are already available and only add those that aren’t, using the Windows key.

But for now, the following hotkeys are implemented:

  • Ctrl+Alt+Up – snap window to the top of the monitor, or if already there, to the bottom of the monitor above it
  • Ctrl+Alt+Down – same for the bottom of the monitor
  • Ctrl+Alt+Left – same for the left of the monitor
  • Ctrl+Alt+Right – same for the left of the monitor
  • Ctrl+Alt+Home – snap the window to the upper left quadrant of the monitor
  • Ctrl+Alt+End – same for the lower left quadrant
  • Ctrl+Alt+PgUp – same for the upper right quadrant
  • Ctrl+Alt+PgDown- same for the lower right quadrant

Those hotkeys also work for the keys on the numeric keypad, regardless whether NumLock is active or not. If you look at your keyboard you will notice why I choose these particular keys: The nicely correspond to the quadrants / halves of the monitor and should it easier to memorize them.

Numpad
(Picture courtesy of Wikipedia.)

Snapping windows to monitor halves / quadrants revisited

$
0
0

In my last post I talked about snapping windows to monitor halves and quadrants. I have been using that code for a few days and found it has a few shortcomings:

  1. If a window has size constraints, these will still be respected (which is good) but this will result in the window not being moved correctly. Snapping to the left and top half works fine, but snapping to the right or bottom half will move part of the window outside the active monitor.
  2. If a window has size constraints, moving from the left half of the right hand side monitor to the right half of left hand side monitor will not work. the same applies from the top half of the bottom monitor to the bottom half of the top monitor.
  3. It’s a bit inconvenient to move a window from a quadrant of one monitor to a quadrant of a different monitor. E.g. sometimes I want to move a window from the top right quadrant of the right hand side monitor to the top right quadrant of the left hand side monitor and vice versa. In order to do that I have to press Ctrl+Alt+Left (moves it to the left half of that monitor) Ctrl+Alt+Left (moves it to the right half of the other monitor) Ctrl+Alt+PgUp (finally moves it to the top right quadrant of that monitor). It would be nice to accomplish this
    1. with less key strokes
    2. without resizing the window

    Currently I am leaning towards Ctrl+Alt+PgUp moving the window to the top right quadrant of the same monitor and pressing it again moving it to the same quadrant of the monitor to the right of that monitor, or if that doesn’t exist, to the monitor above. But I’m not yet sure about the order of monitors here. If you want to voice your opinion, use my corresponding Google+ post

The fix for the constraints issues isn’t that difficult: Just read the form’s constraints and adjust the position accordingly. Here is the new code:

procedure TForm_MoveTo(_frm: TCustomForm; _Position: TdzWindowPositions);

  procedure ToTop(var _Re: TRect; _MinHeight, _MaxHeight: Integer);
  begin
    _Re.Bottom := _Re.Top + TRect_Height(_Re) div 2;
    if TRect_Height(_Re) < _MinHeight then
      _Re.Bottom := _Re.Top + _MinHeight;
    if (_MaxHeight > 0) and (TRect_Height(_Re) > _MaxHeight) then
      _Re.Bottom := _Re.Top + _MaxHeight;
  end;

  procedure ToBottom(var _Re: TRect; _MinHeight, _MaxHeight: Integer);
  begin
    _Re.Top := _Re.Top + TRect_Height(_Re) div 2;
    if TRect_Height(_Re) < _MinHeight then
      _Re.Top := _Re.Bottom - _MinHeight;
    if (_MaxHeight > 0) and (TRect_Height(_Re) > _MaxHeight) then
      _Re.Top := _Re.Bottom - _MaxHeight;
  end;

  procedure ToLeft(var _Re: TRect; _MinWidth, _MaxWidth: Integer);
  begin
    _Re.Right := _Re.Left + TRect_Width(_Re) div 2;
    if TRect_Width(_Re) < _MinWidth then
      _Re.Right := _Re.Left + _MinWidth;
    if (_MaxWidth > 0) and (TRect_Width(_Re) > _MaxWidth) then
      _Re.Right := _Re.Left + _MaxWidth;
  end;

  procedure ToRight(var _Re: TRect; _MinWidth, _MaxWidth: Integer);
  begin
    _Re.Left := _Re.Left + TRect_Width(_Re) div 2;
    if TRect_Width(_Re) < _MinWidth then
      _Re.Left := _Re.Right - _MinWidth;
    if (_MaxWidth > 0) and (TRect_Width(_Re) > _MaxWidth) then
      _Re.Left := _Re.Right - _MaxWidth;
  end;

  function SamePoint(const _pnt1, _pnt2: TPoint): Boolean;
  begin
    Result := (_pnt1.X = _pnt2.X) and (_pnt1.Y = _pnt2.Y);
  end;

  function SameRect(const _re1, _re2: TRect): Boolean;
  begin
    Result := SamePoint(_re1.TopLeft, _re2.TopLeft) and SamePoint(_re1.BottomRight, _re2.BottomRight);
  end;

var
  re: TRect;
  Bounds: TRect;
  NewMonitor: TMonitor;
  Constraints: TSizeConstraints;
begin
  re := _frm.Monitor.WorkareaRect;
  Bounds := _frm.BoundsRect;
  Constraints := _frm.Constraints;
  case _Position of
    dwpTop: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        if SameRect(re, Bounds) then begin
          NewMonitor := MonitorFromPoint(Point((re.Left + re.Right) div 2, re.Top - TRect_Height(re) div 2));
          if Assigned(NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
          end;
        end;
      end;
    dwpBottom: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        if SameRect(re, Bounds) then begin
          NewMonitor := MonitorFromPoint(Point((re.Left + re.Right) div 2, re.Bottom + TRect_Height(re) div 2));
          if Assigned(NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
          end;
        end;
      end;
    dwpLeft: begin
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
        if SameRect(re, Bounds) then begin
          NewMonitor := MonitorFromPoint(Point(re.Left - TRect_Width(re) div 2, (re.Top + re.Bottom) div 2));
          if Assigned(NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
          end;
        end;
      end;
    dwpRight: begin
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
        if SameRect(re, Bounds) then begin
          NewMonitor := MonitorFromPoint(Point(re.Right + TRect_Width(re) div 2, (re.Top + re.Bottom) div 2));
          if Assigned(NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
          end;
        end;
      end;
    dwpTopLeft: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpTopRight: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpBottomLeft: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpBottomRight: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
  end;
  _frm.BoundsRect := re;
end;

It’s already in the dzlib repository on sourceforge, unit u_dzVclUtils.

Snapping a Firemonkey window to monitor halves / quadrants

$
0
0

I always wanted to start playing with Firemonkey but so far just didn’t find the right project. This is my first try to port a VCL utility function to Firemonkey. Note that this will probably not work on all platforms. It’s tested on Windows only.

So, how do we get the code from my last post to work with a Firemonkey program? It turned out to be not too difficult. Monitors have been renamed to Displays, TForm.BoundsRect is now only TForm.Bounds. There doesn’t seem to be an equivalent to TForm.Constraints (even though Simon J. Stuart has posted a TConstraintForm solution on StackOverflow) so we will for now ignore that.

Here is the code:

procedure TForm_MoveTo(_frm: TCustomForm; _Position: TdzWindowPositions);

  procedure ToTop(var _Re: TRect; _MinHeight, _MaxHeight: Integer);
  begin
    _Re.Bottom := _Re.Top + _Re.Height div 2;
    if _Re.Height < _MinHeight then
      _Re.Bottom := _Re.Top + _MinHeight;
    if (_MaxHeight > 0) and (_Re.Height > _MaxHeight) then
      _Re.Bottom := _Re.Top + _MaxHeight;
  end;

  procedure ToBottom(var _Re: TRect; _MinHeight, _MaxHeight: Integer);
  begin
    _Re.Top := _Re.Top + _Re.Height div 2;
    if _Re.Height < _MinHeight then
      _Re.Top := _Re.Bottom - _MinHeight;
    if (_MaxHeight > 0) and (_Re.Height > _MaxHeight) then
      _Re.Top := _Re.Bottom - _MaxHeight;
  end;

  procedure ToLeft(var _Re: TRect; _MinWidth, _MaxWidth: Integer);
  begin
    _Re.Right := _Re.Left + _Re.Width div 2;
    if _Re.Width < _MinWidth then
      _Re.Right := _Re.Left + _MinWidth;
    if (_MaxWidth > 0) and (_Re.Width > _MaxWidth) then
      _Re.Right := _Re.Left + _MaxWidth;
  end;

  procedure ToRight(var _Re: TRect; _MinWidth, _MaxWidth: Integer);
  begin
    _Re.Left := _Re.Left + _Re.Width div 2;
    if _Re.Width < _MinWidth then
      _Re.Left := _Re.Right - _MinWidth;
    if (_MaxWidth > 0) and (_Re.Width > _MaxWidth) then
      _Re.Left := _Re.Right - _MaxWidth;
  end;

  function TryMonitorFromPoint(_pnt: TPoint; out _Display: TDisplay): boolean;
  var
    i: Integer;
    Display: TDisplay;
  begin
    Result := False;
    for i := 0 to Screen.DisplayCount - 1 do begin
      Display := Screen.Displays[i];
      Result := Display.WorkArea.Contains(_pnt);
      if Result then begin
        _Display := Display;
        Exit;
      end;
    end;
  end;

type
  TDummyConstraints = record
    MinWidth, MaxWidth: Integer;
    MinHeight, MaxHeight: Integer;
  end;
var
  re: TRect;
  Bounds: TRect;
  NewMonitor: TDisplay;
  Constraints: TDummyConstraints;
begin
  re := Screen.DisplayFromForm(_frm).WorkareaRect;
  Bounds := _frm.Bounds;
  Constraints.MinWidth := 0;
  Constraints.MaxWidth := 0;
  Constraints.MinHeight := 0;
  Constraints.MaxHeight := 0;
  case _Position of
    dwpTop: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        if re = Bounds then begin
          if TryMonitorFromPoint(Point((re.Left + re.Right) div 2, re.Top - re.Height div 2), NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
          end;
        end;
      end;
    dwpBottom: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        if re = Bounds then begin
          if TryMonitorFromPoint(Point((re.Left + re.Right) div 2, re.Bottom + re.Height div 2), NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
          end;
        end;
      end;
    dwpLeft: begin
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
        if re = Bounds then begin
          if TryMonitorFromPoint(Point(re.Left - re.Width div 2, (re.Top + re.Bottom) div 2), NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
          end;
        end;
      end;
    dwpRight: begin
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
        if re = Bounds then begin
          if TryMonitorFromPoint(Point(re.Right + re.Width div 2, (re.Top + re.Bottom) div 2), NewMonitor) then begin
            re := NewMonitor.WorkareaRect;
            ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
          end;
        end;
      end;
    dwpTopLeft: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpTopRight: begin
        ToTop(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpBottomLeft: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToLeft(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
    dwpBottomRight: begin
        ToBottom(re, Constraints.MinHeight, Constraints.MaxHeight);
        ToRight(re, Constraints.MinWidth, Constraints.MaxWidth);
      end;
  end;
  _frm.Bounds := re;
end;

As you can see, the implementation is very similar to the VCL implementation. With a bit of effort I could probably make them nearly indistinguishable.

It’s in dzlib, in unit u_dzFmxUtils

Now, that was the easy part: Moving the form. The hard part is hooking the form in a way so all I need to do is call TForm_ActivatePositioning as in the VCL. No idea yet on how to accomplish that, but I’m just getting started with Firemonkey.

“Hooking” KeyDown in a Firemonkey form

$
0
0

As said in my last post: " The hard part is hooking the form in a way so all I need to do is call TForm_ActivatePositioning as in the VCL."

As it turns out, that’s even easier to do than in the VCL. No subclassing of the window, just adding a new control is sufficient. As this StackOverflow answer points out, a Firemonkey form calls the DialogKey method of all its child controls when a key is pressed, starting with the one that has got the focus. So, all we’ve got to do is creating a control that handles the keys we want to intercept and add it to the form.

Here is the code:

type
  TFormPositioningActivator = class(TControl)
  private
    FModifier: TShiftState;
  protected
    procedure DialogKey(var Key: Word; Shift: TShiftState); override;
  public
    constructor Create(_Form: TCustomForm; _Modifier: TShiftState); reintroduce;
  end;

{ TFormHookChild }

constructor TFormPositioningActivator.Create(_Form: TCustomForm; _Modifier: TShiftState);
begin
  inherited Create(_Form);
  FModifier := _Modifier;
  Parent := _Form;
end;

procedure TFormPositioningActivator.DialogKey(var Key: Word; Shift: TShiftState);
begin
  inherited;
  if Shift = FModifier then begin
    case Key of
      vkLeft: TForm_MoveTo(self.Parent as TForm, dwpLeft);
      vkRight: TForm_MoveTo(self.Parent as TForm, dwpRight);
      vkUp: TForm_MoveTo(self.Parent as TForm, dwpTop);
      vkDown: TForm_MoveTo(self.Parent as TForm, dwpBottom);
      vkHome: TForm_MoveTo(self.Parent as TForm, dwpTopLeft);
      vkEnd: TForm_MoveTo(self.Parent as TForm, dwpBottomLeft);
      vkPrior: TForm_MoveTo(self.Parent as TForm, dwpTopRight);
      vkNext: TForm_MoveTo(self.Parent as TForm, dwpBottomRight);
    else
      Exit; // so Key doesn't get set to 0
    end;
    Key := 0;
  end;

end;

function TForm_ActivatePositioning(_Form: TForm; _Modifier: TShiftState = [ssCtrl, ssAlt]): TObject;
begin
  Result := TFormPositioningActivator.Create(_Form, _Modifier);
end;

All you have to do is call TForm_ActivatePositioning(Self) in the form’s constructor and be done.

Sarch path dialog behaviour changed from Delphi 2010 to XE

$
0
0

Yesterday, while working with Delphi 10.1 something happened that made me think I had introduced a bug in the search path dialog enhancement of GExperts:

I had dropped some directories from the explorer onto the memo inserted by the GExperts enhancement, switched to the list view and back to the memo, then pressed the “Make Relative” button and exited the dialog with OK. Nothing special here until I noticed that the search path now contained the last directory twice: Once as a relative path and once as an absolute path.

Today I investigated this a bit more and found that the behaviour of the search path editor dialog had changed from Delphi 2010 to XE: From then on the content of the edit field gets added to the search path even if you don’t press the Add button but just the OK button. Since this only happens if it is not already there, you usually don’t notice, unless you have changed the entries in the list from absolute to relative paths. Then you end up with a duplicate.

I disabled the GExperts enhancements to make sure it’s not cause by it: It’s a change in the dialog itself.

SearchPathOddity

Viewing all 650 articles
Browse latest View live