Форум программистов, компьютерный форум, киберфорум
Наши страницы
Jin X
Войти
Регистрация
Восстановить пароль
Оценить эту запись

DateTimePicker – либо дата, либо время... или нет

Запись от Jin X размещена 14.03.2019 в 21:00
Обновил(-а) Jin X 16.03.2019 в 18:43

DateTimePicker – либо дата, либо время... или нет?

Что не так?

На днях я столкнулся с такой проблемой. В DelphiC++Builder) имеется компонент класса TDateTimePicker, позволяющий пользователю вполне удобным образом выбирать дату или время. Ключевой момент: дату ИЛИ время. Что именно – определяется опубликованным (published) свойством Kind, которое может принимать значение dtkDate или dtkTime. Однако компонент имеет также опубликованное свойство Format, которое позволяет задавать свой формат отображения даты и времени, причём записать туда можно сразу и дату, и время. К примеру, вот в таком формате: dd-MMM-yyyy, HH:mm:ss (отображаться это будет так: 14-мар-2019, 15:08:37). Прикол в том, что пользователь может менять как дату, так и время (вне зависимости от значения свойства Kind), однако обновляться при этом будет только значение либо свойства Date, либо свойства Time. Неожиданно? Если внимательно читать документацию, то нет. Учитывая, что это стандартный контрол Windows, это поведение абсолютно нормально.

Что же делать, если мы хотим дать пользователю возможность выбирать и дату, и время?
  • Вариант 1: установить два компонента DateTimePicker (один с Kind = dtkDate, другой с Kind = dtkTime) и читать из них отдельно дату (Date) и время (Time) ответственно.
  • Вариант 2: создать собственный компонент на основе DateTimePicker.
  • Вариант 3: "вмешаться" в работу DateTimePicker, создав класс-перехватчик (т.е. класс-потомок с тем же именем, оставив оригинальный класс в покое) и заставить форму использовать его вместо оригинального класса.
