суббота, 26 марта 2016 г.

Как крутить шарикоподшипник. Ч.3 - именно это.


Есть мнение, что настоящая индустриализация и независимость страны начинается с того, что в стране создаётся производство простейших правильных тел - шариков.

Фигура "Памятник индустриализации"
во дворце культуры ГПЗ-1
Этап создания шарика нами пройден. Пришла пора уже делать настоящий шарикоподшипник. Мы сделаем его с нуля и заставим вращаться.

0. Delphi, File | New | Multi-Device Application, Blank Application. Сохраняем модуль под именем 'uMain', а проект - под именем 'BallBearing'.

1. В предыдущем проекте оказалось, что в окружность диаметром 350 прекрасно вписывается дюжина объектов 50x50. И при этом между ними ещё будет достаточно места для изображения каких-нибудь деталей сепаратора (надеюсь, вы знакомы с устройством подшипника качения). Поэтому такие параметры и будем использовать. Чтобы нахождения краёв и середин компоненты делали сами, бросим на форму TLayout ( размер 350х350, Align = Center ), на него второй TLayout ( Align = Top ), а на него третий TLayout ( Align = Center ). У Layout3 выставим RotationCenter.Y = 3,5 ( [350/2]/50, где 350 - высота большого квадрата, а 50 - высота квадратика).  Это всё очень похоже на часы из предыдущего проекта. 



2. Компоненты TLayout сами ничего не показывают. Нам нужен TCircle. Его положим на Layout3, сделаем ему Align = Center и для свойства TRotationAngle создадим как в примере про один шарик TFloatAnimation ( StopValue = 360, Loop = True, Enabled = True, Duration = 1, Inverse = True ). Проверьте, что всё сделано правильно, а то потом придётся проверять уже не 1, а 12 шариков! Чтобы увидеть вращение шарика (или ролика) положим на кружок две TLine. Одну сделаем LineType = Top, Height = 25 и Position.X и Y = 0 и 25. Другую сделаем LineType = Left, Width = 25 и Position.X и Y = 25 и 0. В результате у нас получился крестик, по которому мы будем наблюдать скорость вращения шарика (или ролика).


3. Шарик готов к вращению и размножению. Берём Layout3 и так же, как это мы делали с часами, копируем Layout3 внутри Layout2 11 раз, а потом бегаем по Structure и у новых Layout4...Layout14 устанавливаем свойство RotationAngle = 30, 60, 90...330.


4. Чтобы компенсировать смещение центра из-за верхнего Layout2, бросаем на Layout1 новый TLayout, прижав его к низу ( Align = Bottom ). После этого уже накладываем на Layout1  кружок - Circle13 ( Align = Center ). Зайдя к нему в раздел Fill скопируем себе цвет #FFE0E0E0, и уберём заливку - Kind = None. Останется тонкая окружность. Идём в раздел Stroke, там ставим толщину Thickness = 20 и Color = #FFE0E0E0, который мы скопировали из заливки. Дав полученному кольцу размеры 320х320, мы пропустим его через середины всех шариков. Но надо ещё нарисовать границы. На Circle13 кидаем Circle14 (Align = Center, Fill.Kind = None, Size = 280x280) и также на Circle13 - Circle15 ( Fill.Kind = None, Align = Client ).


5. В нашем сепараторе не хватает конструктивизма. Добавим-ка обоймы для шариков. Будем действовать привычным способом: чтобы избежать лишних вычислений на Circle15 бросаем TLayout ( Align = Top, Size.Height = 25 ). На новый Layout16 кладём TRoundRect ( Align = HorzCenter, Width = 70 (10 слева + 50 шарик + 10 справа) ). Хотя кольцо у нас толщиной 20, мы делаем высоту обоймы 25 (высота Layout16), чтобы компенсировать изгиб кольца. Дальше привычно вычисляем RotationCenter.Y = 6,4 ( [320/2]/25, где 320 - диаметр внешней окружности, 25 - высота фигуры RoundRect1 ). Остаётся только сделать копии обоймы и разложить их по кругу с шагом 30 градусов.


