Quantcast
Channel: dummzeuch – twm's blog
Viewing all 552 articles
Browse latest View live

A simple way to create an URL label in Delphi

$
0
0

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.


Getting the Windows version

$
0
0

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.

Getting the Windows version revisited

$
0
0

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.

Autocompletion for TEdits revisited

$
0
0

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.

Blocking the Windows Screen Saver in Delphi

$
0
0

Sometimes your program needs to block the screen saver from automatically kicking in. My use case was that the program was recording data and whenever the screen saver was active, the data was lost (No idea why, it probably had something to do with the way HID is implemented in Windows.)
So I was looking for a way to fix that without forcing the user to turn off the screen saver. The methods that used to work under Windows XP no longer work in Windows 7 and later (I don’t care about Vista), so I googled and found this question on StackOverflow. The Windows API functions PowerCreateRequest + PowerSetRequest mentioned in the highest voted answer looked promising. Unfortunately they don’t seem bo be available in Delphi (Delphi 2007, which I used for that project, is too old to know them, but I coudn’t find them in Delphi 10.3 either). The first task was therefore to get a function declaration for Delphi. Google didn’t help here which meant that I had do create them myself. Not a big deal:

type
  TPowerCreateRequest = function(_Context: PReasonContext): THandle; stdcall;
  TPowerSetRequest = function(_Handle: THandle; _RequestType: TPowerRequestType): LongBool; stdcall;
  TPowerClearRequest = function(_Handle: THandle; _RequestType: TPowerRequestType): LongBool; stdcall;

I prefer loading such functions at runtime rather than the program not starting because some external referecne is not avaiable. These functions are exported by kernel32.dll.

  FDllHandle := SafeLoadLibrary(kernel32);
  PowerCreateRequest := GetProcAddress(FDllHandle, 'PowerCreateRequest');
  PowerSetRequest := GetProcAddress(FDllHandle, 'PowerSetRequest');
  PowerClearRequest := GetProcAddress(FDllHandle, 'PowerClearRequest');
  if not Assigned(PowerCreateRequest) or not Assigned(PowerSetRequest) or not Assigned(PowerClearRequest) then
    raise EOsFunc.Create(_('Could not initialize the PowerXxxxRequest functions from kernel32.'));

Usage is not without its own problems. First, I had to declare the constants and parameters:

const
  POWER_REQUEST_CONTEXT_VERSION = 0;
  POWER_REQUEST_CONTEXT_DETAILED_STRING = 2;
  POWER_REQUEST_CONTEXT_SIMPLE_STRING = 1;
type
  PReasonContext = ^TReasonContext;
  TReasonContext = record
    Version: ULONG;
    Flags: DWORD;
    case Boolean of
      False: (
        SimpleReasonString: PWideChar;
        );
      True: (
        Detailed: record
          LocalizedReasonModule: HMODULE;
          LocalizedReasonId: ULONG;
          ReasonStringCount: ULONG;
          ReasonStrings: PPWideChar;
        end;
        );
  end;
type
  TPowerRequestType = (
    PowerRequestDisplayRequired = 0,
    PowerRequestSystemRequired = 1,
    PowerRequestAwayModeRequired = 2,
    PowerRequestExecutionRequired = 3);

Now, how do these functions work?

The first thing to do is creating a power request with PowerCreateRequest. This function requires a PReasonContext pointer which must be initialized correctly. The Version and Flags fields are simple: Assign one of the POWER_REQUEST_CONTEXT_xxx constants declared above. But what aobut the other fields? I decided to go with the simple case, that is: Set Flags to POWER_REQUEST_CONTEXT_SIMPLE_STRING and provide a value for SimpleReasonString.

var
  FRequestHandle: THandle;
  FContext: TReasonContext;
  FReason: array[0..255] of WideChar;
  // [...]
  FContext.Version := POWER_REQUEST_CONTEXT_VERSION;
  FContext.Flags := POWER_REQUEST_CONTEXT_SIMPLE_STRING;
  FContext.SimpleReasonString := @FReason;
  FRequestHandle := PowerCreateRequest(@FContext);
  if FRequestHandle = INVALID_HANDLE_VALUE then
    RaiseLastOSError;

