понедельник, 22 августа 2016 г.

Как видеть русский текст в DFM/FMX

object dlgUserEdit: TdlgUserEdit
Left = 441
Top = 279
AutoSize = True
BorderStyle = bsDialog
Caption = #1048#1079#1084#1077#1085#1077#1085#1080#1077' '#1080#1092#1086#1088#1084#1072#1094#1080#1080

Мне очень всегда хотелось видеть русский текст в DFM/FMX вместо цифр с решётками. С этими решётками ни ориентироваться в тексте, ни искать в файлах нельзя по-хорошему.

И вот, как-то общаясь с Ярославом Бровиным на тему чего бы хорошего нам хотелось бы в Дельфи, я наконец решился и написал по совету Ярослава такую штуку, которая называется RSP-15606. Но кроме Марко Канту, который не очень-то это дело поддержал, её никто особо и не читал, и никто особо не знает про неё. А чтобы она приобрела какую-то важность, за неё, насколько я понял, надо голосовать.

Господа, если вы находите, что русский текст в DFM/FMX вместо цифр с решётками - это хорошо (а я думаю даже, что это - замечательно), то проголосуйте за это. И даже можете, наверно, написать комментарий.

Я в этой системе сам мало что понимаю, но надеюсь, что ваши голоса помогут. А там, если дело пойдёт, то глядишь, и ещё что-нибудь можно будет на благо наше продавить в продукт. Но это уже, конечно, потом, "в будущих многозадачных версиях ДОС"(© Microsoft Inc.)...

пятница, 3 июня 2016 г.

Как изменились размеры окон в Берлине


Решил я наконец-то попробовать Берлинскую версию. И вот странность - перекомпиленное приложение запускается в маленьком окошке и никак не желает расширяться мышкой. Я был обескуражен.

Оказалось, что окно теперь создаётся не с теми размерами, что были заданы в дизайнере, а с какими-то 320х240. А почему мышкой их нельзя изменить? А потому, что у окна когда-то было выставлено
  Resize := ( NewWidth >= 560 ) and ( NewHeight >= 400 );
в FormCanResize. И всё (- телемаркет)! Раньше я читал большие размеры из INI и форма после сздания становилась большой и красивой. Теперь же при попытке задать свойство Height параметр NewWidth, равный текущему слишком маленькому Width=320, блокирует изменение в FormCanResize. Так же точно маленький NewHeight=240 не даёт установить большой Width, считанный из INI

Виноватым я назначил форменное свойство Position. Оно стало задавать эти удивительные  размеры 320х240 создаваемой форме при значениях poDefaultPosOnlypoDesigned(даже!) и всех poХХХCenter, т.е. вообще всех значениях кроме poDefault и poDefaultSizeOnly. Не странно ли?

Кто виноват - ясно. Что делать? Я изменил FormCanResize на

  Resize := ((NewWidth = Width) or ( NewWidth >= 560 )) and
            ((NewHeight = Height) or ( NewHeight >= 400 ));

Уже не так просто, как раньше. Но за то - надёжнее. Возможно, решением проблемы было бы и применение SetBounds для одновременного изменения размеров окна. Но я сделал как сделал.

Теперь можно задать нужные размеры окна путём хаккерской записи нужных цифр в INI перед стартом приложения или получить допустимые цифры путём "привязывания" окна к границе экрана перед закрытием его и сохранением размеров для следующего сеанса. И всё снова стало хорошо.

Впрочем, пользователи новой версии моего приложения вряд ли заметят те трудности, которые возникли у меня.

А что интересного случилось у вас в Берлине?

среда, 1 июня 2016 г.

Как запомнить привязку окна в Win7+

Иллюстрации с сайта windows.microsoft.com

Раньше для сохранения позиции окна приложения я часто использовал простой код:

procedure Store( F: TForm );
var
  wp: TWindowPlacement;
begin
  with TIniFile.Create( ChangeFileExt( Application.ExeName, '.ini' ) ) do
  try
    wp.length := SizeOf( TWindowPlacement );
    GetWindowPlacement( F.Handle, @wp );
    with wp.rcNormalPosition do begin
      WriteInteger( F.ClassName, 'Left', Left );
      WriteInteger( F.ClassName, 'Top', Top );
      WriteInteger( F.ClassName, 'Width', Right - Left );
      WriteInteger( F.ClassName, 'Height', Bottom - Top );
    end;
    WriteBool(F.ClassName, 'Maximized', F.WindowState = wsMaximized );
  finally Free;
  end;
end;

