Increment ImageIndex Constants

When developing applications in Delphi, it is common to have a TImageList that holds the various glyphs used by the application. Those glyphs can then be used by other components like TToolButton by assigning the number of a glyph to the ImageIndex property. When adding new glyphs to the application, the easy solution is to add them to the end of the image list, so existing glyph number remain unchanged. But over time this can make a mess of the image list, making it harder to find existing images in the list that need to be reused by newly added components.

If you want to keep the image list organized, you’ll want to insert new glyphs in the middle of the list, next to the glyphs they relate to. To make sure the existing glyphs are still referenced correctly, need a way to increment the ImageIndex properties with numbers equal or greater to the number of the glyph being inserted. Fortunately, you can easily generate a regular expression that matches these numbers with RegexMagic. Actually incrementing the numbers requires arithmetic. Regular expressions don’t do math. But PowerGREP can do simple math on regex and group matches. So can a single line of Delphi code.

Below you’ll find two examples, one using PowerGREP and another using some quick Delphi code, to increment ImageIndex properties like “ImageIndex = 123” in DFM files. The example also handles constants declarations like “imgSomeName = 123” and property assignments like “ImageIndex := 123”. You can easily adjust these examples to handle differently named identifiers. You can these examples as “Real world: increment ImageIndex constants” in the RegexMagic library.

Increment ImageIndex Constants Using PowerGREP

First we prepare PowerGREP for a new search-and-replace.

  1. Start PowerGREP 5. Version 4 would also work. The free trial version is sufficient to follow this example.
  2. Use the File Selector panel to mark the folder(s) containing your Delphi source code.
  3. Enter “*.pas;*.dfm” into the “include files” box.
  4. Click the Clear button on the Action toolbar.
  5. Set “action type” to “search-and-replace”.
  6. Click the RegexMagic button on the Action toolbar. It is important to launch RegexMagic from PowerGREP. This allows RegexMagic to send its entire formula back to PowerGREP.

Now we can generate the regular expression with RegexMagic.

  1. On the Samples panel, paste in one new sample:
    ImageIndex := 123
    ImageIndex = 99
    imgSomeName = 256
    
  2. Select the first occurrence of ImageIndex in the sample.
  3. Click the Mark button. RegexMagic automatically adds a field using the literal text pattern that matches ImageIndex.
  4. Select img at the start of the third line in the example.
  5. Click the Mark button. RegexMagic adds another field to match img. It puts this field and the previous field into an alternation field. That’s because we left a gap between the two fields that we marked. RegexMagic correctly interprets that we want our regex to be able to match ImageIndex and img separately.
  6. Select SomeName adjacent to img.
  7. Click the Mark button. RegexMagic adds a field to match SomeName in sequence with the img field. So now we have field 2 matching ImageIndex, field 4 matching Img and field 5 matching SomeName.
  8. In the “pattern to match field” drop-down list, select “basic characters” for field 5.
  9. Tick the “lowercase letters”, “uppercase letters”, and “digits” checkboxes.
  10. Set field 5 to repeat between 1 and 64 times. Though you could just tick “unlimited”, setting a reasonable upper limit is a good habit. Though not likely in this case, there are situations where this can prevent a runaway regular expression slowing down your application.
  11. Select the space immediately after ImageIndex on the first line of the example.
  12. Click the Mark button. RegexMagic adds field 6 to match the space. Because we marked this adjacent but out of sequence to another field, RegexMagic adds this as an alternative under field 1 and sets field 1 to be repeated twice. We need to fix that.
  13. With field 6 still selected, click the Move Field Left button. It’ll have Move Field Left as its glyph. This moves field 6 out of the alternation. It is now in sequence with and below field 1. This means our regex first matches one of the two alternatives (ImageIndex or imgSomeName) and the matches the space.
  14. In the “pattern to match field” drop-down list, select “basic characters” for field 6.
  15. Make sure the “whitespace” option is ticked. This way field 6 can match spaces or tabs.
  16. Set field 6 to repeat between 0 and unlimited times.
  17. Select : in the sample.
  18. Click the Mark button. RegexMagic adds field 7 to match a literal colon in sequence with field 6.
  19. Set field 7 to repeat between 0 and 1 times. This makes the colon optional, allowing the regex to handle both constant declarations and property assignments.
  20. Select the = adjacent to the : in the sample.
  21. Click the Mark button. RegexMagic adds field 8 to match a literal equals sign.
  22. Select the space immediately after the equals sign we just marked.
  23. Click the Mark button. RegexMagic adds field 9.
  24. In the “pattern to match field” drop-down list, select “basic characters” for field 9.
  25. Make sure the “whitespace” option is ticked. This way field 9 can match spaces or tabs.
  26. Set field 9 to repeat between 0 and unlimited times.
  27. Select 123 in the sample text.
  28. Click the Mark button. RegexMagic adds field 10. It automatically sets it to match any integer number.
  29. We need to restrict field 10 to the range of numbers we actually want to increment. Tick the “limit the integers to these ranges” checkbox. In the edit control, replace 123 with a dotted range such as 42..999. 42 should be the position in the image list where you’ll be inserting the glyph. 999 should be any number that is surely higher than the highest existing image index. But don’t go overboard with the end of the range as the higher the number the more complex the regex. A sequence of nines makes for a simpler upper end.
  30. Set the “field validation mode” on the Match panel to “strict”. This is necessary to make RegexMagic limit the integer range to exactly what you specify.
  31. Set “begin regex match at” and “end regex match at” to “start of word” and “end of word”. This ensures the regex does not partially match larger numbers. That would yield incorrect results when we increment the number.
  32. On the Action panel, click the New button to add a capturing group.
  33. Enter “number” as the group’s name.
  34. Select “10 integer” in the “field” drop-down list. We now have a capturing group that will give us the value of the ImageIndex constant.
  35. If you generate the regular expression then the Samples panel shows that our regex correctly matches the 3 numbers and their identifiers:
    ImageIndex := 123
    ImageIndex = 99
    imgSomeName = 256
    

Now we can prepare the replacement with RegexMagic.

  1. On the Action panel, set “action to take” to “replace one field”.
  2. Set “field to replace” to “10 integer”.
  3. Set “how to replace” to “enter literal replacement text”.
  4. Enter “%groupnumber:+1%” as the replacement string. RegexMagic doesn’t understand this. But PowerGREP does. It’s a match placeholder that inserts the match of the capturing group named “number” incremented by one.

Now we’re ready to perform the search-and-replace with PowerGREP. Resist the urge to copy and paste the regex from RegexMagic into PowerGREP. Instead click the Send button on the Regex panel in RegexMagic. This button only appears if you launched RegexMagic from PowerGREP (or another application that integrates with RegexMagic).

You can now preview or execute the search-and-replace in PowerGREP. You’ll see this regular expression:

\m(?<before10>(?:ImageIndex|img[0-9A-Za-z]{1,64})[\t ]*:?=[\t ]*)(?<number>[1-9][0-9]{2}|[5-9][0-9]|4[2-9])\M

Required options: Case sensitive; Exact spacing.
Unused options: Dot doesn’t match line breaks.

You’ll also see this replacement string:

${before10}%groupnumber:+1%

RegexMagic added an extra capturing group to the regex and a reference to it to the replacement string. This preserves the matches of fields 1 to 9.

What you don’t see in PowerGREP is that RegexMagic also sent the RegexMagic Formula (all your settings on the Samples, Match, and Action panels) to PowerGREP. PowerGREP does store this. Use the Action|Save or Action|Add to Library to save your PowerGREP action. The RegexMagic Formula will be stored inside the action. When you load the action into PowerGREP at a later time and click the RegexMagic button in PowerGREP then PowerGREP will send the RegexMagic formula back to RegexMagic. This way you can edit the formula in RegexMagic and send a newly generated regex (and updated settings) back to PowerGREP.

