View Issue Details

IDProjectCategoryView StatusLast Update
0029646LazarusLCLpublic2016-02-25 15:34
ReporterZoran VučenovićAssigned ToBart Broersma 
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionfixed 
PlatformOSWindowsOS Version8
Product Version1.7 (SVN)Product Build51618 
Target Version1.8Fixed in Version1.8 
Summary0029646: TCalendar.HitTest method incorectly returns cpDate
DescriptionIn recent Windows versions (probably since Windows 7), the native windows MontCalendar widget has new look. When the user clicks on month name in title (for example February 2016), then the calendar changes to grid of months (see attached file Capture.PNG). Then, when user clicks on a month, the Calendar.HitTest (http://lazarus-ccr.sourceforge.net/docs/lcl/calendar/tcustomcalendar.hittest.html) returns cpDate. I think that it should return cpTitleMonth.
Additional InformationThis affects the incorrect behaviour of TDateTimePicker, TDateEdit and TCalendarDialog. All those controls ask Calendar.HitTest(aPoint) in [cpDate, cpNoWhere] and, when they get cpDate, they incorrectly close the calendar when the user hasn't chosen the date.

This is reported as TDateTimePicker bug (see issue 29617), but it should be solved in TCalendar for Win32/64 widgetset, so I'm creating this bug report.
TagsNo tags attached.
Fixed in Revisionr51694, r51695, r51696
LazTarget1.8
WidgetsetWin32/Win64
Attached Files
  • Capture.PNG (3,314 bytes)
    Capture.PNG (3,314 bytes)
  • current_view.diff (4,199 bytes)
    Index: lcl/calendar.pp
    ===================================================================
    --- lcl/calendar.pp	(revision 51675)
    +++ lcl/calendar.pp	(working copy)
    @@ -55,6 +55,16 @@
         cpTitleYear     // year value in the title
       );
     
    +  { In Windows since Vista native calendar control has four possible views.
    +    In other widgetsets, as well as in older windows, calendar can only have
    +    standard "month view" - grid with days representing a month. }
    +  TCalendarView = (
    +    cvMonth,  // grid with days in one month
    +    cvYear,   // grid with months in one year
    +    cvDecade, // grid with years from one decade
    +    cvCentury // grid with decades of one century
    +  );
    +
       EInvalidDate = class(Exception);
     
       { TCustomCalendar }
    @@ -90,6 +100,7 @@
       public
         constructor Create(AOwner: TComponent); override;
         function HitTest(APoint: TPoint): TCalendarPart;
    +    function GetCalendarView: TCalendarView;
         property Date: String read GetDate write SetDate stored False;
         property DateTime: TDateTime read GetDateTime write SetDateTime;
         property DisplaySettings: TDisplaySettings read GetDisplaySettings
    @@ -173,6 +184,14 @@
         Result := cpNoWhere;
     end;
     
    +function TCustomCalendar.GetCalendarView: TCalendarView;
    +begin
    +  if HandleAllocated then
    +    Result := TWSCustomCalendarClass(WidgetSetClass).GetCurrentView(Self)
    +  else
    +    Result := cvMonth;
    +end;
    +
     procedure TCustomCalendar.Loaded;
     begin
       inherited Loaded;
    Index: lcl/interfaces/win32/win32wscalendar.pp
    ===================================================================
    --- lcl/interfaces/win32/win32wscalendar.pp	(revision 51675)
    +++ lcl/interfaces/win32/win32wscalendar.pp	(working copy)
    @@ -46,6 +46,7 @@
            const AConstraints: TObject): Boolean; override;
         class function  GetDateTime(const ACalendar: TCustomCalendar): TDateTime; override;
         class function HitTest(const ACalendar: TCustomCalendar; const APoint: TPoint): TCalendarPart; override;
    +    class function GetCurrentView(const ACalendar: TCustomCalendar): TCalendarView; override;
         class procedure SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime); override;
         class procedure SetDisplaySettings(const ACalendar: TCustomCalendar; const ASettings: TDisplaySettings); override;
       end;
    @@ -159,6 +160,26 @@
       end;
     end;
     
    +class function TWin32WSCustomCalendar.GetCurrentView(
    +  const ACalendar: TCustomCalendar): TCalendarView;
    +var
    +  CurrentView: LRESULT;
    +begin
    +  Result := inherited GetCurrentView(ACalendar);
    +  if WindowsVersion >= wvVista then begin
    +    if not WSCheckHandleAllocated(ACalendar, 'TWin32WSCustomCalendar.GetCurrentView') then
    +      Exit;
    +
    +    CurrentView := SendMessage(ACalendar.Handle, MCM_GETCURRENTVIEW, 0, 0);
    +    case CurrentView of
    +      MCMV_MONTH: Result := cvMonth;
    +      MCMV_YEAR: Result := cvYear;
    +      MCMV_DECADE: Result := cvDecade;
    +      MCMV_CENTURY: Result := cvCentury;
    +    end;
    +  end;
    +end;
    +
     class procedure TWin32WSCustomCalendar.SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime);
     var
       ST: SystemTime;
    Index: lcl/widgetset/wscalendar.pp
    ===================================================================
    --- lcl/widgetset/wscalendar.pp	(revision 51675)
    +++ lcl/widgetset/wscalendar.pp	(working copy)
    @@ -49,6 +49,7 @@
       published
         class function GetDateTime(const ACalendar: TCustomCalendar): TDateTime; virtual;
         class function HitTest(const ACalendar: TCustomCalendar; const APoint: TPoint): TCalendarPart; virtual;
    +    class function GetCurrentView(const ACalendar: TCustomCalendar): TCalendarView; virtual;
         class procedure SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime); virtual;
         class procedure SetDisplaySettings(const ACalendar: TCustomCalendar; 
           const ADisplaySettings: TDisplaySettings); virtual;
    @@ -74,6 +75,12 @@
       Result := cpNoWhere;
     end;
     
    +class function TWSCustomCalendar.GetCurrentView(const ACalendar: TCustomCalendar
    +  ): TCalendarView;
    +begin
    +  Result := cvMonth;
    +end;
    +
     class procedure TWSCustomCalendar.SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime);
     begin
     end;
    
    current_view.diff (4,199 bytes)