Where FReason is an array of WideChar. My tests showed that the TReasonContext record and the reason string it points to must be available through the lifetime of the reason context. If it isn’t, the reason displayed by the powercfg tool (see below) will be corrupted. Therefore I did not use a WideString but a static array.

After the power request has been created, calls to PowerSetRequest and PowerClearRequest are possible.

  Win32Check(PowerSetRequest(FRequestHandle, PowerRequestDisplayRequired));

This call prevents the screen saver from starting automatically. Ca call to PowerClearRequest supposedly turns that off again (but I haven’t tested it).

I mentionend the powercfg tool above. It’s a Windows command line tool that among other functionality can display processes that have active power requests. e.g.

powercfg /requests
DISPLAY:
[PROCESS] \Device\HarddiskVolume2\Source\dzlib\tests\BlockScreenSaver\BlockScreenSaver.exe
test

SYSTEM:
None.

AWAYMODE:
None.

EXECUTION:
None.

PERFBOOST:
None.

The string “test” the reason I passed to PowerCreateRequests.

I mentioned that failing to preserver the reason string results in a corrupted message in the display. It looked like this:

powercfg /requests
DISPLAY:
[PROCESS] \Device\HarddiskVolume2\Source\dzlib\tests\BlockScreenSaver\BlockScreenSaver
.exe
?a?E?I???↑?E?↑?E?↑?E?↑?E?↑

Note that this tool requires administrator privileges (but power requesets don’t).

I have added this code to my dzlib. It’s in u_dzOsUtils. There is also a simple test / demo program BlockScreenSaver.

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

Installing dotNet 2.0 on Windows 10

$
0
0

In theory it is simple to install the dotNet 2.0 framework on Windows 10: Just go to “Programs and Features”, select “Turn Windows Features on or off”, set the checkmark for “.NET Framework 3.5 (includes .NET 2.0 and 3.0)”, press OK and let Windows download the necessary files from Windows Update.

Unfortunately this only works most of the time. If you are unlucky like me and it doesn’t, you will start an odyssey of downloading installers from Microsoft (which also fail, because they try to download files from Windows Update for whatever reason), using the dism tool and possibly Power Shell to install it offline (both of which failed too in my case) and then either despair or find a reference to the “Missed Features Installer”.

When I arrived there, I was very suspicious (and so should you!) of downloading and using such a 3rd party installer. I used the download from Computer Bild not because I think they are the most brilliant computer magazine in Germany (they are not) but at least I trust them not to distribute malware (which is more than I trust the computer magazine CHIP). In addition, I used Virus Total to scan the installer. It gave me a thumbs up, so I was brave enough to run it.

Guess what? It worked. I now have a working .NET 3.5 and 2.0 framework on my computer and could finally install the program I actually wanted to install: The AVT Universal Package for accessing a camera.

The 3 different License Types for Delphi

$
0
0

I blogged about the Embarcadero License Center (ELC) and how to use it remotely. But I never went to the trouble of explaining the different kinds of license types that are available for Delphi (after all: I don’t sell these licenses, so why bother?). But somebody else just did: CodePartners blogged about Network Licensing in RAD Studio. It gives a pretty good overview.

I for one can definitely recommend switching from Named User to Network Named User licenses if you often need to install Delphi on a new computer (I need that rather often) or have a significant turnover of developers over the years.

Network licensing is supported in Delphi 2007 and later.

New Icons for MMX


Fix for Access Violation in u_dzAutocompleteStrings

$
0
0

A while ago I blogged about the various possibilities for auto completion in TEdits. I created several units that supply auto completion based on

  • a StringList
  • files
  • directories
  • path (basically also directories)

They all followed a basic principle: Implement IAutoComplete and IAutoComplete2 and provide different implementations for the IEnumStrings interface. They worked fine in Delphi 2007 but I never tested them in later versions.

Today I wanted to add auto completion for files to a tool written in Delphi 10.2 and got an Access Violation in my implementation of the IEnumStrings.Next method. I could not figure out the problem, so I wrote a test program using the simplest implementation I had, the one using a StringList, to be sure there wasn’t any other part of the tool that wreaked havoc with the auto completion. The bug was still there and I still could not figure out what the problem was:

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