6. Всё, что лежит на Layout1 относится к сепаратору и вращается отдельно от корпуса. Поэтому Layout1 в Structure закрываем и непосредственно на форме строим внешнее кольцо так же, как мы строили кольцо для сепаратора. При этом толщина основного кольца будет 30, а размеры - 400х400. Диаметр внутренней окружности - 340 (400 - 2*30). Благодаря этому внешняя дорожка качения шариков, диаметр которой 350, будет на 5 погружена в кольцо. Прямо как в настоящих подшипниках.



7. Для вала и внутреннего кольца расположим на форме TCircle ( Аlign = Center, Size = 260x260 ), на нём ещё TCircle ( Аlign = Center, Size = 200x200 ), а на нём изобразим шлиц с помощью TRoundRect (Align = Center, Size = 20x200). Видно, что внутреннее кольцо подшипника также имеет углублённую дорожку качения. Чтобы вал вращался, идём в Circle19.RotationAngle и создаём TFloatAnimation ( StopValue = 360, Loop = True, Enabled = True, Duration = 5 ). Duration выбран исходя из того, что внутренняя дорожка качения имеет диаметр 250 - в 5 раз больше диаметра шарика, который делает оборот за время 1.

8. Кстати, мы так и не создали анимацию на сепаратор с шариками: Layout1.RotationAngle, Create New TFloatAnimation ( StopValue = 360, Loop = True, Enabled = True, Duration = 7 ). Duration выбран исходя из того, что внешняя дорожка качения имеет диаметр 350 - в 7 раз больше диаметра шарика, который делает оборот за время 1. Вроде всё. Нет! Ещё добавим "вишенку на торт" - поставим красную точку на внешнее кольцо, чтобы было видно, что оно НЕ вращается. Я добавил кружок 10х10 и у него в Fill выбрал цвет с чудесным названием Firebrick.


9. F9!

Кажется, я достиг того, чего желал - планетарный механизм в действии! И без единой строчки кода! Хотя, надо сказать, строчки кода при копировании могли бы оказать значительную помощь. Это ладно, когда объектов 12, а когда их 212? Как, например, в зубчатых передачах - попробуй, накопируй их! А как они будут все вращаться? Это же ужас, какая вычислительная нагрузка! Или не так страшен чёрт, как его малюют?


ЗЫ: Исходники здесь: https://github.com/alhymov/BallBearing.git

пятница, 25 марта 2016 г.

Как крутить шарикоподшипник. Ч.2 - часики.


Я уже не помню, как выглядел тот фанерный циферблат с картонными стрелками, который папа сделал, чтобы объяснить мне устройство часов. Помню только то страшное разочарование, когда я понял, что сутки состоят практически только из того, что я вижу с утра до ночи, и нет никакой второй такой же длинной как проживаемый мной день половины суток, которую я из-за своего детского бесправного положения вынужден просто проводить во сне. Мир стал другим. Оказалось, что полночь - это вовсе не такая же середина ночи, как полдень - середина дня. Мои мечты о том, что когда я стану взрослым, я смогу проживать каждый день в два раза больше времени, потому что никто не будет меня заставлять спать вторую, волшебную ночную половину суток, и я смогу играть ещё и ночью столько же, сколько днём - рассыпались в прах. Помню, что я испытал тогда чувство страшной утраты, осознав, что в сутках всего лишь только 24 часа.

Картинка из Википедии
Увлекшись программированием, я делал стрелочные часы на разных языках и под разные операционные системы. И поэтому делать очередные часы на FireMonkey было бы мне совсем не интересно, если бы не то обстоятельство, что здесь часы можно сделать чисто визуальными средствами, совсем не используя никакого "языкового" программирования. Итак, начинаем.

1. Создадим новое пустое мультиплатформенное приложение - File | New | Multi-Device Application, Blank Application. Во избежание потерь я сразу сохраняю приложение, дав модулю 'Unit1.pas' имя 'uMain', а проекту - имя 'Clock'. Затем после каждого шага я нажимаю Ctrl+S, чтобы сохранить работу. Увы, случаи бывают не только счастливые.