Relationships

related to 0029617 resolvedZoran Vučenović Lazarus TDateTimePicker – can’t pick date when year is changed 
related to 0029696 resolvedZoran Vučenović Lazarus CCR TDatetimePicker and TDBDatetimePicker prematurely leaves calendar after selecting a year 

Activities

Zoran Vučenović

2016-02-13 16:01

developer  

Capture.PNG (3,314 bytes)
Capture.PNG (3,314 bytes)

Bart Broersma

2016-02-13 19:55

developer   ~0089994

Last edited: 2016-02-14 00:01

View 5 revisions

Testing on Win7-64 with 32-bit fpc.
Testing TWin32WSCustomCalendar.HitTest gives rather confusing results:
I click on the title (february 2016), this opens the months.
I click a month.
HitPart can be any of these results:
$02020001, $00020000, $01020001, $00020001 (!), $00020003 or $00020002

I cannot see any logic in what it returns.

Note that when you click on a date in the calendar HitPart will be $00020001.
So, at least in certain cases it is impossible to distinguish between clicking a month and clicking a date.

Bart Broersma

2016-02-13 20:03

developer   ~0089995

Win7:
MCHT_NOWHERE seems to be $00100000 (commctrl says: $00000000)
MCHT_CALENDARBK seems to be $00000000 (commctrl says: $00020000)

Zoran Vučenović

2016-02-14 11:54

developer   ~0090005

Last edited: 2016-02-14 12:00

View 2 revisions

Yes, Bart, I see that TWin32WSCustomCalendar.HitTest relies on message MCM_HITTEST, so I tried to search for Microsoft documentation for this and, looking at this page https://msdn.microsoft.com/en-us/library/windows/desktop/bb760991%28v=vs.85%29.aspx , it doesn't seem to have the result which can be used for this special case.

Bart Broersma

2016-02-14 12:23

developer   ~0090007

To me some of the descriptions on that page are a bit cryptic.
E.g the difference between MCHT_TITLE and MCHT_TITLEMONTH is beyond me.

Bart Broersma

2016-02-14 13:19

developer   ~0090008

Last edited: 2016-02-14 13:27