The Access violation happens in the line that assigns something to the first item in the elt TPointerList. As I said: It works fine in Delphi 2007.

Googling turned up a slightly different implementation for this:

    TPointerList(elt)[i] := CoTaskMemAlloc(2 * (Length(wStr) + 1));
    StringToWideChar(wStr, TPointerList(elt)[i], 2 * (Length(wStr) + 1));

Which unfortunately still caused an access violation. Finally I happened about this answer from Remy Lebau on StackOverflow that contained a type defintion with a comment:

type
  TPointerList = array[0..0] of Pointer; //avoid bug of Classes.pas declaration TPointerList = array of Pointer;

Guess what? It solved the problem. The code now works in both, Delphi 2007 and 10.2.

So, what causes the Access Violation? And why not in Delphi 2007?

The declaration of TPointerList in Delphi 2007 looks like this:

  TPointerList = array[0..MaxListSize - 1] of Pointer;

while the one in Delphi 10.2 is this:

  TPointerList = array of Pointer;

The first one is a static array the second one is a dynamic array. A Static array is a block of memory which is allocated automatically by the compiler for the given size, while a dynamic array is a pointer to an array descriptor (initially it’s NIL). Once the dynamic array gets initialized using the SetLength procedure the array descriptor contains the size of the array and enough memory to store the entries. The dynamic array variable points to the first entry.

So the difference between typecasting the elt parameter to a static array of pointers or an dynamic array of pointers causes the Access Violation. The change happened between Delphi XE and Delphi XE2. (I reported this as a bug in the Delphi RTL: RSP-24616. because apparently nobody had.).

elt is declared as an untyped out parameter. That means it is a pointer to some memory that the caller provides and which is to be written to by the Next method.

Typecasting it to a static array of pointers means that we assume that elt points to some memory which is to contain pointers to OleStrings. Typecasting it to a dynamic array of pointers means that we assume that elt points to a pointer to some memory that is to contain pointers, so the first (usually uninitialized) pointer in the array is referenced and since it points to somewhere which should not be written to, writing to it causes an access violation.

Adding the type definition given above fixed the problem.

The difference is the same as between a PInteger and a PPInteger Parameter:

type
  PInteger = ^Integer;
  PPInteger = ^PInteger;

procedure bla(param: PInteger);
begin
  param^ := 5;
end;

proceduer blub(param: PPInteger);
begin
  Param^^ := 5;
end;

var
  InvValue: integer;
begin
  bla(@IntValue);
  blub(@IntValue);
end;

If no type checking took place passing a pointer to an Integer to blub would compile but cause an Access Violation because it tries to use the value of IntValue as a pointer. Since it usually isn’t, the memory that “pointer” references would not be accessible.

Later I found another post on StackOverflow that actually was about that very Access Violation I was trying to find with an answer from Rudy Velthuis.

New GExperts IDE enhancement: Export and Import entries for the Tools menu

$
0
0

The Delphi IDE has the quite useful option to add custom entries to the Tools menu. These entries call external programs with some “Macros” that reference the current IDE status, e.g. the currently active project or source file and some of their properties like the output directory or the current editor column and row.

GExperts already enhances the Tools Properties dialog by adding auto completion for the file name and the working directory (for Delphi 6 and 7 it also adds support for Drag and Drop, but that doesn’t work for later versions).

It has always irked me that there was no easy way to port these custom tools entries from one Delphi version or installation to another. I always had to copy and paste four fields to achieve that.

GExperts now adds two new buttons to export and import the current entry:

These buttons write / read Delphi Tool Menu Entry files (*.dtme), which are in INI file format an look like this:

[Delphi Tool Menu Entry]
Title=Explore &Source Directory
Path=explorer.exe
WorkingDir=
Params=/e,/select, $EDNAME

So, to move an entry from one IDE version to another, export it from the first one, add a new entry to the second one and import the exported file.

I’m thinking about adding a special clipboard entry format for this so creating a file to copy these entries will no longer be necessary, but that’s for some future time (which may be very near but maybe not).
Edit: Done, see here.

