1

I found that after I set the MaxDate of a TDateTimePicker to a non zero value it's impossible to bring it back to zero (disable it). I mean, the MaxDate reads zero but the max range is still active to the value set before. This can be noticed by dropping down the calendar and checking the last visible date.

Is this another bug, or I do not understand how to disable the MaxDate ?

procedure TForm1.Button1Click(Sender: TObject);
begin
 DateTimePicker1.MaxDate:= Date + 10;
 DateTimePicker1.MaxDate:= 0;
 Caption:= IntToStr(Trunc(Test.MaxDate));
end;

P.S: I take care of setting Time to zero first (this is another bug).

Marus Gradinaru
  • 2,824
  • 1
  • 26
  • 55
  • Yes, this appears to be a bug, at least in Delphi 10.2 which I am working with at the moment. – Andreas Rejbrand Aug 15 '18 at 12:20
  • 1
    But even the MSDN documentation for the underlying Win32 control is less than helpful... – Andreas Rejbrand Aug 15 '18 at 12:41
  • That's what I was afraid of... So, I think, the only solution is to set MaxDate to some higher value that I will never use in my application... – Marus Gradinaru Aug 15 '18 at 12:44
  • Unfortunately, that might be the case. I managed to hack the VCL wrapper's `FMaxDate` back to `0.0`, but [DateTime_SetRange](https://learn.microsoft.com/en-us/windows/desktop/api/commctrl/nf-commctrl-datetime_setrange) seems not to appreciate my setting the max to all `0`s (even though that is what `DateTime_GetRange` tells me is the initial value...) – Andreas Rejbrand Aug 15 '18 at 12:47
  • 2
    I am a bit surprised at the poor quality of the MSDN documentation, actually. Nevertheless, it seems like the control's max value is `9999-12-31` (both using the control in action and using `DateTime_SetRange`). – Andreas Rejbrand Aug 15 '18 at 13:01
  • 4
    You can call `DateTime_SetRange(DateTimePicker1.Handle, 0, nil)` to remove the range. Another thing is resetting the internal fields holding the range. – Victoria Aug 15 '18 at 18:44
  • Great ! Thanks ! :) – Marus Gradinaru Aug 15 '18 at 20:02
  • You're welcome! :) Of course, that's an accidentally found undocumented feature; I just tried to send neither of `GDTR_MIN` or `GDTR_MAX` flags pointing at no time range array, and it removes the range restriction (but as @Andreas pointed out, MSDN documentation is poor in this). It makes quite sense but there is no direct API contract about it. – Victoria Aug 16 '18 at 01:06

2 Answers2

3

Victoria discovered accidentally that the Win32 Date and Time Picker control can have its range reset by employing an undocumented trick.

However, Victoria's answer doesn't work in Delphi 10.2 because the VCL wrapper's internal max and min fields aren't reset properly to 0. It doesn't do to change the MinDate and MaxDate properties to 0 -- that will not set the FMinDate and FMaxDate fields to 0 due to the implementation of the property setters.

This will make the control malfunction after that point.

A workaround is to set the fields directly (I also tweaked Victoria's logic a bit to make the code briefer):

type
  TCommonCalendarHelper = class helper for TCommonCalendar
    procedure ResetRangeFields;
  end;

  TDateTimePickerHelper = class helper for TDateTimePicker
  public
    procedure ResetRange;
  end;

{ TDateTimePickerHelper }

procedure TDateTimePickerHelper.ResetRange;
begin
  if DateTime_SetRange(Handle, 0, nil) then
    ResetRangeFields;
end;

{ TCommonCalendarHelper }

procedure TCommonCalendarHelper.ResetRangeFields;
begin
  with Self do
  begin
    FMinDate := 0;
    FMaxDate := 0;
  end;
end;

(The with construct is surprisingly necessary here, see https://stackoverflow.com/a/42936824/282848.)

To try this:

procedure TForm1.FormClick(Sender: TObject);
begin
  DateTimePicker1.MaxDate := IncDay(Now, 4);
  // DateTimePicker1.ResetRange; // uncomment to see resetting in action
end;

Of course, this code relies both on undocumented Win32 features and on VCL implementation details. The dangers are probably fairly small, though. See the comments on Victoria's post for a more thorough discussion on this topic. It might be reasonable to use this code (only) if you know the VCL version. (You could even make it not compile on future VCL versions.)

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • It is hard for me to choose an answer because both are working in my version of Delphi. I will accept Victoria's answer because she discovered that trick, and give you +1 for that method of resetting the internal fields. – Marus Gradinaru Aug 16 '18 at 09:41
2

The following is not documented by MSDN (and was discovered by accident), but it seems to work (at least on Windows 7). You can try calling the following macro (or, send corresponding message) to remove the time range restriction of a date time picker control:

DateTime_SetRange(DateTimePicker1.Handle, 0, nil);

My lucky accidental trial started by not using either GDTR_MIN or GDTR_MAX flags when sending DTM_SETRANGE message pointing at no time range (hence I've passed 0 and nil to the macro).

If you take this as a possible workaround, what remains for VCL is resetting the internal field values, so e.g. making a small helper might become:

uses
  CommCtrl;

type
  TDateTimePickerHelper = class helper for TDateTimePicker
  public
    procedure ResetRange;
  end;

implementation

{ TDateTimePickerHelper }

procedure TDateTimePickerHelper.ResetRange;
var
  DateRange: array[0..1] of TDate;
begin
  DateRange[0] := MinDate; { ← store the current MinDate }
  DateRange[1] := MaxDate; { ← store the current MaxDate }

  MinDate := 0; { ← set the control's MinDate internal field to 0 }
  MaxDate := 0; { ← set the control's MaxDate internal field to 0 }

  if not DateTime_SetRange(Handle, 0, nil) then { ← if the macro fails, then... }
  begin
    MinDate := DateRange[0]; { ← restore the previous MinDate internal field value }
    MaxDate := DateRange[1]; { ← restore the previous MaxDate internal field value }
  end;
end;

Using it to reset the range then:

procedure TForm1.Button1Click(Sender: TObject);
begin
  DateTimePicker1.ResetRange;
end;
Victoria
  • 7,822
  • 2
  • 21
  • 44
  • Are you sure setting `MaxDate` to `0` will actually set the `FMaxDate` field to `0`? – Andreas Rejbrand Aug 16 '18 at 06:42
  • Still, +1 for discovering a way to reset the native Win32 control's range settings and for employing the undocumented hack as responsibly as possible. – Andreas Rejbrand Aug 16 '18 at 07:47
  • @AndreasRejbrand, is this working in your Delphi 10.2 ? At first, when I tried Victoria's `DateTime_SetRange(Handle, 0, nil)` I was disappointed too, because the internal fields remained set. But after that, I saw that assigning zero to them will do the job. So, beside this, are other errors in your Delphi version ? – Marus Gradinaru Aug 16 '18 at 08:37
  • @Marus Nebunu: Victoria's answer doesn't work in Delphi 10.2. When the `MaxValue` property is set to `0`, the `FMaxValue` field is set to `0.99999` or something similar, so VCL will still believe that a max value is set, even though the Win32 control doesn't. When I tried `DateTimePicker1.MaxDate := IncDay(Now, 4); DateTimePicker1.ResetRange;` I got an error message soon after. – Andreas Rejbrand Aug 16 '18 at 08:40
  • But since the Win32 trick is undocumented, and Victoria's and my class helpers rely on VCL implementation details, you have no guarantees that these workarounds will continue to work in future versions of Delphi and Windows, even if they would work perfectly in all current versions. But the good thing is that both Victoria and I check the returned value of `DateTime_SetRange`, so we will fail reasonably gracefully if Microsoft would change its implementation (very unlikely, though, since probably too many apps depends on it and the Win32 desktop is not the hottest thing anyway). – Andreas Rejbrand Aug 16 '18 at 08:44
  • The most likely future failure mode for our class helpers is that they will no longer compile (which is the best possible mode of failure), except for my altering private fields behind the back of the VCL wrapper. – Andreas Rejbrand Aug 16 '18 at 08:45
  • Have you assigned zero to `Time` property first ? I think that `0.9999` is the time portion. – Marus Gradinaru Aug 16 '18 at 08:54
  • @MarusNebunu: When exactly should I do that? (And how on Earth should I know that I have to do that?) I tried it first thing in my `TForm.OnClick` handler (see my A), and I still get the errors I mentioned. – Andreas Rejbrand Aug 16 '18 at 08:59
  • In Delphi 10.2, the setter is `procedure TCommonCalendar.SetMaxDate(Value: TDate); begin if (FMinDate <> 0.0) and (Value < FMinDate) then raise CalExceptionClass.CreateFmt(SDateTimeMin, [DateToStr(FMinDate)]); if FMaxDate <> Value then begin ReplaceTime(TDateTime(Value), TDateTime(EncodeTime(23, 59, 59, 0))); SetRange(FMinDate, Value); FMaxDate := Value; end; end;` The `0,999988425925926` thing comes from `ReplaceTime` with `EncodeTime(23, 59, 59, 0)`. – Andreas Rejbrand Aug 16 '18 at 08:59
  • (I'm sorry, "too many apps depends on it", should have been "too many apps depend on it", but the comment is locked by now.) – Andreas Rejbrand Aug 16 '18 at 09:07
  • @Andreas, you're right! My workaround works for Delphi 2009, but doesn't for Tokyo. Yours works in both. Of course, it's still an undocumented thing. – Victoria Aug 16 '18 at 10:37