Вам решать, какой вариант выбрать. Лично я пошёл по третьему пути (тем более, что у меня уже расставлено несколько DateTimePicker'ов на форме, которые заменять на двойные + менять код было в лом).

Рассказываю подробнее и по шагам
  1. Выбираем на всех компонентах, хранящих дату+время (или только дату), у свойства Kind значение dtkDate (там, где только время, оставляем dtkTime).

  2. Перед(!) объявлением класса формы объявляем класс TDateTimePicker как потомок Vcl.ComCtrls.TDateTimePicker (в Delphi до версий XE включительно родительский класс будет называться ComCtrls.TDateTimePicker). Ещё раз: объявить класс нужно ДО класса формы, на котором расположены компоненты.

    Если у вас несколько форм, на которых используется DateTimePicker, создайте отдельный модуль (unit), а потом добавляйте его в список uses модуля каждой формы, но обязательно после Vcl.ComCtrls (ComCtrls). В общем-то, это можно сделать и для одной формы – тут уж как вам удобнее.

  3. В создаваемом классе создаём метод SetTimeFromCaption, который будет читать время из Caption (т.е. из окна в виде текста), преобразовывать его в TDateTime и записывать в свойство Time.

  4. Перекрываем динамический (dynamic) метод Change. Этот метод вызывается при изменении содержимого компонента. Внутри делаем проверку Kind = dtkDate, и в случае совпадения вызываем метод SetTimeFromCaption и inherited.

Код в студию!

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
type
  // Класс-перехватчик, позволяющий читать изменение и даты, и времени в TDateTimePicker
  // ВАЖНО: При изменении свойства Format может потребоваться изменение PickerDateTimeSeparator и PickerTimeSeparator.
  TDateTimePicker = class(Vcl.ComCtrls.TDateTimePicker)
    class constructor Create;
    procedure Change; override;
  private
    procedure SetTimeFromCaption;
    const
      PickerDateTimeSeparator = ' ';  // разделитель между датой и временем (строка)
      PickerTimeSeparator = ':';      // разделитель часов, минут и секунд (символ)
    class var FmtSettings: TFormatSettings;
  end;
 
  // Далее идёт объявление класса формы
 
. . .
 
implementation
 
. . .
 
// Конструктор класса: настройка параметров (один раз для всех объектов)
class constructor TDateTimePicker.Create;
begin
  FmtSettings := TFormatSettings.Create;
  FmtSettings.TimeSeparator := PickerTimeSeparator;
end;
 
// Обновление времени, если Kind = dtkDate
procedure TDateTimePicker.Change;
begin
  if Kind = dtkDate then SetTimeFromCaption;
  inherited;
end;
 
// Установить время, исходя из текста в окне DateTimePicker'а
procedure TDateTimePicker.SetTimeFromCaption;
var
  NewTime: TDateTime;
  TimeStr: String;
  SepPos: Integer;
begin
  TimeStr := Caption;
  // Удаляем всё до последнего разделителя PickerDateTimeSeparator (вместе с разделителем)
  repeat
    SepPos := Pos(PickerDateTimeSeparator, TimeStr);
    if SepPos = 0 then Break;
    Delete(TimeStr, 1, SepPos + Length(PickerDateTimeSeparator) - 1);
  until False;
  // Преобразуем строку в формат времени
  if TryStrToTime(TimeStr, NewTime, FmtSettings) then
    Time := NewTime;
end;
Согласен, используемый в SetTimeFromCaption метод выглядит несколько костыльно. Но другого способа получения времени из DateTimePicker при значении Kind = dtkDate я, к сожалению, не знаю.
Знаете? Поделитесь в комментариях!

Несколько нюансов
  • Как написано в комментариях, при изменении формата даты/времени (свойства Format) необходимо скорректировать значения констант PickerDateTimeSeparator и PickerTimeSeparator (здесь важен разделитель между датой и временем, а также между часами, минутами, секундами).

  • Манипуляции с объектом класса TFormatSettings нужны для того, чтобы предотвратить проблемы с преобразованием строки, содержащей время, если системный разделитель времени будет отличаться от символа PickerTimeSeparator (в данном случае двоеточия). Дело в том, что символ двоеточия в DateTimePicker.Format означает именно двоеточие, в отличие, например, от функции DateTimeToString модуля System.SysUtils, в котором двоеточие означает системный разделитель часов и минут (установленный настройках операционной системы).

    Сначала я хотел обойтись без конструктора класса и создания объекта FmtSettings, просто меняя и восстанавливая значение общих настроек FormatSettings.TimeSeparator внутри SetTimeFromCaption. Но потом решил, что это колхоз. Представьте ситуацию, что у вас многопоточное приложение, и дополнительный поток использует функции вроде Format, DateTimeToStr, StrToDateTime и т.п., для преобразования даты/времени в строку или обратно. И в этот самый момент наш код меняет FormatSettings.TimeSeparator. Появляются баги, а вы не понимаете откуда (причём, баги не у вас, а у других пользователей, у которых вместо двоеточия в ОС используется, например, точка). Ситуация маловероятная, но любой нормальный программист должен учитывать и такое

  • В методе SetTimeFromCaption я оставляю строку именно после последнего вхождения PickerDateTimeSeparator в Caption, чтобы можно было использовать тот же разделитель ещё и где-то раньше (например, число и месяц тоже могут отделяться пробелом, либо вы можете добавить день недели, отделённый запятой или пробелом). Всё же, время обычно указывается в самом конце и не содержит символов-разделителей вроде запятой, пробела и т.п.

  • Если всё сделано и настроено правильно TryStrToTime всегда будет возвращать True. Однако, если вы хотите отлавливать проблемные ситуации, создайте ещё один метод (скажем, TimeFromCaptionFailure) и вызывайте его из SetTimeFromCaption в случае ошибки преобразования.

  • В компоненте DateTimePicker ранних версий Delphi изменить время при установленном значении Kind = dtkDate нельзя (пользователь не сможет ввести цифры, а нажатие на стрелки вверх-вниз ничего не изменят). Однако при значении Kind = dtkTime можно менять и дату, и время. В этом случае вам придётся преобразовывать строку, содержащую не время, а дату. Это может оказаться немного сложнее (поскольку строка может содержать название месяцев, а порядок расположения дней, месяцев и лет может отличаться на разных компьютерах). Если есть желание написать такой код, welcome! Начиная с Delphi 2009 таких проблем нет.
Размещено в Без категории
Просмотров 113 Комментарии 1
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    Печальная штука, в C++Qt такой проблемы нет и вот интересно есть ли данная проблема в последней версии FireMonkey
    Запись от Avazart размещена 16.03.2019 в 15:13 Avazart на форуме
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru