Mar 192017
 

A while ago I stopped using the formatter branch and officially took over the trunk of the GExperts repository. When I announced that on G+, Stefan Glienke asked me about my plans for GExperts.

My answer today is still the same as back then:

I will release new GExperts versions when I feel like it. And I will continue to call them experimental. There will be no rigorous testing, since nobody has volunteered to take on that responsibility. I will be using the latest development version in my daily work, so for now you can assume that at least the Delphi 2007 and XE2 versions are reasonably stable. This will change once I move on to newer versions, but unfortunately I don’t see that in the near future.

So, if you don’t like this, I would welcome volunteers to step forward and assume the responsibility of testing GExperts preferably with the Delphi versions they use every day. Your reward will be a place in the contributor list and possibly the pain of working with a not quite stable IDE.

Mar 192017
 

Some people have started pestering me about making a new release (you know how you are!). Don’t think that this has done anything to actually make me do it, I simply thought it to be the right time with Delphi 10.2 Tokyo apparently right around the corner.

I have created installers for all supported Delphi versions.

I don’t remember all the new features and bug fixes that went into this release, but I am pretty sure it is more stable and has more features than the previous one.

But anyway:

Please be aware that I mostly work with Delphi 2007, so this version can be regarded as tested quite well, followed by Delphi XE2. The others are only known to compile and new features are usually tested superficially with all versions. This is particularly true for Delphi 6/7 and 2005/2006.

Head over to the Experimental GExperts page to download the latest release it.

Mar 122017
 

For my dzComputerInfo tool I created a window without a title that can still be moved with the mouse. This is quite easy to do:

  1. To remove the title, set BorderStyle to bsNone.
  2. To let the user move it with the mouse, add the following message handler:
type
  TMyForm = class(TForm)
  private
    procedure WMNCHitTest(var Msg: TWMNcHitTest); message WM_NCHITTEST;
  end;

procedure TMyForm .WMNCHitTest(var Msg: TWMNcHitTest);
begin
  inherited;
  if (Msg.Result = htClient) then
    Msg.Result := htCaption;
end;

It tells Windows, that the user clicked on the title rather than the client area. Windows then does the rest, and the user can move the window with the mouse as if he clicked on the window title.

If you also want the window to have a context menu, you’ll have to change the message handler, so it does not affect right mouse clicks:

procedure TMyForm .WMNCHitTest(var Msg: TWMNcHitTest);
var
  Res: SmallInt;
begin
  inherited;
  Res := GetKeyState(VK_RBUTTON);
  if Res >= 0 then
    // only if the right mouse button was not pressed
    // (otherwise the popup menu wont show)
    if (Msg.Result = htClient) then
      Msg.Result := htCaption;
end;

Display and edit keyboard macros with GExperts

 Delphi, GExperts, Uncategorized  Comments Off on Display and edit keyboard macros with GExperts
Mar 112017
 

The GExperts Macro Library expert can now display and edit keyboard macros that have been recorded in the Delphi IDE.

It uses the information I described in my article on Interpreting Delphi IDE Keyboard Macros.

You get to this dialog via the context menu of a macro.

It allows you not only to display and edit text.

But also special keys.

And, on top of that, it works with Unicode, in Unicode aware IDEs (>= Delphi 2005).

(The above are Arabic characters, but I have no idea whether it means anything. I just typed a few keys.)

Just in case you are interested in these two keyboard macros:

ReadOnly Property
Read Write Property

Save these files anywhere and load them into the Macro Library expert.

They are supposed to generate a property for a class’ field: Copy the field definition from the private section to the public section. Position the cursor on the first character and start the macro.

There are at least 30000 Delphi 2007-2010 users

 Delphi  Comments Off on There are at least 30000 Delphi 2007-2010 users
Mar 072017
 

At least that’s a possible interpretation of the download statistics of my dzEditorLineEndsFix tool

As of today, it has been downloaded 33994 times since I wrote the first version in 2014.

Of course there will be duplicates and also those who have updated to later Delphi versions or stopped using Delphi all together after downloading the tool.

On the other hand there will be those who got the source code and compiled it themselves as suggested by Embarcadero, or those who use an alternative solution. So maybe 30000 isn’t such a bad guesstimate.