View 5 revisions

Another potential ptoblem is that HitTest is called in Click or DoubleClik.
This may lead to inconsistent results, since the calendar itself reacts before the Click event is firend, but after MouseDown.
E.g. When the calendar is in the state as attached screenshot (Capture.PNG), then when I click in the "right" spot you get these results:

CalendarMouseDown
*** uHit = 00020001
*** HitPart = 00020001
*** HitPart = MCHT_CALENDARDATE //MouseDown on "Jan"
TCustomCalendar.HitTest: Result = cpDate
CalendarClick
*** uHit = 00020003
*** HitPart = 00020003
*** HitPart = MCHT_CALENDARWEEKNUM //Calendar has changed and mouse is now over weeknumber
TCustomCalendar.HitTest: Result = cpWeekNumber

The problem here is that user can move the mouse after MouseDown and (in this example) choose to select "Dec" instead of "Jan".
So, we cannot rely on HitTest informtion in MouseDown, but neither can we do so in Click.

Zoran Vučenović

2016-02-14 13:38

developer   ~0090009

Bart, if user moves the mouse, that is his responsibility... Let him open the calendar once more and make sure that he doesn't move the mouse this time. :)

And, just to mention, in DateTimePicker I actually use MouseUp (as you noticed here: http://bugs.freepascal.org/view.php?id=29617#c89854 ).

Bart Broersma

2016-02-14 16:25

developer   ~0090012

> Bart, if user moves the mouse, that is his responsibility...

You miss the point (no pun intended).
Even without moving the mouse the result of HitTest can differ between MouseDown/Click/MouseUp, simply because the calendar changes in between, and hence the part that is under the mouse can change.
See the example in note 0090008. No mouse was moved (used the "click-part" on the mousepad to ensure this).

Zoran Vučenović

2016-02-14 23:57

developer   ~0090027

> simply because the calendar changes in between,

Ah, I see now what you mean.

Bart Broersma

2016-02-20 16:31

developer   ~0090166

It seems that this cannot be fixed (at least not on our part)?

Zoran Vučenović

2016-02-22 00:52

developer   ~0090182

>It seems that this cannot be fixed (at least not on our part)?

Seems so. However, in forum, GetMem says that in Delphi, the calendar does close (see http://forum.lazarus.freepascal.org/index.php/topic,31626.msg202839.html#msg202839).
So, if it is possible in Delphi, it might be possible to fix. But I still don't know how.

Zoran Vučenović

2016-02-23 00:14

developer  

current_view.diff (4,199 bytes)
Index: lcl/calendar.pp
===================================================================
--- lcl/calendar.pp	(revision 51675)
+++ lcl/calendar.pp	(working copy)
@@ -55,6 +55,16 @@
     cpTitleYear     // year value in the title
   );
 
+  { In Windows since Vista native calendar control has four possible views.
+    In other widgetsets, as well as in older windows, calendar can only have
+    standard "month view" - grid with days representing a month. }
+  TCalendarView = (
+    cvMonth,  // grid with days in one month
+    cvYear,   // grid with months in one year
+    cvDecade, // grid with years from one decade
+    cvCentury // grid with decades of one century
+  );
+
   EInvalidDate = class(Exception);
 
   { TCustomCalendar }
@@ -90,6 +100,7 @@
   public
     constructor Create(AOwner: TComponent); override;
     function HitTest(APoint: TPoint): TCalendarPart;
+    function GetCalendarView: TCalendarView;
     property Date: String read GetDate write SetDate stored False;
     property DateTime: TDateTime read GetDateTime write SetDateTime;
     property DisplaySettings: TDisplaySettings read GetDisplaySettings
@@ -173,6 +184,14 @@
     Result := cpNoWhere;
 end;
 
+function TCustomCalendar.GetCalendarView: TCalendarView;
+begin
+  if HandleAllocated then
+    Result := TWSCustomCalendarClass(WidgetSetClass).GetCurrentView(Self)
+  else
+    Result := cvMonth;
+end;
+
 procedure TCustomCalendar.Loaded;
 begin
   inherited Loaded;
Index: lcl/interfaces/win32/win32wscalendar.pp
===================================================================
--- lcl/interfaces/win32/win32wscalendar.pp	(revision 51675)
+++ lcl/interfaces/win32/win32wscalendar.pp	(working copy)
@@ -46,6 +46,7 @@
        const AConstraints: TObject): Boolean; override;
     class function  GetDateTime(const ACalendar: TCustomCalendar): TDateTime; override;
     class function HitTest(const ACalendar: TCustomCalendar; const APoint: TPoint): TCalendarPart; override;
