Jamie's and my previously presented solutions have the disadvantage that they require a font with constant character width; by default, however, Lazarus has proportional fonts in which every character has its own width, and the alignment of columns breaks down.
Many controls can "owner-draw" their text, a listbox for example when its Style property is changed to lbOwnerDrawFixed and when the drawing code is implemented in the OnDrawItem event handler. Using the Canvas method TextRect it is possible to expand tabs automatically by setting the canvas' TextStyle.ExpandTabs is true:
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
ARect: TRect; State: TOwnerDrawState);
var
ts: TTextstyle;
listbox: TListbox;
txt: String;
R: TRect;
begin
listbox := TListbox(Control);
if ([odSelected, odFocused] * State <> []) then
begin
listbox.Canvas.Brush.Color := clHighlight;
listbox.Canvas.Font.Color := clHighlightText;
end else
begin
listbox.Canvas.Brush.Color := clWindow;
listbox.Canvas.Font.Color := clWindowText;
end;
listbox.Canvas.FillRect(ARect);
ts := listbox.Canvas.TextStyle;
ts.Expandtabs := true;
txt := listbox.Items[Index];
listbox.Canvas.TextRect(ARect, ARect.Left + 2, ARect.Top, txt, ts);
end;
The tab width here is 8 characters where the character width is understood here as the average character width of the font.
For individual tab widths there is a solution for Windows as mentioned earlier by @Thausand.
If you need a cross-platform solution you can call the following function in the DrawItem event handler instead of listbox.Canvas.TextRect. It gets an array of tab positions (in pixels) and, optionally, the default tab size (in pixels) which is used when there are more tabs in the text than tab positions. When the default tab size is not given, it is assumed to be PixelsPerInch/2 (i.e. 1/2 inch).
procedure DrawTabText(ACanvas: TCanvas; ARect: TRect;
AText: String; ATabPos: Array of Word; ADefaultTabSize: Integer = 0);
var
sa: TStringArray;
i, wPart, tabIdx, tabPos, nextTabPos: Integer;
part: String;
begin
if ADefaultTabSize <= 0 then
ADefaultTabSize := Screen.PixelsPerInch div 2; // 1/2 inch tabs by default
sa := AText.Split(#9);
tabIdx := -1;
tabPos := 0;
for i := 0 to High(sa) do
begin
part := sa[i];
ACanvas.TextOut(ARect.Left + tabPos, ARect.Top, part);
wPart := ACanvas.TextWidth(part);
nextTabPos := tabPos;
repeat
inc(tabIdx);
if tabIdx >= Length(ATabPos) then
nextTabPos := nextTabPos + ADefaultTabSize
else
nextTabPos := ATabPos[tabIdx];
until (nextTabPos > tabPos + wPart);
tabPos := nextTabPos;
end;
end;
I checked several cases, and the procedure seems to work. UTF8 characters are considered correctly as well.