I am also currently working on a small side project called Delphi Tools Manager which will supply that functionality outside the IDE, but it’s not quite ready for prime time yet. I actually already made a release available but had to withdraw it because I found that there are apparently two different ways the IDE stores these entries in the registry.

There is no new GExperts release for now (I am waiting for the next Delphi point release which should be just around the corner. I’m just guessing, I have no internal knowledge about the release schedule.). In the meantime, if you’d like to get this feature, you will have to compile your own GExperts DLL. But that’s far from being rocket science.

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

GExperts adds copy and paste for Delphi Tool menu entries

$
0
0

In my last post I wrote about the export and import feature for custom Tools menu entries that GExperts adds to the Delphi IDE. I also mentioned that I was thinking about adding a custom clipboard format for copying and pasting these entries between multiple Delphi instances / versions.
OK, I did that. GExperts now also adds a popup menu to the Tool Properties dialog with two entries:

  • Copy entry to clipboard
  • Paste entry from clipboard

These entries use a custom clipboard format registered with Windows as "Delphi.ToolsEntry". It’s a binary format with the following structure:

  • Size: UInt32 -> the size of the structure
  • Title: array of WideChar, zero terminated
  • Path: array of WideChar, zero terminated
  • WorkingDir: array of WideChar, zero terminated
  • Params: array of WideChar, zero terminated

With this popup menu it is now possible to copy and paste Tools menu entries between multiple instances of the Delphi IDE. This works also between different Delphi versions, so you can copy the entries from Delphi 10.3 to Delphi 6 and vice versa.

There is no new GExperts release for now (I am waiting for the next Delphi point release which should be just around the corner. I’m just guessing, I have no internal knowledge about the release schedule.). In the meantime, if you’d like to get this feature, you will have to compile your own GExperts DLL.

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

Add “Open with” for any executable to the Explorer context menu

$
0
0

Many tools optionally add an “Open with [Name of tool]” entry to the context menu of the Windows Explorer. Some others don’t, even though it would be useful. Here is how to do it yourself:

  1. Open any text editor (Notepad will do)
  2. Copy and paste the following text into it:
    Windows Registry Editor Version 5.00
    
    [HKEY_CLASSES_ROOT\*\shell\Open with [DisplayName]\command]
    @="[Path\\to\\executable] %1"
    
  3. Replace [DisplayName] with the name of the tool.
  4. Replace [Path\\to\\executable] with the full path of the executable to open. Not that you must replace all backslashes with a dual backslash for this to work.
  5. Save the file as “Add-Open-with-MyTool-to-Context-Menu.reg”. Make sure that it is saved as a .reg file, not as a .txt file! (One of the annoyances of Notepad.)
  6. Open the created file with a double click and let the Registry Editor add it to the Registry

Examples:

Open with (portable) Notepad++

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\*\shell\Open with Notepad++\command]
@="C:\\PortableApps\\Notepad++Portable\\Notepad++Portable.exe %1"

Notepad++ is a free and powerful replacement for Notepad and can be downloaded from notepad-plus-plus.org.

Open with (portable) HxD

Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\*\shell\Open with HxD\command]
@="C:\\PortableApps\\HxD\\HxD64.exe %1"

HxD is a free and powerful hex editor and can be downloaded from https://mh-nexus.de/en/hxd/.

Created entries

The created entries look like this:

On (re-)installing Delphi without active maintenance

$
0
0

Apparently Embarcadero has decided to piss off the remaining Delphi users in the whole world, just to line their pockets. One of the annoyances of Delphi after Delphi 7 has been the enforced online activation. First, there was a 4 weeks grace period until that activation was necessary, but even that grace period has been gone lately. Also, there is a (not documented) limited number of allowed activations. Once you hit that limit, you had to contact Embarcadero to get a “bump”. Apparently that’s a manual process.

Now they have gone a step further:

You won’t get that “bump” unless you have got an active update subscription (which costs several hundred Euros per year).

Let me repeat that: You can’t install your legally paid for perpetual license of Delphi any more, unless pay a yearly fee of several hundred Euros.

Embarcadero’s GM (General Manager?) Atanas Popov wrote this on the topic:

Registration Limits