Interpreting Delphi IDE Keyboard Macros (Updated)

 Delphi, GExperts, Uncategorized  Comments Off on Interpreting Delphi IDE Keyboard Macros (Updated)
Mar 042017
 

Keyboard macros have been part of the Delphi IDE since basically forever (I remember using them in Delphi 5 but I wouldn’t rule it out that they already existed in Delphi 1 which I never used.)

GExperts also has had the Macro Library Expert since I know about it to overcome the Delphi IDE’s shortcoming of having only one keyboard macro.

This expert handles the macros as a black box, storing them in a binary stream which is used to get them from and pass them to the IDE and to save and load them from files.

So far I haven’t seen any documentation of the format these macros are stored in. That’s a real shame because it prevented the Macro Library Expert to become more useful, by allowing to edit existing macros.

I have had a look at data stored in its configuration files and tried to interpret them. Here is what I found:

A keyboard macro is a stream of bytes. It always seems to start with the four bytes 54 50 45 52 (ASCII “TPOR”, no idea what it means. TP looks suspiciously like Turbo Pascal). This seems to be a kind of signature. I have not tried to modify it to see what the IDE makes of it, I just want to understand the actual macros. This signature is followed by a single byte that is either 00 or 01. 00 seems to mark the end of the macro. 01 is followed by 4 bytes, which seem to belong together as two separate words, e.g. 61 00 00 00, and describe a key press. Each such 4 byte sequence is again followed by a single byte that is either 00 or 01. 00 again, marks the end of the macro, 01 means that another another key press follows etc.

54 50 54 52
01  61 00  00 00
00

would be a keyboard macro that simply types the lower case letter “a”.

54 50 54 52
01  61 00  00 00
01  62 00  00 00
00

would be “ab”.

Let’s have a closer look at the four bytes describing a key press:

The first word describes the main key, the second word describes modifier keys. The modifier word can have the following values:

  • 00 00 – meaning the main key is a normal character
  • 88 00 – meaning the main key is a special key like the arrow keys or insert / del / page up/down etc.
  • 10 00 added to it means that the Ctrl key is being pressed
  • 20 00 added to it means that the Shift key is being pressed
  • 40 00 added to it means that the Alt key is being pressed

I have not seen any other values.

Please keep in mind that I represent the bytes in the order they show up in the stream. When interpreting two consecutive bytes as words, you have to switch them around according to Intel convention, so the bytes 10 00 actually mean the the number $0010, and 88 00 is the number $0088.

If the modifier word is $0000, the main key is in its simplest form the ASCII code of a character, e.g. like you can see in GExperts ASCII Chart:

I have yet to check whether the value is actually a WideChar.
update: It definitely is a WideChar, so the code below won’t work correctly for input that uses the high byte of the character.

If the modifier value is <> $0000 it describes a special key, e.g.
$0020 is the Space key, $0021 is PgUp, $0022 is PgDn, $002D is Ins key, $002E is Del etc.

update: The above applies to all Delphi versions that I checked. That includes Delphi 6, 7, 2005, 2006, 2007 and 10.1. Note that Delphi 6 and 7 apparently do not support Unicode characters in keyboard macros. I tried to enter some Arabic characters (Unicode $30xx) and they were converted to characters in $00xx range.

Based on my observations I have so far written the following code to interpret a keyboard macro:

type
  TMenuKeyCap = (
    mkcBkSp, mkcTab, mkcEsc, mkcEnter, mkcSpace, mkcPgUp,
    mkcPgDn, mkcEnd, mkcHome, mkcLeft, mkcUp, mkcRight,
    mkcDown, mkcIns, mkcDel, mkcShift, mkcCtrl, mkcAlt);

var // these resource strings are declared in unit Consts
  MenuKeyCaps: array[TMenuKeyCap] of string = (
    SmkcBkSp, SmkcTab, SmkcEsc, SmkcEnter, SmkcSpace, SmkcPgUp,
    SmkcPgDn, SmkcEnd, SmkcHome, SmkcLeft, SmkcUp, SmkcRight,
    SmkcDown, SmkcIns, SmkcDel, SmkcShift, SmkcCtrl, SmkcAlt);