Я использовал не "внешние" (Left, Top, Width, Height) координаты окна, а "нормальные", полученные ф-ей GetWindowPlacement, т.к. в случае разворачивания окна на весь экран узнать потом его нормальный размер по "внешним" координатам нельзя. Разумеется, в случае, если окно и так имеет нормальный размер, "нормальные" координаты совпадают с "внешними". Вызов всего одной ф-ии работал на все случаи жизни.

 А для восстановления позиции и размеров я делал так:

procedure Restore( F: TForm );
begin
  with TIniFile.Create( ChangeFileExt( Application.ExeName, '.ini' ) ) do
  try
    F.Left   := ReadInteger( F.ClassName, 'Left'  , F.Left );
    F.Top    := ReadInteger( F.ClassName, 'Top'   , F.Top );
    F.Width  := ReadInteger( F.ClassName, 'Width' , F.Width );
    F.Height := ReadInteger( F.ClassName, 'Height', F.Height );
    if ReadBool(F.ClassName, 'Maximized', F.WindowState = wsMaximized ) then
      F.WindowState := wsMaximized;
  finally Free;
  end;
end;

В итоге не только восстанавливались бывшие до закрытия размеры, но и, в случае разворачивания на весь экран пересозданного окна, можно было потом свернуть его в нормальные размеры. Это было очень приятно.

Однако, в новых (конечно, всё относительно) версиях Windows появилась "привязка" окна мышью. Теперь картина стала менее радостной - для программного кода состояние "привязанного" окна TForm.WindowState остаётся прежним, "нормальные" координаты окна и флаги в структуре TWindowPlacement не меняются, и, соответственно, при перезапуске приложения его окно появляется совсем не в том виде, в каком оно было при предыдущем закрытии. Это неприятно.

Процедуру сохранения координат пришлось переписать:

procedure Store( F: TForm );
var
  wp: TWindowPlacement;
  R: Trect;
  M: Boolean;
begin
  M := F.WindowState = wsMaximized;
 
  if M then begin
    wp.length := SizeOf( TWindowPlacement );
    GetWindowPlacement( F.Handle, @wp );
    R := wp.rcNormalPosition;
  end
  else
    GetWindowRect( F.Handle, R );
 
  with TIniFile.Create( ChangeFileExt( Application.ExeName, '.ini' ) ) do
  try
    with R do begin
      WriteInteger( F.ClassName, 'Left', Left );
      WriteInteger( F.ClassName, 'Top', Top );
      WriteInteger( F.ClassName, 'Width', Right - Left );
      WriteInteger( F.ClassName, 'Height', Bottom - Top );
    end;
    WriteBool(F.ClassName, 'Maximized', M );
  finally Free;
  end;
end;
 
Т.е. теперь прежние, полученные ф-ей GetWindowPlacement "нормальные" координаты я беру только в случае, если окно "развёрнуто на весь экран". В остальных случаях я использую "внешние" координаты, полученные ф-ей GetWindowRect.

Вероятно, в будущих версиях Delphi на это обстоятельство обратят внимание. Вероятно, уже сейчас в WinAPI есть методы определения этой ситуации, просто я пока не смог этого найти. Но также вероятно, что ничего этого нет и не будет. Об этом говорит хотя бы поведение окна при привязке, разворачивании и восстановлении.


Eсли вы знаете о том, как иначе определить и использовать новый вид состояния окна, напишите. 

пятница, 8 апреля 2016 г.

Как крутить... - Зубастые планеты.

Честно говоря, говоря, что показать кручение шарикоподшипника было моей целью, я не совсем был честен. После того, как я без всякого напряжения закрутил одинокий шарик, мне сказали, что простой анимацией нельзя сделать не просто планетарный механизм, а именно  планетарный редуктор. Вряд ли кто-то будет спорить, что подшипник - это не совсем то же, что редуктор. Поэтому, как человек честный (пытаюсь по мере сил), я должен честно (по мере возможностей) сделать планетарный редуктор. И сделаю.

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

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



Зуб растёт вниз, т.к. я решил начать с венечной, коронной (или как её ещё там называют?) внешней зубчатой окружности, у которой зубы смотрят во внутрь. Сначала я сделал простой треугольник. Но потом решил, что это будет уж совсем неинтересно, и использовал вместо прямых линий две кривые Безье: TPаth.Data = 'M0,0 C0.25,0.5 0,0.25 0.5,1 C1,0.25 0.75,0.5 1,0'.  Вот с этим зубом, подобно воссоздающему скелет динозавра по одной найденной кости палеонтологу, я и построю свою машину.

