Autocompletion for TEdits revisited

 Delphi, dzLib  Comments Off on Autocompletion for TEdits revisited
Apr 282019
 

I wrote about autocompletion for TEdits before here and here.

Back then I was using the SHAutoComplete API function in the Shlwapi.dll because I am a lazy basterd™. That hasn’t changed really but I also love fiddling with stuff, so some years ago, I actually added directory and general string completion to dzlib (and apparently didn’t blog about it), this time using the IAutoComplete2 interface as described in this answer on StackOverflow, which I mentioned before.

Today I revisited that code and and added file and directory completion to it, in a way that also allows filtering the files with a given file mask. So the user can easily enter directories and eventually select a file:

To add this functionality to a TEdit control, one only needs to add the unit u_dzAutoCompleteFiles to the form and call TEdit_ActivateAutoCompleteFiles in the form’s constructor like this:

constructor Tf_AutoCompleteTest.Create(_Owner: TComponent);
begin
  inherited;
  TEdit_ActivateAutoCompleteFiles(ed_AutoCompleteFiles, '*.dfm');
end;

The magic happens in a helper class TEnumStringFiles derived from the same TEnumStringAbstract class as TEnumStringDirs and TEnumStringStringList, implementing the IEnumString interface. It implements three methods:

  • function Next(celt: LongInt; out elt; pceltFetched: PLongInt): HResult;
  • function Reset: HResult
  • function Skip(celt:LongInt): HResult;

Reset is called whenever Windows thinks it needs to get the autocompletion list. Basically that’s whenever the user presses the backslash key, but apparently there are also other events that trigger it. After that the functions Next and possibly Skip are called to get the actual list.

Since this class is supposed to return directories and files, it uses two instances of TSimpleDirectoryEnum which encapsulates SysUtils.FindFirst and SysUtils.FindNext. One is for directories the other is for files matching a given mask (actually “mask” is not quite the right word as the “mask” is always appended to the base string the user has entered).

Here is the complete unit (but you should really get the code from OSDN to make sure the get the latest version):

unit u_dzAutoCompleteFiles;

interface

uses
  Windows,
  Messages,
  SysUtils,
  Classes,
  StdCtrls,
  ActiveX,
  u_dzfileutils,
  i_dzAutoComplete;

type
  TOnGetBaseFileEvent = procedure(_Sender: TObject; out _Base: string) of object;

type
  ///<summary>
  /// Implementaition of IEnumString for files </summary>
  TEnumStringFiles = class(TEnumStringAbstract, IEnumString)
  private
    FOnGetBase: TOnGetBaseFileEvent;
    FDirEnum: TSimpleDirEnumerator;
    FFileEnum: TSimpleDirEnumerator;
    FBase: string;
    FFilter: string;
    procedure doOnGetBase(out _Base: string);
    function FindNextDirOrFile(out _DirOrFilename: WideString): Boolean;
  protected
    // IEnumString
    function Next(celt: Longint; out elt;
      pceltFetched: PLongint): HResult; override;
    function Skip(celt: Longint): HResult; override;
    function Reset: HResult; override;
  public
    constructor Create(_OnGetBase: TOnGetBaseFileEvent; const _Filter: string);
    destructor Destroy; override;
  end;

procedure TEdit_ActivateAutoCompleteFiles(_ed: TCustomEdit; const _Filter: string);

implementation

{ TEnumStringFiles }

constructor TEnumStringFiles.Create(_OnGetBase: TOnGetBaseFileEvent; const _Filter: string);
begin
  inherited Create;
  FFilter := _Filter;
  FOnGetBase := _OnGetBase;
end;

destructor TEnumStringFiles.Destroy;
begin
  FreeAndNil(FDirEnum);
  FreeAndNil(FFileEnum);
  inherited;
end;

procedure TEnumStringFiles.doOnGetBase(out _Base: string);
begin
  if Assigned(FOnGetBase) then
    FOnGetBase(Self, _Base);
end;

function TEnumStringFiles.FindNextDirOrFile(out _DirOrFilename: WideString): Boolean;
var
  fn: string;
begin
  if Assigned(FDirEnum) then begin
    Result := FDirEnum.FindNext(fn, True);
    if Result then begin
      _DirOrFilename := fn;
      Exit; //==>
    end;
  end;

  if Assigned(FFileEnum) then begin
    Result := FFileEnum.FindNext(fn, True);
    if Result then begin
      _DirOrFilename := fn;
      Exit; //==>
    end;
  end;
  Result := False;
end;

function TEnumStringFiles.Next(celt: Integer; out elt; pceltFetched: PLongint): HResult;
var
  i: Integer;
  wStr: WideString;
begin
  i := 0;
  while (i < celt) and FindNextDirOrFile(wStr) do begin
    TPointerList(elt)[i] := Pointer(wStr);
    Pointer(wStr) := nil;
    Inc(i);
  end;
  if pceltFetched <> nil then
    pceltFetched^ := i;
  if i = celt then
    Result := S_OK
  else
    Result := S_FALSE;
end;

function TEnumStringFiles.Reset: HResult;
begin
  doOnGetBase(FBase);
  FreeAndNil(FDirEnum);
  FreeAndNil(FFileEnum);
  FDirEnum := TSimpleDirEnumerator.CreateForDirsOnly(FBase + '*');
  FFileEnum := TSimpleDirEnumerator.CreateForFilesOnly(FBase + FFilter);
  Result := S_OK;
end;

function TEnumStringFiles.Skip(celt: Integer): HResult;
var
  i: Integer;
  wStr: WideString;
begin
  i := 0;
  while FindNextDirOrFile(wStr) do begin
    Inc(i);
    if i < celt then begin
      Result := S_OK;
      Exit; //==>
    end;
  end;
  Result := S_FALSE;
end;

type
  TAutoCompleteHelperFiles = class(TAutoCompleteHelper)
  private
    FFilter: string;
    procedure HandleOnGetBase(_Sender: TObject; out _Base: string);
  protected
    function CreateEnumStringInt: IEnumString; override;
  public
    constructor Create(_ed: TCustomEdit; const _Filter: string);
  end;

procedure TEdit_ActivateAutoCompleteFiles(_ed: TCustomEdit; const _Filter: string);
begin
  TAutoCompleteHelperFiles.Create(_ed, _Filter);
end;

{ TAutoCompleteHelperFiles }

constructor TAutoCompleteHelperFiles.Create(_ed: TCustomEdit; const _Filter: string);
begin
  FFilter := _Filter;
  inherited Create(_ed);
end;

function TAutoCompleteHelperFiles.CreateEnumStringInt: IEnumString;
begin
  Result := TEnumStringFiles.Create(HandleOnGetBase, FFilter);
end;

procedure TAutoCompleteHelperFiles.HandleOnGetBase(_Sender: TObject; out _Base: string);
begin
  _Base := (FCtrl as TCustomEdit).Text;
end;

end.

The code is in the newly added u_dzAutoCompleteFiles unit in my dzlib.

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

 Posted by on 2019-04-28 at 18:41

Getting the Windows version revisited

 Delphi  Comments Off on Getting the Windows version revisited
Apr 222019
 

In my last blog post Getting the Windows version, I claimed that there is no way to get the actual version number of Windows 10 without reading the version information of the Kernel32.dll and interpreting it 1.

Since then I have been told that there is actually a Registry key that contains the Windows version:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion

It has several entries that can be used to get the same version info that the Winver tool displays to the user. On Windows 10 that’s:

  • ProductName
  • ReleaseId
  • CurrentBuildNumber
  • UBR

On my test installation they get me
“Windows 10 Pro” Version “1809” (OS Build “17762”.”437″)

(The quotes denote values read from the registry.)

By changing the ReleaseId value I verified that Winver (at least the on my Windows 10 installation) also reads these entries.

Some of these entries are available for older versions of Windows too, while some other entries apparently are only available in older versions. On the installations I have available for tests, it looks like this:

Entry Windows 10 Windows 8.1 Windows 7
ProductName x x x
ReleaseId x
CurrentBuildNumber x x x
UBR x x
CSDVersion x

There are some other interesting entries there, but I won’t go into that any further.

So I wrote another function that reads these entries and creates a version string from it. It returns the following results:

Windows Version Output
7 Windows 7 Professional (OS Build 7601.24411) Service Pack 1
8.1 Windows 8.1 Pro (OS Build 9600.19327)
10 Windows 10 Pro Version 1809 (OS Build 17763.437)

There is one catch though: Windows 64 bit versions by default return different view of the registry to 32 bit programs than they do to 64 bit programs. In order for a 32 bit program to read the “real” registry, it must open the registry specifying the KEY_WOW64_64KEY flag in addition to the desired access.

So here is the code:

function TryGetWindowsVersionFromRegistry(out _Values: TWinCurrentRec): Boolean;
var
  Reg: TRegistry;