function KeyCodeToText(Code: Word; Modifier: Word): string;
var
  LoByte: Byte;
begin
  Result := Format('unkown (%.4x)', [Code]);
  if (Modifier and $88) <> 0 then begin
    // special keys
    Result := '';
    if (Modifier and $10) <> 0 then
      Result := Result + MenuKeyCaps[mkcCtrl];
    if (Modifier and $40) <> 0 then
      Result := Result + MenuKeyCaps[mkcShift];
    if (Modifier and $20) <> 0 then
      Result := Result + MenuKeyCaps[mkcAlt];
    LoByte := (Code and $FF);
    case LoByte of
    // $00..$07
      $08, $09: // backspace / tab
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := Result + MenuKeyCaps[mkcEnter];
      $1B: Result := Result + MenuKeyCaps[mkcEsc];
    // $1B..$1F ?
      $20..$28: // space and various special characters
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcSpace) + LoByte - $20)];
    // $29..$2C ?
      $2D..$2E: // Ins, Del
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcIns) + LoByte - $2D)];
    // $2F ?
      $30..$39: // 0..9
        Result := Result + Chr(LoByte - $30 + Ord('0'));
      $41..$5A: // A..Z
        Result := Result + Chr(LoByte - $41 + Ord('A'));
      $60..$69: Result := Result + Chr(LoByte - $60 + Ord('0'));
      $70..$87: Result := Result + 'F' + IntToStr(LoByte - $6F);
    end;
  end else begin
    LoByte := (Code and $FF);
    case LoByte of
      // $00..$07
      $08, $09: // backspace / tab
        Result := MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := MenuKeyCaps[mkcEnter];
      $1B: Result := MenuKeyCaps[mkcEsc];
      // $1B..$1F ?
      $20..$7E: Result := Chr(LoByte);
      $7F: Result := MenuKeyCaps[mkcDel];
      $80..$FF: Result := Chr(LoByte);
    end;
  end;
end;

function TMacroInfo.TryDecode(AStrings: TStrings): Boolean;

  function Read(var Buffer; Count: Longint): Boolean;
  begin
    Result := (FStream.Read(Buffer, Count) = Count);
  end;

var
  Magic: Longword;
  Flag: Byte;
  HiWord: Word;
  LoWord: Word;
  s: string;
begin
  Result := False;
  FStream.Position := 0;
  if not Read(Magic, SizeOf(Magic)) or (Magic <> $524F5054) then
    Exit;
  if not Read(Flag, SizeOf(Flag)) then
    Exit;
  while Flag = $01 do begin
    if not Read(LoWord, SizeOf(LoWord)) then
      Exit;
    if not Read(HiWord, SizeOf(HiWord)) then
      Exit;
      s := KeyCodeToText(LoWord, HiWord);
    AStrings.Add(s);
    if not Read(Flag, SizeOf(Flag)) then
      Exit;
  end;
  Result := True;
end;

It’s not finished and might never be. It’s also far from perfect. I am posting this here in the hope that it might be useful to somebody. I might use it to extend the Macro Library Expert in GExperts to allow limited editing of keyboard macros.

dzComputerInfo tool

 Delphi  Comments Off on dzComputerInfo tool
Mar 032017
 

A few days ago I was working in one of our measurement vehicles and found, that I couldn’t for the life of me tell, which of the 3 computers (soon to become 4) was selected on the KVM switch. This isn’t the first time this has happened to me and my colleagues have the same problem. We tried to solve this with custom background pictures and colours, but these have a tendency to get lost due to bugs in Windows, or operators who don’t like them. Also, they are only visible on the desktop background and any application windows will obscure them.

Up to Windows XP, pressing Ctrl+Alt+Del used to bring up a window displaying computer name and logged on user, but since Windows 7 that useful information has been replaced by a full screen menu without any information on it.

Enter dzComputerInfo. It’s a small tool that I wrote the evening after the above incident which does exactly one thing: It shows a window on top of all other windows displaying the computer name and currently logged on user. Since the window is so small and it places itself automatically just above the start button, it does not really become a nuisance.

The tool and the source code is available from sourceforge, if anybody else thinks he has a use for it.