2. Бросим на форму TRectangle, отцентрируем его Align = Center и увеличим стороны до 350. Это будет циферблат.

3. Находим на Tool Palette и бросаем на квадрат (не забываем поглядывать в окно Structure) компонент TLayout, который при помощи Align = Top прижмём к верху квадрата. Layout1 при этом увеличит свою ширину до размеров квадрата - 350, а в высоту останется таким же, как был - 50. 

4. На Layout1 кидаем новый TRectangle. Его размер оставляем по умолчанию - 50х50. Центрируем фигуру Align=Center. Внимание, внимание - начинается подготовка к фокусу размещения цифр на циферблате: этот квадрат мы будем "вращать" вокруг центра, который находится в середине большого квадрата, т.е. от верхнего края маленького квадрата на расстоянии 350/2 = 175, что составляет 3,5 корпусов маленького квадрата. Поэтому у Rectangle2 ставим RotationCenter.Y = 3,5.

5. На маленький квадрат кладём TLabel, выравниваем по центру Align = Center и устанавливаем большой (TextSettings.Font.Size = 20) жирный (TextSettings.Font.Style = [fsBold]) шрифт. Ещё я включил автоподгон размера компонента по тексту AutoSize = True.  И ещё это - Text = '12'.



6. Одной цифры 12 (традиционно число на часах называется цифрой) будет маловато. Поэтому отмечаем Rectangle1 и копируем его на Layout1 ещё 11 раз, контролируя в окне Structure расположение новых копий и их количество. Количество можно легко отследить по номеру имени Label* - номера меток должны быть от 1 до 12.

7. И вот теперь начинается волшебство. Мы бегаем по списку квадратов в окне Structure, приглядываясь к именам Label* (начиная с Label2), и расставляем этим Rectangle* углы вращения Rotation.Angle = 30, 60, 90 ... 330.


8. Как мы видим, текст в квадратах также вращается. И как, вы думаете, будет выглядеть цифра 6 внизу циферблата, когда мы её туда вставим? Плохо она будет выглядеть. Поэтому цифры надо вeрнуть обратно в нормальное вертикальное положение. Тыча в хорошо видимые на форме Label* расставляем углы вращения Rotation.Angle = -30, -60, -90 ... -330.