In this example that will be very handy. Each time you do this search-and-replace, you’ll want to increment a different range of numbers. By saving the RegexMagic formula, PowerGREP lets you do this quickly by launching RegexMagic from PowerGREP again, changing the range of numbers for field 10, and sending the result back to PowerGREP.

This only works if you always launch RegexMagic from PowerGREP and always use the Send button to commit your regex. If you copy and paste, PowerGREP won’t have the RegexMagic formula, and you won’t be able to use RegexMagic to edit the regex later. You can’t copy and paste a regex from PowerGREP or any other application into RegexMagic.

Increment ImageIndex Constants Using Delphi

First, prepare the regular expression using RegexMagic as described in steps 7 through 41 above. Then we can prepare the replacement. Delphi does not have a feature similar to PowerGREP’s match placeholders. So instead we’ll tell RegexMagic to remove the matched number from the replacement. We’ll add the incremented number with some Delphi code.

  1. On the Action panel, set “action to take” to “replace one field”.
  2. Set “field to replace” to “10 integer”.
  3. Set “how to replace” to “delete the matched text”.
  4. On the Regex panel, select TPerlRegEx (not TRegEx) for your version of Delphi. For this example, we’ll select “Delphi 10.3–11 (TPerlRegEx)”.
  5. Turn off free-spacing, and turn off mode modifiers. Click the Generate button, and you’ll get this regular expression:
    [[:<:]](?<before10>(?:ImageIndex|img[\dA-Za-z]{1,64})[\t ]*:?=[\t ]*)(?<number>[1-9][0-9]{2}|[5-9][0-9]|4[2-9])[[:>:]]

    Required options: Case sensitive; Exact spacing.
    Unused options: Dot doesn’t match line breaks; ^$ don’t match at line breaks; Numbered capture; Greedy quantifiers; Allow zero-length matches.

  6. You’ll also get this replacement string, which preserves the identifier and operator, but drops the number we matched:
    ${before10}
  7. On the Use panel, select the function “use regex object to search and replace through a string”. You’ll see this code:
    var
    	Regex: TPerlRegEx;
    	ResultString: string;
    
    Regex := TPerlRegEx.Create;
    try
    	Regex.RegEx := '[[:<:]](?<before10>(?:ImageIndex|img[\dA-Za-z]{1,64})[\t ]*:?=[\t ]*)(?<number>[1-9][0-9]{2}|[5-9][0-9]|4[2-9])[[:>:]]';
    	Regex.Options := [];
    	Regex.State := [];
    	Regex.Subject := SubjectString;
    	Regex.Replacement := '${before10}';
    	Regex.OnReplace := ComputeReplacement;
    	Regex.ReplaceAll;
    	ResultString := Regex.Subject;
    finally
    	Regex.Free;
    end;
    
    procedure TMyClass.ComputeReplacement(Sender: TObject; var ReplaceWith: string);
    begin
    	// You can vary the replacement text for each match on-the-fly
    	// ReplaceWith defaults to Regex.Replacement, with backreferences substituted
    end;
    

The reason we chose TPerlRegEx over TRegEx is that TPerlRegEx.ReplaceAll uses both the Replacement property and the OnReplace event. When our event handler ComputeReplacement is called, the ReplaceWith parameter holds the replacement string with the backreference already substituted. Since we told RegexMagic to only delete the match of field 10 from the replacement, all we need to do is to write one line of code to append the incremented image index:

procedure TMyClass.ComputeReplacement(Sender: TObject; var ReplaceWith: string);
begin
	ReplaceWith := ReplaceWith + IntToStr(StrToInt(Regex.Groups[Regex.NamedGroup('number')]) + 1)
end;

Related Examples

Reference