We have noticed compliance issues and increased Support efforts related to registration limit increases for customers on older product versions, who are no longer on Update Subscription. It is a standard industry practice to provide support to the most recent versions and to customers who have extended maintenance. We updated our processes and now route all issues raised from users who are not on Update Subscription to Sales and Renewals. We realize this is a change to previous operations and to reduce the impact to development projects, we issued a one-time registration limit increase for all customers who are close to hitting their registration count limit. This should address issues with re-installs of your licensed software on existing or new machines. Further, we will continue to look for options to make this more seamless through automation.
— Atanas Popov in his blog post From the GM: New Updates and Changes to the Registration “Bumps” Policy

I hope the part where he says “Further, we will continue to look for options to make this more seamless through automation.” he is not just trying to stall until the shit storm goes by, because, it won’t.

Just to make this clear:

It is a standard industry practice to provide support to the most recent versions and to customers who have extended maintenance.

It might be a standard industry practice, but in Germany, and I think in most other countries, this is illegal. You sold a perpetual license. So if you restrict your customers from using it, they are not getting the contractually guaranteed service. And you will probably find that some of them do employ lawyers.

At work, we have got 3 Delphi licenses: Two for Delphi 2007 and XE2 and another one (mine) on subscription. I have never used any of the support requests, I keep that subscription only be up to date on the features of the latest releases (and incidentally work on GExperts and other open source projects; with the consent of my employer of course). The other licenses used by two of my coworkers stay at the versions they are now because so far, I found nothing in later versions that would have made me want to update.

We currently are purely a Delphi shop, that is, all software development is done in Delphi. Most of these programs are for internal use only, with some very few exceptions. We do not sell software, we do road condition surveys all over Germany and Europe.

But if we cannot rely on Embarcadero delivering what they contractually agreed to deliver, we will not upgrade and pay for subscription, but probably look elsewhere. Not just because of the cost but also because it has become more difficult to find Delphi developers (Not just good ones. It has become nearly impossible to find any software developer who is willing to use Delphi at all.). I guess by pissing off Delphi developers world wide, Embarcadero will not increase their numbers, so this situation will not improve.

Delphi Tools Manager 1.0.0 released

$
0
0

I mentioned that I am working on the Delphi Tools Manager in my post about GExperts supporting import and export for the Tools menu.

Today I finished the first version of this tool. It supports all Delphi versions from 5 to 10.3 (it is written in Delphi 10.2).

So, what does it do:

  1. It shows a list of all entries in the Delphi IDE’s tool menu in a similar manner as the Tools Options dialog.
  2. It allows the same actions as that dialog: Add/Delete/Edit an entry and change the order of the entries.
  3. It shows the details of each entry when it is selected.
  4. It allows exporting and importing entries, using the same DTME file format as GExperts
  5. Dragging one or more DTME files on the form will add them to the list

If it finds more than one Delphi version installed on the computer, it asks which one you want to modify.

While I have tested this program and found no bugs, I might have overlooked something. You use it on your own risk!

Download from OSDN

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

Delphi Tools Manager contains a virus – claims Microsoft

$
0
0

My recently released Delphi Tools Manager tool according to Virus Total and there the Microsoft virus scanner contains the PUA:Win32/Puwaders.B!ml virus.

The description on Microsoft’s site is as always pretty useless:

Summary

This application was stopped from running on your network because it has a poor reputation. This application can also affect the quality of your computing experience.

So, is this a virus or is it not? My interpretation would be that it isn’t actually a virus but just some glitch with the heuristics employed by that scanner.

No other virus scanner detected anything which is another hint that this isn’t actually a problem at all. Oddly enough, the Windows Defender on my computer hasn’t flagged the file either.

So far I don’t know what to make of it. Should I be worried that my system is infected and therefore the executables I create on it also could be infected? Probably not, since it is unlikely that only one of the engines used by Virus Total detects it and even the Microsoft scanner only detects it in the ZIP file, not in the executable itself.


Setting the drop down width of a Combobox in Delphi

$
0
0

By default, the width of the drop down list of a TComboBox is the same as the width of the control itself, and even in the latest Delphi version there apparently is no property to set it.

Why is that so? Good question. There are probably many third party controls that offer this because it is rather simple to implement. But on the other hand, if it is that simple, why isn’t it a feature of the default control? It can really be a pain in the lower back that some entries are just not displayed correctly as seen in the picture above.