(That start button is from Classic Shell btw.)

Fixed HideNavBar functionality in GExperts

 Delphi, GExperts, Uncategorized  Comments Off on Fixed HideNavBar functionality in GExperts
Feb 262017
 

When Embarcadero added the Navigation Toolbar to the Delphi code editor in Delphi 10 there were a few people who didn’t like it because it took up some more of the vertical screen space. But there was no option to disable it.

Achim Kalwa wrote an expert to hide this toolbar and contributed the code, which I integrated into GExperts. Unfortunately it didn’t work reliably. The Navigation Toolbar came back whenever one opened a new edit window and went away again when switching between tabs, creating an annoying flicker.

Today I investigated the issue and found that apparently the IDE sets the control’s visible property to true whenever it opens a new editor tab. The fix was actually quite simple: Create a new panel, set its Parent to the toolbar’s original parent control and set the toolbar’s parent to this new panel. So it ends up between the toolbar and its parent. Then set this panel to be invisible instead of the toolbar. Since the IDE doesn’t know about this new panel, it does not change its visibility. Voila, problem solved.

Of course it was a bit more involved because I had to size the panel correctly and also make sure that I don’t insert a new panel every time I check for the visibility. But once I had the general principle it was just a matter of fine tuning the solution.

  if TryFindComponentByName(Ctrl, 'TEditorNavigationToolbar', C) then begin
    Ctrl := TWinControl(C);
    ParentCtrl := Ctrl.Parent;
    if Assigned(ParentCtrl) and (ParentCtrl is TPanel) and (ParentCtrl.Name = GX_HideNavbarPanel) then
      pnl := TPanel(ParentCtrl)
    else begin
      pnl := TPanel.Create(ParentCtrl);
      pnl.Parent := ParentCtrl;
      pnl.Name := GX_HideNavbarPanel;
      pnl.Align := alTop;
      pnl.BevelOuter := bvNone;
      pnl.Height := ctrl.Height;
      Ctrl.Parent := pnl;
    end;
    pnl.Visible := FIsNavbarVisible;
    pnl.Enabled := FIsNavbarVisible;
    if FIsNavbarVisible then
      Ctrl.Visible := True;
    Result := True;
  end;

I also found, that in Delphi 10.1 Berlin there is already an option to show or hide the Navigation Toolbar (Tools -> Options -> Editor Options -> Display), so I removed the functionality from GExperts for Delphi 10.1 again.

GExperts formatter branch is dead, long live the trunk

 Delphi, GExperts, Uncategorized  Comments Off on GExperts formatter branch is dead, long live the trunk
Feb 252017
 

As of today, I have stopped developing GExperts in the formatter branch and switched to the trunk of the repository.

If you want to get the current sources, take them from

svn co https://svn.code.sf.net/p/gexperts/code/trunk GExperts

I have updated the Compiling GExperts article accordingly.

Debugging helper for TDataSet

 Delphi, Uncategorized  Comments Off on Debugging helper for TDataSet
Feb 152017
 

I like programming in Delphi, but I don’t particularly like writing applications that work with database backends, but sometimes I just can’t avoid it (have been working on one for several weeks now, and it wasn’t too bad really). But one thing that can drive me nuts is when you have got an error, want to investigate the cause and it’s a bloody nuisance to try and get the value of the current record in a dataset. So, finally I wrote this little helper unit which I want to share with you.

All it does is export one public function TDataset_Dump which is meant to be called from the debugger’s “Evaluate and Modify” window:

TDataset_Dump(SomeDataset)

If called like this, it will magically open a text file in the associated application and display the content of the current record of the dataset you passed to it.

Alternatively you can let it dump the whole dataset like this:

TDataset_Dump(SomeDataset, -1)

Or all remaining records in the dataset (until EOF) like this:

TDataset_Dump(SomeDataset, 0)

Or you can pass it a number of records it should dump:

TDataset_Dump(SomeDataset, 5)

For this to work, you need to add the unit u_dzDatasetDump to the unit with the dataset you want to debug, otherwise the debugger won’t know about the function.

The unit is part of my dzlib library. It needs a few units from it (I’m a lazy bastard ™, so since I use the library in most of my programs I didn’t see the point of trying to avoid dependencies, but it should’t be too difficult to remove them.).