begin
  // We need to read the real registry, not the 32 bit view, because some of the entries
  // don't exist there.
  Reg := TRegistry.Create(KEY_READ or KEY_WOW64_64KEY);
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    Result := Reg.OpenKeyReadOnly('SOFTWARE\Microsoft\Windows NT\CurrentVersion');
    if not Result then
      Exit; //==>

    _Values.BuildLab := TRegistry_ReadString(Reg, 'BuildLab');
    _Values.BuildLabEx := TRegistry_ReadString(Reg, 'BuildLabEx');
    _Values.CSDBuildNumber := TRegistry_ReadString(Reg, 'CSDBuildNumber');
    _Values.CSDVersion := TRegistry_ReadString(Reg, 'CSDVersion');
    _Values.CurrentBuildNumber := TRegistry_ReadString(Reg, 'CurrentBuildNumber');
    _Values.CurrentVersion := TRegistry_ReadString(Reg, 'CurrentVersion');
    _Values.EditionId := TRegistry_ReadString(Reg, 'EditionId');
    _Values.ProductName := TRegistry_ReadString(Reg, 'ProductName');
    _Values.ReleaseId := TRegistry_ReadString(Reg, 'ReleaseId');
    _Values.UBR := TRegistry_ReadInteger(Reg, 'UBR');
  finally
    FreeAndNil(Reg);
  end;
end;

function GetWindowsVersionStringFromRegistry: string;
var
  Values: TWinCurrentRec;
begin
  if not TryGetWindowsVersionFromRegistry(Values) then begin
    Result := _('Unknown Windows 10 version (registry key does not exist)');
    Exit; //==>
  end;

  if Values.ProductName = '' then begin
    Result := _('Unknown Windows version (ProductName entry cannot be read)');
    Exit; //==>
  end;
  Result := Values.ProductName;
  if Values.ReleaseId <> '' then
    Result := Result + ' Version ' + Values.ReleaseId;
  if Values.CurrentBuildNumber <> '' then begin
    Result := Result + ' (OS Build ' + Values.CurrentBuildNumber;
    if Values.UBR <> 0 then
      Result := Result + '.' + IntToStr(Values.UBR);
    Result := Result + ')';
  end;
  if Values.CSDVersion <> '' then
    Result := Result + ' ' + Values.CSDVersion;
end;

That code works even with Delphi 6, but you will have to declare the constant

KEY_WOW64_64KEY        = $0100;

there since it did not exist when Delphi 6 was released. It does exist in Delphi 2007. I haven’t tried other versions.

These functions are again also available in the unit u_dzOsUtils of my dzlib library.

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

—–

1 Actually I did not want to claim that there is no way but that I could not find one.

 Posted by on 2019-04-22 at 15:17

Getting the Windows version

 Delphi  Comments Off on Getting the Windows version
Apr 212019
 

When Microsoft introduced Windows 10 they said it would be the last version of Windows ever. They lied of course, because since then we have had multiple versions of an operating system called “Windows 10”.

Even worse, starting with Windows 8 they made their WinAPI function GetVersionEx lie about the Windows version, if the program did not say it knows about the current version. Which means every program written with older versions of Delphi will report Windows 6.2 (which was the internal version number of Windows 8). Only Windows 8 – and later Windows 10 – compatible Delphi compilers added a manifest to the executables to tell these Windows versions they know about it.

Since many Delphi developers still use ancient versions (there are always a few GExperts downloads for Delphi 6 when I publish an update, and quite a few for Delphi 2007), this is an issue.

I still want the Bug Report Wizard to report the correct Windows version, and not just “Windows 10” but “Windows 10 version 1809” as the WinVer tool says. So I did some digging and found that apparently there are two methods for getting the true version number:

  1. Call the RtlGetVersion API function, which is meant to be called by drivers only and also only returns version 10.0 for Windows 10 so it isn’t of much use to me (but at least it doesn’t lie).
  2. Get the product version from the version information of one of the main Windows DLLs, e.g. kernel32.dll, and use the revision number to determine the Windows 10 version number from it.

Since I could not find any ready made code for this, I rolled my own (maybe I didn’t look hard enough).

And being a generous person I am sharing it with you, including all the bugs I probably have introduced.

It’s in the u_dzOsUtils unit of my dzlib library.

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

EDIT: See also my follow up post Getting the Windows version revisited.

 Posted by on 2019-04-21 at 20:29

A simple way to create an URL label in Delphi

 Delphi  Comments Off on A simple way to create an URL label in Delphi
Apr 192019
 

An URL label is a TLabel on a form which displays some blue, underlined text which, when clicked with the mouse, opens a web page.

This kind of labels is nothing new in Delphi, I remember using them even back in the 1990s, but they always were a pain in the lower back to create:

  • Write the URL into the caption.
  • Set the font color to blue
  • Set the font to underlined
  • Set the cursor to crHandPoint
  • Write an OnClick event handler that opens the desired URL

Lots of boring and repetitive work and on top of that a Windows API call which I kept forgetting.
This led me to simply not use them.

Alternatively there were specialized components which you could use, so you simply dropped a TUrlLabel control on a form, filled in the caption and be done. There were quite a lot of these, and every time I wanted to use them I stopped, thought about it and didn’t. Install a package into the IDE and add another 3rd party dependency to the program just to have such a simple component? Nah!

Today, I came across these labels again on the GExperts About- and Feedback forms. I had to change some of them, and being a lazy basterd™, I decided to make this process easier once and for all. And not by adding a package / unit with a custom component but writing an overloaded pair of helper procedures which I call once and which does everything for me:

procedure TLabel_MakeUrlLabel(_lbl: TLabel); overload;
procedure TLabel_MakeUrlLabel(_lbl: TLabel; const _URL: string; _SetCaption: Boolean = False); overload;

They are used by simply calling them in the form’s constructor or in FormCreate passing a TLabel as parameter:

constructor TMyForm.Create(Owner: TComponent);
begin
  inherited;

  // if the caption already contains the URL to open:
  TLabel_MakeUrlLabel(l_SomeUrlLabel);

  // if it contains some descriptive text instead:
  TLabel_MakeUrlLabel(l_AnotherUrlLabel, 'https://blog.dummzeuch.de');

  // or if the caption should just be set in this code:
  TLabel_MakeUrlLabel(l_YetAnotherUrlLabel, 'https://blog.dummzeuch.de', True);
end;

Everything is taken care of, no changes to the label’s properties and no OnClick event handler necessary.

Here is how it is implemented:

type
  TUrlLabelHandler = class(TComponent)
  private
    FLbl: TLabel;
    FUrl: string;
    procedure HandleOnClick(_Sender: TObject);
  public
    constructor Create(_lbl: TLabel; const _URL: string); reintroduce;
  end;

constructor TUrlLabelHandler.Create(_lbl: TLabel; const _URL: string);
begin
  inherited Create(_lbl);
  FLbl := _lbl;
  FUrl := _URL;
  FLbl.OnClick := HandleOnClick;
  FLbl.Font.Style := FLbl.Font.Style + [fsUnderline];
  FLbl.Font.Color := clBlue;
  FLbl.Cursor := crHandPoint;
  if (FLbl.hint = '') and (Menus.StripHotkey(FLbl.Caption) <> FUrl) then begin
    FLbl.hint := FUrl;
    FLbl.ShowHint := True;
  end;
end;

procedure TUrlLabelHandler.HandleOnClick(_Sender: TObject);
begin
  ShellExecute(Application.Handle, 'open', PChar(FUrl), nil, nil, SW_SHOWNORMAL);
end;

procedure TLabel_MakeUrlLabel(_lbl: TLabel);
begin
  TLabel_MakeUrlLabel(_lbl, Menus.StripHotkey(_lbl.Caption), False);
end;

procedure TLabel_MakeUrlLabel(_lbl: TLabel; const _URL: string; _SetCaption: Boolean = False);
begin
  if _SetCaption then
    _lbl.Caption := _URL;
  TUrlLabelHandler.Create(_lbl, _URL);
end;

Not rocket science either. All it does is setting some properties and then it uses a simple trick to attach an OnClick event handler to the label: It creates a specialized TComponent descendant which sets the label as its owner and assigns one of its methods as the OnClick event handler. Since the component is owned by the label, it will take care of freeing it, so no further action required and no memory leak possible. The OnClick event then executes the WinApi call I mentioned above and which I kept forgetting:

  ShellExecute(Application.Handle, 'open', PChar(FUrl), nil, nil, SW_SHOWNORMAL);

I like neat tricks like those: Call and forget.

For your (or actually my own) convenience I have added it to the unit u_dzVclUtils of my dzlib. Feel free to use it.

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

 Posted by on 2019-04-19 at 19:35

Image Processing Magic

 Delphi  Comments Off on Image Processing Magic
Apr 162019
 

It’s a bit frightening what image processing “magic” can do to a picture:

Original:

Processed:

And that’s not even advanced “magic” but some rather simple code written in Delphi (most of which I can’t publish here unfortunately).

What it does is:

  • Smooth brigthness by applying a brigthness map.
  • Adjust for lens edge blur by selective sharpening.
  • Improve contrast by histogram stretching.

Of course since this is a gray scale picture, the processing is much simpler than it would be for colour pictures.

 Posted by on 2019-04-16 at 11:57

Deleting unwanted entries from the “New” submenu of Windows Explorer

 Batch, Windows  Comments Off on Deleting unwanted entries from the “New” submenu of Windows Explorer
Apr 092019
 

Microsoft Office has the annoying habit of adding itself to various places in the Windows user interface. One of them is the popup menu of the Windows Explorer. It adds one entry for each of the installed programs to the “New” submenu.

Have you ever used them? I have only used that submenu for creating folders, shortcuts and text files, but never ever (apart from trying it once I think) for creating new MS Office documents.

So I have removed these entries several times (every time having to google the process) but every update recreates the entries and today I’ve had enough.

So here is a cmd file which will delete them (works for Office 2016 only). You will have to run it as administrator to work.

@echo off
echo This batch file will delete the Registry entries for
echo the "New" submenu of Windows Explorer
echo added by MS Office 2016.
echo This batch file must be run as Administrator.
echo Continue?
pause
reg delete HKEY_CLASSES_ROOT\.docx\Word.Document.12\ShellNew /f
reg delete HKEY_CLASSES_ROOT\.xlsx\Excel.Sheet.12\ShellNew /f
reg delete HKEY_CLASSES_ROOT\.pptx\PowerPoint.Show.12\ShellNew /f
pause

download link

It uses the built in reg command. The /f switch forces the deletion without confirmation. Maybe you want to remove it.

As always: Check any exectuable code you download from the Internet! It might damage your computer.

 Posted by on 2019-04-09 at 11:27

How to defeat useful tools

 Windows  Comments Off on How to defeat useful tools
Apr 042019
 

Sizer is a very useful tool for sizing and positioning windows. It adds a right click menu to a window’s title bar where you can select one of multiple configured window sizes and positions. It also displays the current window size while you are resizing it by dragging its frame.

Now, why would somebody try to defeat the usability of this tool?

Because they can?

Google and Mozilla did exactly that: By moving the tab bar into the window title. In Chrome, Thunderbird and Firefox right clicking on the title bar no longer brigs up the Sizer menu. In Chrome at least clicking on the window frame still gets this menu. Both browsers also prevent the size display while dragging the frame which at least works for Thunderbird.

And now guess which windows I would like to use Sizer the most?

Some software developers should be shot, just so they don’t have a bad influence on others.

(Since nowadays it apparently is necessary to explicitly say that: No, I don’t want anybody to take this as an ecouragement to commit a felony or act of terror.)

 Posted by on 2019-04-04 at 10:16

Extend the event OnClick of all MenuItems in the screen to execute another block of code

 Delphi, GExperts  Comments Off on Extend the event OnClick of all MenuItems in the screen to execute another block of code
Apr 032019
 

Shameless self promotion:

On StackOverflow somebody asked the question in the title and I answered it. And as you might have guessed, I am mighty proud of having had that idea.

It might be interesting to know that this is how GExperts fixes some of the IDE bugs and enhances some forms: It creates a TComponent descendant which hooks some events of the form to implement the fixes or enhancements. And since it is added to the form, it automatically gests freed in the form’s destructor.

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

 Posted by on 2019-04-03 at 12:18

Things that every desktop program should do

 Delphi  Comments Off on Things that every desktop program should do
Apr 032019
 

There are a few things that every single one of our internal programs should do:

  • Every dialog should be sizeable if it makes any sense at all. Delphi makes that easy with the Align and Anchor properties. Ask yourself the following questions. If you answer any of them with Yes, you should make the dialog sizeable:
    • Is there a list with an unkown number of entries?
    • Is there a text entry field which can contain very long text (e.g. a file name)?
  • If a dialog is sizeable, set a minimum window size that allows all controls to be visible in a reasonable size.
  • If a dialog is sizeable, you should test it after every change. It is embarrassing if one button does not move the way it should.
  • If a dialog is sizeable, it should store its position and size and load it when it is being opened again. But keep in mind, that the screen sizes and arrangements can vary from one computer to another and can even change on the same computer because somebody rearranged the physical positions of his monitors or got a new one.
    • Do not store the position and size centrally for all users. If you do that, some of them will hate you. I prefer HKCU in the registry for that.
    • Always make sure that the dialog is fully visible on a monitor. A dialog that opens outside the visible area is an embarrasment and also a support nightmare.
  • Make it easy for the user to find version information. I prefer having it right in the main window’s title. e.g. “My Super Application [1.0.1.532 2019-04-03]”. The date here comes from the project version which our build scripts automatically fill with the build date.

There are a few functions in the unit u_dzVclUtils of my dzlib that make this a lot easier. They are usually called in the form’s constructor:

If you would like to comment on this, go to this post in the international Delphi Praxis forum.

 Posted by on 2019-04-03 at 11:55