Three more timeline issues from testing printouts of real schedules. The first two are in ExportView (PDF and print are both affected), the third is in screen drawing. Line numbers refer to 1.3.3.10. A demo for all of them is attached.
1. DrawExportTimeLineItems skips bars that start before the page and end after it
When the export is split over several pages along the time axis, a task bar is drawn only on the page where it starts and on the page where it ends. On every page in between the row stays empty. This affects every task that is longer than one timeline page β in practice the parent tasks and long tasks, which are exactly the bars a schedule is read by. The screen is fine.
TTMSFNCCustomGanttChart.DrawExportTimeLine β DrawExportTimeLineItems (VCL.TMSFNCGanttChart.pas, line 9646):
if ((tlStartCol >= AExportTimeLineParams.StartColumn) and (tlStartCol <= lc + 1)) or
((tlEndCol >= AExportTimeLineParams.StartColumn) and (tlEndCol <= lc + 1)) then
The condition asks whether the start column or the end column lies on the page. The third case β start before the page, end after it β is missing. An overlap test covers all three cases:
if (tlStartCol <= lc + 1) and (tlEndCol >= AExportTimeLineParams.StartColumn) then
The rectangle calculated after the condition already handles a bar that begins left of the page (that is today's "end on this page" case), and ExportView clips the timeline part to the page (AGraphics.ClipRect(drawParams.Rect), line 10533), so a long bar does not run into the task list. All drawing events of the item are inside this condition, so we cannot work around it from the application.
Example from our application: A4 landscape, day unit, 35 pages. A parent task from 27.09.2021 to 07.02.2022 appears on the first timeline page (its start) and on the last one (its end); the pages for October, November, December and January show an empty row. A normal task of 70 days (02.11.2021 to 10.01.2022) behaves the same: the page with its start is fine, the page in between is empty, the page with its end is fine.
2. Week, month and year units in the export: bars inside a unit are missing, the first unit is lost, the page loop does not end
With TimeLineUnit = gtuWeek, gtuMonth or gtuYear the export goes wrong in four ways. All four come from the way ExportView and DrawExportTimeLineItems use the column of a unit start where the last column of a page is meant.
GetTimeLineLastColumn (line 10189) starts with Result := 1 and accepts a column only at a unit start (DrawRow) and only if the whole unit (DrawRowSpan) still fits. So its result is the start column of the last unit on the page β and 1 if no unit fits, which cannot be told apart from "column 1 fits". ExportView passes this value as drawParams.LastColumn and continues with lastTimeCol := GetNextTimelineColumn(tempLastTimeCol) (line 10549).
a) Bars inside a unit are not drawn. DrawExportTimeLineItems takes lc := AExportTimeLineParams.LastColumn and draws a task only if its start or end column is <= lc + 1 (line 9646, see point 1). For days that is the last column. For weeks and months it is the first day of the last unit on the page, so a task that starts and ends later in that unit is skipped β on this page and, because it ends before the next page starts, on every page. Months: practically no bars at all. Weeks: a task from Wednesday to Friday in the last week of a page is missing.
b) The first, partial unit is lost. The export rarely starts on the 1st of a month. On page 1 the first unit start comes after the partial first month, and GetTimeLineLastColumn needs the partial month plus the next whole month to fit. If they do not, the result is 1, maxTimeW stays 0, page 1 shows no timeline, and page 2 starts at the first unit start after column 1 β the partial first month is never drawn. The same happens when the task list leaves no room for the timeline on page 1 (curRect.Left < workRect.Right - drawParams.TimeUnitWidth, line 10519): tempLastTimeCol keeps its initial 0, and GetNextTimelineColumn(0) skips the first unit.
c) Nothing at all when no unit starts inside the range. Year unit, range 09.10.2026 to 01.01.2027: one page with an empty timeline β no DrawRow is True before the last column, the result is 1 with maxTimeW = 0. With the range ending on 31.12.2026 (no 1 January at all) the export did not return in our demo; we did not capture the call stack, GetNextTimelineColumn with its while not DrawRow(Result) loop is the likely place.
d) Endless page loop when one unit is wider than the page. A page that starts at a unit start U whose unit does not fit returns 1; the next page starts again at the first unit start after column 1, which is at or before U. HasNextPage never becomes False. Measured in our application: month unit, A4 landscape, 25 pt per day (a 31-day month needs 775 pt, the page had about 757 pt): no result after 9 minutes, the call stack keeps going through AOnNewPage β NewPage.
Demo (attached, GanttTimelineIssues.zip, VCL, Win32, no design-time components): A4 landscape, margins 20 pt, FitToPage = False, Mode = emTimeline, 25 pt per day unless stated. It runs by itself, writes one PDF per case and logs, per task, the pages on which OnBeforeDrawGanttTimeLineItem fired during the export. Every case gets a new chart, and a watchdog ends the process if a case does not return within 60 seconds. Result on our machine (log and PDFs are in the zip):
| Case | Unit, range | Tasks | Result |
|---|---|---|---|
| 1 (point 1) | day, 30.10.2026β01.02.2027, 3 pages | A over 60 work days | A on pages 1 and 3, page 2 empty |
| 2a (b) | month, 09.10.2026β01.01.2027 | one task each on 12.10., 02.11., 01.12. | page 1 empty, October missing; November and December fine |
| 2b (a) | month, 01.10.2026β01.01.2027 | 12.β16.10., 16.β20.11., 07.β11.12. | no bar on any page |
| 2c (a) | week, 31.08.β02.11.2026 | 07.09.; WedβFri 23.β25.09.; WedβFri 14.β16.10. | 23.β25.09. on no page β it lies in the last week unit of page 1 (weeks start on Sunday: 31.08.β05.09., 06.09., 13.09., 20.09.) |
| 2d (c) | year, 09.10.2026β01.01.2027, 3 pt per day (252 pt) | 12.β16.10. | one page, empty timeline, no bar (the screen shows the bar) |
| 2e (d) | month, 01.10.2026β01.01.2027, 30 pt per day (October = 930 pt, usable width 802 pt) | 12.β16.10. | does not end: the demo raises EAbort in OnBeforeDrawHeader when page 51 starts (expected: 3 or 4 pages) |
Suggestion:
- In
DrawExportTimeLineItems, test against the real last column of the page (last unit start + its span β 1), together with the overlap test from point 1. - Let
GetTimeLineLastColumnreport "nothing fits" in a way the caller can recognise (for exampleAStartCol - 1). InExportView, if nothing fits on a page that also shows the task list, keeplastTimeColfor the next page; if nothing fits on a page with the full width, draw the unit clipped (or split it over pages) and continue with the next unit, so the loop always advances. - Start with "no column drawn yet" instead of
tempLastTimeCol := 0, so that a page without a timeline part does not skip the first unit.
Until this is fixed we offer only the day unit in our print dialog.
3. DoOnBeforeDrawTime draws the group rectangle uninitialized when ShowGroup = False
Screen drawing, not export. With TimelineOptions.ShowGroup := False (we use it for month and year units) repainting the timeline header raised "Range check error" in our debug build (one to three messages each time the unit is switched).
TTMSFNCCustomGanttChart.DoOnBeforeDrawTime (line 4861): the group rectangle gr is only assigned inside if FTimelineOptions.FShowGroup and (groupHeight <> 0) then gr := r; (line 4891). The group part further down runs under if gd or FFirstDrawDate then and calls AGraphics.DrawRectangle(gr) (line 4974). gd is False without groups, but FFirstDrawDate is True on the first item of every timeline paint (it is reset at line 4302). So gr is drawn, and its Right is computed, with whatever is on the stack. Values seen in the debugger: gd = False, gr = (4.59e-41; 3.95e+32; 1; 2.88). TTMSFNCGraphicsContextGeneral.FillRect rounds to a TRect β range check error. Without range checking there is no message, but a random rectangle is drawn.
The demo (case 3: month unit, ShowGroup = False) counts the calls of OnBeforeDrawGanttTimeLineGroupTime during one repaint: 1 call (expected 0), with ARect = (Left 2.2e-38; Top 0; Right 1; Bottom 3.1e-37) β the values change from run to run.
Suggestion: skip the group part when ShowGroup is False (for example if FTimelineOptions.FShowGroup and (gd or FFirstDrawDate) then), or initialise gr. Our workaround: OnBeforeDrawGanttTimeLineGroupTime with AAllow := False when ShowGroup is False. The arithmetic on gr before the event (line 4966) still runs on the uninitialized value.
Thanks!
GanttTimelineIssues.zip (762.3 KB)