+    class function GetCurrentView(const ACalendar: TCustomCalendar): TCalendarView; override;
     class procedure SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime); override;
     class procedure SetDisplaySettings(const ACalendar: TCustomCalendar; const ASettings: TDisplaySettings); override;
   end;
@@ -159,6 +160,26 @@
   end;
 end;
 
+class function TWin32WSCustomCalendar.GetCurrentView(
+  const ACalendar: TCustomCalendar): TCalendarView;
+var
+  CurrentView: LRESULT;
+begin
+  Result := inherited GetCurrentView(ACalendar);
+  if WindowsVersion >= wvVista then begin
+    if not WSCheckHandleAllocated(ACalendar, 'TWin32WSCustomCalendar.GetCurrentView') then
+      Exit;
+
+    CurrentView := SendMessage(ACalendar.Handle, MCM_GETCURRENTVIEW, 0, 0);
+    case CurrentView of
+      MCMV_MONTH: Result := cvMonth;
+      MCMV_YEAR: Result := cvYear;
+      MCMV_DECADE: Result := cvDecade;
+      MCMV_CENTURY: Result := cvCentury;
+    end;
+  end;
+end;
+
 class procedure TWin32WSCustomCalendar.SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime);
 var
   ST: SystemTime;
Index: lcl/widgetset/wscalendar.pp
===================================================================
--- lcl/widgetset/wscalendar.pp	(revision 51675)
+++ lcl/widgetset/wscalendar.pp	(working copy)
@@ -49,6 +49,7 @@
   published
     class function GetDateTime(const ACalendar: TCustomCalendar): TDateTime; virtual;
     class function HitTest(const ACalendar: TCustomCalendar; const APoint: TPoint): TCalendarPart; virtual;
+    class function GetCurrentView(const ACalendar: TCustomCalendar): TCalendarView; virtual;
     class procedure SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime); virtual;
     class procedure SetDisplaySettings(const ACalendar: TCustomCalendar; 
       const ADisplaySettings: TDisplaySettings); virtual;
@@ -74,6 +75,12 @@
   Result := cpNoWhere;
 end;
 
+class function TWSCustomCalendar.GetCurrentView(const ACalendar: TCustomCalendar
+  ): TCalendarView;
+begin
+  Result := cvMonth;
+end;
+
 class procedure TWSCustomCalendar.SetDateTime(const ACalendar: TCustomCalendar; const ADateTime: TDateTime);
 begin
 end;
current_view.diff (4,199 bytes)

Zoran Vučenović

2016-02-23 00:15

developer   ~0090197

Yes, there is hope!
See: https://msdn.microsoft.com/en-us/library/windows/desktop/bb760955%28v=vs.85%29.aspx
Now, based on this message, I added new GetCurrentView function to calendar. Now, we will be able to ask what current view is and, only when it is month view, we ask HitTest to close the calendar.

I'm attacihng the patch which adds new enum type TCalendarView and new function TCalendar.GetCurrentView. This function is implemented in win32 interface that, if windows version is Vista or after, then it can return one of four values and in all other cases, as in other widgetsets, it always returns cvDate. I'm attaching the patch (current_view.diff), please test.

Bart Broersma

2016-02-25 11:03

developer   ~0090287

Last edited: 2016-02-25 11:06

View 2 revisions

@Zoran: applied your patch in r51694.
Attempted to fix the issue for TCalendarDialog and TDateEdit in r51695.

I did not see a proper solution to fix it in TCustomCalendar, without breaking backwards compatibility.

Someone else needs to fix TDateTimePicker.
I can't find the correct place to do that. (I end up in some abstract method.)

Zoran Vučenović

2016-02-25 12:08

developer   ~0090292

Bart, I am the maintainer of TDateTimePicker, I will fix it.
Then, I'll look into TDateEdit and TCalendarDialog and try to provide tha patch.