Setting the drop down width is as simple as sending the CB_SETDROPPEDWIDTH message to the control’s handle:

SendMessage(TheCombobox.Handle, CB_SETDROPPEDWIDTH, MinimumWidthInPixels, 0);

It does not allow to shrink the width of the drop down list though, because it sets the minimum width, not the actual width. There is this answer on StackOverflow for that particular problem. The result isn’t very visually appealing though, because the list is left aligned rather than right.

Of course that can easily be fixed by adding the width difference to all coordinates passed to MoveWindow.

But the normal use case is probably that the list is not wide enough, not that it’s too wide.

While they help, these solutions still leave something to wish for: Usually you don’t want to set a fixed width in pixels, you want to make the drop down list wide enough so it fits the entries. So ideally you will take the list of entries, calculate their text width, optionally add the width of the scroll bar and set that as the width for the drop down list.

Of course, I am not the first one who want to do that. I found an ancient article by Zarko Gajic on ThoughtCo (formerly known as about.com) that deals with this issue. The result looks like this:

The article even goes a lot further. When do you call that code? Of course only after you added the items to the list. So, why not put it into the OnDropdown event of the ComboBox? (Because that clutters the sources and you also have to create an event handler for each of these ComboBoxes on your form, that’s why.). It also handles the case where the ComboBox is placed near the right border of the form and claims that it gets truncated there. I could not reproduce that problem. This looks fine to me:

So, why am I blogging about this? As said above, there are plenty of other articles on the web that handle these issues.

Last week I investigated several cases where the SendMessage code did not work at all. It turned out that somewhere between the SendMessage call in the form’s constructor and the time the user clicked the drop down button the window handle of the ComboBox got recreated, losing the changed drop down width. A solution for this would have been to put the code into the OnDropDown event. That would have required 4 event handlers, one for each of the ComboBoxes on that particular form, (or a shared event handler for all 4). I don’t like that, I prefer a solution that once and for all solves that problem by calling some function in the constructor and then forget about. So I went and wrote one:

// taken from:
// https://www.thoughtco.com/sizing-the-combobox-drop-down-width-1058301
// (previously about.com)
// by Zarko Gajic

procedure TComboBox_AutoWidth(_cmb: TCustomComboBox);
const
  HORIZONTAL_PADDING = 4;
var
  itemsFullWidth: Integer;
  Idx: Integer;
  itemWidth: Integer;
begin
  itemsFullWidth := 0;
  // get the max needed with of the items in dropdown state
  for Idx := 0 to -1 + _cmb.Items.Count do begin
    itemWidth := _cmb.Canvas.TextWidth(_cmb.Items[Idx]);
    Inc(itemWidth, 2 * HORIZONTAL_PADDING);
    if (itemWidth > itemsFullWidth) then
      itemsFullWidth := itemWidth;
  end;
  // set the width of drop down if needed
  if (itemsFullWidth > _cmb.Width) then begin
    //check if there would be a scroll bar
    if TComboBoxHack(_cmb).DropDownCount < _cmb.Items.Count then
      itemsFullWidth := itemsFullWidth + GetSystemMetrics(SM_CXVSCROLL);
    SendMessage(_cmb.Handle, CB_SETDROPPEDWIDTH, itemsFullWidth, 0);
  end;
end;

type
  TComboAutoWidthActivator = class(TWindowProcHook)
  protected
    procedure NewWindowProc(var _Msg: TMessage); override;
  public
    constructor Create(_cmb: TCustomComboBox);
  end;

{ TComboAutoWidthActivator }

constructor TComboAutoWidthActivator.Create(_cmb: TCustomComboBox);
begin
  inherited Create(_cmb);
end;

procedure TComboAutoWidthActivator.NewWindowProc(var _Msg: TMessage);
begin
  if _Msg.Msg = CBN_DROPDOWN then
    TComboBox_AutoWidth(TCustomComboBox(FCtrl));
  inherited;
end;

function TComboBox_ActivateAutoWidth(_cmb: TCustomComboBox): TObject;
begin
  Result := TComboAutoWidthActivator.Create(_cmb);