9. Мне так понравилось бегать по Label* на форме,  что так и тянет пробежаться ещё раз и расставить меткам  свойство Text = '1', '2', ... '11'. Стоит ли себе отказывать? Нет, конечно! Получилось так красиво, что я даже добавил на циферблат фирменную часовую надпись TLabel (Align = Bottom, Position.Y = 232, TextSettings.HorzAlign = Center,   TextSettings.VertAlign = Leading, Text = 'FMX design'#13'model 4ACbI'). К сожалению, Object Inspector не позволяет вводить символ #13 в текст метки. Можно использовать текстовый вид формы - Alt+F12 или копировать компонент в редактор кода, и редактировать там.


10. Пришла пора стрелок. На Rectangle1 добавляем новый Rectangle14. Сделаем Align = Center, Width = 25, Height = 125. Это часовая стрелка. Сдвигаем стрелку вверх - Align = None, Position.Y = 75. Центр циферблата - 175. Получается, что вращение стрелки RotationCenter.Y будет на 100 ниже её верхнего края, что составляет от высоты фигуры 100/125 = 0,8. Так и запишем. А теперь создадим TFloatAnimation на RotationAngle. Включим анимацию Enabled = True, зациклим её Loop = True, StopValue = 360, а Duration = 144. Почему 144? Я подумал, что в реальном времени ждать оборота стрелки очень тяжело, и установил, что минутная стрелка будет проходить по цифре в секунду, т.е. на полный круг потратит 12 секунд, а часовая стрелка, как известно, движется в 12 раз медленнее минутной, т.е. 144 секунды.

11. На Rectangle1 добавляем новый Rectangle15. Сделаем Align = Center, Width = 10, Height = 160. Сдвигаем стрелку вверх - Align = None, Position.Y = 50. Вращение стрелки RotationCenter.Y будет на 125 ниже её верхнего края, что составляет от общей высоты фигуры 125/160 = 0,78125. Создаём TFloatAnimation на RotationAngle (Enabled = True, Loop = True, StopValue = 360, Duration = 12). Честно признаюсь, что хотел было сделать стрелку высотой 150, но при делении какое-то иррациональное число получалось. Поэтому я решил немного изменить картинку. И знаете что? По-моему стало даже лучше!


12. F9

После такого опыта, глядя на бодро вращающиеся часовые стрелки, так и хочется спеть "Нам нет преград!" (© А.А. Д'Актиль). И в следующем посте мы приступим уже непосредственно к самому опорному подшипнику качения.



ЗЫ: Исходники здесь: https://github.com/alhymov/Clock.git

среда, 23 марта 2016 г.

Как крутить шарикоподшипник. Ч.1 - квадратный шарик на плоскости*.

* аллюзия на "сферический конь в вакууме"

После того как легко и просто удалось мне закрутить шарик, раззадорился я замахнуться на планетарный механизм. Признаюсь, раззадорили меня задорные комментарии Всеволода Леонова. Спасибо ему большое! :)

Картинка из Википедии

А что тут сложного? То, что количество деталей больше - не принципиально на самом деле. А вот что действительно требует иного подхода - это размещение шариков по нужным местам. Когда был один шарик - я его просто прижал к краю с помощью Align = Left, а теперь нужно уже что-то более универсальное.

Очевидно, что для размещения одинаковых шариков по окружности достаточно их копировать с поворотам относительно общего центра. Поворот у нас есть - RotationAngle. А рядом с ним находится и то свойство, которое отвечает за центр вращения - RotationCenter. Надо только помнить, что координаты RotationCenter измеряются в "корпусах" поворачиваемого объекта и отcчёт идёт от левого верхнего угла объекта.  Вот и всё!

Давайте потренируемся, проверим как это всё работает. Создадим снова пустое мультиплатформенное приложение и, поскольку повороты на кружках можно и не заметить, добавим на форму два квадратика TRectangle



По умолчанию размер фигур 50х50. Чтобы не думать об их расположении, отметим оба квадратика с шифтом, и зададим им Align = Center.


Теперь, когда второй квадратик расположен точно над первым, выделим Rectangle2 в Structure и повернём немного, градусов на 15 - RotationAngle = 15.


А вот теперь вынесем центр вращения из середины фигуры далеко в сторону, поставив RotationCenter.Y = 5 вместо 0,5.


Получается, что центр вращения Rectangle2 теперь по X всё ещё посередине фигуры, а по Y - на 5 его корпусов ниже верхней грани (или на 4 от нижней). Как в этом убедиться? Давайте построим отрезки нужной длины, которые проходят из середины верхних граней квадратов вниз. По идее нижние концы отрезков из обоих фигур должны совпасть в центре вращения.

Бросаем на форму фигуру TLine, совмещаем с квадратиком Align = Center, и разворачиваем её из диагонального положения в вертикальное  RotationAngle = 45.


Искомая длина линии нам известна - пять корпусов квадрата, т.е. 50*5. Нам неизвестны соответствующие ей размеры компонента - Height и Width. На выручку приходит теорема Пифагора и калькулятор: <длина стороны квадрата> = √[(<длина диагонали>)² / 2], т.е. надо извлечь корень из половины квадрата числа 250. Я, округлив до целого, получил 177.

Установив у Line1 Size.Height = 177 и Width = 177, убираем центрирование Align = None и клавишами Ctrl + СтрелкаВниз опускаем линию так, чтобы её верхний конец совпадал с верхней линией квадрата. Конечно, можно было бы и вычислить координаты, но зачем снова голову (или кнопки калькулятора) ломать, когда есть такое замечательное средство визуального "программирования"?


Скопировав линию и выставив новому экземпляру RotattionAngle = 60 (45 + 15) также стрелками перемещаем её до получения искомой композиции.


Очевидно, что мы не ошиблись в расчётах и наши ожидания вполне оправдались. Теперь можно уже браться и за более сложное изделие.

Но погодите, вам ничего не напоминает эта картинка? По-моему, это очень похоже на часовой циферблат! Я просто не могу пройти мимо такой замечательной идеи. Поэтому в следующем посте я сделаю часики, а уж потом обязательно приступлю и к подшипнику.

Продолжение следует...

Обновление: Увлёкшись вращениями, я совершенно выпустил из виду  свойство линии LineType: ( Diagonal, Top, Left ). Заменив установленное по умолчанию значение Diagonal, можно совершенно забыть как о древнем математике Пифагоре, так и о современном калькуляторе. Всё уже сделано за нас!

понедельник, 21 марта 2016 г.

Как писать в EventLog?

Админы ко мне тут как-то подходят, окружают, к стенке слегка прижимают с разных сторон и, пристально глядя в глаза, тихим голосом объясняют, что они теперь отвечают за ту программку, которая на старом компе крутится - а там то свет выключат, то ещё что, и им очень неудобно бегать туда и кнопки нажимать для того, чтобы включить комп и залогиниться. Опять же - не сразу им о неприятностях сообщают, а только когда эти неприятности уже прямо неприятными становятся. Это плохо. Раз уж на админов это дело повесили (кто-то наконец догадался это сделать), то админы хотят, чтобы для всего этого не приложение работало, а служба, которая без логина, которая на кластере и вообще гораздо лучше. - Могёшь? - спрашивают. Я говорю, что в принципе-то вроде как есть возможность, не пробовал только ещё... - Вот! - говорят, - Заодно и скилы прокачаешь! Да, прямо так и сказали про скилы. И про логи, я ещё запомнил, что-то говорили.

Ну, службу создать - это как нефиг делать: File|New|Other - Service Application, и вот у меня модуль с наследником от TService, у которого даже есть метод LogMessage - писать в журнал. Только непонятно как.

Недолго погуглив, нахожу я кучу клонов перевода 2005 г. статьи 2002 г. неизвестного автора с "FMI Solutions". Там и знаков в примерах кода не хватает, и вообще Delphi тогда была ещё не юникодная - страшно брать такое.

А вот "Создание служб Windows в Delphi с использованием VCL" Алексеева Александра от 21-05-2008 мне понравилась, хотя там говорилось, что про категории сообщений знать мне ещё рано, т.к. "Для простых служб, которые вы будете писать в начале, это явно излишнее, поэтому разбирательство с категориями можно отложить на потом.

Отложив категории на потом, я выяснил, что прописанные в примере идентификаторы сообщений при передаче в LogMessage не работают. Дело в том, что пример содержит модификаторы номеров сообщений - Severity, Facility, которые вместе с заданным номером сообщения и образуют идентификатор, который надо отдавать в LogMessage. Ещё одно - сообщения, созданные мной просто в редакторе Delphi, отказывались отображаться нормально в Журнале Событий - кодировка Ansi, установленная в редакторе Delphi по умолчанию,  почему-то не пошла. И тут я выяснил, что и не всякая юникодная кодировка подходит, а нужна UTF-16LE. Вот это, пожалуй, главное, что я выяснил сам, не найдя прямых указаний в текстах.

А что же категории? В конце своей статьи Александр дал список ссылок, среди которых нашлась  статья "Пример использования Private Object Security в Delphi" товарища Набережных С. Н., в которой хоть про EventLog нет ничего, зато есть список литературы, в котором указана книга 
  1. Дж.Рихтер, Дж.Кларк, "Программирование серверных приложений для Microsoft Windows 2000", "Питер", ИТД "Русская редакция", 2001 г.
Очень рекомендую эту книжку. Понятным языком там и про категории, и про параметры, и про символьные имена идентификаторов и про то, что читать события из EventLog - это несколько сложнее, чем два байта переслать.

И вот я подумал - а почему бы не показывать события моей службы штатными средствами, с помощью c:\windows\eventvwr.exe, которому какой-то там запрос или имя канала передать надо в командной строке. Я даже нашёл чудную программу C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\ECMangen.exe - Manifest Generator Tool, которая предоставляет графический интерфейс как для создания шаблонов трассировочных сообщений, так и для запросов по Журналу Событий!

Воодушевлённый успехами пошёл я к админам, чтобы блеснуть эрудицией и поинтересоваться тем, как они настраивают перенаправление событий с локальной машины на администраторскую или как они запросы составляют или как каналы сообщений используют. Тут, понятно, выдумывать ничего не надо, а надо спросить у людей, чем они пользуются, и себе куда-нибудь в OnClick вставить ShellExecute с соответствующими параметрами.

- Что?! - Что ты, что ты?! - замахали на меня админы руками - Какой Журнал Событий? Ну его к Лешему, не надо! Пиши просто в файл, и будет нам счастье! А с этим EventLog вообще лучше не связываться!

...

Вот я вам что скажу: если вы вдруг задались вопросом "Как писать в EventLog?", пойдите к  тем, кто этот EventLog по-идее должен читать, и спросите - а надо ли им это?

четверг, 17 марта 2016 г.

Как крутить шарик в окружности

Андрей Совцов целых два поста запостил целых двух высококвалифицированных авторов про то, как кружок в кружке крутится. В первом посте Всеволод Леонов предлагает довольно сложную колебательную систему синусо-косинусоидальных прямоугольных координат( 1 ), а во втором сам Андрей задействовал движение по заданной траектории( 2 ). Любопытно, что самый простой вариант реализации так и не дан, а всё какие-то замудрости. Я считаю, что если начинать знакомство с темой, то с максимально простых примеров - Ex simplicibus ad composita!

1. Delphi, File | New | Multi-Device Application, Blank Application

2. Tool Palette, TCircle 2 раза, TPie 1 раз.

3. Structure, Pie1 кладём на Circle2, а Circle2 - на Circle1.

4. Object Inspector:
  Circle1 -  Align = Center,  Size.Width и Height = 350,
  Circle2 - Align = Left, размер оставим 50,
  Pie1 уменьшим на пару шагов, до 33 и поставим Align = Center.


5. Если кружок должен крутится, то что надо анимировать? Конечно, угол поворота!
Circle1.RotationAngle - Create New TFloatAnimation. В полученной анимации конечный угол StopValue = 360, зацикливаем Loop = True, включаем Enabled = True. Для Circle2 делаем всё то же самое, но учитывая вращение в обратную сторону добавим Inverse = True.

6. Немного математики. Маленький кружок, катаясь внутри большого, должен своей длиной окружности за время большого оборота покрыть длину большой окружности. Длина окружности прямо пропорциональна диаметру, а диаметры у нас - 50 и 350 - отличаются в 7 раз. Это значит, что пока кружок сделает полный круг, он должен обернуться вокруг себя 7 раз. Поэтому поставим продолжительность анимации большого круга object FloatAnimation1.Duration = 7 (секунд), а маленького FloatAnimation2.Duration = 1.

7. F9 - смотрим и думаем, как запустить процесс в другую сторону, как ускорить его или замедлить.

Всё. А вы что, господа педагоги, понапридумывали? Мне кажется, что простые решения - это именно то, что в инструменте привлекает начинающего разработчика в первую очередь. Нет?

суббота, 12 марта 2016 г.

Как интегрировать календарь в TValueListEditor

Бес в ребро мне сунул TValueListEditor: смотри, говорит, как мало места всё будет занимать, как стройненько всё смотрится и это всё простыми штатными средствами! Накидал я тестовый проект - показалось это мне действительно мило.

БЫЛО и СТАЛО:



Да только вот беда - после использования TDateTimePicker редактировать дату в отдельном диалоге - ну, как-то не comme il faut. А что делать?

Погуглил я - люди советуют показывать свой редактор в нужном месте в нужный момент, подменяя встроенный InplaceEditor. При этом поведение такого контрола надо очень сильно обрезать, чтобы клавиатурные реакции у него согласовывались с TValueListEditor. Но тогда уж не проще ли вообще свой компонент писать? Или поискать готовый?

Сторонних компонентов искать я не захотел (с обоснованием причин), а попробовал TMonthCalendar высунуть, как примерно это с TCustomListBox сделано в TInplaceEditList. Но TMonthCalendar категорически не захотел клавиатуру перехватывать. У него вообще с фокусом беда: он его получает только по Tab, когда свойство TabStop включено. А мышкой - хоть дырку в экране протыкай - никак. Поэтому, наверно, по дефолту TabStop и выключено.

Стало быть, напрашивается компромиссный вариант: положить TMonthCalendar на отдельную форму (я даже FormStyle=fsStayOnTop поставил) без заголовка и без рамочки и светить её в нужном месте. У формы на деактивацию (это когда мышкой мимо щёлкнули) и ESC (KeyPreview=True, OnKeyPress=FormKeyPress) вешаем закрытие Close.


Штатно после броска на форму календарь имеет серые поля по бокам. Я поджал его до квадратного состояния и при создании формы обжимаю её вокруг календаря (BoundsRect := MonthCalendar1.BoundsRect;). Теперь стало похоже на  TDateTimePicker. Замечу, что создатели Object Inspector поступают иначе - рисуют окно по ширине поля редактирования и раскрывают календарь в середине насколько возможно широко, т.е. видны серые поля по бокам, но зато могут появится дополнительные месяцы:



Ещё одно отличие между TDateTimePicker и Object Inspector в том, что у первого календарик выпадает слева, т.е. "в начале поля", а у второго - справа, ближе к кнопке. Я попробовал и так и так, и выбрал второй вариант выравнивая, оставив простой одномесячный размер календаря.

type
  //Access ToInplaceEditor
  TVLE = class(Vcl.ValEdit.TValueListEditor)  end;
 
procedure PopUp( AVLE: TValueListEditor; ACalBack: TProc<Boolean,TDateTime> );
var
  H: Integer;
  R: TRect;
begin
  with TfrmCalendar.Create( Application ) do begin
    FVLE := AVLE; //Save for FormClose
    FCallBack := ACalBack;
    Date := StrToDateDef( FVLE.Cells[ 1, FVLE.Row ], System.SysUtils.Date );
    BoundsRect := MonthCalendar1.BoundsRect; //No space around Calendar
    TVLE( FVLE ).InplaceEditor.Enabled := False; //No click on edit button
    R := AVLE.CellRect( AVLE.Col, AVLE.Row ); //Locstion of current editor
    H := R.Height;
    R.TopLeft := FVLE.{We are in other form}ClientToScreen( R.BottomRight );
    if R.Top + Height > Screen.Height
    then
      R.Top := R.Top - Height - H;
    Top := R.Top;
    Left := R.Left - Width;
    Show;
  end;
end;

Мне, правда, понадобился "хитрый хакерский залом", чтобы при деактивации окна мышкой, тыкающей на гриде снова в кнопку открытия календаря, календарь просто закрывался и не появлялся бы снова. Для избежания повторного открытия календаря я дизаблю TValueListEditor.InplaceEditor вместе с его кнопкой редактирования. А поскольку InplaceEditor - не публичное свойство, я TValueListEditor оборачиваю в наследника, который даёт мне пользоваться в текущем модуле своими protected свойствами. Ну, а что делать?

Ещё момент немножко скользкий такой есть: когда по ESC закрываем форму, то сначала срабатывает FormClose, а потом FormDeactivate, который снова готов вызвать FormClose. Так-то ничего страшного, но ситуация подозрительная. Поэтому в FormClose я поднимаю флаг, установив Tag := 1, а в FormDeactivate я этот флаг проверяю.

procedure TfrmCalendar.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Tag := 1; //InClose
  Action := caFree;
  TVLE( FVLE ).InplaceEditor.Enabled := True;
  if FOK then
    FVLE.Cells[ 1, FVLE.Row ] := DateToStr( Date );
  if Assigned( FCallBack ) then
    FCallBack( FOK, Date );
end;
 
procedure TfrmCalendar.FormDeactivate(Sender: TObject);
begin
  if Tag = 0 then
    Close;
end;
 
И всё? А вот и нет! А как поймать выбор даты? Как узнать, что пользователь бегал-бегал по календарю, выбирал-выбирал и вот - выбрал? Простой OnClick срабатывает на всё, что ни попадя, включая смену режима отображения. Где же событие OnSelect? Нету! Забыли сделать!

Снова не обойтись без "хакерства":

type
  TMonthCalendar = class( Vcl.ComCtrls.TMonthCalendar )
    procedure CNNotify(var Message: TWMNotifyMC); message CN_NOTIFY;
  end;
 
  TfrmCalendar = class(TForm)
    MonthCalendar1: TMonthCalendar;
    procedure FormKeyPress(Sender: TObject; var Key: Char);
...
procedure TfrmCalendar.WMClose(var Message: TMessage);
begin
  FOK := Message.WParam <> 0;
  inherited;
end;
 
function IsBlankSysTime(const ST: TSystemTime): Boolean;
type
  TFast = array [0..3] of DWORD;
begin
  Result := (TFast(ST)[0] or TFast(ST)[1] or TFast(ST)[2] or TFast(ST)[3]) = 0;
end;
 
{ TMonthCalendar }
 
procedure TMonthCalendar.CNNotify(var Message: TWMNotifyMC);
begin
  inherited;
  if ( Message.NMHdr^.code =  MCN_SELECT )
  and not IsBlankSysTime( Message.NMSelChange^.stSelStart )
  and Assigned( Parent )
  then
    PostMessage( Parent.Handle, WM_CLOSE, 1, 0 );
end;
 
Ф-ю IsBlankSysTime я честно выпилил из исходников TMonthCalendar.

Добавим теперь в TValueListEditor (он у меня называется vle) инициализацию кнопки и обработчик события.

procedure Tgsm2gd_main.FormCreate(Sender: TObject);
var
  K: uSettings.TKey;
begin
  laVersion.Caption :=  'Версия: ' + GetVersionInfoText;
  for K := Low( TKey ) to High( TKey ) do begin
...
    with vle.ItemProps[ Integer( K ) ] do begin
...
      case K of
        uSettings.WildCards, uSettings.Date:
          EditStyle := esEllipsis;
...
        else
          EditStyle := esSimple;
      end;
    end;
  end;
end;
 
{ TValueListEditor }
 
procedure Tgsm2gd_main.vleEditButtonClick(Sender: TObject);
...
begin
  case uSettings.TKey( vle.Row ) of
    uSettings.Date: uCalendar.PopUp( vle );
    uSettings.WildCards: WildCardsEdit;
  end;
end;

И получаем вполне приемлемый вариант ввода даты в TValueListEditor.


Конечно, валидация у TValueListEditor не слишком надёжная. Но это уже другая песня.

ЗЫ: Исходники здесь: https://github.com/alhymov/VleMonCal.git

пятница, 11 марта 2016 г.

Как написать что-нибудь после "try"

Я вообще не слишком люблю автозамены при наборе текста. А при наборе кода, когда оно само, меня не спрашивая, вдруг что-то такое - бац! - и ты уже Бог знает в какой-то рамочке где-то посреди внезапно появившегося кода - это просто ужасно раздражает.

Нынче с чего-то вдруг попробовал я написать после слова try нечто. Только после "y" пробел нажал - Трах-Тибидох-Тибидох! Ой, ещё раз попробуем - ... - и опять такая же дрянь непрошенная. Что делать?


По идее, заходим во View|Templates, находим try и выключаем. Делаю так, но оно продолжает работать! Я тогда перегружаюсь - а оно живее, кажется, чем было!


Ладно, не можешь убить - сделай неопасным. Пытаемся изменить шаблон так, чтобы он больше не реагировал на пробел после игрека. Файл с шаблоном носит имя trycf - вот это имя и установим в самом шаблоне вместо "try".


Однако, сохранить обновление не получается. Дело оказывается даже не в ReadOnly атрибуте каталога $(BDS)\ObjRepos\en\Code_Templates\Delphi, а в том, чтобы дать себе права на файл в операционной системе (Windows 7).


И вот теперь, когда всё готово, я наконец могу написать в строке после слова try всё, что угодно!