Zoran Vučenović

2016-02-25 14:01

developer   ~0090296

I fixed TDateTimePicker (rev. 51696). Please test.

@Bart:
I couldn't just ask GetCurrentView when I use MouseUp, because of the problem you described in 0090008. As a workaround I ask in LM_MOUSEDOWN and save the value in temporary variable.

Seems that, for that same reason, your correction of TDateEdit doesn't work well (when in year view, that is, when grid of months is shown, it still closes).

> I can't find the correct place to do that. (I end up in some abstract method.)

The correct place is lclcalwrapper.pas. I made this abstraction to provide user the possibility to use some calendar control other then LCL TCalendar - see http://wiki.freepascal.org/DateTimeCtrls_Package#Using_some_custom_calendar_control_for_drop-down

Bart Broersma

2016-02-25 14:15

developer   ~0090297

Please close if OK.

Zoran Vučenović

2016-02-25 15:34

developer   ~0090300

Okay.

Issue History

Date Modified Username Field Change
2016-02-13 16:01 Zoran Vučenović New Issue
2016-02-13 16:01 Zoran Vučenović File Added: Capture.PNG
2016-02-13 16:02 Zoran Vučenović Relationship added related to 0029617
2016-02-13 16:33 Zoran Vučenović Additional Information Updated View Revisions
2016-02-13 19:55 Bart Broersma Note Added: 0089994
2016-02-13 19:56 Bart Broersma Note Edited: 0089994 View Revisions
2016-02-13 19:56 Bart Broersma Note Edited: 0089994 View Revisions
2016-02-13 20:03 Bart Broersma Note Added: 0089995
2016-02-13 23:59 Bart Broersma Note Edited: 0089994 View Revisions
2016-02-14 00:01 Bart Broersma Note Edited: 0089994 View Revisions
2016-02-14 11:54 Zoran Vučenović Note Added: 0090005
2016-02-14 12:00 Zoran Vučenović Note Edited: 0090005 View Revisions
2016-02-14 12:23 Bart Broersma Note Added: 0090007
2016-02-14 13:19 Bart Broersma Note Added: 0090008
2016-02-14 13:22 Bart Broersma Note Edited: 0090008 View Revisions
2016-02-14 13:23 Bart Broersma Note Edited: 0090008 View Revisions
2016-02-14 13:26 Bart Broersma Note Edited: 0090008 View Revisions
2016-02-14 13:27 Bart Broersma Note Edited: 0090008 View Revisions
2016-02-14 13:38 Zoran Vučenović Note Added: 0090009
2016-02-14 16:25 Bart Broersma Note Added: 0090012
2016-02-14 23:57 Zoran Vučenović Note Added: 0090027
2016-02-20 16:31 Bart Broersma Note Added: 0090166
2016-02-22 00:52 Zoran Vučenović Note Added: 0090182
2016-02-22 00:55 Zoran Vučenović Relationship added related to 0029696
2016-02-23 00:14 Zoran Vučenović File Added: current_view.diff
2016-02-23 00:15 Zoran Vučenović Note Added: 0090197
2016-02-25 11:03 Bart Broersma Note Added: 0090287
2016-02-25 11:06 Bart Broersma Note Edited: 0090287 View Revisions
2016-02-25 12:08 Zoran Vučenović Note Added: 0090292
2016-02-25 14:01 Zoran Vučenović Note Added: 0090296
2016-02-25 14:15 Bart Broersma Fixed in Revision => r51694, r51695, r51696
2016-02-25 14:15 Bart Broersma LazTarget - => 1.8
2016-02-25 14:15 Bart Broersma Note Added: 0090297
2016-02-25 14:15 Bart Broersma Status new => resolved
2016-02-25 14:15 Bart Broersma Fixed in Version => 1.8
2016-02-25 14:15 Bart Broersma Resolution open => fixed
2016-02-25 14:15 Bart Broersma Assigned To => Bart Broersma
2016-02-25 14:15 Bart Broersma Target Version => 1.8
2016-02-25 15:34 Zoran Vučenović Note Added: 0090300
2016-02-25 15:34 Zoran Vučenović Status resolved => closed