end;

The TCombobox_AutoWidth procedure is taken from Zarko Gajic’s article, but adapted to take a TCustomComboBox parameter rather than TComboBox (And while I am writing this I have got a few ideas what to improve on it.) The TComboAutoWidthActivator class is based on the TWindowProcHook class I used earlier. It simply hooks the control’s WindowProc, checks for the CBN_DROPDOWN message (which will later cause the OnDropDown event to be called) and calls TCombobox_AutoWidth.

So, my form’s contructor now looks like this:

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

  TCombobox_ActiveAutoWidth(cmb_One);
  TCombobox_ActiveAutoWidth(cmb_Two);
  TCombobox_ActiveAutoWidth(cmb_Three);
  TCombobox_ActiveAutoWidth(cmb_Four);
end;

Much cleaner than the solution with all those OnDropDown handlers.

The code above is part of u_dzVclUtils of my dzlib.

One possible improvement would be to have yet another function to enumerate over all controls on a form, find all ComboBoxes and call for them. I’m Not sure it’s worth the trouble, but it wouldn’t be rocket science either.

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

project.exe is open in another program errors

$
0
0

I have been using two tools to add additional data to my executable files in post build scripts for years:

Both tools open the freshly built executable and append chunks of data to it. And both started to fail with the error “[project].exe is open in another program” more often lately.

I never managed to find out exactly what causes the problem. First I suspected BitDefender antivirus, but the error still occurs after I have switched to Windows Defender, so it’s not just BitDefender but probably any antivirus software that is causing the trouble.

As a first try I added a 2 seconds delay between the compiling/linking and the execution of each of these tools. It seemed to help, the errors became much less frequently but they still occurred. Also those 4 seconds always felt like eternity (the whole compile usually takes less than 10 seconds, so 4 additional seconds is a whopping 40%).

So, a few days ago I had enough. I’ve got the source code of these tools, so why not simply add several tries to opening the file? That’s what I did: Both tools now try to open the file and if that fails wait for a second and try again. Rinse and repeat up to 10 times. In my tests, it rarely took more than 2 tries, usually the first one succeeded. That means it shaved these 4 seconds off the build time again with the rare exception of adding them again at very few occasions.

The changes to assemble are in the GnuGettext svn repository.

And here is a patch to jcldebug.pas which I used for MakeJclDbg. (If somebody wants to submit this as a pull request on github, go ahead.)

Both executables are available from my dzlib+buildtools project on OSDN.

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

Troubleshooting FTDI driver installation

$
0
0

So I don’t forget:
If the drivers for an FTDI USB-serial adapter don’t install resulting in an "unknown device", try to remove all other USB connected devices.

In my case it was a PeakCAN USB adapter, connected via a rather long USB extension cable, that caused the problem (not sure whether it was the adapter itself or the extension cable, that caused it, but I assume the latter, because these adapters usually don’t cause any trouble).

Temporarily disconnecting the cable let me install those USB serial adapter drivers.

The problem was reproducible: It happened after each reboot.

Another unproductive hour spent troubleshooting a Windows driver problem. I just hate Windows and its opaque driver installation method. Give me real error messages Microsoft, so I at least get a clue what’s wrong!

TStringGrid has Rows and Cols properties

$
0
0

From the “How could I have missed this for decades?” department:

The TStringGrid component in Delphi has got two interesting properties which I didn’t know about:

I found out about rows when I searched for code for sorting a StringGrid and found something at SwissDelphiCenter that uses this property. Then I looked it up in the online help and found that there is also a similar property for columns.

Now the question is: Did I really miss this or have I already forgotten? And if the latter, how many times? 😉

Delphi Shortcut Finder for 10.x

$
0
0

Nicholas Ring wrote a useful Delphi IDE plugin called Delphi Shortcut Finder and made it open source on GitHub.

Unfortunately he updated it last in 2015, so it has become yet another abandoned Delphi tool.

I had a need for something like this today and remembered the tool, but unfortunately not its name. So it took me a while to find it again.

Just in case somebody else needs it, here are precompiled packages for

Note that installing it requires Virtual Tree View to be installed already.

The sources I used are here

Viewing all 552 articles
Browse latest View live