1. Привычно бросаю на форму TLayout  (Align = Center, Size = 350x350) и на него ещё один TLayout (Align = Top). Вот тут придётся задуматься - сколько делать зубов и какого размера, чтобы они могли все уместиться на окружности. 

На настоящем редукторе (не знаю от чего он) я насчитал 35 зубов. У меня для ровного счёта пусть будет 36 - по 1 на 10º окружности. Тогда для диаметра 350 ширина зуба: 350*π/36 ≈ 30. Прикинув, что если оставить высоту TPath по-умолчанию 50 и дать такой же высоты зубы сателлитам и центральному валу, то в механизме уж очень мало место для движения получается, я решил сократить размер зуба по высоте в два раза. Мне показалось, что отнимать по 25 с двух сторон даже как-то удобнее, чем, скажем, 30. 

Итак, установив Layout2.Height = 25, бросаем на Layout2 новенький TPath (Data = 'M0,0 C0.25,0.5 0,0.25 0.5,1 C1,0.25 0.75,0.5 1,0', Align = Center, Width = 30, Height = 25,  RotationCenter.Y = 7). 7 - это 350/2/25, где 350 - диаметр окружности, а 25 - высота зуба. Оказалось, что высота в 25 - это действительно очень удобная высота.

Потом я скопировал зуб 8 раз - получилось 9 зубов. А потом я взял все эти зубы и сделал ещё 3 копии - вот и вышло 36 штук. Теперь - только пробежаться и расставить RotationAngle = 10, 20..360. Оказывается, это совсем не трудно.


2. Пришла пора подумать о шестерёнках-звёздочках-планетах-спутниках. Для начала я скопировал  сделанный уже круг зубов и повыдергивал из него каждый второй, а потом ещё раз каждый второй. Осталось 9. На самом деле не важно, как поступите вы, главное, чтобы RotationAngle = 0, 40, ..., 320. 

Прикинув диаметр внутренней окружности зубов (9*30/3 = 90, где 3 ≈ π) и внешний диаметр (90 + 2*25 = 140), я решил, что это слишком много - центральный вал (солнечная шестерня) будет слишком узким (350 - 2*140 = 70). Поэтому я уменьшил ширину зубов до 25. По предварительным прикидкам внешний диаметр сателлита должен быть 125 (≈ 25<в>*2+[25<ш>*9/3], где 25<в> - высота, а 25<ш> - ширина зуба), и RotationCenter.Y = 2,5 (125/2/25). Однако, я быстро убедился, что квадратики 25х25, размещённые на окружности ø75, не соприкасаются углами. Поэтому я уменьшил диаметр вписанной окружности до 60 - RotationCenter.Y = 2,2 ([25*2 + 60]/2/25). 

То, что получилось, ещё на звёздочку похоже не очень - зубы смотрят не в ту сторону. Это потому, что RotationCenter.Y находится ниже корпуса фигуры. Если мы поместим его выше, то тогда зубы закрутятся не вниз, а вверх, образуя нормальное зубчатое колесо. Для этого RotationCenter.Y должен быть отрицательным. Надо только понимать, что при этом позиция фигуры будет на целый корпус ближе к центру вращения. Поэтому просто поменять знак - недостаточно, надо RotationCenter.Y ещё и уменьшить на 1 по модулю: -1,2. Можно отметить зубы  в Structure и поменять свойство у всех разом. А чтобы оставить звёздочку внутри большого кольца, изменим Layout4.Align с Top на Bottom


Невозможно не заметить, что при таком расположении зубов наша шестерня должна быть прижата к верхнему краю. Поэтому на Layout3 кладём новый TLayout (Align = Top, Height = 110 - высота звезды). Но перетаскивать на новенького Layout4 не спешим. Надо предусмотреть ещё, что нам понадобятся одно вращение вокруг общего центра для создания клонов и другое вращение вокруг собственной оси для анимации. Поэтому на Layout5 добавляем Layout6 (Align = HorzCenter), а на него - Layout7 (Align = Client), а уже на Layout7 перетаскиваем Layout4.

Но что же получилось? Зубцы вгрызаются друг в друга! Опустим звёздочку на 10 единиц ниже, установив размер Layout3 в 330х330. И давайте заполним зияющую пустоту, поместив на  Layout7 окружность (Align = Center, Size = 60x60). Чтобы окружность не съезжала, добавим на Layout4 уравновешивающий TLayout (Align = Top, Height = 25). Для получения монолитности звезды щёлкаем на окружности правой кнопкой мыши и выбираем Control|Send to Back. Если результата сразу не видно, пощёлкайте вокруг - всё обязательно станет на свои места.

Итак, можно уже устанавливать вращение. Для Layout3.RotationAngle выбираем Create New TFloatAnimation, Duration = 36 (по секунде на зуб), Enabled = True, Loop = True, StopValue = 360. Для Layout7.RotationAngle тоже выбираем Create New TFloatAnimation, Duration = 9 (по секунде на зуб), Enabled = True, (внимание!) Inverse = True, Loop = True, StopValue = 360. Посмотрим, что получилось.


Запустив полученное приложение, можно увидеть, что сначала звёздочка крутится вполне реалистично. Но уже начиная где-то с 3-го круга, она заметно начинает отставать в собственном вращении, и её зубцы начинают всё больше и больше накладываться на зубцы коронного колеса, что, разумеется, сильно портит впечатление от проделанной нами работы.

Очевидно, что между двумя TAnimation накапливается погрешность расчётов. Если с гладкими шариками такие рассогласования сильно в глаза не бросаются, то при анимации зубчатых передач такого рода "скольжения" не заметить уже невозможно. Что же делать? Выходит, что вся затея изначально была обречена на неудачу?

Не будем отчаиваться! Давайте просто выравняем раскадровку анимаций, установив им одинаковую продолжительность действия. Сделаем FloatAnimation2.Duration = 36, как у FloatAnimation1. А чтобы увеличенное время не пропадало зря, увеличим соответственно и угол поворота - StopValue = 1440 вместо прежних 360. Таким образом мы не только согласовали орбитальное и собственное вращения шестерни, но и получили удобный способ изменять общий период вращения - достаточно перейти в тестовое представление дизайнера и поиском и заменой пройтись по всем Duration, сколько бы их ни было.

3. Размножим орбитальную группировку. Мне не приходилось видеть редукторы с одним сателлитом, обычно - с тремя или четырьмя. Но, как говорится, стрелять - так стрелять! Сделаем сразу полдюжины.

Установим Layout6.RotationCenter.Y = 1,5 (330/2/110, где 330 - высота большого Layout3, 110 -  высота самого Layout6). После этого сделаем копии Layout6 внутри Layout5, чтобы всего было 6 звёзд. Теперь бегаем по RotationAngle и расставляем 60, 120, ..., 300.

Шестёрка шестерёнок получилась плотной, но в этом нет ничего страшного. Легко можно посчитать, что радиус проходящей через центры звёздочек окружности - 110. Поскольку вершины правильного шестиугольника находятся друг от друга на расстоянии радиуса описывающей окружности, наши звёздочки располагаются как раз так, что их собственные описывающие внешние окружности попарно соприкасаются в одной общей точке. Можно было бы даже подумать, что при вращении звёздочки будут чиркать друг о друга кончиками зубьев. Но этого не случится, т.к. количество зубцов у них - нечётное, т.е. напротив каждого зубца как раз находится вырез у соседней звёздочки. Здорово я угадал с размерами?

Наложим на сателлиты водило. Делаем почти так же, как делали сепаратор подшипника. Сначала на Layout3, чтобы компенсировать смещение центра из-за верхнего Layout5, бросаем  новый соразмерный TLayout, прижав его к низу (Align = Bottom, Height = 110). Потом на Layout3 кладём кружок - (Align = Center, Fill.Kind = None, Stroke.Color = #FFE0E0E0 - скопировали из заливки, Stroke.Thickness = 40, Size = 260x260). На этот Circle7 кидаем Circle8 (Align = Center, Fill.Kind = None, Size = 180x180) и потом также на Circle7 добавляем Circle9 (Fill.Kind = None, Align = Client).

Получившееся кольцо остро нуждается в украшениях. Добавим торцы осей сателлитов. На Circle9 кладём TLayout (Align = Top, Height = 40), на него - Circle10 (Align = Center, Size = 20x20, RotationCenter.Y = 6). Размножаем Circle10 до 6 штук и ставим кружкам RotationAngle = 60, ..., 300. Так всё планеты встали на свою орбиту и никуда с неё уже не денутся.


4. Приступим к центру. Новое зубчатое колесо - новые расчёты. Сколько сделать зубцов для солнечной шестерни? Кажется, что между внутренними зубцами планет мы можем воткнуть по два. Стало быть, всего 12. А с каким диаметром? Для примера, зубцы коронной передачи имеют внутреннюю окружность с радиусом 300 (350 - 25*2), а внешняя окружность сателлитов - 330, т.е. на 30 больше. Значит, если внутренний диаметр сателлитов 110, то размер солнечной шестерни должен быть 140. Начнём?

Возьмём уже готовую шестерню - Layout7, скопируем её на форму. Получилось не очень здорово, но мы не привыкли отступать: Layout7.Align = Center, Size = 140x140. Уже лучше, но надо продолжать. Возьмём 3 любых PathXX на Layout32, и сделаем их копии рядом. Потом отметим в Structure все 12  зубцов (или зубов? зубьев?), сделаем им всем RotationCenter.Y = -1,8 (1 - 140/2/25) и пойдём расставлять каждому по отдельности RotationAngle = 30, 60, ..., 330.

Однако, получилось, что зубцы колёс пересекаются. Давайте, повернём Layout31.RotationAngle на 15... нет, лучше на 17,5 градусов. И да, заполняющую окружность увеличим до 100х100... нет, лучше до 96.

Разберёмся с унаследованной анимацией. Она теперь не инверсная (Inverse = False) и за указанный период должна делать не 4 (36/9) оборота, а только 3 (36/12), т.е. StopValue = 1080. Однако, очень важно отметить, что беря для вращения соотношение 9/12 между солнцем и планетами мы не учитывали, что это всё движется внутри короны. Чтобы это учесть, математикой заниматься не будем, а просто перетащим Layout31 куда-нибудь на Circle7. Всё?

Нет, запуск показывает неприглядную картину - солнце дёргается, отстаёт. И не помогает даже FloatAnimation8.StartFromCurrent = True! Что делать? На Circle7 добавим Layout34 (Align = Center), а уже на него перетащим Layout31. FloatAnimation8 же спустим с Layout31 на Layout34. StartFromCurrent можно выключить. Вот теперь - всё!


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

Кстати, я заметил, что изобретённый мной зуб механической передачи трётся о своих соседей просто страшным образом. Он явно не заточен по эвольвенте. Если вы можете улучшить конструкцию, добро пожаловать: https://github.com/alhymov/Planetary.git

Возможно, продолжение следует...

пятница, 1 апреля 2016 г.

Как послать доллар через RESTDebugger

Выдали мне партнёры точку доступа к данным и сказали: "Пиши в HTTP-заголовке 'X-Auth: abracadabraabracadabraabracadabra$abracadabraabracadabraabracadabra', а то неавторизованный доступ будет".

Я беру Fiddler (я его всегда беру), захожу в Composite и делаю запрос как учили. В результате - дерево, JSON. А как бы табличкой посмотреть?

Тут вспомнился мне RESTDebugger наш. Вот, думаю, сейчас и посмотрим. Запускаю, а мне - "неавторизованный доступ". Как это? Я же скопировал! Смотрю в том же Fiddler и вижу - 'X-Auth: abracadabraabracadabraabracadabra%24abracadabraabracadabraabracadabra', т.е. '%24' вместо '$'.

Ага, проблема понятна - в RESTDebugger надо у параметра включить TRESTRequestParameterOption.poDoNotEncode в Options. Только у RESTDebugger нет в диалоге редактирования параметров ничего про Options. А почему бы не сделать?

Исходники RESTDebugger, писанные на FireMonkey, нам даны в $(BDS)\source\data\rest\restdebugger. Но не все - не хватает uRESTDebuggerResStrs. Его я взял из блога какого-то хорошего человека ZACares-hk. Честно сказать, не уверен, что это человек - может, ангел.

Я добавил галку в uCustomHeaderDlg_frm, инициализировал её при передаче параметра для редактирования в Tfrm_CustomHeaderDlg.Create, в uMain_frm подредактировал Tfrm_Main.DoAddCustomParameter и Tfrm_Main.DoEditCustomParameter, а также в uRESTObjects дополнил TRESTRequestParams.AsJSONObject и TRESTRequestParams.FromJSONObject. Ещё, правда, пришлось на второй закладке главной формы список параметров поправить в размерах - он наезжал на кнопки и уезжал вниз за пределы вкладки. И всё заработало!


Не знаю, пользуется ли кто этим RESTDebugger, собирается ли кто из официальных разработчиков что-то с ним делать, или может ли кто этих разработчиков попросить включить эти изменения в дистрибутив, чтобы "в будущих многозадачных версиях ДОС"* не пришлось всё заново перекомпиливать, но вот: https://github.com/alhymov/RESTDebugger-.git 

*-врезалась мне в память цитата из "Руководства по написанию драйверов для MS-DOS".

суббота, 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 - смотрим и думаем, как запустить процесс в другую сторону, как ускорить его или замедлить.

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