OK, so here goes the unit:

///<summary>
/// Add this unit only for debug purposes.
/// It exports TDataset_Dump for dumping a dataset from the evaluate and modify dialog
/// of the Delphi debugger. </summary>
unit u_dzDatasetDump;

interface

uses
  SysUtils,
  Classes,
  DB;

///<summary>
/// @param Count:  1 -> Dump the current record only (default)
///               -1 -> Dump the whole dataset
///                0 -> Dump from the current record until EOF
///               >1 -> Dump a maximum of n records starting from the current
/// @returns the file name the data was written to </summary>
function TDataset_Dump(_ds: TDataset; _Count: Integer = 1): string;

implementation

uses
  u_dzLineBuilder,
  u_dzVariantUtils,
  u_dzFileUtils,
  u_dzExecutor,
  u_dzOsUtils;

const
  DUMP_ALL = -1;

procedure TDataset_DumpHeaders(_ds: TDataset; _sl: TStrings);
var
  lb: TLineBuilder;
  i: Integer;
  fld: TField;
begin
  lb := TLineBuilder.Create;
  try
    for i := 0 to _ds.FieldCount - 1 do begin
      fld := _ds.Fields[i];
      lb.Add(fld.FieldName);
    end;
    _sl.Add(lb.Content)
  finally
    FreeAndNil(lb);
  end;
end;

procedure TDataset_DumpCurrent(_ds: TDataset; _sl: TStrings);
var
  lb: TLineBuilder;
  i: Integer;
  fld: TField;
begin
  lb := TLineBuilder.Create;
  try
    for i := 0 to _ds.FieldCount - 1 do begin
      fld := _ds.Fields[i];
      lb.Add(Var2Str(fld.Value));
    end;
    _sl.Add(lb.Content)
  finally
    FreeAndNil(lb);
  end;
end;

procedure TDataset_DumpToEof(_ds: TDataset; _sl: TStrings);
begin
  while not _ds.Eof do begin
    TDataset_DumpCurrent(_ds, _sl);
    _ds.Next;
  end;
end;

procedure TDataset_DumpAll(_ds: TDataset; _sl: TStrings);
begin
  _ds.First;
  TDataset_DumpToEof(_ds, _sl);
end;

function TDataset_Dump(_ds: TDataset; _Count: Integer = 1): string;
var
  sl: TStringList;
  bm: Pointer;
begin
  Result := '';
  sl := TStringList.Create;
  try
    if not Assigned(_ds) then begin
      sl.Add('<dataset not assigned>');
      if _Count = -1 then begin
        // do nothing this is a dummy call to fool the linker
        Exit; //==>
      end;
    end else if not _ds.Active then begin
      sl.Add('<dataset not active>');
    end else if _ds.IsEmpty then begin
      sl.Add('<dataset is empty>');
    end else begin
      bm := nil;
      _ds.DisableControls;
      try
        bm := _ds.GetBookmark;
        TDataset_DumpHeaders(_ds, sl);
        if _Count = DUMP_ALL then begin
          TDataset_DumpAll(_ds, sl);
        end else begin
          if _ds.Eof then begin
            sl.Add('<dataset is at eof>');
          end else if _Count = 0 then begin
            TDataset_DumpToEof(_ds, sl);
          end else begin
            while (not _ds.Eof) and (_Count > 0) do begin
              TDataset_DumpCurrent(_ds, sl);
              _ds.Next;
              Dec(_Count);
            end;
          end;
        end;
      finally
        _ds.GotoBookmark(bm);
        _ds.EnableControls;
      end;
    end;
    if Assigned(_ds) then
      Result := _ds.Name;
    if Result = '' then
      Result := 'NONAME';
    Result := itpd(TFileSystem.GetTempPath) + 'dump_of_' + Result + '.txt';
    sl.SaveToFile(Result);
    OpenFileWithAssociatedApp(Result, True);
  finally
    FreeAndNil(sl);
  end;
end;

initialization
  // Make sure the linker doesn't eliminate the function
  TDataset_Dump(nil, -1);
end.
%d bloggers like this: